Skip to content

fix(core): keep nested tool_call arguments open on Responses; name the target in bridged validation errors - #12901

Merged
yiliang114 merged 37 commits into
mainfrom
fix/issue-12889-bridge-arg-validation
Oct 6, 2026
Merged

yiliang114 merged 37 commits into
mainfrom
fix/issue-12889-bridge-arg-validation

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Two changes for #12889:

  1. Responses converter: when the Responses request builder adds an empty properties map to a nested object schema that has none (an Azure compatibility patch), it now also marks that object additionalProperties: true unless the schema states otherwise. This keeps tool_call's free-form arguments explicitly open. The root of a tool schema keeps the bare patch, since a zero-argument tool's top level really is empty.
  2. Target-named validation errors: when a deferred tool reached through tool_call fails its own validation, the error names the target and says where to read its schema, for example Deferred tool "web_fetch" (called through tool_call) rejected the arguments: params must have required property 'url'. Pass arguments matching the schema returned by tool_search for "web_fetch". This applies to both the terminal scheduler and the ACP session. A direct call's error is unchanged.

No bridge pre-check (removed, kept at archive/12901-bridge-arg-precheck). No auto-reveal of the target after an empty call (removed, kept at archive/12901-empty-arg-auto-reveal; see the scope decision).

Why it's needed

In #12889 the model received a deferred web tool's schema through an OpenAI Responses-compatible provider, then kept calling tool_call with {"arguments":{}} until loop protection stopped the turn. A clearer error alone did not recover that run. On the Responses wire only, the nested arguments object went out as {type: "object", properties: {}}. A backend that constrains decoding to the schema may read that as "empty object only". The Chat Completions and Anthropic converters have no such patch. Whether the reporter's backend actually constrained decoding this way is unconfirmed: under default JSON Schema rules, an empty properties map alone does not close an object.

The labelled error makes a genuine argument mistake attributable to the target instead of to tool_call's envelope.

Reviewer Test Plan

How to verify

  • Responses request for a tool whose schema has a nested {type: "object"} without properties: the nested node carries properties: {} and additionalProperties: true; an explicit additionalProperties is kept; the root node is unchanged.
  • Bridged call with {} to a target requiring url: INVALID_TOOL_PARAMS, answered under tool_call, message names the target, the missing field and tool_search. The same arguments sent directly give the original message.
  • Alternating invalid bridge/direct/bridge calls to the same deferred target retain one retry budget: all three return INVALID_TOOL_PARAMS, and the third reports RETRY LOOP DETECTED despite the bridge-specific message prefix.
  • Acceptance: rerun the Deferred tool_call schema allows empty arguments for tools with required fields #12889 E2E on the reporter's Responses provider with the original prompt.

Evidence (Before & After)

At 67ea7b62ab3f6cfc06497f041a8d34546a7eba73, frozen-lockfile installation and its full prepare build, full typecheck, changed-test lint/format and 622 targeted core tests plus the three ACP bridge outcomes passed. The mixed bridge/direct/bridge regression detects a mutation that keys retries by the labelled display message instead of the raw target validation error; the production source was restored and is byte-identical to the starting head. Independent observation confirms the three calls retain one retry budget. N/A for UI. This verifies schema/error/retry boundaries locally; the original #12889 provider E2E has not been run.

Remaining-feedback report.

Tested on

OS Status
🍏 macOS Local build, typecheck and targeted regressions passed; original-provider E2E not run
🪟 Windows ⚠️
🐧 Linux ⚠️

Risk & Scope

  • Main risk or tradeoff: Responses requests change shape for nested schemaless objects (now explicitly open), including task_create.metadata and task_update.metadata as well as tool_call.arguments. That matches their JSON Schema meaning, but a strict backend could treat the explicit flag differently. The labelled error only touches an error that was already returned.
  • Not validated / out of scope: the E2E above; any mid-session change to the declared tool set (needs its own issue and a maintainer decision).
  • Breaking changes / migration notes: none.

Linked Issues

Refs #12889 (close after the E2E passes). The schema-contract consolidation stays in #12999.

中文说明

改动

针对 #12889 的两处改动:

  1. Responses 转换器: Responses 请求构建器会给没有 properties 的嵌套 object schema 补一个空的 properties(兼容 Azure 的补丁)。现在补的同时会标上 additionalProperties: true(schema 自己写了的除外),让 tool_call 的自由形式 arguments 明确保持开放。工具 schema 的根节点仍按原样补丁,因为零参数工具的顶层本来就是空的。
  2. 带目标名的校验报错: 通过 tool_call 调用的延迟工具,如果被它自己的校验拒绝,报错会写明目标工具以及去哪看它的 schema,例如 Deferred tool "web_fetch" (called through tool_call) rejected the arguments: params must have required property 'url'. Pass arguments matching the schema returned by tool_search for "web_fetch".。终端调度器和 ACP 会话都适用,直接调用的报错不变。

不再有桥接预校验(已删除,保存在 archive/12901-bridge-arg-precheck)。也不再有空参调用后自动亮出目标工具(已删除,保存在 archive/12901-empty-arg-auto-reveal,见范围决定)。

原因

在 #12889 中,模型通过 OpenAI Responses 兼容 provider 拿到了一个延迟网页工具的 schema,然后一直用 {"arguments":{}} 调用 tool_call,直到被循环保护终止。只把报错写清楚并没有让那次实跑恢复。只有在 Responses 接口上,嵌套的 arguments 会被发成 {type: "object", properties: {}},按 schema 约束解码的后端可能把它理解为"只能是空对象"。Chat Completions 和 Anthropic 的转换器没有这个补丁。报告者的后端是否真的这样约束解码尚未确认:按 JSON Schema 默认规则,仅有空的 properties 并不会让对象变成封闭的。

带名字的报错让真实的参数错误能归到目标工具上,而不是被误认为是 tool_call 外壳的问题。

评审测试计划

如何验证

  • 一个 schema 里有嵌套 {type: "object"}、没写 properties 的工具,构建 Responses 请求时:嵌套节点带有 properties: {} 和 additionalProperties: true;显式写了的 additionalProperties 保持不变;根节点不变。
  • 对一个要求 url 的目标用 {} 发起桥接调用:返回 INVALID_TOOL_PARAMS,以 tool_call 名义回复,报错写明目标、缺失字段和 tool_search。同样的参数直接调用时报错保持原样。
  • 同一延迟工具交替接受无效的桥接/直接/桥接调用时,共享同一个重试预算:三次均为 INVALID_TOOL_PARAMS,第三次报告 RETRY LOOP DETECTED,桥接消息增加的目标前缀不应重置预算。
  • 验收: 在 Deferred tool_call schema allows empty arguments for tools with required fields #12889 报告者的 Responses provider 上用原始提示词重新做端到端测试。

