From d8ba5524f40ee15df336dafb3f90da9a960a6018 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:13:57 +0900 Subject: [PATCH 1/5] fix(responses): parse code-mode declarations in bounded linear time The code-mode collaboration detector introduced in #6977 validated each exec tool declaration fence with /^(?:args|input): \S[\s\S]*\): Promise< [^>\s][\s\S]*>; \};$/. Two overlapping unrestricted repetitions let a fence body full of "): Promise --- .../responses/code-mode-collab-surface.ts | 59 ++++++++++++++++--- structure/subagents.md | 3 + .../code-mode-collab-surface.test.ts | 28 +++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/server/responses/code-mode-collab-surface.ts b/src/server/responses/code-mode-collab-surface.ts index cd3f807a61d..afd76f3c515 100644 --- a/src/server/responses/code-mode-collab-surface.ts +++ b/src/server/responses/code-mode-collab-surface.ts @@ -66,13 +66,39 @@ interface RenderedTool { name: string; raw: string } /** The declaration fence codex-rs opens: three backticks and "ts". */ const DECLARATION_FENCE_SHAPE: Fence = { char: "`", length: 3 }; -/** A complete declaration as render_code_mode_sample emits it, single- or multi-line. */ -function isCompleteDeclaration(body: readonly string[], name: string): boolean { +const DECLARATION_PREFIX = "declare const tools: { "; +const PROMISE_SEPARATOR = "): Promise<"; +const DECLARATION_SUFFIX = ">; };"; +// Bounds keep a caller-controlled description linear work: a rendered listing is a heading, a +// description, and a small declaration per tool, so anything this large cannot be the codex-rs +// shape this parser exists to recognize. +const MAX_DESCRIPTION_LENGTH = 1 << 20; +const MAX_DECLARATION_LENGTH = 64 * 1024; + +const isSpace = (ch: string | undefined): boolean => ch === undefined || /\s/.test(ch); + +/** + * The declared tool name when body is a complete declaration as render_code_mode_sample emits it, + * single- or multi-line, else undefined. Fixed boundaries plus one forward scan for the Promise + * separator: no backtracking, so a fence body full of candidate separators stays linear work. + */ +function completeDeclarationName(body: readonly string[]): string | undefined { const text = body.join("\n"); - const prefix = "declare const tools: { " + name + "("; - if (!text.startsWith(prefix)) return false; + if (text.length > MAX_DECLARATION_LENGTH) return undefined; + if (!text.startsWith(DECLARATION_PREFIX) || !text.endsWith(DECLARATION_SUFFIX)) return undefined; + const signature = text.slice(DECLARATION_PREFIX.length, -DECLARATION_SUFFIX.length); + const openParen = signature.indexOf("("); + const name = openParen > 0 ? signature.slice(0, openParen) : ""; + if (!/^[A-Za-z_$][\w$]*$/.test(name)) return undefined; // A named, typed parameter (`args` for functions, `input` for freeform) and a non-empty return type. - return /^(?:args|input): \S[\s\S]*\): Promise<[^>\s][\s\S]*>; \};$/.test(text.slice(prefix.length)); + const rest = signature.slice(openParen + 1); + const typed = rest.startsWith("args: ") ? 6 : rest.startsWith("input: ") ? 7 : -1; + if (typed < 0 || isSpace(rest[typed])) return undefined; + for (let at = rest.indexOf(PROMISE_SEPARATOR, typed + 1); at >= 0; at = rest.indexOf(PROMISE_SEPARATOR, at + 1)) { + const ch = rest[at + PROMISE_SEPARATOR.length]; + if (ch !== ">" && !isSpace(ch)) return name; + } + return undefined; } /** codex-rs normalize_code_mode_identifier: invalid identifier characters become `_`. */ @@ -103,9 +129,21 @@ function renderedHeading(match: RegExpExecArray): RenderedTool | undefined { * alone. Lines inside a quoted fence can neither open a section nor declare a tool. */ function renderedNestedTools(description: string): RenderedTool[] { + if (description.length > MAX_DESCRIPTION_LENGTH) return []; const lines = description.split("\n"); const rendered: RenderedTool[] = []; const pending: RenderedTool[] = []; + // Ascending indexes into `pending` per name, so a declaration finds its owner in one lookup + // instead of rescanning every pending heading. + const pendingByName = new Map(); + const dropPendingTo = (length: number): void => { + while (pending.length > length) { + const removed = pending.pop()!; + const indexes = pendingByName.get(removed.name)!; + indexes.pop(); + if (indexes.length === 0) pendingByName.delete(removed.name); + } + }; let fence: Fence | undefined; for (let i = 0; i < lines.length; i += 1) { const line = lines[i]!; @@ -116,7 +154,11 @@ function renderedNestedTools(description: string): RenderedTool[] { const match = TOOL_HEADING.exec(line); if (match) { const heading = renderedHeading(match); - if (heading) pending.push(heading); + if (heading) { + const indexes = pendingByName.get(heading.name); + if (indexes) indexes.push(pending.length); else pendingByName.set(heading.name, [pending.length]); + pending.push(heading); + } continue; } if (line === DECLARATION_LEAD && lines[i + 1] === DECLARATION_FENCE) { @@ -126,10 +168,11 @@ function renderedNestedTools(description: string): RenderedTool[] { const body = lines.slice(i + 2, close); // The outermost pending heading with the declared name owns it, so a same-name heading quoted // inside the tool's own description cannot take over the genuine section. - const owner = pending.findIndex(tool => isCompleteDeclaration(body, tool.name)); + const declared = completeDeclarationName(body); + const owner = declared === undefined ? -1 : pendingByName.get(declared)?.[0] ?? -1; if (owner >= 0) { rendered.push(pending[owner]!); - pending.length = owner; + dropPendingTo(owner); } i = close; continue; diff --git a/structure/subagents.md b/structure/subagents.md index c06a44368c3..ee99e149215 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -448,6 +448,9 @@ neither prove nor contradict a native surface. The two checks are asymmetric on purpose: evidence needs a rendered section, while the contradiction check reads every heading and declaration line, quoted or not, so text copied into the listing can push a turn toward `null` but cannot hide another surface. +The parse is request input, so it is bounded and linear-time: descriptions over 1 MiB carry no +nested evidence (the contradiction scan still applies), declarations over 64 KiB are not +recognized, and the fence body is validated by fixed boundaries and a name-indexed owner lookup. Nested evidence never produces `v2`. When the model supports tool search (every OpenCodex catalog row), codex-rs defers the V1 tools and leaves them out of the description, and no other per-request V1 signal exists: codex-rs sends its own `` item only on V2 and pins the diff --git a/tests/codex-integration/code-mode-collab-surface.test.ts b/tests/codex-integration/code-mode-collab-surface.test.ts index 0f5dfebc0b1..1132a703a9e 100644 --- a/tests/codex-integration/code-mode-collab-surface.test.ts +++ b/tests/codex-integration/code-mode-collab-surface.test.ts @@ -346,3 +346,31 @@ describe("a caller-defined exec contributes no nested evidence", () => { expect(surfaceOf([{ type: "custom", name: "exec", description: foreign }])).toBeNull(); }); }); + +describe("an adversarial declaration body stays linear work", () => { + // Regression: the declaration check must not backtrack per candidate Promise separator, and the + // owner lookup must not re-validate the body once per pending heading. On the pre-fix regex this + // ~393 KiB body starved the event loop for seconds well inside the default test timeout. + const separators = "): Promise { + const section = [heading("multi_agent_v1__spawn_agent"), listing].join("\n"); + expect(surfaceOf(codeModeTools(execDescription(section)))).toBeNull(); + }); + + test("many pending same-name headings do not multiply the parse", () => { + const headings = Array.from({ length: 256 }, () => heading("multi_agent_v1__spawn_agent")).join("\n"); + expect(surfaceOf(codeModeTools(execDescription(headings, listing)))).toBeNull(); + }); + + test("a description beyond the nested-listing bound contributes no evidence", () => { + const padded = SPAWN_DESCRIPTION + "\n" + "padding ".repeat(200_000); + expect(surfaceOf(codeModeTools(execDescription(nested("multi_agent_v1__spawn_agent", padded))))).toBeNull(); + }); +}); From b95748951e76726605fe0dfbc522678e6f064e20 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:27:15 +0900 Subject: [PATCH 2/5] Merge fork dev 3bb7a5612 into collab-decl-linear-parse Upstream refactored codeModeV1Evidence into analyzeCodeModeExec for the new deferred-collab-surface lane; the linear-time declaration parse now backs both callers. subagents.md grew to 599 lines upstream, so the new bound contract is compressed to keep the doc at the 600-line budget. --- structure/subagents.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/structure/subagents.md b/structure/subagents.md index 651b26c76fe..e3456605c8a 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -448,9 +448,7 @@ neither prove nor contradict a native surface. The two checks are asymmetric on purpose: evidence needs a rendered section, while the contradiction check reads every heading and declaration line, quoted or not, so text copied into the listing can push a turn toward `null` but cannot hide another surface. -The parse is request input, so it is bounded and linear-time: descriptions over 1 MiB carry no -nested evidence (the contradiction scan still applies), declarations over 64 KiB are not -recognized, and the fence body is validated by fixed boundaries and a name-indexed owner lookup. +The parse is bounded and linear-time: over-1 MiB descriptions carry no nested evidence, over-64 KiB declarations are not recognized, and each fence body is checked once against fixed boundaries. Nested evidence never produces `v2`. When the model supports tool search (every OpenCodex catalog row), codex-rs defers the V1 tools and leaves them out of the description. Its own `` item exists only on V2 and it pins the version per session, so neither the saved From 0f5e34cd01bf0d915ce059d66d2925f7955c9d06 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:50:26 +0900 Subject: [PATCH 3/5] fix(responses): validate the whole declaration before crediting a rendered tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit completeDeclarationName returned the tool name at the first "): Promise<" hit without checking the rest of the signature, so "Promise; garbage" still counted as a rendered V1 declaration. The parameter list now ends only at the ')' that balances the opening paren, and the return type must hold generic depth positive to the stripped suffix — a '>' that reaches zero mid-tail is surplus text, while '=>' arrows and nested generics still parse. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../responses/code-mode-collab-surface.ts | 32 +++++++++++++++---- .../code-mode-collab-surface.test.ts | 13 ++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/server/responses/code-mode-collab-surface.ts b/src/server/responses/code-mode-collab-surface.ts index 39b875200ad..35c84885b89 100644 --- a/src/server/responses/code-mode-collab-surface.ts +++ b/src/server/responses/code-mode-collab-surface.ts @@ -90,8 +90,9 @@ const isSpace = (ch: string | undefined): boolean => ch === undefined || /\s/.te /** * The declared tool name when body is a complete declaration as render_code_mode_sample emits it, - * single- or multi-line, else undefined. Fixed boundaries plus one forward scan for the Promise - * separator: no backtracking, so a fence body full of candidate separators stays linear work. + * single- or multi-line, else undefined. Fixed boundaries plus one forward scan that balances the + * parameter list's brackets and the return type's generics: no backtracking, so a fence body full + * of candidate separators stays linear work. */ function completeDeclarationName(body: readonly string[]): string | undefined { const text = body.join("\n"); @@ -105,11 +106,30 @@ function completeDeclarationName(body: readonly string[]): string | undefined { const rest = signature.slice(openParen + 1); const typed = rest.startsWith("args: ") ? 6 : rest.startsWith("input: ") ? 7 : -1; if (typed < 0 || isSpace(rest[typed])) return undefined; - for (let at = rest.indexOf(PROMISE_SEPARATOR, typed + 1); at >= 0; at = rest.indexOf(PROMISE_SEPARATOR, at + 1)) { - const ch = rest[at + PROMISE_SEPARATOR.length]; - if (ch !== ">" && !isSpace(ch)) return name; + // The parameter list ends at the ')' that balances the opening '(': a ')' nested inside the + // parameter's own type (e.g. `args: (a: string) => Promise`) cannot pose as the separator. + let depth = 1; + let close = -1; + for (let at = typed; at < rest.length; at += 1) { + const ch = rest[at]!; + if (ch === "(" || ch === "[" || ch === "{") depth += 1; + else if (ch === ")" || ch === "]" || ch === "}") depth -= 1; + if (depth === 0) { close = at; break; } } - return undefined; + if (close < 0 || !rest.startsWith(PROMISE_SEPARATOR, close)) return undefined; + const head = rest[close + PROMISE_SEPARATOR.length]; + if (head === ">" || isSpace(head)) return undefined; + // The return type runs to the stripped suffix, which already supplied Promise's final '>': + // generic depth must stay positive to the end, so a '>' that reaches zero ('Promise; + // garbage') is surplus text, and a '=>' arrow never counts as a closer. + let generic = 1; + for (let at = close + PROMISE_SEPARATOR.length; at < rest.length; at += 1) { + const ch = rest[at]!; + if (ch === "<") generic += 1; + else if (ch === ">" && rest[at - 1] !== "=") generic -= 1; + if (generic === 0) return undefined; + } + return name; } /** codex-rs normalize_code_mode_identifier: invalid identifier characters become `_`. */ diff --git a/tests/codex-integration/code-mode-collab-surface.test.ts b/tests/codex-integration/code-mode-collab-surface.test.ts index 1132a703a9e..f543587b66f 100644 --- a/tests/codex-integration/code-mode-collab-surface.test.ts +++ b/tests/codex-integration/code-mode-collab-surface.test.ts @@ -220,11 +220,24 @@ describe("code mode without V1 evidence stays null", () => { test.each([ ["an abbreviated parameter list", "declare const tools: { multi_agent_v1__spawn_agent(...): Promise; };"], ["an empty return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise<>; };"], + ["surplus text after the return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; garbage>; };"], + ["an unbalanced parameter type", "declare const tools: { multi_agent_v1__spawn_agent(args: {: Promise; };"], ])("%s is not a rendered declaration", (_label, declaration) => { const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBeNull(); }); + test.each([ + ["a nested generic return type", "Promise>"], + ["a function-typed parameter", undefined], + ])("%s still completes the declaration", (_label, returnType) => { + const declaration = returnType === undefined + ? "declare const tools: { multi_agent_v1__spawn_agent(args: (task: string) => void): Promise; };" + : "declare const tools: { multi_agent_v1__spawn_agent(args: {}): " + returnType + "; };"; + const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); + expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBe("v1"); + }); + test("a raw-name suffix that does not normalize to its heading is not a heading", () => { // Placed in the namespace description, ahead of the genuine heading it would otherwise outrank. const namespace = "## docs\n" + heading("docs__lookup", "multi_agent_v1__spawn_agent"); From 9c3bade016d090ccb4b5804b02ddaf3172ce0d89 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:12:31 +0900 Subject: [PATCH 4/5] fix(responses): skip quoted type text and require full generic closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bracket scan still counted delimiters inside quoted literals — a parameter type like { choice: ")" } made a valid declaration unreadable — and the return-type check never verified the depth it left behind, so a missing generic closer still credited the declaration. String and template literals are now skipped with escape awareness in both scans, and the return type must end at exactly the depth the stripped suffix owns: early zero is surplus, a leftover depth is incomplete. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../responses/code-mode-collab-surface.ts | 25 ++++++++++++++++--- .../code-mode-collab-surface.test.ts | 11 ++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/server/responses/code-mode-collab-surface.ts b/src/server/responses/code-mode-collab-surface.ts index 35c84885b89..1e62d00b168 100644 --- a/src/server/responses/code-mode-collab-surface.ts +++ b/src/server/responses/code-mode-collab-surface.ts @@ -106,12 +106,27 @@ function completeDeclarationName(body: readonly string[]): string | undefined { const rest = signature.slice(openParen + 1); const typed = rest.startsWith("args: ") ? 6 : rest.startsWith("input: ") ? 7 : -1; if (typed < 0 || isSpace(rest[typed])) return undefined; + // Quoted type text is opaque to structure: a ")" inside `{ choice: ")" }` must not close the + // parameter list, and a "<" inside a string literal must not open a generic. One forward scan + // with escape-aware skipping keeps the whole check linear. + const skipQuoted = (at: number): number => { + const quote = rest[at]!; + let i = at + 1; + while (i < rest.length) { + const c = rest[i]!; + if (c === "\\") i += 2; + else if (c === quote) return i; + else i += 1; + } + return rest.length; + }; // The parameter list ends at the ')' that balances the opening '(': a ')' nested inside the // parameter's own type (e.g. `args: (a: string) => Promise`) cannot pose as the separator. let depth = 1; let close = -1; for (let at = typed; at < rest.length; at += 1) { const ch = rest[at]!; + if (ch === '"' || ch === "'" || ch === "`") { at = skipQuoted(at); continue; } if (ch === "(" || ch === "[" || ch === "{") depth += 1; else if (ch === ")" || ch === "]" || ch === "}") depth -= 1; if (depth === 0) { close = at; break; } @@ -119,17 +134,19 @@ function completeDeclarationName(body: readonly string[]): string | undefined { if (close < 0 || !rest.startsWith(PROMISE_SEPARATOR, close)) return undefined; const head = rest[close + PROMISE_SEPARATOR.length]; if (head === ">" || isSpace(head)) return undefined; - // The return type runs to the stripped suffix, which already supplied Promise's final '>': - // generic depth must stay positive to the end, so a '>' that reaches zero ('Promise; - // garbage') is surplus text, and a '=>' arrow never counts as a closer. + // The return type runs to the stripped suffix, which already supplied exactly one '>' for the + // Promise's '<': generic depth must stay positive to the end and land at exactly 1 — a '>' that + // reaches zero ('Promise; garbage') is surplus text, a depth left above 1 + // ('Promise' arrow never counts. let generic = 1; for (let at = close + PROMISE_SEPARATOR.length; at < rest.length; at += 1) { const ch = rest[at]!; + if (ch === '"' || ch === "'" || ch === "`") { at = skipQuoted(at); continue; } if (ch === "<") generic += 1; else if (ch === ">" && rest[at - 1] !== "=") generic -= 1; if (generic === 0) return undefined; } - return name; + return generic === 1 ? name : undefined; } /** codex-rs normalize_code_mode_identifier: invalid identifier characters become `_`. */ diff --git a/tests/codex-integration/code-mode-collab-surface.test.ts b/tests/codex-integration/code-mode-collab-surface.test.ts index f543587b66f..0fa3f362424 100644 --- a/tests/codex-integration/code-mode-collab-surface.test.ts +++ b/tests/codex-integration/code-mode-collab-surface.test.ts @@ -222,18 +222,17 @@ describe("code mode without V1 evidence stays null", () => { ["an empty return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise<>; };"], ["surplus text after the return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; garbage>; };"], ["an unbalanced parameter type", "declare const tools: { multi_agent_v1__spawn_agent(args: {: Promise; };"], + ["a return type missing its generic closer", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; };"], ])("%s is not a rendered declaration", (_label, declaration) => { const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBeNull(); }); test.each([ - ["a nested generic return type", "Promise>"], - ["a function-typed parameter", undefined], - ])("%s still completes the declaration", (_label, returnType) => { - const declaration = returnType === undefined - ? "declare const tools: { multi_agent_v1__spawn_agent(args: (task: string) => void): Promise; };" - : "declare const tools: { multi_agent_v1__spawn_agent(args: {}): " + returnType + "; };"; + ["a nested generic return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise>; };"], + ["a function-typed parameter", "declare const tools: { multi_agent_v1__spawn_agent(args: (task: string) => void): Promise; };"], + ["a quoted bracket inside the parameter type", "declare const tools: { multi_agent_v1__spawn_agent(args: { choice: \")\" }): Promise; };"], + ])("%s still completes the declaration", (_label, declaration) => { const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBe("v1"); }); From ce1fa40b2201e04364cc816ed71282c22520f78a Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:35:50 +0900 Subject: [PATCH 5/5] fix(responses): ignore comments in declaration delimiter scans Skip line and block comments outside quoted type text so ordinary rendered field descriptions cannot change bracket or generic depth. Reject unterminated opaque regions and exercise comment-heavy declarations below the existing size bound. Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../responses/code-mode-collab-surface.ts | 44 ++++++++++++++----- structure/subagents.md | 2 +- .../code-mode-collab-surface.test.ts | 16 +++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/server/responses/code-mode-collab-surface.ts b/src/server/responses/code-mode-collab-surface.ts index 1e62d00b168..0515d60ad96 100644 --- a/src/server/responses/code-mode-collab-surface.ts +++ b/src/server/responses/code-mode-collab-surface.ts @@ -106,19 +106,33 @@ function completeDeclarationName(body: readonly string[]): string | undefined { const rest = signature.slice(openParen + 1); const typed = rest.startsWith("args: ") ? 6 : rest.startsWith("input: ") ? 7 : -1; if (typed < 0 || isSpace(rest[typed])) return undefined; - // Quoted type text is opaque to structure: a ")" inside `{ choice: ")" }` must not close the - // parameter list, and a "<" inside a string literal must not open a generic. One forward scan - // with escape-aware skipping keeps the whole check linear. - const skipQuoted = (at: number): number => { + // Quoted text and comments are opaque to delimiter matching. Each skip advances + // past its text once; an unterminated region cannot contain a closing declaration. + const isOpaqueTypeText = (at: number): boolean => { + const ch = rest[at]; + return ch === '"' || ch === "'" || ch === "`" + || ch === "/" && (rest[at + 1] === "/" || rest[at + 1] === "*"); + }; + const skipTypeText = (at: number): number | undefined => { const quote = rest[at]!; + if (quote === "/" && rest[at + 1] === "/") { + let i = at + 2; + while (i < rest.length && rest[i] !== "\n" && rest[i] !== "\r" + && rest[i] !== "\u2028" && rest[i] !== "\u2029") i += 1; + return i < rest.length ? i : undefined; + } + if (quote === "/" && rest[at + 1] === "*") { + const end = rest.indexOf("*/", at + 2); + return end < 0 ? undefined : end + 1; + } let i = at + 1; while (i < rest.length) { - const c = rest[i]!; - if (c === "\\") i += 2; - else if (c === quote) return i; + const ch = rest[i]!; + if (ch === "\\") i += 2; + else if (ch === quote) return i; else i += 1; } - return rest.length; + return undefined; }; // The parameter list ends at the ')' that balances the opening '(': a ')' nested inside the // parameter's own type (e.g. `args: (a: string) => Promise`) cannot pose as the separator. @@ -126,7 +140,12 @@ function completeDeclarationName(body: readonly string[]): string | undefined { let close = -1; for (let at = typed; at < rest.length; at += 1) { const ch = rest[at]!; - if (ch === '"' || ch === "'" || ch === "`") { at = skipQuoted(at); continue; } + if (isOpaqueTypeText(at)) { + const end = skipTypeText(at); + if (end === undefined) return undefined; + at = end; + continue; + } if (ch === "(" || ch === "[" || ch === "{") depth += 1; else if (ch === ")" || ch === "]" || ch === "}") depth -= 1; if (depth === 0) { close = at; break; } @@ -141,7 +160,12 @@ function completeDeclarationName(body: readonly string[]): string | undefined { let generic = 1; for (let at = close + PROMISE_SEPARATOR.length; at < rest.length; at += 1) { const ch = rest[at]!; - if (ch === '"' || ch === "'" || ch === "`") { at = skipQuoted(at); continue; } + if (isOpaqueTypeText(at)) { + const end = skipTypeText(at); + if (end === undefined) return undefined; + at = end; + continue; + } if (ch === "<") generic += 1; else if (ch === ">" && rest[at - 1] !== "=") generic -= 1; if (generic === 0) return undefined; diff --git a/structure/subagents.md b/structure/subagents.md index e3456605c8a..23233beb0a2 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -448,7 +448,7 @@ neither prove nor contradict a native surface. The two checks are asymmetric on purpose: evidence needs a rendered section, while the contradiction check reads every heading and declaration line, quoted or not, so text copied into the listing can push a turn toward `null` but cannot hide another surface. -The parse is bounded and linear-time: over-1 MiB descriptions carry no nested evidence, over-64 KiB declarations are not recognized, and each fence body is checked once against fixed boundaries. +The parse uses bounded linear scans: descriptions over `1 << 20` string code units carry no nested evidence, declarations over `64 * 1024` code units are not recognized, and quoted text and comments are skipped while matching delimiters; return generics must close fully. Nested evidence never produces `v2`. When the model supports tool search (every OpenCodex catalog row), codex-rs defers the V1 tools and leaves them out of the description. Its own `` item exists only on V2 and it pins the version per session, so neither the saved diff --git a/tests/codex-integration/code-mode-collab-surface.test.ts b/tests/codex-integration/code-mode-collab-surface.test.ts index 0fa3f362424..2644ae0cffa 100644 --- a/tests/codex-integration/code-mode-collab-surface.test.ts +++ b/tests/codex-integration/code-mode-collab-surface.test.ts @@ -223,6 +223,9 @@ describe("code mode without V1 evidence stays null", () => { ["surplus text after the return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; garbage>; };"], ["an unbalanced parameter type", "declare const tools: { multi_agent_v1__spawn_agent(args: {: Promise; };"], ["a return type missing its generic closer", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; };"], + ["an unterminated quoted return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise<\"unterminated>; };"], + ["an unterminated block comment", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; };"], + ["a line comment consuming the closing suffix", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; };"], ])("%s is not a rendered declaration", (_label, declaration) => { const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBeNull(); @@ -232,6 +235,11 @@ describe("code mode without V1 evidence stays null", () => { ["a nested generic return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise>; };"], ["a function-typed parameter", "declare const tools: { multi_agent_v1__spawn_agent(args: (task: string) => void): Promise; };"], ["a quoted bracket inside the parameter type", "declare const tools: { multi_agent_v1__spawn_agent(args: { choice: \")\" }): Promise; };"], + ["an apostrophe in a field line comment", "declare const tools: { multi_agent_v1__spawn_agent(args: {\n // The agent's task.\n message: string;\n}): Promise; };"], + ["an unmatched bracket in a field line comment", "declare const tools: { multi_agent_v1__spawn_agent(args: {\n // Optional unmatched bracket ) in prose.\n message: string;\n}): Promise; };"], + ["delimiters in a block comment", "declare const tools: { multi_agent_v1__spawn_agent(args: { /* ) < ' */ message: string }): Promise; };"], + ["a comment in the return type", "declare const tools: { multi_agent_v1__spawn_agent(args: {}): Promise; };"], + ["comment markers inside quoted type text", "declare const tools: { multi_agent_v1__spawn_agent(args: { marker: \"/* not a comment */ //\" }): Promise<\"// quoted < )\">; };"], ])("%s still completes the declaration", (_label, declaration) => { const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBe("v1"); @@ -385,4 +393,12 @@ describe("an adversarial declaration body stays linear work", () => { const padded = SPAWN_DESCRIPTION + "\n" + "padding ".repeat(200_000); expect(surfaceOf(codeModeTools(execDescription(nested("multi_agent_v1__spawn_agent", padded))))).toBeNull(); }); + + test("comment-heavy declarations below the bound retain their evidence", () => { + const comments = "/* ): Promise<' */".repeat(2048); + const declaration = `declare const tools: { multi_agent_v1__spawn_agent(args: { ${comments} message: string }): Promise; };`; + expect(declaration.length).toBeLessThan(64 * 1024); + const snippet = [heading("multi_agent_v1__spawn_agent"), "exec tool declaration:", FENCE + "ts", declaration, FENCE].join("\n"); + expect(surfaceOf(codeModeTools(execDescription(nested("docs__lookup", snippet))))).toBe("v1"); + }); });