feat(protocol): local_task_protocol — read-side module, golden corpus tests, reader switch-over (step 3) - #1954
Conversation
The new header must be in confine_user_content's _HEADER_KEYS like every other header — otherwise a message body containing a literal 'interaction_type: ...' line survives into the task file undefanged and could forge the field under a full-scan reader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests (step 3a) The Local Task Protocol's task-file half as pure stdlib functions (the result-marker half already lives in result_markers.py, #873): - schema constants: INTERACTION_TYPES (single source, asserted equal to the gateway whitelist), PRIORITIES, ACCESS_TIERS, KNOWN_HEADER_KEYS (from a 3.4k-file archive corpus survey) - valid_task_id — the filename/traversal gate for ids arriving in content - parse_task_headers — the safe stop-at-task: reader (PR #982 delimiter rule) - parse_task_headers_trusted — full-scan last-wins, for writers that neutralize bodies (_one_line strip or confine_user_content defang); last-wins is load-bearing for the gateway's access_tier-written-last defense - archive rules: month partitioning, find_archived_task candidate walk, iter_archived_tasks corpus sweep Writers untouched (read-side-first). The module docstring records the real shape taxonomy the corpus survey established: TS writers + agent-api /task are task-last; every Python bridge is task-mid — which means stop-at-task: readers under-read bridge files (e.g. parse_priority_from_text has never seen a bridge-written priority: field). Reader consolidation and writer convergence are follow-up steps with per-bridge byte-identical acceptance. Tests: 33 checks — per-shape fixtures (discord/gateway/voice/chat/phone- legacy/health/forged-body), id validation, archive rules, vocabulary parity with the gateway, plus a live-corpus sweep (3,397 archived files parse clean) that skips gracefully where no workspace exists (CI). R1 honored: stdlib-only, no I/O beyond the explicit archive helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch filter predates this staging branch — without it, refactor-step PRs (like this one) get zero CI. Same pattern staging-workspace-revamp used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Cold review — strong module, one substantive finding + a couple of minor notes. 🔍 The trust-model split is the best part: Finding (non-blocking, but the reader-consolidation follow-up must account for it):
So the docstring "Mirrors the gateway bridge's
The gateway's Options: relax the regex to match Minor:
Nice work — the finding is about the next step's safety, not this PR's correctness. — Sutando (@sutando-wu-air.agent) |
Same branch-filter gap as ci.yml — without it, refactor-step PRs with Python changes skip the 95% diff-coverage bar until the staging→main merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 32%. Diff CoverageDiff: origin/staging-interaction-planes...HEAD, staged and unstaged changes
Summary
|
… 3b, reader 1/3) parse_priority_from_text delegates header extraction to the protocol module's safe parser; the legacy blank/'---' stop is preserved locally as a documented pre-#982 quirk. Deliberate tightening: header keys are canonical lowercase at column 0 — asserted as the ONLY behavioral delta in the new invariance test, which dual-runs a verbatim copy of the legacy implementation against the new one over adversarial fixtures and the live corpus (3,399 archived files, zero verdict changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
john-the-dev
left a comment
There was a problem hiding this comment.
LGTM on the protocol module itself. A few notes:
Strong points:
- The two-parser design (
parse_task_headersvsparse_task_headers_trusted) with explicit trust model documentation is the right call — this is exactly the PR #982 injection surface documented clearly once and for all - Stdlib-only is correctly enforced (R1 invariant)
- Golden corpus tests are the right verification strategy for a read-side refactor
- Additive only — no existing code path changes, so zero regression surface here
One flag for the write-side phase: parse_task_headers_trusted returns LAST occurrence wins for access_tier. The PR body says the gateway writes access_tier last to beat a forged claim — make sure the write-side PR documents that ordering invariant on the write path too, not just in this docstring.
Base is staging-interaction-planes, so this won't land until that branch merges. No concerns on the content.
|
Review result: changes requested. Blocking finding:
Also, GitHub's coverage gate is red because Focused checks run in
Reviewed by Qingyun's Codex. |
…_lenient (step 3b, reader 2/3) New protocol parser: parse_task_headers_lenient — full scan, FIRST occurrence per key wins. This is the shape-union reader: the archive proves producer field order changed across eras (May-2026 voice tasks are task-MID; today's are task-last), and _task_source classifies files of any age. The stricter stop-at-task: parser flips the DM-fallback verdict on 23 real archived voice tasks — caught by this commit's corpus dual-run, which now asserts extraction-identical over all 3,401 files. Known pre-existing first-wins spoofability (a body line can supply a key the file legitimately lacks) is documented at the parser and pinned by a fixture; hardening it changes historical verdicts and is an owner decision, not a read-side refactor. Gateway _valid_tid deliberately NOT switched: it is a remote-compat gate (any dot/dash id from older gateways), a different contract from the local task-id canon in valid_task_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re-review result on Blocking findings:
Focused checks run in
Failing focused check:
Reviewed by Qingyun's Codex. |
…ority tests/task-priority-stop-at-task-delimiter.test.py (and any other consumer) loads task_priority standalone via importlib without src/ on sys.path — the bare import broke it in CI, which also failed the coverage gate (suite dies under instrumentation before coverage is measurable). Same self-pathing pattern the bridges use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
That refactor's staging branch merged 2026-06-01 (M0); the filter entry is dead config. Owner-requested cleanup alongside the staging-interaction-planes addition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the unmeasurable discord glue The coverage gate runs without a live workspace (corpus sweeps skip) and without discord.py (the bridge can't import under instrumentation): - find_archived_task / iter_archived_tasks now have fixture-based tests (candidate walk across live/processed/flat/month dirs, stray-dir and malformed-id gating) instead of relying on the live-corpus path. - the two _task_source glue lines get pragma: no cover with the rationale at the site — their semantics are pinned by the invariance test's corpus dual-run, but the module itself is unimportable in the gate env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… reader (Codex blocker) The trusted/lenient parsers' body looked like the full work item but silently dropped continuation lines (health-check failure bullets, phone hint/transcript sections). A header scan cannot losslessly split task-mid continuation content from trailing headers, so stop pretending: - TaskHeaders.body is now DOCUMENTED per parser: full work item for the task-last safe parser; scalar first-task:-line value for trusted/lenient. - New task_body(text): the shape-independent lossless work-item reader — everything from the first task: line onward, verbatim. For task-mid files that includes trailing header lines; the honest trade is stated at the function. - Tests pin the contract: trusted body excludes the health bullet (scalar by contract, not by accident), task_body keeps health bullets and phone hint/transcript, equals safe-parser body on task-last shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three Codex blockers are now resolved on this head:
CI is fully green (coverage gate included). Ready for re-review. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed the current head (4519641) and found two issues worth fixing before this becomes the shared parser surface:
-
[P2]
src/local_task_protocol.py:196promotes anykey:line into headers, butsrc/task_body_guard.py:40only defangs a smaller header set. A Discord/Telegram body containinginstructions:,hint:, orfrom:survivesconfine_user_content()and is returned as trusted metadata by the lenient/trusted parsers. Please either restrict accepted parser keys or make the parser and guard share the same guarded header list. -
[P2]
src/local_task_protocol.py:170andsrc/local_task_protocol.py:193truncate task-mid bodies to only the firsttask:line. Real Discord and health-check tasks can have continuation lines before the remaining headers, so future consumers usingTaskHeaders.bodywould silently lose details. The corpus sweep currently checks parse/no-id only, not body fidelity.
Verification run locally against the refreshed PR head: tests/local-task-protocol.test.py, tests/task-priority-invariance.test.py, tests/discord-task-source-invariance.test.py, tests/discord-bridge-dm-fallback-source-guard.test.py, py_compile, and git diff --check.
|
Re-review result on The prior direct-import failure, archive-walk coverage gap, and task-mid body contract issue are resolved on this head. Remaining blocking finding:
Focused checks run in
Reviewed by Qingyun's Codex. |
…Codex P2s) Both P2s from the re-review, fixed as one coherent mechanism: 1. Parsers promote ONLY KNOWN_HEADER_KEYS to headers, and task_body_guard._HEADER_KEYS now IS that list (imported — single source of truth). Lockstep is the invariant: no key a parser would trust can survive undefanged in user-supplied content. Side benefit: junk key-shaped lines (transcript 'Caller: hi') can no longer masquerade as metadata. 2. With the vocabulary boundary in place, task-mid bodies become losslessly splittable: trusted/lenient body = the task: line's content plus every subsequent NON-vocabulary line — health-check bullets and phone transcript dialogue stay in body, trailing real headers are excluded. Every post-task: line lands in exactly one of headers/body. Tests: lockstep assertion (guard set == parser vocabulary), forged non-classic keys (instructions/hint/from/transcript/attempts) defanged, health bullets + transcript preserved in trusted body, and a corpus-wide body-fidelity sweep (3,415 archived files: no non-header line lost). Full suite: injection-guard, dm-fallback-guard, both invariance tests, interaction-type, gateway, priority — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@cla-assistant check |
|
Both P2s from the re-review are fixed on the current head, as one mechanism:
Full suite green locally (injection-guard, dm-fallback-guard, both invariance tests, interaction-type, gateway, priority). CI re-running. |
…ice (steps 3+4 → main) (#1962) * feat(tasks): additive interaction_type header at every task producer (interaction-planes step 1) A task producer is a (source, interaction_type) pair, not a source alone. This stamps the pair at every write site — purely additive, old readers ignore the extra line, no consumer behavior changes: - message: slack/telegram/discord bridges, twilio SMS + voicemail, chat template - realtime_audio: voice work-tool (task-bridge.ts), phone work-tool (delegateTask) - tool_initiated: writeChatTask (Gemini inline tool), agent-api /task (agent-to-agent) - system_event: health-check --emit-task, context-drop (app + task-bridge), github-webhook, twilio incoming-call, phone call-end summary - remote-gateway-bridge: passes through the gateway's interaction_type when present, defaults to message (all current gateway traffic is Matrix room messages) tests/interaction-type-header.test.py sweeps every producer write site and fails if a source: serialization lacks the paired interaction_type within the write window — so new producers can't silently ship without it. First step of the interaction-planes refactor (snapshot: v0.5.0-pre-interaction-planes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tasks): harden gateway interaction_type + pair source: phone on delegateTask Round-2 improvements on the same PR: - remote-gateway-bridge: whitelist the pass-through against the six-value interaction-plane vocabulary — the gateway is outside the trust boundary, so an unknown remote value degrades to "message" instead of landing verbatim in the local task file. - conversation-server delegateTask: add the missing source: phone so the producer carries the full (source, interaction_type) pair. This activates two consumers that always listed "phone" but never matched because the writer predates the source header: task_priority phone→urgent, and discord-bridge DM_FALLBACK_SOURCES (a result landing after the call ended now DMs the owner instead of rotting unclaimed). - call-end summary writer deliberately does NOT get source: phone — its task instructions already have the core DM the owner, so the bridge fallback would deliver the same summary twice. Comment at the writer + a serialized- form-only test assertion guard the double-DM regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gateway): unit-cover the interaction_type whitelist serialization The #1933 coverage gate (first PR through it) flagged the new remote-gateway-bridge lines as uncovered — the sweep test greps write sites but never executes them. This exercises _write_task directly: whitelisted pass-through, unknown-value degradation to message (trust boundary), absent-field default, full vocabulary round-trip, and the access_tier-last ordering guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(protocol): local_task_protocol — read-side module, golden corpus tests, reader switch-over (step 3) (#1954) * fix(task-body-guard): defang interaction_type in untrusted bodies The new header must be in confine_user_content's _HEADER_KEYS like every other header — otherwise a message body containing a literal 'interaction_type: ...' line survives into the task file undefanged and could forge the field under a full-scan reader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(protocol): local_task_protocol read-side module + golden corpus tests (step 3a) The Local Task Protocol's task-file half as pure stdlib functions (the result-marker half already lives in result_markers.py, #873): - schema constants: INTERACTION_TYPES (single source, asserted equal to the gateway whitelist), PRIORITIES, ACCESS_TIERS, KNOWN_HEADER_KEYS (from a 3.4k-file archive corpus survey) - valid_task_id — the filename/traversal gate for ids arriving in content - parse_task_headers — the safe stop-at-task: reader (PR #982 delimiter rule) - parse_task_headers_trusted — full-scan last-wins, for writers that neutralize bodies (_one_line strip or confine_user_content defang); last-wins is load-bearing for the gateway's access_tier-written-last defense - archive rules: month partitioning, find_archived_task candidate walk, iter_archived_tasks corpus sweep Writers untouched (read-side-first). The module docstring records the real shape taxonomy the corpus survey established: TS writers + agent-api /task are task-last; every Python bridge is task-mid — which means stop-at-task: readers under-read bridge files (e.g. parse_priority_from_text has never seen a bridge-written priority: field). Reader consolidation and writer convergence are follow-up steps with per-bridge byte-identical acceptance. Tests: 33 checks — per-shape fixtures (discord/gateway/voice/chat/phone- legacy/health/forged-body), id validation, archive rules, vocabulary parity with the gateway, plus a live-corpus sweep (3,397 archived files parse clean) that skips gracefully where no workspace exists (CI). R1 honored: stdlib-only, no I/O beyond the explicit archive helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run CI on staging-interaction-planes PRs and pushes The branch filter predates this staging branch — without it, refactor-step PRs (like this one) get zero CI. Same pattern staging-workspace-revamp used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run the coverage gate on staging-interaction-planes PRs too Same branch-filter gap as ci.yml — without it, refactor-step PRs with Python changes skip the 95% diff-coverage bar until the staging→main merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): task_priority reads via local_task_protocol (step 3b, reader 1/3) parse_priority_from_text delegates header extraction to the protocol module's safe parser; the legacy blank/'---' stop is preserved locally as a documented pre-#982 quirk. Deliberate tightening: header keys are canonical lowercase at column 0 — asserted as the ONLY behavioral delta in the new invariance test, which dual-runs a verbatim copy of the legacy implementation against the new one over adversarial fixtures and the live corpus (3,399 archived files, zero verdict changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): discord _task_source reads via parse_task_headers_lenient (step 3b, reader 2/3) New protocol parser: parse_task_headers_lenient — full scan, FIRST occurrence per key wins. This is the shape-union reader: the archive proves producer field order changed across eras (May-2026 voice tasks are task-MID; today's are task-last), and _task_source classifies files of any age. The stricter stop-at-task: parser flips the DM-fallback verdict on 23 real archived voice tasks — caught by this commit's corpus dual-run, which now asserts extraction-identical over all 3,401 files. Known pre-existing first-wins spoofability (a body line can supply a key the file legitimately lacks) is documented at the parser and pinned by a fixture; hardening it changes historical verdicts and is an owner decision, not a read-side refactor. Gateway _valid_tid deliberately NOT switched: it is a remote-compat gate (any dot/dash id from older gateways), a different contract from the local task-id canon in valid_task_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): self-sufficient local_task_protocol import in task_priority tests/task-priority-stop-at-task-delimiter.test.py (and any other consumer) loads task_priority standalone via importlib without src/ on sys.path — the bare import broke it in CI, which also failed the coverage gate (suite dies under instrumentation before coverage is measurable). Same self-pathing pattern the bridges use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: retire staging-workspace-revamp from workflow branch filters That refactor's staging branch merged 2026-06-01 (M0); the filter entry is dead config. Owner-requested cleanup alongside the staging-interaction-planes addition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(protocol): cover the archive-walk helpers with fixtures; pragma the unmeasurable discord glue The coverage gate runs without a live workspace (corpus sweeps skip) and without discord.py (the bridge can't import under instrumentation): - find_archived_task / iter_archived_tasks now have fixture-based tests (candidate walk across live/processed/flat/month dirs, stray-dir and malformed-id gating) instead of relying on the live-corpus path. - the two _task_source glue lines get pragma: no cover with the rationale at the site — their semantics are pinned by the invariance test's corpus dual-run, but the module itself is unimportable in the gate env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): define per-parser body contract + lossless task_body() reader (Codex blocker) The trusted/lenient parsers' body looked like the full work item but silently dropped continuation lines (health-check failure bullets, phone hint/transcript sections). A header scan cannot losslessly split task-mid continuation content from trailing headers, so stop pretending: - TaskHeaders.body is now DOCUMENTED per parser: full work item for the task-last safe parser; scalar first-task:-line value for trusted/lenient. - New task_body(text): the shape-independent lossless work-item reader — everything from the first task: line onward, verbatim. For task-mid files that includes trailing header lines; the honest trade is stated at the function. - Tests pin the contract: trusted body excludes the health bullet (scalar by contract, not by accident), task_body keeps health bullets and phone hint/transcript, equals safe-parser body on task-last shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): vocabulary-locked parsers + lossless task-mid bodies (Codex P2s) Both P2s from the re-review, fixed as one coherent mechanism: 1. Parsers promote ONLY KNOWN_HEADER_KEYS to headers, and task_body_guard._HEADER_KEYS now IS that list (imported — single source of truth). Lockstep is the invariant: no key a parser would trust can survive undefanged in user-supplied content. Side benefit: junk key-shaped lines (transcript 'Caller: hi') can no longer masquerade as metadata. 2. With the vocabulary boundary in place, task-mid bodies become losslessly splittable: trusted/lenient body = the task: line's content plus every subsequent NON-vocabulary line — health-check bullets and phone transcript dialogue stay in body, trailing real headers are excluded. Every post-task: line lands in exactly one of headers/body. Tests: lockstep assertion (guard set == parser vocabulary), forged non-classic keys (instructions/hint/from/transcript/attempts) defanged, health bullets + transcript preserved in trusted body, and a corpus-wide body-fidelity sweep (3,415 archived files: no non-header line lost). Full suite: injection-guard, dm-fallback-guard, both invariance tests, interaction-type, gateway, priority — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(delegation): TaskDelegationService — LocalTaskBackend byte-identical + RelayTaskBackend over agent-api (step 4, #1947) (#1956) * fix(task-body-guard): defang interaction_type in untrusted bodies The new header must be in confine_user_content's _HEADER_KEYS like every other header — otherwise a message body containing a literal 'interaction_type: ...' line survives into the task file undefanged and could forge the field under a full-scan reader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(protocol): local_task_protocol read-side module + golden corpus tests (step 3a) The Local Task Protocol's task-file half as pure stdlib functions (the result-marker half already lives in result_markers.py, #873): - schema constants: INTERACTION_TYPES (single source, asserted equal to the gateway whitelist), PRIORITIES, ACCESS_TIERS, KNOWN_HEADER_KEYS (from a 3.4k-file archive corpus survey) - valid_task_id — the filename/traversal gate for ids arriving in content - parse_task_headers — the safe stop-at-task: reader (PR #982 delimiter rule) - parse_task_headers_trusted — full-scan last-wins, for writers that neutralize bodies (_one_line strip or confine_user_content defang); last-wins is load-bearing for the gateway's access_tier-written-last defense - archive rules: month partitioning, find_archived_task candidate walk, iter_archived_tasks corpus sweep Writers untouched (read-side-first). The module docstring records the real shape taxonomy the corpus survey established: TS writers + agent-api /task are task-last; every Python bridge is task-mid — which means stop-at-task: readers under-read bridge files (e.g. parse_priority_from_text has never seen a bridge-written priority: field). Reader consolidation and writer convergence are follow-up steps with per-bridge byte-identical acceptance. Tests: 33 checks — per-shape fixtures (discord/gateway/voice/chat/phone- legacy/health/forged-body), id validation, archive rules, vocabulary parity with the gateway, plus a live-corpus sweep (3,397 archived files parse clean) that skips gracefully where no workspace exists (CI). R1 honored: stdlib-only, no I/O beyond the explicit archive helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run CI on staging-interaction-planes PRs and pushes The branch filter predates this staging branch — without it, refactor-step PRs (like this one) get zero CI. Same pattern staging-workspace-revamp used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run the coverage gate on staging-interaction-planes PRs too Same branch-filter gap as ci.yml — without it, refactor-step PRs with Python changes skip the 95% diff-coverage bar until the staging→main merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): task_priority reads via local_task_protocol (step 3b, reader 1/3) parse_priority_from_text delegates header extraction to the protocol module's safe parser; the legacy blank/'---' stop is preserved locally as a documented pre-#982 quirk. Deliberate tightening: header keys are canonical lowercase at column 0 — asserted as the ONLY behavioral delta in the new invariance test, which dual-runs a verbatim copy of the legacy implementation against the new one over adversarial fixtures and the live corpus (3,399 archived files, zero verdict changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): discord _task_source reads via parse_task_headers_lenient (step 3b, reader 2/3) New protocol parser: parse_task_headers_lenient — full scan, FIRST occurrence per key wins. This is the shape-union reader: the archive proves producer field order changed across eras (May-2026 voice tasks are task-MID; today's are task-last), and _task_source classifies files of any age. The stricter stop-at-task: parser flips the DM-fallback verdict on 23 real archived voice tasks — caught by this commit's corpus dual-run, which now asserts extraction-identical over all 3,401 files. Known pre-existing first-wins spoofability (a body line can supply a key the file legitimately lacks) is documented at the parser and pinned by a fixture; hardening it changes historical verdicts and is an owner decision, not a read-side refactor. Gateway _valid_tid deliberately NOT switched: it is a remote-compat gate (any dot/dash id from older gateways), a different contract from the local task-id canon in valid_task_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * wip(delegation): step-4 seam — backends, relay watcher, agent-api endpoints * fix(protocol): self-sufficient local_task_protocol import in task_priority tests/task-priority-stop-at-task-delimiter.test.py (and any other consumer) loads task_priority standalone via importlib without src/ on sys.path — the bare import broke it in CI, which also failed the coverage gate (suite dies under instrumentation before coverage is measurable). Same self-pathing pattern the bridges use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: retire staging-workspace-revamp from workflow branch filters That refactor's staging branch merged 2026-06-01 (M0); the filter entry is dead config. Owner-requested cleanup alongside the staging-interaction-planes addition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(delegation): TaskDelegationService — LocalTaskBackend byte-identical, RelayTaskBackend over agent-api (step 4, #1947) The seam between delegation logic and where tasks/results live: - src/task-delegation.ts: TaskDelegationService interface (submit + result I/O primitives), LocalTaskBackend (today's exact file I/O), RelayTaskBackend (same ops over agent-api HTTP with bearer), selectBackend (boot-time probe: writable tasks/ → local; else CORE_API_URL → relay; else loud throw — the 2026-05-18 silent-drop lesson applied at the seam). - task-bridge.ts: workTool + writeChatTask submit via the backend (local mode = byte-identical writes). The LOCAL result watcher is untouched — in relay mode a separate loop covers the delegation-critical subset only: timeout sweep + results for tasks THIS process submitted (consuming other task-* results would steal them from core-host consumers; deliver-once). Core-host-only flows (voice-/question-/proactive-, context consolidation) stay core-host-only by design. - agent-api.py: 4 delegation endpoints (POST /delegation/tasks, GET /delegation/results[/name], POST /delegation/archive). Stricter auth than the rest of the API: a core with no SUTANDO_API_TOKEN refuses delegation outright (403) instead of the local-dev allow. Ids via local_task_protocol.valid_task_id; names via the CodeQL-shaped _safe_path (which appends .txt itself — callers pass stems); archive matches task-bridge's month-partitioned layout via archive_month_dir. Tests: tests/task-delegation.test.ts (5 — byte-identity, watcher-I/O mirror, probe order, loud-throw) and tests/agent-api-delegation.test.py (14-check E2E against a real HTTP server on an ephemeral port: auth both ways, byte-identical submit, traversal rejection, list/read round-trip, month-partitioned archive). Existing task-bridge suites green (8+10), tsc clean, full python suite unchanged. Cross-machine E2E (Air voice-agent → MBP core) needs the second machine — flagged in the PR for owner scheduling; the relay path is otherwise covered by the HTTP E2E above. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(protocol): cover the archive-walk helpers with fixtures; pragma the unmeasurable discord glue The coverage gate runs without a live workspace (corpus sweeps skip) and without discord.py (the bridge can't import under instrumentation): - find_archived_task / iter_archived_tasks now have fixture-based tests (candidate walk across live/processed/flat/month dirs, stray-dir and malformed-id gating) instead of relying on the live-corpus path. - the two _task_source glue lines get pragma: no cover with the rationale at the site — their semantics are pinned by the invariance test's corpus dual-run, but the module itself is unimportable in the gate env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): define per-parser body contract + lossless task_body() reader (Codex blocker) The trusted/lenient parsers' body looked like the full work item but silently dropped continuation lines (health-check failure bullets, phone hint/transcript sections). A header scan cannot losslessly split task-mid continuation content from trailing headers, so stop pretending: - TaskHeaders.body is now DOCUMENTED per parser: full work item for the task-last safe parser; scalar first-task:-line value for trusted/lenient. - New task_body(text): the shape-independent lossless work-item reader — everything from the first task: line onward, verbatim. For task-mid files that includes trailing header lines; the honest trade is stated at the function. - Tests pin the contract: trusted body excludes the health bullet (scalar by contract, not by accident), task_body keeps health bullets and phone hint/transcript, equals safe-parser body on task-last shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(state-paths): allowlist task-delegation.ts (type-union false positive) The flagged token is the `kind: 'tasks' | 'results'` TYPE UNION on the injected archive callback — the module never resolves workspace paths itself; dirs are injected by task-bridge, which resolves via workspace_default. Rationale recorded in the allowlist entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): vocabulary-locked parsers + lossless task-mid bodies (Codex P2s) Both P2s from the re-review, fixed as one coherent mechanism: 1. Parsers promote ONLY KNOWN_HEADER_KEYS to headers, and task_body_guard._HEADER_KEYS now IS that list (imported — single source of truth). Lockstep is the invariant: no key a parser would trust can survive undefanged in user-supplied content. Side benefit: junk key-shaped lines (transcript 'Caller: hi') can no longer masquerade as metadata. 2. With the vocabulary boundary in place, task-mid bodies become losslessly splittable: trusted/lenient body = the task: line's content plus every subsequent NON-vocabulary line — health-check bullets and phone transcript dialogue stay in body, trailing real headers are excluded. Every post-task: line lands in exactly one of headers/body. Tests: lockstep assertion (guard set == parser vocabulary), forged non-classic keys (instructions/hint/from/transcript/attempts) defanged, health bullets + transcript preserved in trusted body, and a corpus-wide body-fidelity sweep (3,415 archived files: no non-header line lost). Full suite: injection-guard, dm-fallback-guard, both invariance tests, interaction-type, gateway, priority — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(delegation): extract route bodies as module functions + direct-call coverage The coverage gate's tracer misses handler-thread execution, so the HTTP E2E alone left the route bodies unattributed (77.5% on changed lines). The four delegation route bodies are now module-level (status, payload) functions — the handler is thin dispatch — and the test exercises them BOTH ways: HTTP round-trip for dispatch+auth, direct main-thread calls for the bodies (submit good/bad-id/empty, list, read good/404/traversal, archive move/already-gone/bad-tid). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(delegation): serve the E2E on the main thread + cover the OSError branch The coverage gate's tracer misses handler-THREAD execution, so even with the route bodies extracted, the thin dispatch lines stayed unattributed. Inverted the E2E threading: plain HTTPServer with a handle_request loop on the MAIN thread, requests issued from a worker thread — every handler line now runs traced. Also covers delegation_list_results' unreadable-dir branch. 25 checks green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(delegation): exercise every guard branch — the real coverage gap Corrected diagnosis: the missed lines were never thread-attribution — they were the UNTAKEN guard branches (wrong-bearer on the GET/archive routes, malformed-JSON except paths, tokenless refusal on read/archive). Local repro under the gate's exact invocation (coverage run --rcfile=.coveragerc) now shows zero missing delegation lines. Also hardened the test client: the server may close the socket right after an error response, so reading the error body races ECONNRESET — status codes are what the assertions need; the body read is best-effort now. 32 checks green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(delegation): positive relay config + task-identity coherence + archive ownership (Codex P1s) All three P1s from the re-review: 1. CORE_API_URL is now POSITIVE configuration: set → relay, regardless of local writability. The probe-first order made relay unreachable on any normal voice-host checkout (default workspace is <repo>/workspace — always writable), silently stranding delegated tasks locally. Local dirs are now created only on the local selection path, so a relay host grows no unused tasks/ + results/ dirs. Tests assert relay-wins-on-writable and no-local-dirs-in-relay-mode. 2. /delegation/tasks enforces identity coherence: the body's embedded id: header must equal the request id — a mismatched write would split task identity across result polling, dedupe, archive, and history. 3. /delegation/archive enforces ownership + no-clobber: the source name must be exactly <task_id>.txt (a relay client archives only ITS OWN result), and an occupied archive slot gets the epoch-suffixed name the bridges already use instead of overwriting history. Suites: 36 py E2E checks + 6 TS checks green; tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…extraction (LiveAgentRuntime slice 1) (#1964) * feat(tasks): additive interaction_type header at every task producer (interaction-planes step 1) A task producer is a (source, interaction_type) pair, not a source alone. This stamps the pair at every write site — purely additive, old readers ignore the extra line, no consumer behavior changes: - message: slack/telegram/discord bridges, twilio SMS + voicemail, chat template - realtime_audio: voice work-tool (task-bridge.ts), phone work-tool (delegateTask) - tool_initiated: writeChatTask (Gemini inline tool), agent-api /task (agent-to-agent) - system_event: health-check --emit-task, context-drop (app + task-bridge), github-webhook, twilio incoming-call, phone call-end summary - remote-gateway-bridge: passes through the gateway's interaction_type when present, defaults to message (all current gateway traffic is Matrix room messages) tests/interaction-type-header.test.py sweeps every producer write site and fails if a source: serialization lacks the paired interaction_type within the write window — so new producers can't silently ship without it. First step of the interaction-planes refactor (snapshot: v0.5.0-pre-interaction-planes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(tasks): harden gateway interaction_type + pair source: phone on delegateTask Round-2 improvements on the same PR: - remote-gateway-bridge: whitelist the pass-through against the six-value interaction-plane vocabulary — the gateway is outside the trust boundary, so an unknown remote value degrades to "message" instead of landing verbatim in the local task file. - conversation-server delegateTask: add the missing source: phone so the producer carries the full (source, interaction_type) pair. This activates two consumers that always listed "phone" but never matched because the writer predates the source header: task_priority phone→urgent, and discord-bridge DM_FALLBACK_SOURCES (a result landing after the call ended now DMs the owner instead of rotting unclaimed). - call-end summary writer deliberately does NOT get source: phone — its task instructions already have the core DM the owner, so the bridge fallback would deliver the same summary twice. Comment at the writer + a serialized- form-only test assertion guard the double-DM regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(gateway): unit-cover the interaction_type whitelist serialization The #1933 coverage gate (first PR through it) flagged the new remote-gateway-bridge lines as uncovered — the sweep test greps write sites but never executes them. This exercises _write_task directly: whitelisted pass-through, unknown-value degradation to message (trust boundary), absent-field default, full vocabulary round-trip, and the access_tier-last ordering guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(protocol): local_task_protocol — read-side module, golden corpus tests, reader switch-over (step 3) (#1954) * fix(task-body-guard): defang interaction_type in untrusted bodies The new header must be in confine_user_content's _HEADER_KEYS like every other header — otherwise a message body containing a literal 'interaction_type: ...' line survives into the task file undefanged and could forge the field under a full-scan reader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(protocol): local_task_protocol read-side module + golden corpus tests (step 3a) The Local Task Protocol's task-file half as pure stdlib functions (the result-marker half already lives in result_markers.py, #873): - schema constants: INTERACTION_TYPES (single source, asserted equal to the gateway whitelist), PRIORITIES, ACCESS_TIERS, KNOWN_HEADER_KEYS (from a 3.4k-file archive corpus survey) - valid_task_id — the filename/traversal gate for ids arriving in content - parse_task_headers — the safe stop-at-task: reader (PR #982 delimiter rule) - parse_task_headers_trusted — full-scan last-wins, for writers that neutralize bodies (_one_line strip or confine_user_content defang); last-wins is load-bearing for the gateway's access_tier-written-last defense - archive rules: month partitioning, find_archived_task candidate walk, iter_archived_tasks corpus sweep Writers untouched (read-side-first). The module docstring records the real shape taxonomy the corpus survey established: TS writers + agent-api /task are task-last; every Python bridge is task-mid — which means stop-at-task: readers under-read bridge files (e.g. parse_priority_from_text has never seen a bridge-written priority: field). Reader consolidation and writer convergence are follow-up steps with per-bridge byte-identical acceptance. Tests: 33 checks — per-shape fixtures (discord/gateway/voice/chat/phone- legacy/health/forged-body), id validation, archive rules, vocabulary parity with the gateway, plus a live-corpus sweep (3,397 archived files parse clean) that skips gracefully where no workspace exists (CI). R1 honored: stdlib-only, no I/O beyond the explicit archive helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run CI on staging-interaction-planes PRs and pushes The branch filter predates this staging branch — without it, refactor-step PRs (like this one) get zero CI. Same pattern staging-workspace-revamp used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run the coverage gate on staging-interaction-planes PRs too Same branch-filter gap as ci.yml — without it, refactor-step PRs with Python changes skip the 95% diff-coverage bar until the staging→main merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): task_priority reads via local_task_protocol (step 3b, reader 1/3) parse_priority_from_text delegates header extraction to the protocol module's safe parser; the legacy blank/'---' stop is preserved locally as a documented pre-#982 quirk. Deliberate tightening: header keys are canonical lowercase at column 0 — asserted as the ONLY behavioral delta in the new invariance test, which dual-runs a verbatim copy of the legacy implementation against the new one over adversarial fixtures and the live corpus (3,399 archived files, zero verdict changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): discord _task_source reads via parse_task_headers_lenient (step 3b, reader 2/3) New protocol parser: parse_task_headers_lenient — full scan, FIRST occurrence per key wins. This is the shape-union reader: the archive proves producer field order changed across eras (May-2026 voice tasks are task-MID; today's are task-last), and _task_source classifies files of any age. The stricter stop-at-task: parser flips the DM-fallback verdict on 23 real archived voice tasks — caught by this commit's corpus dual-run, which now asserts extraction-identical over all 3,401 files. Known pre-existing first-wins spoofability (a body line can supply a key the file legitimately lacks) is documented at the parser and pinned by a fixture; hardening it changes historical verdicts and is an owner decision, not a read-side refactor. Gateway _valid_tid deliberately NOT switched: it is a remote-compat gate (any dot/dash id from older gateways), a different contract from the local task-id canon in valid_task_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): self-sufficient local_task_protocol import in task_priority tests/task-priority-stop-at-task-delimiter.test.py (and any other consumer) loads task_priority standalone via importlib without src/ on sys.path — the bare import broke it in CI, which also failed the coverage gate (suite dies under instrumentation before coverage is measurable). Same self-pathing pattern the bridges use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: retire staging-workspace-revamp from workflow branch filters That refactor's staging branch merged 2026-06-01 (M0); the filter entry is dead config. Owner-requested cleanup alongside the staging-interaction-planes addition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(protocol): cover the archive-walk helpers with fixtures; pragma the unmeasurable discord glue The coverage gate runs without a live workspace (corpus sweeps skip) and without discord.py (the bridge can't import under instrumentation): - find_archived_task / iter_archived_tasks now have fixture-based tests (candidate walk across live/processed/flat/month dirs, stray-dir and malformed-id gating) instead of relying on the live-corpus path. - the two _task_source glue lines get pragma: no cover with the rationale at the site — their semantics are pinned by the invariance test's corpus dual-run, but the module itself is unimportable in the gate env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): define per-parser body contract + lossless task_body() reader (Codex blocker) The trusted/lenient parsers' body looked like the full work item but silently dropped continuation lines (health-check failure bullets, phone hint/transcript sections). A header scan cannot losslessly split task-mid continuation content from trailing headers, so stop pretending: - TaskHeaders.body is now DOCUMENTED per parser: full work item for the task-last safe parser; scalar first-task:-line value for trusted/lenient. - New task_body(text): the shape-independent lossless work-item reader — everything from the first task: line onward, verbatim. For task-mid files that includes trailing header lines; the honest trade is stated at the function. - Tests pin the contract: trusted body excludes the health bullet (scalar by contract, not by accident), task_body keeps health bullets and phone hint/transcript, equals safe-parser body on task-last shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): vocabulary-locked parsers + lossless task-mid bodies (Codex P2s) Both P2s from the re-review, fixed as one coherent mechanism: 1. Parsers promote ONLY KNOWN_HEADER_KEYS to headers, and task_body_guard._HEADER_KEYS now IS that list (imported — single source of truth). Lockstep is the invariant: no key a parser would trust can survive undefanged in user-supplied content. Side benefit: junk key-shaped lines (transcript 'Caller: hi') can no longer masquerade as metadata. 2. With the vocabulary boundary in place, task-mid bodies become losslessly splittable: trusted/lenient body = the task: line's content plus every subsequent NON-vocabulary line — health-check bullets and phone transcript dialogue stay in body, trailing real headers are excluded. Every post-task: line lands in exactly one of headers/body. Tests: lockstep assertion (guard set == parser vocabulary), forged non-classic keys (instructions/hint/from/transcript/attempts) defanged, health bullets + transcript preserved in trusted body, and a corpus-wide body-fidelity sweep (3,415 archived files: no non-header line lost). Full suite: injection-guard, dm-fallback-guard, both invariance tests, interaction-type, gateway, priority — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * test(voice): step-5 behavior anchors — tool-table snapshot + tuned-region tripwires Pins the voice agent's tuned behavior surface before the LiveAgentRuntime extraction moves anything: 126 importable tool definitions (name, description, execution mode, parameter keys + per-key descriptions) snapshotted to a committed fixture, plus SHA-256 tripwires on the non-importable tuned regions of voice-agent.ts (instructions factory, greeting/reconnect factory, tool-table composition — the module runs main() at load, so tests cannot import it). Every step-5 slice must keep these green or deliberately regenerate with the diff called out. Verified environment-independent: identical anchors with an empty CLAUDE_CONFIG_DIR (manifest tools resolve from the repo tree). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(voice): extract tuned prompt factories into voice-agent-config.ts (step 5a-1) PURE MOVE of the greeting factory and system-instruction factory out of voice-agent.ts (which runs main() at module load and is therefore untestable) into an importable module. voice-agent.ts keeps its state (mode resolver, meeting flag, session gates) and threads it in through VoiceConfigContext; env-dependent reads (stand identity, voice context, repo URL, agent context) run verbatim by default with test-only overrides. Equivalence evidence: - all 87 tuned string-array entries identical IN ORDER between the old region and the new module (programmatic comparison) - template-literal set identical except the intentional override branch - behavior anchors upgraded from source-hash tripwires to REAL factory output: fixed-env instruction hash, exact meeting-greeting string, conditional-variant assertions (meeting/googleSearch/mode-marker placement), reconnect replay-guard shape — 10/10 green across normal, repeat, and empty-CLAUDE_CONFIG_DIR runs tsc clean; task-bridge suites green. Phone (conversation-server) untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(anchors): exclude env-dependent tools from the tool-table fixture First CI run of the anchors showed the tool table is machine-specific after all: manifest-loaded skill tools (gws-gmail read_email, report_feedback, screenshot_explain, …) load on installed hosts but not on CI — the empty-CLAUDE_CONFIG_DIR simulation missed them because the manifest scan also covers other paths. inline-tools now exports envDependentToolNames (manifest tools + presenter-sentinel conditionals) and the anchor pins the 80-tool STATIC surface, which is identical everywhere. Excluding per-env extras is sound: a tool absent from an env's exclusion set is also absent from that env's tool list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(anchors): mask installed-tool lines from the instructions hash The prompt deliberately embeds the installed tool list, so a raw hash of buildInstructions output can never be env-stable (CI lacks the manifest tools). The anchor now hashes the instructions with the two dynamic segments masked (the joined tool-names line and per-tool '. Instant.' lines) — pinning all tuned prose exactly — with a separate well-formedness assertion that the dynamic lines exist. 11/11 in both local and empty-config runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What
Step 3a of the interaction-planes refactor (base:
staging-interaction-planes): the Local Task Protocol's task-file half as a pure, stdlib-only module —src/local_task_protocol.py— plus golden tests grounded in the real archive corpus. No existing code path changes; writers and current readers are untouched (read-side-first, per the approved step-3 plan). The result-marker half already exists (src/result_markers.py, #873) and stays where it is.What's in the module
INTERACTION_TYPES(single source of truth; a test asserts it equals the gateway whitelist from feat(tasks): additive interaction_type header at every task producer (interaction-planes step 1) #1953),PRIORITIES,ACCESS_TIERS,KNOWN_HEADER_KEYSfrom a survey of 3,401 archived task files.valid_task_id— the traversal gate for ids that arrive in message content ([deduped: <id>]holders become filenames).parse_task_headers— stop-at-task:(the PR fix(agent-api): close task-file field injection via /task body #982 delimiter rule). Safe default for unknown provenance.parse_task_headers_trusted— full-scan, last occurrence wins (load-bearing: the gateway writes its locally-decidedaccess_tierlast precisely to beat remote claims). Only for writers that neutralize bodies.YYYY-MMpartitioning, thefind_archived_taskcandidate walk (mirrors_isVoiceTask's), corpus iteration.The finding worth reading
The corpus + writer survey established that the two shapes split differently than assumed: only the TS writers (chat/voice/context-drop) and agent-api
/taskare task-last; every Python bridge (discord/slack/telegram/gateway/health-check/github-webhook) is task-mid, each safe via its own mechanism (_one_linestrip, sanitizer, orconfine_user_contentZWSP defang). Concrete consequence, now documented: stop-at-task:readers under-read bridge files —parse_priority_from_texthas never seen a bridge-writtenpriority:header. Reader consolidation (switching consumers to this module) and writer convergence on task-last are the follow-up steps, per-bridge, with byte-identical acceptance against archived samples.Related fix pushed to #1953 (where the header was introduced):
interaction_typeadded totask_body_guard._HEADER_KEYSso untrusted bodies can't carry an undefanged copy of the new header.Tests
tests/local-task-protocol.test.py— 33 checks: per-shape fixtures (discord task-mid, gateway incl. forged-access_tierlast-wins, voice, chat forged-body, phone-legacy, health-check), id validation both ways, archive rules, vocabulary parity with the gateway whitelist, plus a live corpus sweep (all 3,397 archived task files parse without throwing) that skips gracefully in CI.Corpus findings for the tracking record
5 sources appear in the archive but not in the producer inventory (
matrixrtc-voice×42,mini-watch×28,email×8,system-magic-word×2,smoke-test×1 — likelyREMOTE_TASK_PROVIDERrelabels or retired writers) and 2 undocumented headers (attempts:,reminder:). Also noted during the writer survey: agent-api's Twilio SMS/voicemail tasks carry noaccess_tierheader, and headerless tasks default to full processing — worth an owner decision in a separate issue.🤖 Generated with Claude Code