证据(前后对比)

在 67ea7b62ab3f6cfc06497f041a8d34546a7eba73 上,冻结 lockfile 安装及 prepare 完整构建、完整 typecheck、修改测试 lint/格式、622 项 core 定向测试与 ACP 桥接的三种结果均通过。新增桥接/直接/桥接回归能够检出“以带标签的展示消息代替目标原始校验错误作为重试键”的变异;生产源码已恢复,逐字节与起始 head 相同。独立观察确认三次调用共享一个重试预算。无 UI 变化。本地验证覆盖 schema/报错/重试边界,#12889 原提供商的端到端测试仍未执行。

剩余反馈处理报告。

测试平台

OS 状态
🍏 macOS 本地构建、类型检查、定向回归通过;原提供商 E2E 未执行
🪟 Windows ⚠️
🐧 Linux ⚠️

风险与范围

  • 主要风险:Responses 请求里无 schema 的嵌套对象形态有变化(现在明确为开放);除 tool_call.arguments 外,还包括 task_create.metadata 和 task_update.metadata。这与它们在 JSON Schema 中的含义一致,但严格的后端可能会对这个显式标记有不同处理。带名字的报错只改动了一个本来就会返回的报错。
  • 未验证 / 不在范围内:上述端到端测试;任何会话中途改变已声明工具集的做法(需要单独的 issue 和维护者决定)。
  • 破坏性变更:无。

关联 Issue

Refs #12889(端到端测试通过后关闭)。schema 契约的整合仍在 #12999。

…t schema

The tool_call bridge declaration deliberately types arguments as a bare
object so the model-facing schema stays byte-stable across catalog
changes, which makes an empty {} envelope-valid even when the deferred
target requires fields. resolveDeferredToolCall validated only that
envelope and returned the arguments verbatim, so the target's
required-field error surfaced post-unwrap in the scheduler as a bare
Ajv message (params must have required property 'url') with no target
name or remedy; models resubmitted the same empty object until the
validation-retry loop guard stopped the turn
(LOOP_DETECTED/invalid_tool_params_stagnation).

Pre-validate the bridged arguments with the target's own
validateToolParams before resolving. A failure now returns an
INVALID_TOOL_PARAMS bridge refusal naming the target and the missing
field, which the scheduler's existing bridge-refusal accounting already
counts toward the retry-loop threshold. The pre-check runs on a
structuredClone because SchemaValidator coerces values in place; the
scheduler re-validates the returned arguments at build time, so valid
calls are unaffected.

Fixes #12889

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmukuf6832h
…heck

resolveDeferredToolCall now pre-validates bridged arguments against the
target schema (#12889), but omni media-policy targets do not hold their
final arguments at that point: both frontends run evaluateMediaPolicyToolCall
AFTER bridge resolution (coreToolScheduler before buildInvocation, ACP
Session.runTool), and that gate is what resolves resourceId to inputPath and
merges defaultArguments/lockedArguments, while BaseMediaPolicyTool's
validateToolParams deliberately checks the NATIVE schema rather than the
model-visible projection tool_search returned.

So the pre-check refused calls the very next stage accepted. A bridged
{resourceId, outputDir} call — the shape omni/media-guidance.ts tells the
model to send — was refused with "provide exactly one of inputPath ... or
resourceId" even though resourceId was provided, and for path-less media the
model can never learn the real path, so it cannot repair the call. An
operator-pinned modelAccess.lockedArguments.outputDir was unwinnable both
ways: projectMediaPolicyToolDeclaration strips the locked key from the
model-visible properties AND required, so omitting it failed the pre-check
while sending it failed the gate. Each refusal fed
recordBatchRetryableToolError and gained RETRY_LOOP_STOP_DIRECTIVE, ending the
turn in LOOP_DETECTED / invalid_tool_params_stagnation on a call shape that
previously worked. All 14 omniPolicyToolFactories tools inherit the first
trigger; the 13 with a native required outputDir also reach the second.

Key the exemption off mediaPolicyDescriptor: the code-level fact the gate
itself keys off (it passes every non-policy tool through untouched), so the
exemption covers exactly the targets whose arguments a downstream stage
completes. Nothing fails open — the gate still emits named invalid_params
refusals before build(), and build() re-validates the merged arguments against
the native schema.

Acceptance test added to the existing #12889 block: a target whose
model-visible schema omits a key its validateToolParams requires (native
required: ['inputPath','outputDir'], projected required: []) resolves with
projection-satisfying arguments instead of being refused.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmul3pj9gc4
@yiliang114
yiliang114 dismissed a stale review via 9d6a39d September 28, 2026 11:02
…policy gates

Round-2 review follow-up on the #12889 pre-validation:

- Yield the pre-check when the request is stamped wasOutputTruncated, so a
  max_tokens-cut bridged envelope surfaces as truncation (the scheduler's
  existing truncation guards own the message) instead of a schema mismatch.
- Consult the caller's per-target execution policy (scheduler execution
  allowlist / ACP permission-manager enablement) before validating
  arguments, so a denied target keeps its specific EXECUTION_DENIED.
- Media-policy targets now pre-check against the model-visible projection
  (target.schema) instead of skipping validation: a missing model-visible
  required field is refused naming the target, while gate-completed
  arguments (resourceId handles, operator-locked keys) stay unrefusable at
  the bridge.
- Plumb the validated targetName through bridgeResolutionError and key
  per-tool retry accounting on it (widening the batch-start prune to match),
  so alternating failures against distinct targets accrue per-target and
  reach the retry-loop threshold.
- Coalesce AgentTool.refreshSubagents kicks onto the in-flight refresh —
  validation now fires twice per bridged Agent call — while the change
  listener re-arms one follow-up so a mid-refresh change is not dropped.
- Read mediaPolicyDescriptor typed instead of through a structural cast.
- Tests pin: the resourceId trigger shape, a projector-derived projection
  fixture, the throwing-validator fall-through, and scheduler coverage for
  the truncation / policy-denial / per-target-accounting orderings.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmulbkgfd2o
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Round-2 findings addressed in 2c91d42cd7 (pushed 9d6a39dd95..2c91d42cd7). All 9 fixed; per-finding notes below. Every behavior change carries a test that is red against the pre-fix code (mutation-checked by deleting the fix and watching the new test fail).

