chore: clean up LOBE-XXX code annotations (#15135)

* chore: clean up LOBE-XXX annotations from codebase comments

- Remove 【LOBE-XXX】 bracket markers
- Remove LOBE-XXXX references from inline comments
- Clean up test descriptions containing LOBE identifiers
- Preserve linear.app URLs and code-level regex patterns
- Generated: 2026-05-23 02:30:09

* 🐛 fix(tests): restore () in arrow callbacks broken by annotation cleanup

The LOBE-XXX annotation cleanup script over-matched `(LOBE-XXXX', () =>`
and stripped the callback `()`, leaving invalid syntax like
`describe(..., => {` and `it(..., async => {` across 24 test files.

This caused parse failures in Test Packages, Test Desktop App, Test
Database lint, and Test App shard runs. Restoring `()` / `async ()`
unblocks the suites while keeping the ticket-text cleanup intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hintFormat-test): restore label + ellipsis in stripMarkdownLinks fixture

The annotation cleanup stripped `LOBE-8516` from a markdown-link's
*label* (`[LOBE-8516](/task/T-1)` → `[](/task/T-1)`), which then survived
`stripMarkdownLinks` because the pattern requires non-empty link text —
the test expected the link to disappear and asserted equality on a
LOBE-free output. The same line also lost a `.` from the trailing
`...` indicator in both input and expected strings.

