Tag embedded subagent gap-fill rows as delivery mirrors#88687
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 13, 2026, 9:14 AM ET / 13:14 UTC. Summary PR surface: Source +7, Tests +9. Total +16 across 2 files. Reproducibility: yes. at source level: current main writes gap-fill rows with CLI child metadata, while replay filtering drops only Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the narrow writer-side delivery-mirror tagging if maintainers accept the raw transcript metadata compatibility change, while keeping the linked issue or companion hardening path open for the remaining defense-in-depth decision. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main writes gap-fill rows with CLI child metadata, while replay filtering drops only Is this the best way to solve the issue? Yes: tagging the display-only row correctly at write time is the narrowest primary fix because the replay and transcript mirror code already define AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 78c66742ab97. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +7, Tests +9. Total +16 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Nice catch. I like that this keeps the gap-fill rows on the existing One small follow-up question: do we have any downstream transcript/debug tooling that keys off |
ac11ae0 to
24ccf84
Compare
24ccf84 to
b1764cf
Compare
Embedded assistant gap-fill transcript rows are display-only delivery state. Store them as OpenClaw delivery mirrors so provider replay filters do not mistake child provider text for provider conversation state. Constraint: Embedded gap-fill transcript rows are display-only state and must match existing transcript-only replay filters. Rejected: Preserve child runtime provider/model metadata for gap-fill rows | that makes display mirrors look like provider conversation state. Confidence: high Scope-risk: narrow Directive: Do not tag embedded assistant gap-fill mirrors with child provider/model metadata unless replay filters are updated with an equivalent transcript-only contract. Tested: git diff --check origin/main...HEAD; oxlint changed files; targeted Vitest attempt-execution/replay-history suites (3 files, 95 passed); tsgo core-test project; codex review --base origin/main. Not-tested: No live provider-managed thinking/signature conversation was replayed end to end; external raw transcript consumers may observe the intentional gap-fill metadata change.
b1764cf to
5dcee2e
Compare
|
No downstream tooling keys off The only readers of the I took your second point and made the shape change explicit rather than implicit — added a "Metadata change" callout to the PR body with a before/after table ( |
|
Closing this because the proposed metadata change breaks the shipped embedded gap-fill contract.
The duplicate-row failure mode is already handled at the correct boundary by tail dedupe, including trailing custom rows, in #83635. The consecutive-assistant Anthropic failure is now fixed structurally by #87346, which preserves signed thinking/tool-call blocks and passed a live Anthropic tool-result-adjacency test plus the full CI matrix. Evidence that would change this decision: a narrower writer distinction proving a specific gap-fill row is redundant while preserving missing-canonical-turn fallback rows, with an end-to-end test showing both replay exclusion for the redundant case and replay retention for the only-answer case. Thanks for the careful investigation and proof work here; it exposed an important ownership distinction even though this metadata rewrite is not safe to land. |
Refs #87329 (Fix 1 — PRIMARY: tag echo messages correctly at write time)
User-facing bug
Subagent announce / embedded handoff gap-fill can append a user-visible assistant mirror using the child runtime provider and model metadata, such as
anthropic/claude-*, instead of marking the row as an OpenClaw transcript-only delivery mirror. Replay filters exclude transcript-only mirrors byprovider: "openclaw"withmodel: "delivery-mirror"/gateway-injected, so a display-only mirror with child provider metadata can later be replayed back into a provider turn. In conversations with provider-managed thinking/signature blocks, that stale mirror can surface as errors likethinking blocks cannot be modified.Exact repro
Source-level reproduction:
persistCliTurnTranscriptfor an embedded assistant gap-fill with:embeddedAssistantGapFill: trueresult.meta.agentMeta.provider: "claude-cli"or another runtime providerresult.meta.finalAssistantVisibleTextapi: "cli",provider: "claude-cli", andmodel: "opus".api: "openai-responses",provider: "openclaw", andmodel: "delivery-mirror".Fix summary
openclaw/delivery-mirror.Metadata change (explicit callout)
This PR intentionally changes the persisted shape of embedded gap-fill assistant rows, so it is more than a replay fix — raw transcript / debug readers should be aware:
api"cli""openai-responses"provider"claude-cli")"openclaw"model"opus")"delivery-mirror"usageScope is strictly the
gapFillbranch inpersistCliTurnTranscript(src/agents/command/attempt-execution.ts:365). Normal (non-gap-fill) CLI rows are untouched and keepapi:"cli"+ real provider/model/usage.Downstream
api:"cli"consumers — verified none affected. The.apifield is read only by the LLM transport/provider layer for foreign-tool-call detection (src/llm/providers/transform-messages.ts:100,openai-responses-shared.ts:202, comparingsource.api === model.api). Gap-fill rows never reach those comparisons: rows taggedprovider:"openclaw"+model:"delivery-mirror"are inTRANSCRIPT_ONLY_OPENCLAW_MODELS(src/agents/embedded-agent-runner/replay-history.ts:241) and are dropped from the replay copy before any provider prompt is built. Every other gap-fill/delivery-mirror filter in the tree keys on theprovider/modelpair, notapi.Targeted tests
src/agents/command/attempt-execution.cli.test.tsto prove embedded gap-fill rows are tagged asopenclaw/delivery-mirror.delivery-mirrorrows remain excluded from embedded provider replay.Real behavior proof
Behavior addressed: Embedded subagent assistant gap-fill rows are now stored as OpenClaw delivery mirrors, so provider replay filters do not treat display-only text as provider conversation state.
Real environment tested: Local OpenClaw checkout on branch
fix/subagent-announce-delivery-metadata, commit5dcee2eae5, rebased onto currentorigin/main; Nodev24.4.1.Exact steps or command run after this patch:
git diff --check origin/main...HEAD./node_modules/.bin/oxlint src/agents/command/attempt-execution.ts src/agents/command/attempt-execution.cli.test.tsnode scripts/run-vitest.mjs src/agents/command/attempt-execution.cli.test.ts src/agents/embedded-agent-runner/replay-history.test.tsEvidence after fix:
Writer and replay halves verified together. The merge-readiness ask is to show the gap-fill row is written as
openclaw/delivery-mirrorand is then excluded from the next provider prompt. These two real test suites prove the two halves of exactly that path end to end:src/agents/command/attempt-execution.cli.test.ts(25 tests passed): the embedded gap-fill assertion (embeddedAssistantGapFill: true) reads back the persisted transcript row and proves it is written asapi: "openai-responses",provider: "openclaw",model: "delivery-mirror", while non-gap-fill CLI rows keep real provider/model metadata.src/agents/embedded-agent-runner/replay-history.test.ts(35 tests passed):normalizeAssistantReplayContentis given a history containing exactly thatprovider: "openclaw"/model: "delivery-mirror"row alongside a real provider reply, and the test asserts the delivery-mirror row is dropped from the replay copy (outlength 2) while the real provider reply survives. That is the row never re-entering the provider prompt.Together: the shape the writer persists (suite 1) is precisely the shape the replay filter excludes (suite 2). Copied terminal summary:
oxlintpassed on the changed source and test files (exit 0).git diff --check origin/main...HEADpassed.Live replay-path output — ran a single script that chains the patched writer to the replay filter on real functions (no hand-built input): it writes a session store, calls
persistCliTurnTranscriptwithembeddedAssistantGapFill: true, reads the persisted JSONL row back off disk, then passes that exact row throughnormalizeAssistantReplayContent(src/agents/embedded-agent-runner/replay-history.ts):This is the full patched writer-to-replay path on real code: the patched writer persists the gap-fill row to disk as
openclaw/delivery-mirror, and that exact persisted row is then excluded from the rebuilt replay (3 → 2 rows) so it never re-enters the provider prompt, while the genuine provider reply is retained.Observed result after fix: Embedded gap-fill transcript rows use
api: "openai-responses",provider: "openclaw", andmodel: "delivery-mirror"while normal CLI transcript rows still preserve real provider/model metadata.What was not tested: No live provider-managed thinking/signature conversation was replayed end to end against a running embedded agent and a real provider. That path requires a live provider session, which was not available in this environment; the writer-to-replay contract is instead proven by the two connected real test suites above on the actual production functions (
persistCliTurnTranscriptoutput shape andnormalizeAssistantReplayContentexclusion).Not tested / known gaps
api: "cli"for embedded gap-fill rows, but external scripts that read raw transcript metadata may observe the intentional gap-fill metadata change from child CLI provider/model toopenclaw/delivery-mirror.AI-assisted disclosure
This patch was prepared with AI assistance. I used local code inspection, targeted regression tests, and explicit command output to validate the behavior above.