R2-1 [Critical] — truncation must win over the schema pre-check. Fixed as suggested: DeferredToolCallOptions.wasOutputTruncated skips the pre-check, threaded from resolveToolCallBridgeRequest (request.wasOutputTruncated). Resolution stays intact, so the existing guards own the outcome: the Kind.Edit rejection at coreToolScheduler.ts:3137 fires for a truncated bridged write_file, and non-Edit targets get TRUNCATION_PARAM_GUIDANCE appended at :3208. New coreToolScheduler.test.ts case beside createTruncationTestScheduler: bridged write_file with {file_path} + wasOutputTruncated: true asserts the truncation rejection and the absence of params must have required property 'content'; red without the option (verified by dropping the pass-through).

R2-2 — resourceId trigger shape. Added: locked-outputDir media-policy target called with {resourceId} and no inputPath resolves without error. This is the shape omni/media-guidance.ts instructs, and it is exactly the reviewer's CASE 6; red if the media-policy branch is removed or falls back to native validateToolParams (missing locked outputDir). Verified by mutation.

R2-3 — hand-written projection literal. The test fixture now derives its schema through the real projectMediaPolicyToolDeclaration (with lockedArguments: {outputDir}) instead of pinning a literal. The fixture's native schema was also made representative: required: ['outputDir'] with resourceId present, matching all 14 shipped omni tools (none require inputPath natively). Assertions pin the derived split (locked key absent from properties and required), so projector drift is now detectable.

R2-4 — exemption skipped validation entirely. Narrowed: media-policy targets are pre-checked against the model-visible projection (target.schema.parametersJsonSchema, schema-only via SchemaValidator) instead of skipping validation. A bridged {} with no lockedArguments is now refused naming the target and 'outputDir' (new test); the locked-key carve-out from R1-1 stays because the projection strips locked keys from required (existing test green); the value-level io rule still runs only post-gate at build(), so {resourceId} is not refused at the bridge. build()'s native validation is untouched, per the media-policy-tool.ts:229-233 constraint. Red under the shipped skip (verified by mutation).

R2-5 — structural cast. Dropped; target.mediaPolicyDescriptor is read typed (AnyDeclarativeTool already exposes the getter). The rename mutant is now a compile error; tsc --noEmit clean.

R2-6 — policy denial must precede the pre-check. Added DeferredToolCallOptions.isTargetExecutionAllowed, consulted in resolveDeferredToolCall right after the existing policy-denial checks (consistent with the "policy denials precede the hidden-tool gate" principle at tool-call.ts), before the argument pre-check. The scheduler passes this.isToolExecutionAllowed; ACP passes pm.isToolEnabled when a permission manager exists. A denied bridged target now gets EXECUTION_DENIED naming the policy instead of INVALID_TOOL_PARAMS, and the denied tool's validator never runs. Scheduler test: deferred+hidden web_fetch with required schema, denied by the allowlist, called with {} → EXECUTION_DENIED, no required property 'url' (the pre-existing allowlist test indeed did not discriminate — its MockTool has no params). Red without the pass-through (verified by mutation). The scheduler's post-resolution check at :2726 stays as defense in depth.

R2-7 — AgentTool.validateToolParams is not pure. Fixed at the kick site per the suggestion: refreshSubagents() coalesces concurrent kicks onto the in-flight refresh promise, so the doubled validation from a bridged Agent call costs one scan + one setTools. The subagentManager change listener uses a separate path that re-arms exactly one follow-up refresh when one is in flight, so a change landing mid-refresh is not dropped (the constraint at agent.ts:1128-1130 is untouched: validation stays synchronous, the kick is still fired). New agent.test.ts case: two synchronous validateToolParams({subagent_type: 'missing', ...}) → one listSubagents call; a third after settle re-scans. Red without the guard (verified by mutation).

R2-8 — per-target retry accounting. bridgeResolutionError now carries targetName (populated from resolution.targetName), the refusal branch keys recordBatchRetryableToolError on targetName ?? reqInfo.name, and the batch-start prune presence set includes both the wrapper and the validated target name. Threshold unchanged (VALIDATION_RETRY_LOOP_THRESHOLD = 3); Session.ts:13421 already consumes the same field, untouched. New test: two deferred+hidden targets, three batches of two alternating invalid bridged calls — batch 3 carries RETRY LOOP DETECTED on both. Red with wrapper keying (verified by mutation), which also confirms the prune widening is load-bearing across batches.

R2-9 — untested swallow branch. Added: a target whose validateToolParams throws still resolves (no error, correct tool), documenting that the throw is left to build(). Red with the try/catch removed (verified by mutation).

Verification: packages/core — tool-call.test.ts 42/42; coreToolScheduler.test.ts 458 passed + 4 failed (the 4 Plan shell routing failures reproduce identically on the pre-change tree in this environment — timing/git-init dependent, unrelated); agent.test.ts 354 passed + 21 failed (same pre-existing environment failures as the base); tool-search.test.ts + goal-tool-result-provenance.test.ts 91/91; tsc --noEmit clean for packages/core and packages/cli; eslint clean on all touched files. Not run locally: packages/cli unit tests (this worktree has no built workspace dists; the ACP change is an additive optional pass-through and the default test config stubs getPermissionManager: null, leaving those paths inert — CI covers them).

yiliang114 and others added 2 commits September 28, 2026 23:57
The Lint & Static job failed at the "Run Prettier" step on
packages/core/src/core/coreToolScheduler.test.ts and
packages/core/src/tools/tool-call.ts. Reformat both with the
repository's pinned prettier (3.6.1); no semantic change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmulf520ncq
…ounting, and agent refresh

Round-3 review follow-ups for the tool_call bridge argument pre-check:

- Scheduler: forward the permission-manager policy as a pre-check
  suppression so a pm-denied bridged target surfaces the loop's richer
  EXECUTION_DENIED (deny-rule attribution) instead of an
  INVALID_TOOL_PARAMS refusal for a call that could never run (R3-3).
- Scheduler: drop the post-resolution isToolExecutionAllowed gate —
  resolution already consults the same constructor-fixed predicate on the
  same target name, so the copy was unreachable and free to drift (R3-2).
- Scheduler: widen the retry-counter presence set only for
  INVALID_TOOL_PARAMS bridge refusals (the one error type that accrues),
  so an EXECUTION_DENIED batch no longer preserves stale counters (R3-4),
  and record bridged pre-check refusals in a channel-marked namespace so
  they no longer prefix-prune (or get pruned by) the same target's direct
  validation failures every mixed batch (R3-5).
- tool-call: narrow the pre-check to the target's model-visible schema
  (SchemaValidator) for all targets, so value-level rules (fs stats,
  content scans, the AgentTool refresh kick) run exactly once at build()
  time instead of twice per bridged call (R3-12).