Substitute a neutral Chinese label (`发布计划`) so the link continues
to exercise the multi-link substitution path, and restore the full
`...` ellipsis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Arvin Xu <arvinxx@lobehub.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-05-23 17:18:18 +08:00
committed by GitHub
parent f685d5c217
commit ddb5794826
130 changed files with 265 additions and 265 deletions
+1 -1
View File
@@ -810,7 +810,7 @@ export function registerBotCommand(program: Command) {
name: 'group-allowlist',
});
// ── watch-keywords (LOBE-8891) ────────────────────────
// ── watch-keywords () ────────────────────────
registerWatchKeywordsCommand(bot);
@@ -605,7 +605,7 @@ export default class HeterogeneousAgentCtr extends ControllerModule {
}
}
// ─── AskUserQuestion MCP server (LOBE-8725) ───
// ─── AskUserQuestion MCP server () ───
/**
* Lazy single-instance MCP server for CC's AskUserQuestion replacement.
@@ -651,7 +651,7 @@ export default class HeterogeneousAgentCtr extends ControllerModule {
// `alwaysLoad: true` is the undocumented CC flag that promotes our
// server's tool out of the deferred set so the model calls it directly
// (no ToolSearch hop). See LOBE-8725 spike notes — falls back to the
// (no ToolSearch hop). See spike notes — falls back to the
// 2-hop ToolSearch path if a future CC drops the flag, no breakage.
const config = {
mcpServers: {
@@ -717,7 +717,7 @@ describe('HeterogeneousAgentCtr', () => {
* `stdout.on('end')` handler can schedule `pipeline.flush()` onto the
* broadcast queue), then drain the queue, then broadcast complete.
*/
describe('exit-before-end ordering (LOBE-8516 phase 0 race)', () => {
describe('exit-before-end ordering (phase 0 race)', () => {
let broadcasts: Array<{ channel: string; data: any }>;
beforeEach(() => {
@@ -808,7 +808,7 @@ describe('HeterogeneousAgentCtr', () => {
});
});
describe('app-quit cleanup of AskUserQuestion temp configs (LOBE-8725)', () => {
describe('app-quit cleanup of AskUserQuestion temp configs ()', () => {
// The async exit-handler cleanup races Electron's main-process teardown
// and used to leak `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` on
// every quit. The controller now unlinks pending intervention temp
@@ -28,7 +28,7 @@ export const claudeCodeDriver: HeterogeneousAgentDriver = {
args: [
...DESKTOP_CLAUDE_CODE_ARGS,
// Wire the controller-managed temp mcp.json (AskUserQuestion server,
// see LOBE-8725) when present. Path-based config is required — CC
// see ) when present. Path-based config is required — CC
// does not accept inline JSON for `--mcp-config`.
...(mcpConfigPath ? ['--mcp-config', mcpConfigPath] : []),
...(resumeSessionId ? ['--resume', resumeSessionId] : []),
@@ -530,7 +530,7 @@ export class GeneralChatAgent implements Agent {
// Silent-drop diagnostic: LLM emitted raw tool_calls but every one
// failed to resolve to a known tool (e.g. malformed names without the
// `____` separator). Surface this in reasonDetail so dashboards can
// distinguish it from a genuine no-tool completion. See LOBE-8696.
// distinguish it from a genuine no-tool completion. See .
const rawToolCallCount = result?.tool_calls?.length ?? 0;
const hasUnresolvedToolCalls = rawToolCallCount > 0;
@@ -151,7 +151,7 @@ describe('GeneralChatAgent', () => {
expect(result).toEqual(expectCompressionInstruction(state.messages));
});
// LOBE-8973 Bug B: state.tools must feed into the compression budget,
// Bug B: state.tools must feed into the compression budget,
// otherwise large tool manifests (16-22K tokens observed on openrouter)
// slip past the threshold and overflow the model context window.
it('should fold state.tools into the compression budget on init', async () => {
@@ -227,7 +227,7 @@ describe('GeneralChatAgent', () => {
});
});
// Regression for LOBE-8696: when the LLM emits tool_calls whose names
// Regression for when the LLM emits tool_calls whose names
// can't be resolved (e.g. `activateTools` instead of
// `lobe-activator____activateTools`), the agent used to silently finish
// with "completed without tool calls". Surface the unresolved names so
@@ -766,7 +766,7 @@ describe('GeneralChatAgent', () => {
expect(result).toEqual(expectCompressionInstruction(state.messages));
});
// LOBE-8973 follow-up: when state.forceFinish is set, RuntimeExecutors strips
// follow-up: when state.forceFinish is set, RuntimeExecutors strips
// every tool before the LLM call (buildStepToolDelta returns deactivatedToolIds
// ['*']). The compression budget must mirror that stripping — otherwise the
// tool schemas push the budget over threshold and we burn an extra summarization
@@ -1172,7 +1172,7 @@ describe('AgentRuntime', () => {
expect(result.nextContext?.payload).toHaveProperty('toolCount', 3);
});
// Regression test for LOBE-7759: Gemini 3 thoughtSignature must survive the
// Regression test for Gemini 3 thoughtSignature must survive the
// OpenAI ToolsCalling -> ChatToolPayload normalization in call_tools_batch,
// otherwise Gemini 3 400s on the second tool_call turn.
it('should preserve thoughtSignature when normalizing call_tools_batch ToolsCalling payload', async () => {
@@ -1431,9 +1431,9 @@ describe('AgentRuntime', () => {
});
});
describe('Multi-Round Batch Tool Execution (LOBE-1657)', () => {
describe('Multi-Round Batch Tool Execution ()', () => {
/**
* This test verifies the fix for LOBE-1657:
* This test verifies the fix for
* When executing multiple rounds of batch tool calls, tool messages should not be duplicated.
*
* Root cause: The mergeToolResults method was extracting ALL tool messages from each result,
@@ -132,7 +132,7 @@ describe('tokenCounter', () => {
expect(result.currentTokenCount).toBe(0);
});
// LOBE-8973 Bug B: tool definitions also occupy the input window, so a
// Bug B: tool definitions also occupy the input window, so a
// message payload that fits when tools are absent can overflow once tool
// definitions are accounted for. Without this, compression only fires on
// message size and leaves the tool budget to silently push the request
@@ -63,7 +63,7 @@ export interface CompressionCheckResult {
*
* Uses {@link countContextTokens} under the hood, so the input estimate
* accounts for tool calls, reasoning, and tool definitions in addition to
* `content` (see LOBE-8964 for the calibration data).
* `content` (see for the calibration data).
*/
export function shouldCompress(
messages: UIChatMessage[],
@@ -6,8 +6,8 @@
*
* Variables (auto-injected by context-engine):
* - {{date}} - Current date (e.g., "12/25/2023")
* - {{model}} - Current model ID (requires LOBE-1803)
* - {{provider}} - Current provider (requires LOBE-1803)
* - {{model}} - Current model ID (requires )
* - {{provider}} - Current provider (requires )
*/
export const supervisorSystemRole = `You are LobeAI, an intelligent team coordinator developed by LobeHub, powered by {{model}}. You are orchestrating the multi-agent group "{{GROUP_TITLE}}". Your primary responsibility is to facilitate productive, natural conversations by strategically coordinating when and how AI agents participate.
@@ -213,7 +213,7 @@ class AgentManagementExecutor extends BaseExecutor<typeof AgentManagementApiName
}
// Register afterCompletion to execute the agent.
// Runtime routing is fully delegated to dispatchNonHeteroSubAgent (LOBE-8927).
// Runtime routing is fully delegated to dispatchNonHeteroSubAgent ().
ctx.registerAfterCompletion(async () => {
const get = useChatStore.getState;
@@ -76,7 +76,7 @@ const formatTimeout = (ms: number | undefined): string | undefined => {
};
/**
* Dedicated inspector for CC's long-running `Monitor` tool (LOBE-8998).
* Dedicated inspector for CC's long-running `Monitor` tool ().
*
* Visual contract:
* [Monitor] <MonitorIcon> <description-or-command> · <timeout> [✓/✗]
@@ -44,7 +44,7 @@ export const ClaudeCodeInspectors = {
translationKey: ClaudeCodeApiName.Grep,
}),
// Monitor is a long-running tracked tool — its turns drive a SignalCallbacks
// accordion below the AssistantGroup (LOBE-8998). The dedicated inspector
// accordion below the AssistantGroup (). The dedicated inspector
// uses the lucide `Monitor` (screen) icon to match the tool name.
[ClaudeCodeApiName.Monitor]: MonitorInspector,
[ClaudeCodeApiName.Read]: ReadInspector,
@@ -51,7 +51,7 @@ interface QABlockProps {
* One question/answer pair for the completed Render. The original question
* stays visible (header + body); the answer renders as one card per picked
* option (multi-select fans out into multiple rows). When `answer` is
* absent — older messages persisted before LOBE-8725 added structured
* absent — older messages persisted before added structured
* storage — we show a `—` placeholder so the layout stays uniform.
*/
const QABlock = memo<QABlockProps>(({ question, answer }) => {
@@ -26,7 +26,7 @@ export enum ClaudeCodeApiName {
/**
* Synthetic apiName the adapter rewrites the local
* `mcp__lobe_cc__ask_user_question` MCP tool to. Routes the dedicated
* intervention UI for CC's clarifying-question flow (LOBE-8725); not
* intervention UI for CC's clarifying-question flow (); not
* something CC's CLI emits directly.
*/
AskUserQuestion = 'askUserQuestion',
@@ -38,7 +38,7 @@ export enum ClaudeCodeApiName {
* Long-running command monitor (CC 2.1+). Spawns `command` as a tracked
* background task; CC re-invokes the LLM each time the task pushes new
* stdout (`system task_started` registers the task, `task_notification`
* terminates it — see LOBE-8998 in the adapter). Rendered by a dedicated
* terminates it — see in the adapter). Rendered by a dedicated
* `MonitorInspector` so the chip iconography matches the SignalCallbacks
* accordion underneath.
*/
@@ -104,7 +104,7 @@ export interface SkillArgs {
/**
* Arguments for CC's built-in `Monitor` tool — long-running command monitor.
* CC spawns `command` as a tracked background task; `system task_started`
* registers it and `system task_notification` ends it (see LOBE-8998 in the
* registers it and `system task_notification` ends it (see in the
* CC adapter). Each stdout push between those two lifecycle events fires a
* new LLM turn that's surfaced as a SignalCallbacks entry in the UI.
*
@@ -8,7 +8,7 @@
* (not `directory`), so cwd fell back to `process.cwd()`. With no glob/include
* filter, `tool.*name.*mcp` matched every dist/* bundle and tsbuildinfo.
*
* See LOBE-8666 / the agent screenshot that reported the leak.
* See / the agent screenshot that reported the leak.
*/
import { describe, expect, it, vi } from 'vitest';
@@ -436,7 +436,7 @@ export class MessagesEngine {
// PlaceholderVariablesProcessor only walks `message.content`, so it MUST
// run after the hoist or it would silently miss every placeholder buried
// inside an assistantGroup. (Regression discovered while wiring lobehub
// skill identity placeholders — see LOBE-6882.)
// skill identity placeholders — see .)
new PlaceholderVariablesProcessor({ variableGenerators: variableGenerators || {} }),
// =============================================
@@ -714,7 +714,7 @@ describe('ToolNameResolver', () => {
expect(result).toEqual([]);
});
// Regression for LOBE-8696: some models (e.g. deepseek-v4-pro) drop the
// Regression for some models (e.g. deepseek-v4-pro) drop the
// `<identifier>____` prefix and emit only the bare API name. When that
// bare name uniquely matches an API in the available manifests, we should
// recover the identifier from the manifest instead of silently dropping
@@ -23,7 +23,7 @@ const log = debug('context-engine:processor:MessageContentProcessor');
* does not declare vision capability. Dropping the part silently loses the
* conversational signal that an image ever existed, while leaving the raw part
* in the payload causes provider-side 400s (e.g. DeepSeek rejects the
* `image_url` variant outright — see LOBE-7214).
* `image_url` variant outright — see ).
*/
export const VISION_DOWNGRADE_PLACEHOLDER = '[image omitted: not supported by this model]';
@@ -134,7 +134,7 @@ export class ToolCallProcessor extends BaseProcessor {
// Sanitize `arguments` as a last line of defense against historical messages
// whose tool_calls arguments are invalid JSON (e.g. persisted before the
// server-side sanitizer landed, or produced by an older client). Strict
// providers like NVIDIA NIM otherwise 400 on the entire request. See LOBE-7761.
// providers like NVIDIA NIM otherwise 400 on the entire request. See .
const tool_calls = message.tools.map(
(tool: any): MessageToolCall => ({
function: {
@@ -148,7 +148,7 @@ describe('MessageContentProcessor', () => {
);
});
// LOBE-7214 regression: historical messages are often persisted in the
// regression: historical messages are often persisted in the
// multimodal parts form (content is an array of {type: 'text' | 'image_url'}).
// They bypass the legacy `imageList` code path. Switching to a non-vision
// model (e.g. deepseek-chat) previously caused the processor to forward the
@@ -954,7 +954,7 @@ describe('MessageContentProcessor', () => {
});
});
// LOBE-7214: assistant multimodal content (image generation output) must
// assistant multimodal content (image generation output) must
// also be downgraded when the target model lacks vision. Without this,
// image parts get serialized back to `image_url` and DeepSeek 400s.
it('should downgrade assistant multimodal image parts to placeholder text when vision is disabled', async () => {
@@ -4,7 +4,7 @@ import type { PipelineContext } from '../../types';
import { PlaceholderVariablesProcessor } from '../PlaceholderVariables';
/**
* Regression for LOBE-6882 placeholder approach.
* Regression for placeholder approach.
*
* Confirms that PlaceholderVariablesProcessor does substitute `{{...}}` tokens
* inside `role: 'tool'` messages. If this test ever fails, it means the
@@ -79,7 +79,7 @@ describe('PlaceholderVariablesProcessor — tool message substitution', () => {
expect(result.messages[0].content).toBe('agent={{agent_id}}');
});
// Regression for LOBE-9408: a tool error result (e.g. budget-exceeded) can
// Regression for a tool error result (e.g. budget-exceeded) can
// arrive with `content: undefined`. The content-preview logging step used to
// call `JSON.stringify(undefined).slice(...)` — which throws because
// `JSON.stringify(undefined)` returns `undefined`, not a string — crashing
@@ -264,7 +264,7 @@ describe('ToolCallProcessor', () => {
});
});
// LOBE-7761: protect against history poisoning from invalid tool_call arguments
// protect against history poisoning from invalid tool_call arguments
it('should sanitize invalid tool arguments in history to "{}"', async () => {
const processor = new ToolCallProcessor(defaultConfig);
const context = createContext([
@@ -275,7 +275,7 @@ describe('ToolCallProcessor', () => {
tools: [
{
apiName: 'executeCode',
// exact shape from the LOBE-7761 NVIDIA/Qwen trace
// exact shape from the NVIDIA/Qwen trace
arguments: '{, "description": "Create data models", "language": "python"}',
id: 'call_1',
identifier: 'code',
@@ -346,7 +346,7 @@ describe('AgentDocumentInjector', () => {
expect(injected).not.toContain('Progressive content hidden');
});
// Regression: LOBE-9385 — `policyLoad: 'disabled'` rows were being routed
// Regression: — `policyLoad: 'disabled'` rows were being routed
// into the full-content bucket (the old `!== 'progressive'` filter), so
// documents the user explicitly turned off still got inlined into the LLM
// payload. The disabled row must show up in neither bucket.
@@ -131,7 +131,7 @@ describe('SystemRoleInjector', () => {
expect(result.metadata.systemRoleInjected).toBe(true);
});
it('should append systemRole after AgentDocumentBeforeSystemInjector output (regression for LOBE-6892)', async () => {
it('should append systemRole after AgentDocumentBeforeSystemInjector output (regression for )', async () => {
const provider = new SystemRoleInjector({
systemRole: '你是一个思维活跃的工程师,擅长 Python、JavaScript、Docker、SQL。',
});
@@ -231,7 +231,7 @@ export class ContextTreeBuilder {
// Recursively collect all assistant messages in this group
this.messageCollector.collectAssistantGroupMessages(message, idNode, children);
// Append external-signal callback blocks (LOBE-8998) at the END of
// Append external-signal callback blocks () at the END of
// children — one block per source tool that fired callbacks. They
// ride INSIDE the AssistantGroup but BELOW the main-chain zigzag,
// since the toolless reactive replies aren't part of the
@@ -240,7 +240,7 @@ export class ContextTreeBuilder {
children.push(...signalCallbacks);
// After the callbacks block, append the post-task-summary turns
// (LOBE-8998) — toolless assistants tagged with
// () — toolless assistants tagged with
// `signal.type === 'task-completion'`, fired by the LLM after CC's
// `task_notification` ended a long-running tool. They're peers of
// the callbacks under the same tool_result; collecting them here
@@ -223,7 +223,7 @@ export class FlatListBuilder {
processedIds,
);
// Gather external-signal callback blocks (LOBE-8998) for any
// Gather external-signal callback blocks () for any
// tool in the chain that fired toolless reactive replies
// (Monitor stdout pushes, etc.). Snapshot now so the UI doesn't
// need to query messageMap; mark callback messages as processed
@@ -233,7 +233,7 @@ export class FlatListBuilder {
allMessages,
);
// Post-task-summary turns (LOBE-8998) — toolless siblings of
// Post-task-summary turns () — toolless siblings of
// the callbacks under the same tool_result, tagged with
// `signal.type === 'task-completion'`. Belong inside the same
// AssistantGroup, rendered AFTER the SignalCallbacks accordion.
@@ -942,7 +942,7 @@ export class FlatListBuilder {
}
// Snapshot signal-callback blocks onto the virtual group message
// (LOBE-8998) so AssistantGroupMessage can render `<SignalCallbacks>`
// () so AssistantGroupMessage can render `<SignalCallbacks>`
// without re-querying the store. Each callback Message becomes a
// compact UISignalCallback with content + model/provider/sequence.
if (signalCallbackBlocks && signalCallbackBlocks.length > 0) {
@@ -961,7 +961,7 @@ export class FlatListBuilder {
}));
}
// Snapshot post-task-summary turns as content blocks (LOBE-8998).
// Snapshot post-task-summary turns as content blocks ().
// They render after `<SignalCallbacks>` inside the same group, via
// a second `<Group>` that pulls live content from the store using
// the block id (no need to denormalize text here — keeps streaming
@@ -6,7 +6,7 @@ import type { ContextNode, IdNode, Message, MessageNode, SignalCallbacksNode } f
* Locally duplicated to avoid a cross-package import for a single
* structural type.
*
* Phase 2 (LOBE-8999) promotes this to a dedicated `messages.signal`
* Phase 2 () promotes this to a dedicated `messages.signal`
* jsonb column. To migrate, swap the `metadata?.signal` lookup in
* `getMessageSignal` below for `(msg as any).signal ?? msg.metadata?.signal`
* UI and node shape are unchanged.
@@ -25,7 +25,7 @@ interface MessageSignal {
* knows whether the step will end up using tools, so the collector
* must defang that mismatch here.
*
* Phase 2 compat seam (LOBE-8999): when the `messages.signal` column
* Phase 2 compat seam (): when the `messages.signal` column
* lands, prefer it over `metadata.signal`.
*/
const getMessageSignal = (msg: Message): MessageSignal | undefined => {
@@ -118,7 +118,7 @@ export class MessageCollector {
// Only continue if the next assistant has the SAME agentId
// Different agentId means it's a different agent responding (e.g., via speak tool)
const isSameAgent = nextMsg.agentId === groupAgentId;
// Skip signal-tagged toolless callbacks (LOBE-8998) — they're a
// Skip signal-tagged toolless callbacks () — they're a
// side-channel under the same parent tool and get collected
// separately by `collectFlatSignalCallbacks`.
if (getMessageSignal(nextMsg)) continue;
@@ -204,7 +204,7 @@ export class MessageCollector {
}
/**
* Flat-list variant find post-task-summary assistants (LOBE-8998),
* Flat-list variant find post-task-summary assistants (),
* i.e. toolless assistants tagged with
* `metadata.signal.type === 'task-completion'`, fired by the LLM after
* CC delivers `task_notification` for a long-running tool.
@@ -278,7 +278,7 @@ export class MessageCollector {
}
// Find the next main-chain assistant under this tool. Signal-tagged
// toolless siblings (Monitor callbacks etc., LOBE-8998) share the
// toolless siblings (Monitor callbacks etc., ) share the
// same parent tool but live on a side-channel — skip them here so
// the main chain still walks the real follower. The signal blocks
// are emitted separately by `collectSignalCallbacks`.
@@ -370,7 +370,7 @@ export class MessageCollector {
}
/**
* Collect post-task-summary toolless siblings (LOBE-8998) assistants
* Collect post-task-summary toolless siblings () assistants
* tagged with `metadata.signal.type === 'task-completion'`, fired by
* the LLM after CC delivers `system task_notification` for a long-
* running tool (Monitor, etc.). Each one belongs inside the same
@@ -495,7 +495,7 @@ export class MessageCollector {
// Pick the next main-chain assistant under this tool. Mirror the
// skip rule used by `collectAssistantGroupMessages`: signal-tagged
// toolless siblings (Monitor callbacks etc., LOBE-8998) share the
// toolless siblings (Monitor callbacks etc., ) share the
// parent tool but live on a side-channel — if they appear before
// the real follower, blindly taking children[0] would end the
// walk on a callback node and truncate the AssistantGroup tail.
@@ -429,9 +429,9 @@ describe('ContextTreeBuilder', () => {
});
// ────────────────────────────────────────────────────
// LOBE-8998: AssistantGroupNode embeds SignalCallbacksNode children
// AssistantGroupNode embeds SignalCallbacksNode children
// ────────────────────────────────────────────────────
describe('AssistantGroup with signal callbacks (LOBE-8998)', () => {
describe('AssistantGroup with signal callbacks ()', () => {
it('appends SignalCallbacksNode at the end of AssistantGroup children', () => {
const signalMeta = (sequence: number) => ({
signal: {
@@ -707,9 +707,9 @@ describe('FlatListBuilder', () => {
});
// ────────────────────────────────────────────────────
// LOBE-8998: signal callbacks attached on virtual AssistantGroup
// signal callbacks attached on virtual AssistantGroup
// ────────────────────────────────────────────────────
describe('signal callbacks (LOBE-8998)', () => {
describe('signal callbacks ()', () => {
it('attaches signalCallbacks to the virtual group and processes callback messages', () => {
const signalMeta = (sequence: number) =>
({
@@ -206,7 +206,7 @@ describe('MessageCollector', () => {
});
it('skips signal-tagged callbacks when locating the group tail', () => {
// LOBE-8998: when [signal callback, next tool-using assistant]
// when [signal callback, next tool-using assistant]
// both live under the same tool, the tail finder must follow the
// real main-chain assistant — taking children[0] blindly lands on
// the callback (which is a leaf) and truncates the AssistantGroup.
@@ -304,7 +304,7 @@ describe('MessageCollector', () => {
});
// ────────────────────────────────────────────────────
// LOBE-8998: external signal callback collection
// external signal callback collection
// ────────────────────────────────────────────────────
describe('collectSignalCallbacks', () => {
const mkAssistant = (id: string, opts?: Partial<Message>): Message => ({
@@ -522,7 +522,7 @@ describe('MessageCollector', () => {
});
// ────────────────────────────────────────────────────
// LOBE-8998: collectAssistantGroupMessages skips signal-tagged children
// collectAssistantGroupMessages skips signal-tagged children
// ────────────────────────────────────────────────────
describe('collectAssistantGroupMessages with signal callbacks', () => {
it('skips signal-tagged callbacks when picking the main-chain follower', () => {
+1 -1
View File
@@ -51,7 +51,7 @@ If you don't have it, please run \`openssl rand -base64 32\` to create one.
const client = new NeonPool({ connectionString });
// NeonPool runs over WebSocket; transient drops surface as 'error' on the pool.
// Without a listener Node escalates it to uncaughtException — on Vercel this killed
// the entire Lambda 1800+ times in 5 minutes (see LOBE-8704).
// the entire Lambda 1800+ times in 5 minutes (see ).
client.on('error', (err: Error) => {
console.error('[NeonPool] idle client error (swallowed to prevent process crash):', {
code: (err as NodeJS.ErrnoException).code,
@@ -455,7 +455,7 @@ describe('DocumentModel', () => {
});
describe('findBySource', () => {
// Crawl dedupe (LOBE-9384) leans on this finder — same URL + sourceType
// Crawl dedupe () leans on this finder — same URL + sourceType
// must always return the existing row so repeated crawls update in place
// instead of stacking new rows.
it('finds a document by (source, sourceType)', async () => {
+1 -1
View File
@@ -162,7 +162,7 @@ export class DocumentModel {
*
* Crawl-style ingestion flows (`sourceType: 'web'`) use this to dedupe by URL
* so repeated crawls of the same page update the existing row instead of
* appending a fresh one see LOBE-9384.
* appending a fresh one see .
*/
findBySource = async (
source: string,
@@ -389,7 +389,7 @@ describe('CompressionRepository', () => {
});
/**
* Tests for LOBE-2066: MessageGroup aggregation in queryMessage
* Tests for MessageGroup aggregation in queryMessage
*
* These tests verify the expected behavior for querying messages with compression groups,
* specifically focusing on:
+1 -1
View File
@@ -13,7 +13,7 @@ type LazyLoaderFactory = () => Promise<new () => FileLoaderInterface>;
// caused the chunk to back-reference the main bundle for `detectUtf16NoBom`,
// re-evaluating the main entry and re-running `new App()` →
// `protocol.registerSchemesAsPrivileged` after app ready → throw on every
// readFile of .md / .json / .ts / etc. See LOBE-* for the regression.
// readFile of .md / .json / .ts / etc. See for the regression.
const lazyFileLoaders: Record<Exclude<SupportedFileType, 'txt'>, LazyLoaderFactory> = {
doc: async () => {
const { DocLoader } = await import('./doc');
@@ -337,7 +337,7 @@ describe('ClaudeCodeAdapter', () => {
});
});
describe('ToolSearch tool_reference content (LOBE-7369)', () => {
describe('ToolSearch tool_reference content ()', () => {
// CC CLI serializes ToolSearch results as `tool_reference` blocks — no
// `text` or `content` field — which the generic array mapper dropped to
// empty content, leaving the tool message in DB with `content: ''` and
@@ -458,7 +458,7 @@ describe('ClaudeCodeAdapter', () => {
});
});
describe('Read tool image content (LOBE-7338)', () => {
describe('Read tool image content ()', () => {
// CC's `Read` on images returns a `tool_result` whose `content` is an
// `image` block (base64). The generic mapper had no branch for it so
// resultContent collapsed to '' and the UI's StatusIndicator stuck on the
@@ -2242,9 +2242,9 @@ describe('ClaudeCodeAdapter', () => {
});
// ────────────────────────────────────────────────────
// LOBE-8998: external signal detection (Monitor task callbacks)
// external signal detection (Monitor task callbacks)
// ────────────────────────────────────────────────────
describe('external signal detection (LOBE-8998)', () => {
describe('external signal detection ()', () => {
const init = (adapter: ClaudeCodeAdapter) => {
adapter.adapt({
model: 'claude-sonnet-4-6',
@@ -578,7 +578,7 @@ export class ClaudeCodeAdapter implements AgentEventAdapter {
// ─── Private handlers ───
private handleSystem(raw: any): HeterogeneousAgentEvent[] {
// CC's long-running task lifecycle (Monitor, etc., LOBE-8998).
// CC's long-running task lifecycle (Monitor, etc., ).
// `task_started` registers a task that may fire callback turns;
// `task_notification` (terminal) drops it. While a task is alive,
// any new turn without preceding user input is treated as a signal
@@ -976,11 +976,11 @@ export class ClaudeCodeAdapter implements AgentEventAdapter {
// blocks — no `text` / `content` field. Without this branch the
// mapper returns '' for every reference, filter drops them all,
// and the tool message lands in DB with empty content — leaving
// the UI's StatusIndicator stuck on the spinner (LOBE-7369).
// the UI's StatusIndicator stuck on the spinner ().
if (c?.type === 'tool_reference' && c.tool_name) return c.tool_name;
// `Read` on images yields `{type: 'image', source: {...}}` blocks
// with no text. Drop a minimal placeholder so the tool message
// has non-empty content (LOBE-7338); richer image echo is a
// has non-empty content (); richer image echo is a
// follow-up that needs structured ToolResultData.
if (c?.type === 'image') {
const mediaType = c.source?.media_type || 'image';
@@ -1290,7 +1290,7 @@ export class ClaudeCodeAdapter implements AgentEventAdapter {
this.currentMessageId = messageId;
this.stepIndex++;
// Signal-callback detection (LOBE-8998): if this turn opened
// Signal-callback detection (): if this turn opened
// WITHOUT a preceding `user` event AND a long-running task is
// still active, the LLM was re-invoked by the task pushing an
// update — tag the resulting assistant turn accordingly. Otherwise
@@ -1,6 +1,6 @@
/**
* Producer-side MCP server + per-op bridge for Claude Code's AskUserQuestion
* via local HTTP MCP. See `LOBE-8725` for the full design.
* via local HTTP MCP. See `` for the full design.
*
* Used by:
* - Electron main (`HeterogeneousAgentCtr`) local app
@@ -2,7 +2,7 @@
* Producer-side helpers for converting external agent CLI output into the
* unified `AgentStreamEvent` wire shape. Imported by:
* - Electron main (`HeterogeneousAgentCtr`) desktop CC / Codex flow
* - The future `lh hetero exec` CLI sandbox + terminal flow (LOBE-8516)
* - The future `lh hetero exec` CLI sandbox + terminal flow ()
*
* Consumers (renderer executor, server `heteroIngest` handler) never need to
* touch adapters every event reaching them is already an `AgentStreamEvent`.
+1 -1
View File
@@ -53,7 +53,7 @@ export interface StreamStartData {
* `metadata.signal` so MessageCollector can collect signal-tagged
* toolless assistants into a SignalCallbacksNode.
*
* Phase 2 (LOBE-8999) promotes the persisted shape to a dedicated
* Phase 2 () promotes the persisted shape to a dedicated
* `messages.signal` column; the event peer field name stays
* `externalSignal` regardless.
*/
@@ -134,7 +134,7 @@ describe('file operations', () => {
it('should truncate single very long lines to per-line cap', async () => {
const filePath = path.join(tmpDir, 'long-line.txt');
// 27KB single line of base64-like text — the LOBE-8703 scenario.
// 27KB single line of base64-like text — the scenario.
await writeFile(filePath, 'A'.repeat(27_000));
const result = await readLocalFile({ path: filePath });
@@ -283,7 +283,7 @@ describe('anthropicHelpers', () => {
type: 'function',
function: {
name: 'search',
// LOBE-7761 Qwen shape — upstream sanitize should catch this, but
// Qwen shape — upstream sanitize should catch this, but
// if it doesn't we want noise in the logs rather than a silent drop.
arguments: '{, "query": "anthropic"}',
},
@@ -342,7 +342,7 @@ describe('anthropicHelpers', () => {
});
it('recovers tool_call input from element[0] when arguments parse to a multi-element array', async () => {
// LOBE-8201 — model emitted long writeLocalFile args containing many
// — model emitted long writeLocalFile args containing many
// unescaped quotes, which JSON.parse re-segmented into a top-level array.
// element[0] usually still carries the first legit key (e.g. `content`),
// so prefer partial recovery over total loss.
@@ -289,7 +289,7 @@ describe('google contextBuilders', () => {
});
it('recovers functionCall.args from element[0] when arguments parse to an array', async () => {
// LOBE-8201 — same defense as Anthropic: prefer partial recovery from
// — same defense as Anthropic: prefer partial recovery from
// element[0] over total loss when malformed JSON parses to an array.
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const message = {
@@ -252,7 +252,7 @@ export const buildGoogleMessages = async (messages: OpenAIChatMessage[]): Promis
// This handles cross-provider scenarios (e.g., OpenAI → Gemini switch) where
// historical tool_calls lack thoughtSignature, as well as multi-turn Gemini
// conversations where earlier turns may have lost their signatures.
// @see https://linear.app/lobehub/issue/LOBE-8662
// @see https://linear.app/lobehub/issue/
for (const content of filteredContents) {
if (content.role === 'model' && content.parts) {
for (const part of content.parts) {
@@ -275,7 +275,7 @@ export const buildGoogleMessages = async (messages: OpenAIChatMessage[]): Promis
* schema may place `enum` on non-STRING types (e.g. number, boolean)
* or `required` on non-OBJECT types.
*
* @see https://linear.app/lobehub/issue/LOBE-8661
* @see https://linear.app/lobehub/issue/
*/
export const sanitizeGeminiSchema = (schema: any): any => {
if (!schema || typeof schema !== 'object') return schema;
@@ -151,7 +151,7 @@ export interface OpenAICompatibleFactoryOptions<T extends Record<string, any> =
* provided model list before dispatching to upstream. If the estimated
* prompt tokens strictly exceed the model's context window, the
* request is aborted with a structured `ExceededContextWindow` error
* see LOBE-8974.
* see .
*
* This is for providers like NVIDIA / DeepSeek where the harness does
* not cap `max_tokens` itself but we still want to fail fast on doomed
@@ -485,7 +485,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
// Pre-flight: abort doomed requests before invoking handlePayload so
// providers don't waste a round-trip to upstream just to get a 400.
// See LOBE-8974.
// See .
if (chatCompletion?.contextPreFlight) {
const { models: preFlightModels, ...preFlightOptions } = chatCompletion.contextPreFlight;
assertContextWithinWindow(processedPayload, preFlightModels, preFlightOptions);
@@ -1058,7 +1058,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
// Pre-flight context-window failures get a structured payload so the
// UI can offer fork / switch-model affordances instead of surfacing a
// raw provider 400. See LOBE-8974.
// raw provider 400. See .
if (error instanceof ContextExceededPreFlightError) {
log('pre-flight context exceeded: %s', error.message);
return AgentRuntimeError.chat({
@@ -163,7 +163,7 @@ describe('parseToolCalls', () => {
]);
});
// LOBE-8199: NVIDIA NIM (z-ai/glm5, qwen3.5-MoE) and some proxies open a
// NVIDIA NIM (z-ai/glm5, qwen3.5-MoE) and some proxies open a
// tool_call with function.name=null as a start marker; the real name
// arrives in a subsequent delta.
it('should coerce null function.name on the first delta and patch it in from a later delta', () => {
@@ -9,7 +9,7 @@ import { MessageToolCallSchema } from '../types';
// carried by a subsequent delta. Passing `null` through the strict
// MessageToolCallSchema throws ZodError mid-stream and kills the entire
// operation. Coerce to '' so parsing succeeds; the merge logic below patches
// the name in once a later delta supplies it. See LOBE-8199.
// the name in once a later delta supplies it. See .
const normalizeChunkForParse = <T extends Omit<MessageToolCallChunk, 'index'>>(chunk: T): T => {
if (chunk.function && chunk.function.name == null) {
return { ...chunk, function: { ...chunk.function, name: '' } };
@@ -56,7 +56,7 @@ export const openAIParams = {
chatCompletion: {
// DeepSeek upstream rejects requests where input alone exceeds the
// model context window with a 400 carrying `max_completion=0` in the
// message. Fail fast before round-tripping. See LOBE-8974.
// message. Fail fast before round-tripping. See .
contextPreFlight: { models: deepseekChatModels },
handlePayload: buildDeepSeekOpenAIPayload,
},
@@ -511,7 +511,7 @@ describe('createGoogleImage', () => {
});
// Regression: nano banana 4K selection used to be silently dropped because
// imageSize was gated on aspectRatio !== 'auto'. See LOBE-9115.
// imageSize was gated on aspectRatio !== 'auto'. See .
it('should pass imageSize when resolution is set even if aspectRatio is auto', async () => {
// Arrange
const realBase64ImageData =
@@ -180,7 +180,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: enum should only be copied for STRING type properties
// enum should only be copied for STRING type properties
it('should strip enum from non-STRING types', () => {
const openAISchema = {
name: 'test',
@@ -207,7 +207,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: enum with empty array should be stripped even for STRING type
// enum with empty array should be stripped even for STRING type
it('should strip empty enum arrays', () => {
const openAISchema = {
name: 'test',
@@ -229,7 +229,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: required should only be copied for OBJECT types
// required should only be copied for OBJECT types
it('should strip required from non-OBJECT types', () => {
const openAISchema = {
name: 'test',
@@ -400,7 +400,7 @@ describe('Google generateObject', () => {
expect(sanitizeGeminiSchema(undefined)).toBeUndefined();
});
// LOBE-8661: nullable string enums should be preserved
// nullable string enums should be preserved
it('should preserve enum on nullable STRING types (type: array with string)', () => {
const schema = {
properties: {
@@ -425,7 +425,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: nullable object required should be preserved
// nullable object required should be preserved
it('should preserve required on nullable OBJECT types (type: array with object)', () => {
const schema = {
properties: {
@@ -452,7 +452,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: should strip enum from nullable non-STRING types
// should strip enum from nullable non-STRING types
it('should strip enum from nullable non-STRING types (type: array without string)', () => {
const schema = {
properties: {
@@ -476,7 +476,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: recurse into definitions/$defs
// recurse into definitions/$defs
it('should sanitize schemas under definitions', () => {
const schema = {
definitions: {
@@ -505,7 +505,7 @@ describe('Google generateObject', () => {
});
});
// LOBE-8661: recurse into $defs
// recurse into $defs
it('should sanitize schemas under $defs', () => {
const schema = {
$defs: {
@@ -1315,7 +1315,7 @@ describe('Google generateObject', () => {
expect(result).toEqual([{ arguments: {}, name: 'simple_function' }]);
});
// LOBE-8661: buildGoogleTool should sanitize schema to strip enum from non-STRING types
// buildGoogleTool should sanitize schema to strip enum from non-STRING types
it('should sanitize enum from non-STRING types in tool parameters', () => {
const tool: any = {
function: {
@@ -1350,7 +1350,7 @@ describe('Google generateObject', () => {
warnSpy.mockRestore();
});
// LOBE-8661: buildGoogleTool should sanitize nested tool parameters
// buildGoogleTool should sanitize nested tool parameters
it('should sanitize nested enum/required in tool parameters', () => {
const tool: any = {
function: {
@@ -1396,7 +1396,7 @@ describe('Google generateObject', () => {
warnSpy.mockRestore();
});
// LOBE-8661: buildGoogleTool should preserve nullable string enum
// buildGoogleTool should preserve nullable string enum
it('should preserve enum on nullable STRING type in tool parameters', () => {
const tool: any = {
function: {
@@ -82,7 +82,7 @@ const convertSchema = (schema: any): any => {
// Only include enum if type is STRING and enum is non-empty.
// Gemini proto: enum is only allowed for STRING type.
// @see https://linear.app/lobehub/issue/LOBE-8661
// @see https://linear.app/lobehub/issue/
if (schema.enum && schema.enum.length > 0 && isStringType(schema.type)) {
converted.enum = schema.enum;
}
@@ -100,7 +100,7 @@ const convertSchema = (schema: any): any => {
// Only include required if type is OBJECT and required is non-empty.
// Gemini proto: required is only allowed for OBJECT type.
// @see https://linear.app/lobehub/issue/LOBE-8661
// @see https://linear.app/lobehub/issue/
if (schema.required && schema.required.length > 0 && isObjectType(schema.type)) {
converted.required = schema.required;
}
@@ -47,7 +47,7 @@ describe('LobeMinimaxAI - handlePayload', () => {
});
it('caps max_tokens when input + tools fill most of the context window', () => {
// Mimic the LOBE-7017 scenario: many large tool definitions.
// Mimic the scenario: many large tool definitions.
// MiniMax-M2.7: contextWindow=204_800, maxOutput=131_072. Need >72k tokens
// of input to push the dynamic cap below maxOutput.
const heavyTool = {
@@ -23,7 +23,7 @@ export const params = {
// exceed the model context window (returns 400 "requested 0 output
// tokens and your prompt contains at least N+1 input tokens"). Fail
// fast so the UI can surface a fork / switch-model affordance instead
// of a raw provider error. See LOBE-8974.
// of a raw provider error. See .
contextPreFlight: { models: nvidiaChatModels },
handlePayload: (payload) => {
const { model, thinking, messages, ...rest } = payload;
@@ -209,7 +209,7 @@ describe('assertContextWithinWindow', () => {
).toThrow(ContextExceededPreFlightError);
});
it('attaches LOBE-8974 structured payload via toPayload()', () => {
it('attaches structured payload via toPayload', () => {
const longContent = 'a'.repeat(20_000);
try {
assertContextWithinWindow(
@@ -233,7 +233,7 @@ describe('assertContextWithinWindow', () => {
});
it('does NOT reject a near-limit prompt that still fits within the window', () => {
// Regression test for LOBE-8974 PR review feedback: the helper was
// Regression test for PR review feedback: the helper was
// previously deducting a 1024 buffer + 1024 minOutputTokens and would
// throw for a 198.5k-token prompt against a 200k-token window even
// though the upstream would accept it. With the corrected threshold,
@@ -34,7 +34,7 @@ export interface ResolveSafeMaxTokensOptions {
* `minOutputTokens` for completion (or already exceed the model's context
* window). Caught by `openaiCompatibleFactory` and surfaced as an
* `ExceededContextWindow` chat error carrying structured diagnostic fields
* see LOBE-8974 for the rationale of failing fast instead of issuing a
* see for the rationale of failing fast instead of issuing a
* doomed upstream request.
*/
export class ContextExceededPreFlightError extends Error {
@@ -161,7 +161,7 @@ export interface AssertContextWithinWindowOptions {
* completion the upstream will pick its own `max_tokens` default once
* the request is dispatched. Rejecting near-limit-but-fitting prompts
* (e.g. 198.5k tokens against a 200k window) would block valid requests
* that the upstream would happily serve. See LOBE-8974 review feedback.
* that the upstream would happily serve. See review feedback.
*/
export const assertContextWithinWindow = (
payload: Pick<ChatStreamPayload, 'messages' | 'model' | 'tools'>,
@@ -180,7 +180,7 @@ export const MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSche
performance: ModelPerformanceSchema.optional(),
reactions: z.array(EmojiReactionSchema).optional(),
scope: z.string().optional(),
// External-signal lineage for Monitor-style callback turns (LOBE-8998).
// External-signal lineage for Monitor-style callback turns ().
signal: MessageSignalSchema.optional(),
subAgentId: z.string().optional(),
toolExecutionTimeMs: z.number().optional(),
@@ -323,7 +323,7 @@ export interface MessageMetadata {
* External-signal lineage for messages produced as reactive replies
* to an out-of-band trigger (Monitor stdout push, webhook callback,
* scheduled tick, ) rather than a fresh user turn. Phase-1 storage
* Phase 2 (LOBE-8999) promotes this to a dedicated `messages.signal`
* Phase 2 () promotes this to a dedicated `messages.signal`
* jsonb column.
*
* Conversation-flow groups signal-tagged TOOLLESS assistants into a
@@ -372,7 +372,7 @@ export interface MessageMetadata {
* Persisted form of an external-signal trigger context stamped on
* messages produced as reactive replies to out-of-band events.
*
* Phase 1 lives under `MessageMetadata.signal`; Phase 2 (LOBE-8999)
* Phase 1 lives under `MessageMetadata.signal`; Phase 2 ()
* promotes to a dedicated `messages.signal` column with the same
* shape (plus `rootSourceId` / `scopeKey` for agent-signal alignment).
*/
+2 -2
View File
@@ -253,7 +253,7 @@ export interface UIChatMessage {
search?: GroundingSearch | null;
sessionId?: string;
/**
* External-signal callback blocks (LOBE-8998). Set on virtual
* External-signal callback blocks (). Set on virtual
* assistantGroup messages built by FlatListBuilder when the chain
* contains toolless assistants triggered by repeated tool_results
* (Monitor stdout push pattern). Rendered as `<SignalCallbacks>`
@@ -268,7 +268,7 @@ export interface UIChatMessage {
*/
targetId?: string | null;
/**
* Post-task summary blocks (LOBE-8998). Set on virtual assistantGroup
* Post-task summary blocks (). Set on virtual assistantGroup
* messages by FlatListBuilder when the chain contains toolless
* assistants tagged with `signal.type === 'task-completion'` the
* final-summary turn the LLM emits after CC delivers
@@ -21,7 +21,7 @@ describe('sanitizeToolCallArguments', () => {
expect(sanitizeToolCallArguments(undefined)).toBe('{}');
});
it('falls back to "{}" on LOBE-7761 shape "{, ..."', () => {
it('falls back to "{}" on shape "{, .."', () => {
// exact shape from the reported NVIDIA/Qwen trace
const input = '{, "description": "Create data models", "language": "python"}';
expect(sanitizeToolCallArguments(input)).toBe('{}');
@@ -7,7 +7,7 @@ import { safeParseJSON, safeParsePartialJSON } from './safeParseJSON';
* Strict providers (e.g. NVIDIA NIM) validate the full message history on
* every request. A single malformed `arguments` string even one produced
* many turns ago causes a 400 on the entire request, terminating the op
* and wasting all accumulated tokens. See LOBE-7761.
* and wasting all accumulated tokens. See .
*
* Behavior:
* - Valid JSON returned as-is (preserves prompt-cache keys).
@@ -50,7 +50,7 @@ describe('htmlToMarkdown', () => {
expect(result.content.length).toBeLessThan(html.length);
}, 20000);
it('should not crash on HTML with invalid CSS selectors (LOBE-6869)', () => {
it('should not crash on HTML with invalid CSS selectors ()', () => {
// Regression: happy-dom throws TypeError on pages with CSS selectors it cannot parse.
// htmlToMarkdown must not propagate this — it should fall back to raw HTML conversion.
const html = `
@@ -67,7 +67,7 @@ describe('htmlToMarkdown', () => {
expect(result.content).toContain('Valid content');
});
it('should not crash on HTML with external stylesheet links (LOBE-6869)', () => {
it('should not crash on HTML with external stylesheet links ()', () => {
// Regression: happy-dom's HTMLLinkElement.#loadStyleSheet can crash on CSS parsing.
// disableCSSFileLoading should prevent this path entirely.
const html = `
+1 -1
View File
@@ -8,7 +8,7 @@
* - useSearchParams -> @/app/[variants]/(main)/hooks/useSearchParams
* - useRouter -> @/app/[variants]/(main)/hooks/useRouter
*
* @see RFC 147: LOBE-2850 - Phase 3
* @see RFC 147: - Phase 3
*/
import { readFile, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
@@ -181,7 +181,7 @@ const GroupMessage = memo<GroupMessageProps>(
the same "agent reply" block. The ChatItem body gap (16px) would
otherwise stretch them apart and the natural narrative initial
reply callbacks summary reads as three disconnected
sections (LOBE-8998).
sections ().
*/}
<Flexbox gap={4}>
{children && children.length > 0 && (
@@ -162,7 +162,7 @@ const findBlockById = (
}
// Post-task summary blocks live in a separate field on virtual
// assistantGroup messages so they render AFTER `<SignalCallbacks>`
// (LOBE-8998). Same lookup contract as `children` — the renderer
// (). Same lookup contract as `children` — the renderer
// identifies blocks by id regardless of which slot they came from.
if ((message as { taskCompletions?: AssistantContentBlock[] }).taskCompletions) {
const block = (
@@ -485,7 +485,7 @@ describe('Generation Actions', () => {
expect(mockCompleteOperation).toHaveBeenCalledWith('test-op-id');
});
it('should delete message BEFORE regeneration to prevent message not found issue (LOBE-2533)', async () => {
it('should delete message BEFORE regeneration to prevent message not found issue ()', async () => {
// This test verifies the fix:
// When "delete and regenerate" is called, if regeneration happens first,
// it switches to a new branch, causing the original message to no longer
@@ -71,7 +71,7 @@ export default defineFixtures({
content: 'Issue thread describing the /devtools route rollout.',
engines: ['bing'],
title: 'Builtin render devtools issue',
url: 'https://linear.example.com/issue/LOBE-8114',
url: 'https://linear.example.com/issue/',
},
],
},
@@ -61,7 +61,7 @@ describe('UserUpdater', () => {
rerender(<UserUpdater />);
// Regression: interests / firstName / latestName must NOT be wiped by the
// session sync. (LOBE-8597 — wiped interests caused the home daily-brief
// session sync. (— wiped interests caused the home daily-brief
// recommendation SWR key to reset and refetch with empty interestKeys.)
expect(useUserStore.getState().user?.interests).toEqual(['内容创作', '编程']);
expect(useUserStore.getState().user?.firstName).toBe('A');
@@ -29,7 +29,7 @@ const UserUpdater = memo(() => {
// than replace it — fields like `interests`, `firstName`, `latestName` are
// populated by `useInitUserState` (one-shot SWR) and would otherwise be
// wiped on every focus, breaking downstream selectors (e.g. the daily-brief
// recommendation SWR key resets to empty interests and refetches). LOBE-8597.
// recommendation SWR key resets to empty interests and refetches). .
//
// Guard the merge by user id: if the session switches to a different
// account (e.g. another tab signed in as a different user, focus refetch
@@ -73,7 +73,7 @@ const styles = createStaticStyles(({ css }) => ({
// Module-scoped so a click on any topic cancels a pending click on another.
// Per-item refs can't do that, which lets rapid clicks across items all
// fire — each racing to write activeTopicId (see LOBE-7785).
// fire — each racing to write activeTopicId (see ).
let pendingSingleClickTimer: ReturnType<typeof setTimeout> | null = null;
const cancelPendingSingleClick = () => {
@@ -59,7 +59,7 @@ const styles = createStaticStyles(({ css }) => ({
// Module-scoped so a click on any topic cancels a pending click on another.
// Per-item refs can't do that, which lets rapid clicks across items all
// fire — each racing to write activeTopicId (see LOBE-7785).
// fire — each racing to write activeTopicId (see ).
let pendingSingleClickTimer: ReturnType<typeof setTimeout> | null = null;
const cancelPendingSingleClick = () => {
@@ -17,9 +17,9 @@ describe('stripMarkdownLinks', () => {
it('replaces multiple markdown links in one string', () => {
expect(
stripMarkdownLinks(
'帮我整理 [2.1 改版](/agent/inbox/tpc_a) 和 [LOBE-8516](/task/T-1) 的下一步...',
'帮我整理 [2.1 改版](/agent/inbox/tpc_a) 和 [发布计划](/task/T-1) 的下一步...',
),
).toBe('帮我整理 2.1 改版 和 LOBE-8516 的下一步...');
).toBe('帮我整理 2.1 改版 和 发布计划 的下一步...');
});
it('preserves the trailing ellipsis used as a typing indicator', () => {
@@ -57,7 +57,7 @@ interface AutoLinkPattern {
}
// Bare references the model might emit without the markdown link form.
// Used as a fallback so e.g. plain "LOBE-8516" inside the welcome still
// Used as a fallback so e.g. plain "" inside the welcome still
// becomes clickable.
const AUTO_LINK_PATTERNS: AutoLinkPattern[] = [
{
@@ -9,7 +9,7 @@ const log = debug('lobe-server:agent:finalize-abandoned');
/**
* Reverse-trigger finalization for an operation whose Vercel function was
* killed mid-flight. Called by the agent-gateway DO inactivity watchdog when
* an op has gone silent past the threshold see LOBE-8533.
* an op has gone silent past the threshold see .
*
* Body: `{ operationId: string, reason: string }`
*
@@ -364,7 +364,7 @@ export const createRuntimeExecutors = (
// Get parentId from payload (parentId or parentMessageId depending on payload type)
const parentId = llmPayload.parentId || (llmPayload as any).parentMessageId;
// Parent existence preflight (LOBE-7158 / LOBE-7154):
// Parent existence preflight ():
// If the parent was deleted concurrently (e.g. user deleted topic mid-run),
// assistant message creation below would hit a PG FK violation AFTER we've
// already done the LLM call and spent tokens. Check first — fail fast,
@@ -924,7 +924,7 @@ export const createRuntimeExecutors = (
// self-reflection signal the model needs to fix its own output.
// Sanitization happens later, only at the persist boundaries
// (DB write and state.messages push) to protect strict providers
// replaying history. See LOBE-7761.
// replaying history. See .
const payload = resolvedCalls.map((p) => ({
...p,
executor: resolved.executorMap?.[p.identifier],
@@ -1062,7 +1062,7 @@ export const createRuntimeExecutors = (
// Sanitize tool_call `arguments` before persisting to DB so malformed
// JSON (e.g. Qwen emitting `{, ...}`) can't poison future context
// builds and 400 strict providers like NVIDIA NIM. See LOBE-7761.
// builds and 400 strict providers like NVIDIA NIM. See .
const persistedTools =
toolsCalling.length > 0
? toolsCalling.map((t) => ({
@@ -1790,7 +1790,7 @@ export const createRuntimeExecutors = (
// Finally persist to database. In resumption mode (skipCreateToolMessage),
// the pending tool message already exists from request_human_approve, so
// we update it in-place rather than inserting a new row — inserting would
// either duplicate the tool_call_id or violate parent_id FK (LOBE-7154).
// either duplicate the tool_call_id or violate parent_id FK ().
let toolMessageId: string | undefined;
try {
if (payload.skipCreateToolMessage) {
@@ -1954,7 +1954,7 @@ export const createRuntimeExecutors = (
} catch (error) {
// Persist-level failures (parent FK violation etc.) must propagate so
// the step fails — otherwise the swallow-and-continue path keeps
// running the agent on a broken conversation chain. See LOBE-7158.
// running the agent on a broken conversation chain. See .
if (isPersistFatal(error)) throw error;
if (ctx.hookDispatcher) {
@@ -2294,7 +2294,7 @@ export const createRuntimeExecutors = (
// Normalize BEFORE publishing — clients treat `error` stream
// events as terminal and surface `event.data.error` directly, so
// a raw SQL error here would leak driver text to the user before
// the ConversationParentMissing throw is consumed. See LOBE-7158.
// the ConversationParentMissing throw is consumed. See .
const fatal = isParentMessageMissingError(error)
? createConversationParentMissingError(parentMessageId, error)
: error instanceof Error
@@ -2900,7 +2900,7 @@ export const createRuntimeExecutors = (
// newState.messages. When the approval resumes, the `call_tool`
// executor (skip-create branch) appends the resolved tool message
// to state.messages itself. Pushing a placeholder here produced
// two entries for the same tool_call_id — see LOBE-7151 review P2.
// two entries for the same tool_call_id — see review P2.
log(
'[%s:%d] Created pending tool message %s for %s',
@@ -3017,7 +3017,7 @@ export const createRuntimeExecutors = (
error,
);
// Normalize BEFORE publishing so clients surface the typed business
// error instead of the raw driver text (see LOBE-7158 review).
// error instead of the raw driver text (see review).
const fatal = isParentMessageMissingError(error)
? createConversationParentMissingError(parentMessageId, error)
: error instanceof Error
@@ -250,7 +250,7 @@ describe('RuntimeExecutors', () => {
);
});
it('should throw ConversationParentMissing if parent preflight misses (LOBE-7158)', async () => {
it('should throw ConversationParentMissing if parent preflight misses ()', async () => {
// parent existence preflight — if the parent row was deleted between
// operation kickoff and call_llm, fail fast before spending LLM tokens
// on a chain that would hit a FK violation anyway.
@@ -1521,8 +1521,8 @@ describe('RuntimeExecutors', () => {
expect(result.nextContext!.phase).toBe('tool_result');
});
it('should re-throw when messageModel.create fails (LOBE-7158: no silent swallow)', async () => {
// Before LOBE-7158 we silently swallowed this error and returned
it('should re-throw when messageModel.create fails (no silent swallow)', async () => {
// Before we silently swallowed this error and returned
// `parentMessageId: undefined`, which let the operation continue into
// the next step and re-hit the same failure without context. The fix
// requires the executor to propagate so the whole step fails.
@@ -1548,7 +1548,7 @@ describe('RuntimeExecutors', () => {
await expect(executors.call_tool!(instruction, state)).rejects.toThrow('Database error');
});
it('should throw ConversationParentMissing on a parent_id FK violation (LOBE-7158)', async () => {
it('should throw ConversationParentMissing on a parent_id FK violation ()', async () => {
// Simulate the drizzle + postgres-js wrapped error shape.
const fkError: any = new Error(
'Failed query: insert into "messages" ... violates foreign key constraint',
@@ -2330,8 +2330,8 @@ describe('RuntimeExecutors', () => {
expect(result.nextContext!.phase).toBe('tools_batch_result');
});
it('should propagate persist failures instead of silently falling back (LOBE-7158)', async () => {
// Before LOBE-7158 we fell back to the original parentMessageId here,
it('should propagate persist failures instead of silently falling back ()', async () => {
// Before we fell back to the original parentMessageId here,
// which was itself the deleted parent that caused the failure — so the
// next step would hit the same FK violation with no context. The fix
// requires the batch to short-circuit on persist failure.
@@ -2361,7 +2361,7 @@ describe('RuntimeExecutors', () => {
);
});
it('should throw ConversationParentMissing on a parent_id FK violation (LOBE-7158)', async () => {
it('should throw ConversationParentMissing on a parent_id FK violation ()', async () => {
const fkError: any = new Error(
'Failed query: insert into "messages" ... violates foreign key constraint',
);
@@ -2447,8 +2447,8 @@ describe('RuntimeExecutors', () => {
expect(result.nextContext!.phase).toBe('tools_batch_result');
});
it('should fail the batch if tool message creation fails for any tool (LOBE-7158)', async () => {
// Before LOBE-7158 we swallowed per-tool persist failures and kept
it('should fail the batch if tool message creation fails for any tool ()', async () => {
// Before we swallowed per-tool persist failures and kept
// going. The fix requires the batch to abort — a FK violation on one
// tool means every concurrent tool has the same doomed parent.
mockMessageModel.create
@@ -2614,7 +2614,7 @@ describe('RuntimeExecutors', () => {
);
});
// LOBE-5143: After DB refresh, state.messages stores raw UIChatMessage[]
// After DB refresh, state.messages stores raw UIChatMessage[]
// and call_llm re-injects context via serverMessagesEngine on each invocation
it('should store raw UIChatMessage[] from DB after refresh (context re-injected by call_llm)', async () => {
// DB only stores raw user/assistant/tool messages, NOT MessagesEngine injections
@@ -3229,8 +3229,8 @@ describe('RuntimeExecutors', () => {
});
});
it('should propagate persist failures instead of silently swallowing (LOBE-7158)', async () => {
// The pre-LOBE-7158 behavior logged the error and kept walking the
it('should propagate persist failures instead of silently swallowing ()', async () => {
// The pre-behavior logged the error and kept walking the
// aborted-tool list. That left a half-persisted state and hid the real
// cause from ops. Now we fail fast.
mockMessageModel.create
@@ -17,7 +17,7 @@ import { formatPgError, pgErrorType, unwrapPgError } from './pgError';
* 3. Anything else falls back to `error.message` + `error.name`, or
* `"Unknown error"` when the value isn't even an Error.
*
* See LOBE-7158 / LOBE-7334 for the motivation: Drizzle wraps driver errors
* See for the motivation: Drizzle wraps driver errors
* as `"Failed query: insert into ..."` and buries the real diagnostic fields
* under `.cause`, which left the agent-gateway dashboard unable to bucket
* DB failures by SQLSTATE.
@@ -26,7 +26,7 @@ export const PERSIST_FATAL_MARKER = 'persistFatal';
* Detect whether an error returned by `messageModel.create` is a `parent_id`
* FK violation meaning the parent message no longer exists. Most commonly
* caused by the parent being deleted concurrently with agent execution
* (see LOBE-7154 / LOBE-7158).
* (see ).
*
* `drizzle` + `postgres-js` wrap the raw PG error as `.cause`, so the check
* looks at both the top level and the cause.
+1 -1
View File
@@ -6,7 +6,7 @@
* (e.g. transaction runners), so walking a few layers is necessary. Without
* unwrapping, the runtime only sees the generic `"Failed query: insert into ..."`
* wrapper message, which strips every diagnostic field the Agent Harness
* dashboard needs to classify the failure (see LOBE-7158 / LOBE-7334).
* dashboard needs to classify the failure (see ).
*
* @see https://www.postgresql.org/docs/current/errcodes-appendix.html
*/
@@ -141,7 +141,7 @@ describe('createServerToolsEngine', () => {
expect(availablePlugins).toContain('additional-tool');
});
it('drops device manifests from every source when excludeIdentifiers is set (LOBE-8768)', () => {
it('drops device manifests from every source when excludeIdentifiers is set ()', () => {
// Simulate a plugin + an additional manifest that claim the device
// identifiers. The pre-merge `buildAllowedBuiltinTools` filter only
// touches builtins; the post-merge `excludeIdentifiers` wall is what
@@ -568,7 +568,7 @@ describe('createServerAgentToolsEngine', () => {
});
it('should enable RemoteDevice in bot conversations when caller is trusted (canUseDevice=true)', () => {
// The `!isBotConversation` clause was dropped in LOBE-8715 — the
// The `!isBotConversation` clause was dropped in — the
// confused-deputy concern that motivated it is now handled at a
// stricter layer (`canUseDevice` from `resolveDeviceAccessPolicy`).
// For owner / first-party turns the proxy is legitimately useful in
@@ -780,7 +780,7 @@ describe('createServerAgentToolsEngine', () => {
});
});
describe('canUseDevice gate (LOBE-8715 device access policy)', () => {
describe('canUseDevice gate (device access policy)', () => {
it('drops LocalSystem when canUseDevice is false even on a desktop caller', () => {
// External bot sender impersonating a desktop session must not get
// local-system back through Phase 6.4 dispatch.
@@ -77,14 +77,14 @@ export const createServerToolsEngine = (
// Get builtin tool manifests from the (possibly pre-filtered) list. The
// filter is one half of the hard wall keeping device tools out of an
// external bot sender's manifestSchemas — see `buildAllowedBuiltinTools`
// and LOBE-8768. The enableChecker rules below are defense-in-depth
// and . The enableChecker rules below are defense-in-depth
// because `allowExplicitActivation` lets activator-driven activation
// bypass them.
const builtinManifests = builtinToolsOverride.map((tool) => tool.manifest as LobeToolManifest);
// Combine all manifests, then drop anything whose identifier the caller
// has explicitly forbidden for this turn. The post-merge filter closes
// the second half of the LOBE-8768 wall: an installed plugin or a
// the second half of the wall: an installed plugin or a
// Skill/Klavis manifest claiming `lobe-remote-device` would otherwise
// slip through `buildAllowedBuiltinTools` (which only touches the
// builtin source).
@@ -242,7 +242,7 @@ export const createServerAgentToolsEngine = (
// Physically drop device-tool manifests for turns whose access policy
// denies them. Without this filter, `lobe-activator`'s explicit
// activation could resolve the manifest and bypass the rule-layer
// gates below (LOBE-8768).
// gates below ().
builtinTools: buildAllowedBuiltinTools({ canUseDevice, disableLocalSystem }),
// Add default tools based on configuration
defaultToolIds: isChatMode ? chatModeAllowedToolIds : defaultToolIds,
@@ -27,7 +27,7 @@ export interface ServerAgentToolsEngineConfig {
* `manifestSchemas`. Defaults to the full `builtinTools` array from
* `@lobechat/builtin-tools`. Callers gating device tools per-turn pass
* `buildAllowedBuiltinTools(...)` here so an external bot sender cannot
* resolve `lobe-remote-device` via the activator (LOBE-8768).
* resolve `lobe-remote-device` via the activator ().
*/
builtinTools?: readonly LobeBuiltinTool[];
/** Default tool IDs that will always be added */
@@ -39,7 +39,7 @@ export interface ServerAgentToolsEngineConfig {
* builtin, and additional manifests. Filtering builtins alone is not
* enough: an installed plugin or a Skill/Klavis manifest can declare
* `identifier: 'lobe-remote-device'` and slip past `buildAllowedBuiltinTools`.
* This is the final post-merge wall referenced in LOBE-8768.
* This is the final post-merge wall referenced in .
*/
excludeIdentifiers?: ReadonlySet<string>;
}
@@ -372,12 +372,12 @@ describe('Multi-Round Tool Execution', () => {
expect(finalState.status).toBe('done');
// After LOBE-5143: state.messages stores parse()-processed UIChatMessage[]
// After state.messages stores parse-processed UIChatMessage[]
// Tool messages are wrapped in virtual 'assistantGroup' nodes by conversation-flow parse()
// The chain detector combines consecutive assistant+tool rounds into a single assistantGroup
expect(finalState.messages.length).toBeGreaterThan(0);
// After LOBE-5143: state.messages stores parse()-processed UIChatMessage[]
// After state.messages stores parse-processed UIChatMessage[]
// Tool messages are wrapped in virtual 'assistantGroup' nodes by conversation-flow parse()
// The chain detector combines consecutive assistant+tool rounds into a single assistantGroup
expect(finalState.messages.length).toBeGreaterThan(0);
+1 -1
View File
@@ -385,7 +385,7 @@ const AgentStreamEventSchema = z.object({
/**
* Schema for `aiAgent.heteroIngest` accepts a batch of producer-side
* `AgentStreamEvent`s from `lh hetero exec`. `topicId` is required (operationId
* topic reverse-lookup is unreliable per LOBE-8516 design decision).
* topic reverse-lookup is unreliable per design decision).
*/
const HeteroIngestSchema = z.object({
agentType: z.enum(['claude-code', 'codex']),
+1 -1
View File
@@ -541,7 +541,7 @@ export const messengerRouter = router({
/**
* Best-effort confirmation back to the IM platform after a successful link.
* Slack needs `tenantId` to resolve the right per-workspace bot token; Telegram
* is a global bot and ignores it. PR2.4 (LOBE-8453) rewires the Slack binder
* is a global bot and ignores it. PR2.4 () rewires the Slack binder
* to receive `InstallationCredentials` via the router's installation store
* until then this entry point falls back to no-op for Slack (binder.createClient
* returns null in PR1's intermediate state).
@@ -875,7 +875,7 @@ export class AgentRuntimeService {
// Finalize tracing snapshot. The error catch below uses the same
// recorder so propagated failures still write the canonical S3
// snapshot instead of orphaning the partial (LOBE-8533).
// snapshot instead of orphaning the partial ().
await this.traceRecorder.finalize(operationId, {
appendEventsToLastStep: completionSignalEvents,
completionReason: reason,
@@ -968,7 +968,7 @@ export class AgentRuntimeService {
// RuntimeExecutors) leave the partial as an orphan at
// `_partial/<op>.json.zst` and the canonical
// `agent-traces/<agentId>/<topicId>/<op>.json.zst` returns 404 — see
// LOBE-8533.
// .
//
// `failedStep` synthesizes a step record for the failure because the
// real step never reached `appendStepToPartial` — it threw before the
@@ -163,7 +163,7 @@ export class HumanInterventionHandler {
* tools to be resolved or (b) resume LLM once this is the last one.
* Returning a `phase: 'user_input'` nextContext while pendingToolsCalling
* is non-empty would cause executeStep to run runtime.step immediately,
* resuming the LLM with an unresolved batch see LOBE-7151 review P1.
* resuming the LLM with an unresolved batch see review P1.
*/
private rejectAndContinue(
state: any,
@@ -40,7 +40,7 @@ export interface FinalizeParams {
* Synthetic step record for the error path. The real failing step never
* reached `appendStep` because the executor threw before the partial push,
* so the catch caller passes this to keep step counts aligned with the
* assistant message that triggered the call. See LOBE-8533.
* assistant message that triggered the call. See .
*/
failedStep?: { startedAt: number; stepIndex: number };
state: any;
@@ -198,7 +198,7 @@ describe('HumanInterventionHandler.process', () => {
it('stays paused (nextContext=undefined) when other tools are still pending', async () => {
// makeState() has 2 pending; pluginQuery resolves tool-call-1 → 1 left.
// Returning a `phase: 'user_input'` context here would resume the LLM
// before the remaining pending tools are decided (LOBE-7151 review P1).
// before the remaining pending tools are decided (review P1).
const state = makeState();
mockDBPluginQuery.mockResolvedValueOnce({ toolCallId: 'tool-call-1' });
@@ -597,7 +597,7 @@ describe('AgentRuntimeService.executeStep - Redis failure in error handler', ()
});
});
describe('AgentRuntimeService.executeStep - error-path snapshot finalize (LOBE-8533)', () => {
describe('AgentRuntimeService.executeStep - error-path snapshot finalize ()', () => {
it('finalizes a snapshot with completionReason=error and a synthetic failed step when the executor throws', async () => {
const snapshotStore = {
get: vi.fn(),
@@ -263,7 +263,7 @@ describe('AiAgentService.execAgent - device auto-activation', () => {
});
describe('Web UI scenario (no botContext/discordContext)', () => {
// LOBE-9378: regular chat used to leave activeDeviceId undefined when no
// regular chat used to leave activeDeviceId undefined when no
// device was bound, which caused the local-system system prompt's
// {{workingDirectory}} / {{hostname}} placeholders to reach the LLM as
// literals. The model would then waste the first N steps groping for cwd.
@@ -428,7 +428,7 @@ describe('AiAgentService.execAgent - device auto-activation', () => {
// the runtime bound device. Setup: topic.metadata says device-002, but the
// only online device is device-001. If the topic metadata were reused as
// boundDeviceId, activeDeviceId would be undefined (device-002 is offline).
// After LOBE-9378 auto-activate, we instead pick the most-recent online
// After auto-activate, we instead pick the most-recent online
// device (device-001) — proving the topic's stale metadata wasn't honored.
it('should not reuse topic boundDeviceId when no explicit deviceId is provided', async () => {
mockDeviceProxy.isConfigured = true;
@@ -531,7 +531,7 @@ describe('AiAgentService.execAgent - device auto-activation', () => {
// different mock shape. Topic metadata stores device-002, but only
// device-001 is online; if topic metadata leaked into boundDeviceId,
// activeDeviceId would be undefined (since device-002 is offline). The
// post-LOBE-9378 auto-activate picks device-001 instead, confirming the
// post-auto-activate picks device-001 instead, confirming the
// stale topic.metadata.boundDeviceId path is dead.
it('should not reuse topic metadata bound device when no deviceId is supplied', async () => {
mockDeviceProxy.isConfigured = true;
@@ -148,7 +148,7 @@ const createBaseAgentConfig = (overrides: Record<string, any> = {}) => ({
...overrides,
});
describe('AiAgentService.execAgent - device tool pipeline (LOBE-5636)', () => {
describe('AiAgentService.execAgent - device tool pipeline ()', () => {
let service: AiAgentService;
const mockDb = {} as any;
const userId = 'test-user-id';
@@ -38,7 +38,7 @@ export type DeviceAccessReason =
*
* 1. The platform's chat-adapter encodes every inbound thread as 1:1
* (no group / channel handling), so the "external user @s the bot in
* a group" attack surface from LOBE-8715 doesn't exist on this
* a group" attack surface from doesn't exist on this
* platform today.
* 2. The platform's settings schema has no `userId` field, so an owner
* ID can't be configured even if we wanted to gate on it.
@@ -34,7 +34,7 @@ describe('deviceToolRegistry', () => {
);
});
it('strips BOTH device tools when canUseDevice=false (closes LOBE-8768 B1)', () => {
it('strips BOTH device tools when canUseDevice=false (closes B1)', () => {
const result = buildAllowedBuiltinTools({
canUseDevice: false,
disableLocalSystem: false,
@@ -14,7 +14,7 @@
* physical filter on `builtinTools` before they reach the
* `ToolsEngine.manifestSchemas` or the activator-discovery
* `toolManifestMap`. Routing every builtin discovery through this
* helper closes the activator bypass documented in LOBE-8768 (an
* helper closes the activator bypass documented in (an
* external sender could otherwise `activateTools(["lobe-remote-device"])`
* because the manifest was still resolvable in the engine even when
* the rule-layer gate denied it).
@@ -59,7 +59,7 @@ export interface AllowedBuiltinToolsParams {
*
* Defense-in-depth note: the rule-layer gates in `AgentToolsEngine` are
* kept as a secondary line of defense, but they are bypassed by
* `allowExplicitActivation` (LOBE-8768 B2), so the **physical** filter
* `allowExplicitActivation` (B2), so the **physical** filter
* here is the only reliable enforcement point.
*/
export const buildAllowedBuiltinTools = (params: AllowedBuiltinToolsParams) => {
+4 -4
View File
@@ -1176,7 +1176,7 @@ export class AiAgentService {
// bypassing the engine's enabledToolIds exclusion. Skipping the
// assignment here closes that bypass at the source.
//
// Resolution order (LOBE-9378):
// Resolution order ():
// 1. boundDeviceId (topic-bound > agent-bound): use if online; if offline,
// respect the explicit choice and stay unrouted — don't silently fall
// back to a different device, that would surprise the user.
@@ -1185,7 +1185,7 @@ export class AiAgentService {
// recency / first-online would be a guess that could route tool calls
// to the wrong machine. This applies uniformly to regular chat and
// IM/Bot — the previous "regular-chat does nothing" path was the bug
// behind LOBE-9378 (the local-system system prompt's
// behind (the local-system system prompt's
// `{{workingDirectory}}` reached the LLM as a literal, wasting the
// first N steps groping for cwd).
activeDeviceId = !canUseDevice
@@ -1253,7 +1253,7 @@ export class AiAgentService {
// installed plugin, a LobeHub Skill, or a Klavis manifest declaring
// `identifier: 'lobe-remote-device'` would otherwise reach the
// activator-discovery map and let an external bot sender enable it
// (LOBE-8768). Centralising the check at the ingest layer means
// (). Centralising the check at the ingest layer means
// every future manifest source automatically inherits the wall.
const isManifestIngestAllowed = (identifier: string): boolean =>
canUseDevice || !isDeviceToolIdentifier(identifier);
@@ -1419,7 +1419,7 @@ export class AiAgentService {
// 9.4. Fetch device system info for placeholder variable replacement.
//
// Decoupled from activeDeviceId routing (LOBE-9378): pulled into a helper
// Decoupled from activeDeviceId routing (): pulled into a helper
// so the device whose info populates the template (`{{hostname}}`,
// `{{workingDirectory}}`, etc.) is a separate decision from the device
// that tool calls route to. Today they're aligned — but future policy
+8 -8
View File
@@ -542,7 +542,7 @@ export class BotMessageRouter {
const groupSettings: GroupSettings = extractGroupSettings(info.settings);
const userAllowlist: UserAllowlist = extractUserAllowlist(info.settings);
/**
* Operator-configured keywords (LOBE-8891). When non-empty, a non-@mention
* Operator-configured keywords (). When non-empty, a non-@mention
* non-command message in a subscribed group thread still wakes the bot if
* its text contains any keyword case-insensitive, word-boundary aware
* (see `messageMatchesWatchKeyword`). Empty list keeps the legacy
@@ -887,7 +887,7 @@ export class BotMessageRouter {
return false;
};
// LOBE-8981: single-user thread relaxation. A subscribed channel thread
// single-user thread relaxation. A subscribed channel thread
// with only one human follower is effectively a private 1:1 with the
// bot, so we drop the @mention requirement for follow-ups. Once a
// second human posts we revert to mention-only mode and announce the
@@ -1044,7 +1044,7 @@ export class BotMessageRouter {
// type `/new` directly without mentioning the bot), but they are NOT exempt
// from the access gates below.
//
// LOBE-8981: a subscribed channel thread with only one human follower
// a subscribed channel thread with only one human follower
// is functionally a private 1:1 with the bot, so the @mention
// requirement is dropped while `count <= 1`. Tracked + counted here
// regardless of which exemption ultimately fires so the
@@ -1057,7 +1057,7 @@ export class BotMessageRouter {
context?.skipped?.some((m) => m.isMention === true) === true ||
isSingleHumanThread;
const isCommand = looksLikeCommand(message.text);
// LOBE-8891: operator-configured keyword match also wakes the bot in a
// operator-configured keyword match also wakes the bot in a
// subscribed group thread. Skipped (debounced) siblings are inspected
// too so a keyword queued behind a non-trigger still fires — same
// pattern as the mention check above.
@@ -1075,7 +1075,7 @@ export class BotMessageRouter {
message.author.userName,
thread.id,
);
// LOBE-8981: first skip in this thread → tell participants the bot
// first skip in this thread → tell participants the bot
// is now mention-only so newcomers don't think it broke. Dedupe by
// thread id so we never announce more than once.
if (!thread.isDM && humanCount >= 2) {
@@ -1135,7 +1135,7 @@ export class BotMessageRouter {
}
let merged = BotMessageRouter.mergeSkippedMessages(message, context);
// LOBE-8891: when a keyword (and not a mention / DM / command) is what
// when a keyword (and not a mention / DM / command) is what
// wakes the bot, prepend the matched entries' operator-authored
// instructions to the user message so the agent gets a directive
// rather than only the raw chatter. Mentions, DMs, and commands are
@@ -1236,7 +1236,7 @@ export class BotMessageRouter {
// regex listener can serve both:
//
// • DM path: every message in a DM thread (when DM policy allows it).
// • Channel keyword path (LOBE-8891): non-DM messages whose text — or
// • Channel keyword path (): non-DM messages whose text — or
// a debounced sibling's text — contains a configured watch keyword.
// This is the only way to wake the bot in a parent channel on
// platforms like Discord, where `shouldSubscribe` returns false for
@@ -1320,7 +1320,7 @@ export class BotMessageRouter {
}
let merged = BotMessageRouter.mergeSkippedMessages(message, context);
// LOBE-8891: same instruction-injection rule as `onSubscribedMessage`
// same instruction-injection rule as `onSubscribedMessage`
// — prepend the matched entries' operator-authored instructions when
// the keyword (not a mention / DM / command) is what wakes the bot.
// DMs are explicit user intent and never get the prefix.
@@ -296,7 +296,7 @@ describe('BotCallbackService', () => {
await service.handleCallback(body);
// Crucially: never hits agent_bot_providers — that lookup throws for
// messenger-originated runs and was the cause of LOBE-8654.
// messenger-originated runs and was the cause of .
expect(mockFindByPlatformAndAppId).not.toHaveBeenCalled();
expect(mockMessengerGetInstallationStore).toHaveBeenCalledWith('telegram');
expect(mockMessengerStoreResolveByKey).toHaveBeenCalledWith('telegram:singleton');
@@ -69,7 +69,7 @@ const mockOnNewMention = vi.hoisted(() => vi.fn());
const mockOnSubscribedMessage = vi.hoisted(() => vi.fn());
const mockOnNewMessage = vi.hoisted(() => vi.fn());
const mockOnSlashCommand = vi.hoisted(() => vi.fn());
// Default state mocks for the LOBE-8981 participant tracking. Tests that
// Default state mocks for the participant tracking. Tests that
// care about the multi-human transition reassign `mockGetList` to seed the
// pre-existing participant list.
const mockGetList = vi.hoisted(() => vi.fn().mockResolvedValue([]));
@@ -103,7 +103,7 @@ vi.mock('@/server/services/aiAgent', () => ({
const mockHandleMention = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockHandleSubscribedMessage = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
// Default to "platform does not opt into thread isolation" so existing tests
// keep their pre-LOBE-8891 behaviour. Individual tests can replace this via
// keep their pre-behaviour. Individual tests can replace this via
// `.mockResolvedValueOnce(...)` to simulate Discord's auto-thread upgrade.
const mockOpenThreadForChannelWake = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
@@ -292,7 +292,7 @@ vi.mock('../platforms', () => ({
if (!operatorId || explicit.includes(operatorId)) return { ids: explicit };
return { ids: [...explicit, operatorId] };
},
// LOBE-8891: mirror the production helpers just well enough that
// mirror the production helpers just well enough that
// BotMessageRouter.registerHandlers can read a clean list of entries.
// Tests can populate `settings.watchKeywords` with the canonical
// `[{ keyword, instruction? }]` shape to exercise both the keyword-match
@@ -436,7 +436,7 @@ describe('BotMessageRouter', () => {
mockHandleMention.mockResolvedValue(undefined);
mockHandleSubscribedMessage.mockResolvedValue(undefined);
mockOpenThreadForChannelWake.mockResolvedValue(undefined);
// LOBE-8981 participant tracking — restore defaults wiped by
// participant tracking — restore defaults wiped by
// clearAllMocks. Empty list = fresh single-human thread; individual
// describes / tests override as needed.
mockGetList.mockResolvedValue([]);
@@ -597,7 +597,7 @@ describe('BotMessageRouter', () => {
});
describe('onSubscribedMessage policy', () => {
// LOBE-8981 introduced single-human thread relaxation: a non-mention
// introduced single-human thread relaxation: a non-mention
// post in a thread with ≤1 known humans now reaches the agent. Most of
// the existing policy tests are about the multi-human gate (keyword
// wake, command pass-through, allowlist rejection), so seed two
@@ -652,7 +652,7 @@ describe('BotMessageRouter', () => {
}
it('should skip non-mention messages in a multi-human group thread', async () => {
// LOBE-8981: post-fix the gate keys off thread.isDM || mention ||
// post-fix the gate keys off thread.isDM || mention ||
// singleHumanThread. Default beforeEach seeds two known participants,
// a third sender keeps the thread in multi-human territory.
const handler = await loadSubscribedHandler();
@@ -668,7 +668,7 @@ describe('BotMessageRouter', () => {
expect(mockHandleSubscribedMessage).not.toHaveBeenCalled();
});
it('should respond to non-mention messages while the channel thread is still single-human (LOBE-8981)', async () => {
it('should respond to non-mention messages while the channel thread is still single-human ()', async () => {
// Override the default multi-human seed: no prior participants →
// tracker records alice as participant #1 → gate lets her through
// without an explicit @mention.
@@ -682,7 +682,7 @@ describe('BotMessageRouter', () => {
expect(mockHandleSubscribedMessage).toHaveBeenCalledTimes(1);
});
it('should announce mention-only mode once when a second human joins (LOBE-8981)', async () => {
it('should announce mention-only mode once when a second human joins ()', async () => {
// Alice is already tracked; bob's first non-mention post is the
// multi-human transition.
mockGetList.mockResolvedValue(['alice-id']);
@@ -739,7 +739,7 @@ describe('BotMessageRouter', () => {
expect(mockHandleSubscribedMessage).toHaveBeenCalledTimes(1);
});
it('should wake on a watch-keyword match in a subscribed group thread (LOBE-8891)', async () => {
it('should wake on a watch-keyword match in a subscribed group thread ()', async () => {
const handler = await loadSubscribedHandler({
dmPolicy: 'open',
watchKeywords: [{ keyword: 'bug' }, { keyword: 'outage' }],
@@ -785,7 +785,7 @@ describe('BotMessageRouter', () => {
});
it('should not change behaviour when watchKeywords is empty/missing', async () => {
// Sanity: existing call sites must keep their pre-LOBE-8891 semantics.
// Sanity: existing call sites must keep their pre-semantics.
const handler = await loadSubscribedHandler({ dmPolicy: 'open' });
const thread = makeThread({ isDM: false });
const message = makeMessage({ isMention: false, text: 'there is a bug somewhere' });
@@ -1297,7 +1297,7 @@ describe('BotMessageRouter', () => {
expect(thread.post).not.toHaveBeenCalled();
});
// ---- LOBE-8891: channel-side keyword wake via catch-all ----
// ---- channel-side keyword wake via catch-all ----
//
// Discord (and any platform that opts out of subscribing top-level
// channels via `shouldSubscribe`) never fires `onSubscribedMessage` for
@@ -29,7 +29,7 @@ describe('buildBotContext', () => {
expect(ctx.senderExternalUserId).toBe('sender-2');
});
it('fails closed when operatorUserId is undefined (LOBE-8768 contract)', () => {
it('fails closed when operatorUserId is undefined (contract)', () => {
const ctx = buildBotContext({
...baseParams,
authorUserId: 'sender-1',
+1 -1
View File
@@ -364,7 +364,7 @@ export function normalizeAllowFromEntries(raw: unknown): Array<{ id: string; nam
return [];
}
// ---------- Watch keywords (LOBE-8891) ----------
// ---------- Watch keywords () ----------
/**
* Entry shape persisted under `settings.watchKeywords`. `keyword` is what the

Some files were not shown because too many files have changed in this diff Show More