- AgentTool: route the unknown-subagent_type validation kick through the
  listener's arm-once path so a kick landing mid-scan queues one follow-up
  instead of being dropped (R3-6), guard the armed follow-up against
  post-dispose execution (R3-8), and catch the armed chain per this file's
  void-boundary contract (R3-9).

Tests: pin the ACP policy-denial path (R3-1), the listener mid-refresh arm
(R3-7), the policy-gate/hidden-gate ordering (R3-10), and each behavior
fix above with its own mutation-checked case.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-issue-patrol/jmuluuwer2v

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2E verification for #12889

Result: the original user-visible failure still reproduces at 4c7ebf961cc6126e0e26c8139a365b238c645753. The new bridge validation runs and improves the error message, but the model repeats the same empty arguments and the ACP turn still ends under tool-call loop protection.

Setup

  • Built and ran the PR base fc4e01b9fc381952c15592ead9e0cf170754dd8d, then checked out and built PR head 4c7ebf961cc6126e0e26c8139a365b238c645753 in the same local Git worktree.
  • Used a fresh ACP session for each run, an isolated Qwen configuration and runtime, Direct mode (tools.codeModeOnly: false), and the same configured gpt-6-astra model through the OpenAI Responses-compatible provider. The private endpoint and credentials are omitted.
  • Sent the original prompt: 帮我找一下 opus-5.5 代理集群解决 图遍历算法的新闻. Captured ACP events and inspected the provider logs for the model's raw tool-call arguments. One matched base/head pair was run; a second head run confirmed the same final failure.

Observed behavior

Base PR head
Model calls tool_search(select:web_fetch), then three tool_call calls with {"name":"web_fetch","arguments":{}} Same sequence and arguments
Tool errors Three target-tool errors: params must have required property 'url' Three bridge refusals naming web_fetch, the missing url, and tool_search as the way to inspect the schema
ACP turn Tool-call loop protection stopped this turn Same result; the requested news was not retrieved

The PR branch returned this error for each empty call:

[tool_call bridge refused] Deferred tool "web_fetch" rejected the arguments: params must have required property 'url'. Pass arguments matching the schema returned by tool_search for "web_fetch".

Both revisions built successfully. On the PR head, packages/core/src/tools/tool-call.test.ts passed 44/44 tests. A separate direct call to resolveDeferredToolCall confirmed that the base accepts {} and fails later in the target's build(), while the PR head rejects {} at bridge resolution and still accepts complete arguments.

Conclusion

The PR fixes the bridge's validation and error attribution, but it does not resolve #12889's reproduced end-to-end failure for this model/provider combination. The model still sends {} after receiving the richer error. I would keep the user-visible recovery criterion open until an ACP E2E run with the original prompt completes successfully, or narrow the PR's stated scope to bridge validation and diagnostics.

This observation covers the specified model/provider combination and prompt; it does not establish behavior for other models or providers.

中文说明

在同一 worktree 中依次构建并运行 base fc4e01b9 与 PR head 4c7ebf9,使用隔离配置、相同的 gpt-6-astra + OpenAI Responses 兼容 provider、ACP 路径和 issue 原提示词。两版均先搜索到 web_fetch,随后连续三次调用 tool_call,arguments 都是 {},最终由循环保护结束任务。PR head 确实把缺少 url 的错误提前到桥接层,并补上目标工具名与修正提示;模型仍未恢复,新闻检索未完成。PR head 的定向测试为 44/44 通过。建议保留端到端恢复的验收条件,或将 PR 的目标明确收窄为参数预校验和错误诊断。

yiliang114 and others added 2 commits September 29, 2026 16:04
…p failure

Two review findings on the bridged tool_call argument pre-validation:

- tool-call: validate a per-call structural copy of the target's
  parametersJsonSchema. Ajv caches a compiled schema by object identity for
  the life of the process, and AgentTool mutates its own parameterSchema in
  place, so the first bridged call pinned every later one to the pre-refresh
  shape and refused a property the target had since advertised. The copy
  resolves through the JSON-text tier, which still shares one compiled
  validator per distinct schema text.
- coreToolScheduler: log the permission-manager lookup failure that the
  suppressArgumentPreCheck guard swallows. On the refusal path _schedule
  continues ahead of the permission gate, so the catch was the only place
  that saw the error and it recorded nothing.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmumcpwj7eg
TS4111 rejects dot access on a Record<string, unknown> index signature.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmumcpwj7eg
Picks up #13000 (5ef7983), which accepts the takeover scan's settlement in
the durable-local fault gate. Without it, Hosted process fault gates fails with:

  DurableLocalRuntimeFaultGateTest.brokerCrashAdoptsOriginalWorkerWithoutReplaying:41
  timed out waiting for original journal settlement; last value: ALREADY_SETTLED

introduced by #12964 (2f5a62e) reconciling executions on session takeover.

This branch does not touch packages/sdk-java/runtime-broker, so the merge takes
main's version wholesale.

Co-authored-by: Qwen-Coder <[email protected]>
yiliang114 and others added 3 commits September 29, 2026 21:57
…wn label

The armed follow-up chain attached its .catch AFTER .finally. Because
.finally propagates the original rejection and its callback is synchronous,
that handler could only ever receive the IN-FLIGHT refresh's rejection while
its message said 'Follow-up subagent refresh failed'. runRefreshSubagents'
finally awaits llmClient.setTools(), which can reject, so a transient setTools
failure was reported as a failed follow-up scan even when the follow-up
succeeded. The follow-up's own rejection is already reported by
requestRefresh's 'Subagent refresh failed' handler.

Move the .catch before the .finally so each rejection keeps its own label;
the rejection stays handled (no unhandledRejection) and the arm still runs.

Tests: extend 'absorbs a setTools rejection from an armed follow-up refresh'
to assert both labels, and add 'reports an in-flight refresh rejection under
the in-flight label only' (setTools rejects once, then resolves) pinning that
the follow-up label is never logged for the in-flight error. Both go red if
the chain is reordered back.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmumpkv7rf3
…amily

The channel marker keeps a bridged pre-check refusal from prefix-pruning the
same target's direct validation failure WITHIN a batch, but the batch-start
presence prune widened the presence set only for requests carrying an
INVALID_TOOL_PARAMS bridge refusal. Each batch therefore preserved just the
channel it contained and deleted the other, so a model alternating between
bridging a deferred target and calling it directly reset both counters every
turn: neither ever reached VALIDATION_RETRY_LOOP_THRESHOLD and RETRY LOOP
DETECTED was never injected. That is a regression against the merge base,
where both channels produced the same message under the same key and accrued
together.

Fix both halves, since widening presence alone is not safe:
- presence: a request naming X now preserves both `X` and
  `bridgeRetryToolName(X)`, so alternating channels across batches accrue
  toward one threshold.
- clearing: `clearRetryCountsForTool` now also deletes the bridge-marked
  keys. A resolved bridge renames the request to the target, so the widened
  presence set keeps its stale bridge count alive across the successful
  execution; without the clearing half that resurrects the premature
  RETRY LOOP DETECTED the marker's prune was relied on to drop.

Unrelated tools are unaffected: the widening is keyed on names present in the
current batch, and an EXECUTION_DENIED refusal still keeps the wrapper name,
so it cannot retain the denied target's counters.

Tests: add 'accrues alternating bridged and direct failures of one target
across separate batches' (six single-request batches; the directive fires
once, on the fifth) and 'clears a target's bridge-marked counter when a
bridged call to it succeeds'; extend the existing mixed-channel test to also
assert the directive on the bridged half, which its name already claimed.
Dropping the presence widening turns the first red; reverting
clearRetryCountsForTool to the bare prefix turns the second red.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmumpkv7rf3
ACP wired the permission manager into isTargetExecutionAllowed, but that
option is the outer owner's execution-allowlist hook — only agent-core
supplies one, and this frontend reads neither an execution allowlist nor a
disallowedTools blocklist. So with `permissions.deny: ["web_fetch"]` driven
through ACP, resolution short-circuited with the bridge's scheduler-flavoured
wording ("execution allowlist or disallowedTools blocklist"), pointing the
operator at two knobs that cannot be the cause and never naming the deny rule,
while the identical configuration on the terminal frontend prints
`Matching deny rule: "permissions.deny: web_fetch"`.

Mirror the scheduler's split instead: the permission manager goes into
suppressArgumentPreCheck, so a pm-denied target skips the argument pre-check
(no INVALID_TOOL_PARAMS strike for a call that could never run) and the L1
enablement gate below stays ACP's only policy authority — carrying ACP's own
denial wording and its isTrustedLiveTool exemption, exactly as a direct call
to the same target already does.

The load-bearing invariants are unchanged: the call is still denied before
execution, still EXECUTION_DENIED, the target is still never built, and no
invalid-parameter strike is recorded.

Test: retarget `keeps EXECUTION_DENIED for a policy-denied bridge target
instead of a parameter pre-check refusal` from the bridge wording to ACP's own
`Tool "web_fetch" is disabled.`, and assert the bridge wording is gone.
Reverting the wiring turns it red.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmumpkv7rf3
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Scope check at 2059aea: keep this PR limited to bridge argument pre-validation and correct error attribution. Two remaining correctness issues need closure: a rejected permission lookup can escape the pre-check, and the Agent fork instructions require name even when the advertised schema omits it. The Agent refresh-coalescing changes are independent of the current schema-only pre-check; I will assess removing that expansion rather than grow it further. This does not claim to fix the separate model retry behavior in #12889. No full local suite or remote CI polling in this pass.

yiliang114 and others added 2 commits September 30, 2026 02:53
The new `keeps a %s target policy failure within its ACP call` cases stub
`getPermissionManager` with only `isToolEnabled`. The independent sibling
call in the same batch still reaches `evaluatePermissionRules`, which calls
`pm.hasRelevantRules()` whenever the tool's own default permission is not
`deny` -- and the sibling declares `getDefaultPermission: 'allow'`. The
missing method threw, so the sibling's functionResponse carried
`pm.hasRelevantRules is not a function` instead of its output and the
containment assertion (`response: { output: 'sibling completed' }`) failed
for both `denied` and `unavailable`.

No PM rules are configured in these cases, so `hasRelevantRules` returning
false is the faithful stub: the sibling is then governed by its own
`getDefaultPermission`, which is what the test already asserts. The
assertions themselves are untouched -- this only makes the fixture
callable. Also applies Prettier formatting to the new block.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmun0ao3qfm
Bring the branch up to date with main (16 commits, base was a631573) so
the CI lint-gate freshness check and the required checks run against a
current base. Clean merge, no conflicts.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmun0ao3qfm
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Local UI verification on Linux: PASS for the bridged validation error, N/A for the Responses converter

  • Mode: merge-reference, real interactive TUI driven through tmux
  • Revision: 05f4fd8952 (merge base) → 15aac030ea (head)
  • Runtime: node scripts/dev.js straight from source, isolated HOME, empty workspace, qwen3.8-max, 150x42 pane, identical prompt text in both arms
  • Surface: TUI

Scenario — the #12889 path, reached the way a model reaches it: review the schema through the bridge, then hand the target bad arguments and do not repair them.

先用 tool_search 查 web_fetch 的 schema,然后用 tool_call 调用 web_fetch,arguments 传一个空对象 {}。即使它报错也不要重试、不要修正参数、不要改用别的工具,把返回的错误原文一字不改地贴给我。

Both arms first produced ✓ 工具搜索 select:web_fetch / Reviewed 1 tool(s), then the call failed. The failure row is the whole difference:

Before 05f4fd8952:

  x 网络抓取 {}
    params must have required property 'url'

After 15aac030ea:

  x 网络抓取 {}
    Deferred tool "web_fetch" (called through tool_call) rejected the arguments: params must have required property 'url'. Pass arguments matching
    the schema returned by tool_search for "web_fetch".

网络抓取 is just the zh display name for web_fetch — the UI ran in Chinese in both arms. The model then quoted the same string back verbatim in both arms, so what the model reads is what the row shows.

Read against #12889: the unlabelled params must have required property 'url' sits under a tool_call envelope and reads as a fault in {name, arguments}; the labelled version names the tool that rejected it and says where the correct schema comes from.

  • Control: /context detail is byte-identical at both revisions (built-in tools 10.9k tokens, 21 declared, startup context 831), so nothing here moved the declaration footprint.
  • Regression pin: not run locally. The tests added in this round are the pin for this behavior; CI on the head was 22 pass / 1 pending / 8 skipped when I wrote this. I did not re-execute them.
  • Not covered by this run: keeping nested tool_call arguments open on Responses is wire-level and needs a Responses-API provider, and the Session changes surface in an ACP client rather than in this TUI. Both are outside what a terminal capture can show.

Resolve the single conflict in packages/core/src/core/coreToolScheduler.ts
as a union of two orthogonal generalizations of the same base expression:
main generalized which guidance string is attached (hadIncompleteArguments
-> INCOMPLETE_ARGS_PARAM_GUIDANCE, #12970), this branch generalized which
message body it is attached to (describeBridgedArgumentError when the call
arrived through the tool_call bridge). Each side keeps its own behavior.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmur8h2ce0s
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

Re-triage after a base merge. Head is now 8bbced957d (current main 1abccdb26a merged in), which clears the CONFLICTING state — GitHub now reports MERGEABLE.

Why the standing CHANGES_REQUESTED is stale rather than live:

  • qwen-code-ci-bot's vote (2026-10-02T06:54Z) is bound to d1799544b2, superseded by later pushes.
  • tanzhenxin's vote (2026-09-29T03:02Z) is bound to 4c7ebf961c.
  • All 45 review threads are resolved; 0 unresolved.

The merge contains no new production logic. Its only hand-written part is the conflict resolution in packages/core/src/core/coreToolScheduler.ts, a union of two orthogonal generalizations of the same base expression: main's three-way paramGuidance (wasOutputTruncated / hadIncompleteArguments, from #12970) and this branch's targetMessage (naming the bridged target). Comparing git's own predicted merge tree against the committed tree (git diff 31b033f5f9 HEAD --stat) touches that one file, so there are no drive-by edits.

Local verification on the merged head: packages/core vitest over coreToolScheduler.test.ts + tool-call.test.ts + responses-converter.test.ts = 621/621 pass; packages/cli Session.test.ts = 1118/1118 pass; packages/core tsc --noEmit clean; prettier clean.

@yiliang114
yiliang114 dismissed stale reviews from ghost October 3, 2026 06:18

Stale: this review targets an ancestor of the current head; its actionable findings were addressed in later pushes (remaining open threads, if any, are Suggestion-level and deferred).

@yiliang114
yiliang114 enabled auto-merge October 3, 2026 09:06
yiliang114 and others added 2 commits October 3, 2026 18:26
main's 1a5aae8 (#13167, run Managed session tools in a Runtime worker)
and this branch both edited the runTool catch block. Keep both behaviours:
main's ManagedRuntimeOutcomeUnknownError re-throw runs first so the typed
error still reaches the outer catch, then this branch's bridged tool_call
argument labelling wraps everything else.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmus7hcqa2c
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closed the remaining local feedback in 67ea7b62ab3f without expanding production scope.

  • Added one regression for invalid bridge/direct/bridge calls to the same target: all return INVALID_TOOL_PARAMS, response names remain model-facing, and only the third adds RETRY LOOP DETECTED. The existing implementation already uses the raw validation error for the shared key. A labelled-message mutation fails the new regression, then the unchanged production source passes it.
  • Updated both English and Chinese risk/test-plan sections: nested Responses openness also affects task_create.metadata and task_update.metadata. Replaced the stale “not run locally” statement with current evidence and retained the original-provider acceptance gap.
  • The earlier ACP three-outcome regression remains in place and was rerun. Structured ACP errorType already comes from the original target error on this head; the old wrapping concern does not require another source change. Broader schema traversal consolidation remains tracked in core: the deferred tool_call bridge enforces a declaration-schema layer that 8 tool families never enforce themselves #12999.

Validation: frozen-lockfile install and full prepare build, full typecheck, 622 core tests + 3 ACP bridge cases, changed-test lint/format/diff checks passed. Independent test-engineer observation confirmed the mixed retry regression; test-value review found it minimal and non-duplicative. Commit hooks preserved the tested test-file bytes, and all production files are unchanged by this repair.

The original #12889 prompt still requires the reporter's gpt-6-astra/OpenAI Responses service configuration, which is unavailable locally. These schema/error/retry controls do not establish that provider's model recovery, so #12889 remains open. The owner checkout failure is already repaired on main in #13306; independent native Git verification covers that repair without claiming a historical job rerun.

@yiliang114
yiliang114 requested review from doudouOUC and qqqys October 3, 2026 18:25
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@tanzhenxin Your review offered two ways forward — keep the user-visible recovery criterion open until an ACP E2E run with the original prompt passes, or narrow the PR's stated scope to bridge validation and diagnostics. The PR now does both, so re-requesting your eyes on the current head 67ea7b62ab3f.

The scope is narrowed to what was actually verified. The description claims only the Responses-converter change and the target-named validation errors, states the decoding-constraint hypothesis as unconfirmed ("under default JSON Schema rules, an empty properties map alone does not close an object"), lists that E2E under Not validated / out of scope, and links the issue as Refs #12889 (close after the E2E passes) rather than Fixes. Both Tested-on rows carry "original-provider E2E not run".

The recovery criterion stays open. #12889 is still open, and the closeout note records that the reporter's gpt-6-astra / OpenAI Responses configuration isn't available locally, so the schema/error/retry controls don't establish that provider's model recovery.

Your observation isn't disputed, and nothing since it claims to change the outcome: between 4c7ebf961c and 67ea7b62ab3f the only PR-side commit is 67ea7b62ab3f itself, which touches packages/core/src/core/coreToolScheduler.test.ts alone (+38, no production files) — everything else in that range is main merged in. So the repeated empty arguments and the loop-protection ending you measured still describe current behaviour exactly, and the E2E acceptance stays tracked on #12889 rather than being claimed here.

If the narrowed scope reads right to you, could you refresh the request-changes? Non-blocking either way — CI is green at this head (23 checks pass / 0 fail) and the branch is MERGEABLE; the request-changes is the only thing holding it.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@wenshao

wenshao commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Maintainer-local verification, round 9: ✅ merge-ready — 2436 scripted assertions executed, 2436 pass / 0 fail. Verified head 67ea7b62ab3f6cfc06497f041a8d34546a7eba73 (unchanged since CI round 8), A/B base = merge-base 1a5aae80d9cdaa6d700f23cdc355e4ac0e6f2647, trial merge into current main (41f97112ff) conflict-free.

Follow-up to the sandboxed round 8 (same head). What this round adds: a real-environment E2E A/B on macOS (real built CLI over real loopback HTTP against a scripted fake OpenAI Responses server, replaying the #12889 scenario, headless + real TUI), closure of round 8's open ACP-mutation gap (4/4 mutants killed), F-5 disproven by measurement, macOS gate datapoints, and a trial merge against a main that touched 3 of the PR's 9 files since the merge-base. Advisory evidence for reviewers — not an approval.

中文摘要

结论:merge-ready(2436 条脚本断言全过)。 第 9 轮为维护者本地跟进轮,head 与第 8 轮 CI 沙箱相同。增量:(1) 真实环境端到端 A/B——真实构建的 CLI 走真实 HTTP 打脚本化假 Responses 服务器复现 #12889:base 臂线上 schema 为 {type:"object",properties:{}}、报错为裸 params must have required property 'url';head 臂嵌套节点多 additionalProperties:true、报错带目标名与 tool_search 指引。两臂各 14/14 断言通过,真实 TUI 截图同结论。(2) 补完第 8 轮被砍的 ACP 变异矩阵:4/4 变异体被杀、0 存活、基线 1139/1139 全绿。(3) 第 8 轮的 F-5(类型化错误重抛顺序无测试固定)被测量推翻——删除重抛行让 3 个测试变红。(4) 试合并当前 main 无冲突,合并树 622/622 全绿。继承第 8 轮两个非阻塞 Note:F-4(遍历不进入 $defs 等 12 个兄弟关键字,base 相同)与 C4(strict structured outputs 下显式 additionalProperties:true 会 400,但该线路从不发 strict,两臂实测确认)。未覆盖:报告者自家 provider 的 #12889 验收(前提仍未证实,PR 自身亦声明押后)、逐 commit 归因、Windows/Linux 本机跑。

Previous-round findings (round 8 → now). Head unchanged; all four PR-touched production files re-hashed identical to round 8's citations, so its harness findings carry forward on an identical input closure: F-2 fixed (carried), F-3 fixed (carried + statically re-verified), F-4 stands (pre-existing walk boundary, identical on base), C4 stands (re-measured on the real wire: no strict key emitted on either arm), F-1 closed this round (matrix below), F-5 disproven (deleting the re-throw turns 3 outcome-unknown tests red — the ordering is pinned).

Real-environment E2E A/B (recorded wire bytes; 14/14 assertions per arm, 28 total):

cell base 1a5aae80 head 67ea7b62
tool_call nested arguments node on the Responses wire {type:"object",properties:{}} {type:"object",properties:{},additionalProperties:true}
root patch / strict flag additionalProperties:false kept / absent same
error the model receives for tool_call {name:"web_fetch", arguments:{}} params must have required property 'url' (bare) Deferred tool "web_fetch" (called through tool_call) rejected the arguments: … Pass arguments matching the schema returned by tool_search for "web_fetch".
turn completes (exit 0, no retry loop) ✓ ✓

01-e2e-ab-responses-wire

Real TUI (tmux-driven, same fake server, same prompt) — base renders the bare #12889 symptom; head renders the labelled error:

02-tui-head-labelled

03-tui-base-unlabelled

ACP mutation matrix (round 8's cut rows, run to completion; Session.test.ts = 1139 tests/row, sequential, sha256-verified restore): baseline GREEN 1139/1139; positive control KILLED; no-flag KILLED (build-error case); drop-!toolBuildSucceeded KILLED (execute-error case); always-label KILLED 9/1139; swallow-typed-error KILLED 3/1139. 4 mutants killed / 0 survived.

04-acp-mutation-matrix

Trial merge into current main: conflict-free (merge commit a6d490ef7c); the 3 affected core suites re-run on the merged tree: 622/622 green. Gates at head (macOS arm64, Node 24): typecheck exit 0 / 0 error TS; eslint + prettier on the 9 changed files exit 0 (both with planted-violation liveness proofs); Session.test.ts 1139/1139; 3 core suites 622/622.

Not covered: the reporter-provider #12889 acceptance run (the premise that the reporter's backend constrains decoding remains unconfirmed — the PR body says the same and defers closing #12889); round 8's 47-schema census (carried on a byte-identical converter input); per-commit attribution (37 commits, aggregate diff verified); Windows/Linux local runs. The update available banner in the TUI captures is this machine's network access, unrelated to the PR; the payload stayed inside the scratch $HOME, host qwen untouched (still 0.24.7).

Harness scripts and raw logs (requests-{base,head}.jsonl, mx-acp-*.json, typecheck log) preserved locally in tmp/pr12901-verify-20261005-220624/; happy to push them to the assets branch on request.

— Qwen Code · maintainer-local verification (round 9)

@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical-only scan at head 67ea7b62ab3f6cfc06497f041a8d34546a7eba73 — complete.

Verdict: COMMENT. I found no Critical in this diff, and both Criticals I filed on this PR in earlier rounds are now resolved. This is not an approval for one reason only: a maintainer's CHANGES_REQUESTED is still live and its stated acceptance criterion is, by the author's own account at this head, unmet.

My two prior Criticals are resolved — by deletion, which is the strongest form

My review at 0ab0c146 filed R6-1 (the bridge pre-check's structuredClone + flipped additionalProperties could claim a schema $id in the module-level Ajv singleton, making the target's later build-time validation throw schema with key or id already exists and hit SchemaValidator.validate's documented fail-open return null, silently disabling validation for the process) and R4-1 (the pre-check enforced the model-visible schema layer on every target, so it could refuse a bridged call the target's own validateToolParams would have normalized and accepted).

Both are gone at this head because the pre-check itself is gone. packages/core/src/tools/tool-call.ts is now +14/-0: a single pure addition of describeBridgedArgumentError, a string formatter. No structuredClone, no SchemaValidator.validate call, no additionalProperties flip survives anywhere in this PR's diff, so neither the shared-Ajv-state chain nor the stricter-than-target entrance has any code left to run through. The PR body records the removed mechanism on archive branches, and the description now states the bridge pre-check was dropped.

R4-1's direction is also closed positively rather than merely vacated: the replacement only relabels a rejection the target's own build() already raised, so it cannot refuse a call the target would accept. That is the property the finding asked for.

Current diff — no Critical

I read all five production files at this head and checked the four places a defect could plausibly hide:

  • The retry budget is still keyed on the raw error, not the relabelled one. recordBatchRetryableToolError(reqInfo.name, invocationOrError.message) at coreToolScheduler.ts:3400 uses the target's raw message and raw name, while only displayError and finalError carry targetMessage. Had the count keyed on targetMessage, a bridged and a direct call to the same target would accrue into separate buckets and alternating calls would never trip loop protection — the exact regression the new shares validation retries across bridged and direct calls test pins with expect(message.includes('RETRY LOOP DETECTED')).toBe(index === 2) across a bridge/direct/bridge sequence. The body reports this test was mutation-checked against keying by the labelled message.
  • The discriminator has a negative control. The relabel is gated on reqInfo.modelFacingName !== undefined in the scheduler, and leaves a direct call's validation error unlabelled asserts not.toContain('Deferred tool') for the same missing-url failure sent directly. Both directions of the gate are pinned rather than just the new one.
  • The ACP relabel cannot misattribute an execution failure as an argument rejection. Session.ts:16044 gates on bridgedThroughToolCall && !toolBuildSucceeded. toolBuildSucceeded is not new to this PR — it is declared at :14035, set at :14049, and already consumed at :16105 and :16112 (recordInvalidToolParams: !toolBuildSucceeded) for precisely this build-versus-execution distinction, so the new use reuses an established flag rather than introducing a second notion of it.
  • The converter change preserves every stated exception. isRoot is threaded only from the public entry (normalizeResponsesParameters passes true); array-item and property recursion default to false, so nested objects are the only ones marked. The additionalProperties: true write sits inside the existing properties === undefined patch branch and is guarded by out['additionalProperties'] === undefined, so an explicit additionalProperties: false survives and the root keeps the bare patch a zero-argument tool needs. The tests pin root-versus-nested across properties, array items, anyOf, oneOf and allOf.

The added describeBridgedArgumentError export in packages/core/src/index.ts is consumed by the ACP session in packages/cli, so the cross-package import is the reason for the barrel entry rather than a speculative widening.

The one reason this is not an approval

@tanzhenxin's CHANGES_REQUESTED (submitted 2026-09-29 against 4c7ebf96) is still present in the review collection and reviewDecision still reports CHANGES_REQUESTED. It is not a code-defect finding but an E2E acceptance result: the original #12889 user-visible failure reproduced at that head, and the stated condition for clearing it is "an ACP E2E run with the original prompt completes successfully, or narrow the PR's stated scope to bridge validation and diagnostics."

At the current head the PR body still lists "Acceptance: rerun the #12889 E2E on the reporter's Responses provider with the original prompt" as an open test-plan item and states plainly: "This verifies schema/error/retry boundaries locally; the original #12889 provider E2E has not been run." The Tested-on table repeats that. So the first half of the maintainer's condition is unmet and admitted unmet.

Two things I want to record rather than adjudicate:

  • That reproduction predates the additionalProperties: true converter change, which is this PR's actual root-cause hypothesis for the empty-arguments loop. Its observation table may therefore no longer hold at this head. But the body itself grades the decoding-constraint premise "unconfirmed", and no rerun exists, so recovery cannot be established by reading code — and an unconfirmed historical blocker is treated as blocking under this channel's rules.
  • @wenshao approved at this head. The approval carries an empty body, so there is no scope disclosure to borrow coverage from, and it does not clear reviewDecision while the earlier request stands.

All 37 inline review threads read; zero unresolved. Every thread I checked was verified against the head source rather than accepted on its resolved flag — the two threads whose replies say "not taking this one this round" and "escalating as human-gated" both concerned the pre-check that has since been deleted outright, which supersedes the dispute rather than leaving it open.

CI

Healthy at this head — Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, both Desktop Shell legs and review-pr all pass, with no failing check.

Next step

Nothing here asks for a code change; on the code this is ready. To clear the merge gate, either run the #12889 E2E on the reporter's Responses provider with the original prompt and post the outcome, or ask @tanzhenxin to withdraw or convert the change request now that the scope is narrowed to converter behaviour plus error attribution and the decoding premise is explicitly marked unconfirmed.

Changed files span packages/core/src/core/, packages/core/src/tools/ and packages/cli/src/acp-integration/, so require_code_owner_review applies. This review is submitted as qqqys.

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope. Source only — responses-converter.ts (schema patching logic + isRoot propagation), tool-call.ts (describeBridgedArgumentError, exports), coreToolScheduler.ts (labelled-message assembly, retry counter keying), Session.ts (bridged-build-failure labelling), and the four test files. Not reviewed: E2E on the original #12889 provider (explicitly out of scope); Windows/Linux runtime behaviour (PR flags both ⚠️).

Prior-round ledger. Round 1 (0ab0c146, DISMISSED) found two blockers in the argument pre-check ($id shared-registry pollution, stricter-than-target validation). Both are resolved: the pre-check is entirely removed at this head. No surviving round-1 finding.


Findings

No blocking findings.

What was checked

normalizeResponsesSchemaNode recursive call structure — confirmed from source that all recursive call sites (array-items branch, per-property iteration at nextProps[k], items field, and combinator-list processing) omit the isRoot parameter, defaulting to false. Only the entry point normalizeResponsesSchemaNode(schema, true) passes true. Result: exactly the top-level parameter object is exempted from the additionalProperties: true patch; every nested {type:"object"} node added by the Azure compat pass gets the flag unless additionalProperties is already specified. Tests for the explicit-false and explicit-typed-schema cases confirm the guard is respected.

describeBridgedArgumentError labelling scope — the condition bridgedThroughToolCall && !toolBuildSucceeded in Session.ts correctly limits labelling to calls where the target's own build() threw. Execute failures (toolBuildSucceeded = true) pass through the original error unchanged. The ACP it.each parameterised test covers the success / build-error / execute-error paths and verifies this distinction explicitly.

Retry counter budget sharing — confirmed by code comment ("Counts accumulate per (tool, error message) pair") and by the new shares validation retries across bridged and direct calls test: the counter is keyed on reqInfo.name + invocationOrError.message (the raw validation error before wrapping), so bridged and direct calls to the same target with identical invalid arguments consume the same budget. targetMessage is used only for the displayed error, not the counter key; the labelled-message mutation in the test verifies the production source uses the raw key.

Public API extension — describeBridgedArgumentError is exported from packages/core/src/index.ts. This matches the existing pattern for resolveDeferredToolCall and DEFERRED_TOOL_CALL_* prefixes, and is needed by the CLI package's Session.ts. Additive, no breaking change.

Cross-check against existing reviews

  • tanzhenxin CHANGES_REQUESTED (4c7ebf961c): E2E with gpt-6-astra still reproduced the loop at that head. The current head explicitly narrows scope to bridge validation and diagnostics, acknowledges the provider E2E as unverified, and keeps #12889 open. The scope narrowing is what tanzhenxin offered as the alternative path forward.
  • qwen-code-dev-bot / chiga0 round-1 criticals (0ab0c146): both concerned the now-removed pre-check; resolved by deletion.
  • qqqys (0ab0c146): same two criticals, same resolution.
  • doudouOUC (a674e3a6, "one residual"): the comment was filed against an intermediate head; the subsequent commit log shows compileIsolated and the $id-related fixes were applied, and the current head removes the pre-check entirely.
  • wenshao (67ea7b62, maintainer local): 2436 assertions pass, real-environment E2E A/B on macOS, merge-ready verdict.

No blocking findings. Approval blockers: none.

Reviewed with AI assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants