Skip to content

(MOT-4590) fix(harness): namespace-correct trigger wakes and agent-contract hardening - #953

Merged
andersonleal merged 6 commits into
mainfrom
fix/shell-worker
Aug 26, 2026
Merged

(MOT-4590) fix(harness): namespace-correct trigger wakes and agent-contract hardening#953
andersonleal merged 6 commits into
mainfrom
fix/shell-worker

Conversation

@andersonleal

@andersonleal andersonleal commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Problem

Live postmortem (session console-04e02cb7, tech-leader calc pipeline): an orchestrator armed a one-shot state wake, spawned its planner, the child wrote the watched key — and the wake never fired. The session parked forever at fires: 0 and the pipeline silently died after the planning stage.

Root cause: the harness registered binding triggers by dispatching the engine::register_trigger function, which pins the trigger's provider, home, and target namespaces to default. In a namespaced compose stack the provider lookup misses the real state provider (the binding parks as PENDING forever, invisible to engine::registered-triggers::*), and a fire would resolve harness::trigger::deliver in default, where this harness never registers. The engine's trigger registry is also in-memory, so the old path's "durability" did not survive engine restarts either.

Fixes

harness — wake correctness

  • Register delivery triggers over the worker channel: the SDK stamps the connection namespace on both ends and replays registrations on reconnect. Bindings store a synthetic sdk:<binding-id> id torn down through the kept SDK handle (an engine-side delete alone would be resurrected by the reconnect replay).
  • Startup replay: the durable binding store is the authority; boot re-arms every surviving binding and catch-up delivers one-shot state wakes whose key was written while no trigger existed — a parked session self-rescues on the next harness restart.
  • Registration appends a loud "ARMED but currently PARKED" advisory when no connected worker provides the trigger type (previously reported plain success).

harness — agent-contract hardening (from auditing every error across six live pipeline runs)

  • harness::triggers::unregister accepts id as an alias for subscription_id (the sibling engine contract uses id; models carry it over).
  • Lifecycle deadlines are relative-only: expires_in_ms, resolved server-side after the idempotency key. Absolute expires_at is retired — models repeatedly guessed months-stale epochs — and deny_unknown_fields names the retired field instead of silently dropping a deadline.
  • Turn-record persists retry once on caller-side invocation timeout: an observed ~10s engine-wide stall aborted an otherwise healthy turn by ~200ms.
  • The identity prompt anchors output to "the language of the user's message" — the previous "user's language" wording made the model infer a locale from nothing and 2 of 5 runs answered in French.
  • delegates_to profile gating removed everywhere (harness gate, directory field/UI, INT-026 rewritten as agent_identity): which agent a spawn names is the prompt's decision.

iii-directory

  • Two user-global roots join the merged scans, both shadowed by their project-local counterparts: global_agents_skills_folder (~/.agents/skills, read-only) and global_agents_folder (~/.iii/agents, profiles edited in place — it is iii's own directory; create stays anchored to the project root).
  • The browser UI renders real messages from {code, message} rejections instead of [object Object].

shell

  • coder::create-file / update-file accept a flat single {path, content} as a one-entry batch (hand-rolled Deserialize so the published schema the goldens pin is unchanged); garbage payloads get the contract named back.
  • shell::exec spawn failures name the missing program and hint at PATH.

console

  • The register-trigger card renders Expires: in 2h for expires_in_ms (legacy expires_at rendering kept for old transcripts).

Verification

  • cargo test green per worker: harness 444 (+ integration fixtures), iii-directory 398 + 33 UI tests, shell full suite incl. golden schemas; clippy -D warnings and fmt clean on all three; console tsc -b clean (the one failing console test is a pre-existing TriggerActivityCard hover flake untouched by this PR).
  • Six end-to-end live pipeline runs on a namespaced stack (verify-wake-fix-1..6): wakes fired 100% since the channel registration, agent discovery worked with no delegates_to, expires_in_ms was adopted by the model with zero friction, and the final run completed plan → implement → test → review with zero errors in all five sessions.

Engine-side follow-ups (tracked in MOT-4590, out of scope here)

engine::register_trigger should default namespaces from the calling connection; engine::triggers::info defaults to default and returns a misleading NOT_FOUND; pending triggers are invisible to registered-triggers::list/info; the ~10s engine-wide stall (suspect: trace-store sqlite maintenance) and the ineffective auto_vacuum=INCREMENTAL pragma on existing DBs.

https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC

Summary by CodeRabbit

  • New Features
    • Added support for user-global agent profiles and skills, with project profiles taking precedence.
    • File creation and updates now accept both batch and single-file formats.
    • Agent spawning is no longer restricted by delegation lists.
  • Improvements
    • Trigger and subscription expirations now use relative durations, with clearer status displays.
    • Triggers recover more reliably after reconnects, including missed one-time notifications.
    • Host command errors now identify the failed program and provide troubleshooting guidance.
    • Directory errors preserve more meaningful details.

…annel

Binding triggers were registered by dispatching engine::register_trigger,
which pins provider, home, and target namespaces to `default`: in a
namespaced stack the binding parks as PENDING forever and its fire would
resolve harness::trigger::deliver where the harness never registers
(console-04e02cb7 postmortem — `fires: 0` while the watched key was
written). Register over the worker channel instead: the SDK stamps the
connection namespace on both ends and replays the registration on
reconnect. Bindings store a synthetic `sdk:<binding-id>` trigger id torn
down through the kept SDK handle, so the reconnect replay list forgets it
too.

Durability moves where it always really was: the engine registry is
in-memory, so the startup sweep now re-arms every surviving binding from
the durable store and delivers one-shot state wakes whose key was written
while no trigger existed. Registration appends a loud "ARMED but PARKED"
advisory when no connected worker provides the trigger type.

Contract hardening from the live-run error audit: `harness::triggers::
unregister` accepts `id` as an alias for `subscription_id`; the lifecycle
deadline is relative-only (`expires_in_ms`, resolved after the dedup key —
absolute `expires_at` retired after repeated stale-epoch guesses, with
deny_unknown_fields naming the retired field); turn-record persists retry
once on caller-side invocation timeout; the identity prompt anchors output
to the language of the user's message.

Claude-Session: https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
Which agent a spawn names is the prompt's decision — the profile body
steers it, nothing gates it. The frozen AgentIdentity keeps only the id
(old stored records carrying the retired key still deserialize), and
INT-026 becomes agent_identity.rs: the identity/enrich/spawn-side
assertions stay, the denied-spawn leg goes.

Claude-Session: https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
…roots

Two machine-wide roots join the merged scans, both defaulting to the
user's home and shadowed by their project-local counterparts:

- global_agents_skills_folder (~/.agents/skills): read-only, cumulative
  whole-namespace shadowing across agents roots (local > global managed >
  project agents > user-global agents).
- global_agents_folder (~/.iii/agents): per-id merge; unlike the
  external-tooling skills roots this is iii's own directory, so update and
  delete edit the resolved profile IN PLACE — create stays anchored to the
  project agents_folder and a colliding id names the global file.

delegates_to leaves the directory surface with the harness gate (field,
unknown_delegates, and the UI picker), and the browser UI renders real
messages from {code, message} rejections instead of "[object Object]".
Both new roots are pinned in every test config — the defaults resolve to
the developer's REAL home directories.

Claude-Session: https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
coder::create-file / update-file accept a flat single {path, content} as a
one-entry batch (models frequently send the spec unwrapped; the raw serde
"missing field `files`" bounced a whole implement run). Deserialize is
hand-rolled — serde(try_from) would swap the published schemars schema the
goldens pin. A payload with neither shape gets the contract named back.

shell::exec spawn failures name the program ("spawn \"python\": ... —
program not found; is it installed and on PATH?") instead of a bare OS
error with no subject.

Claude-Session: https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
The register-trigger card shows `Expires: in 2h` for the new
lifecycle.expires_in_ms contract; the absolute expires_at rendering stays
for transcript cards recorded before the field was retired.

Claude-Session: https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 26, 2026 11:06pm
workers-tech-spec Ready Ready Preview Aug 26, 2026 11:06pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 43 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ee78222-4b98-42db-95cb-36734dc56d50

📥 Commits

Reviewing files that changed from the base of the PR and between badf680 and c9b5a66.

📒 Files selected for processing (44)
  • console/web/src/components/chat/engine/RegisterTriggerView.tsx
  • console/web/src/components/chat/engine/parsers.ts
  • docs/architecture/agent-profile-storage.md
  • harness/README.md
  • harness/architecture/reactive-triggers.md
  • harness/architecture/trigger-bindings.md
  • harness/prompts/default.txt
  • harness/src/agents.rs
  • harness/src/bindings/gc.rs
  • harness/src/bindings/mod.rs
  • harness/src/bindings/store.rs
  • harness/src/deps.rs
  • harness/src/functions/send.rs
  • harness/src/functions/spawn.rs
  • harness/src/functions/subscribe.rs
  • harness/src/functions/triggers_list.rs
  • harness/src/state.rs
  • harness/src/subagent.rs
  • harness/src/types/turn.rs
  • harness/tests/golden/schemas/harness.spawn.json
  • harness/tests/golden/schemas/harness.triggers.unregister.json
  • harness/tests/integration/src/scenarios/agent_identity.rs
  • harness/tests/integration/src/scenarios/mod.rs
  • harness/tests/integration/src/scenarios/wake_expiry_notice.rs
  • iii-directory/README.md
  • iii-directory/config.yaml.example
  • iii-directory/skills/SKILL.md
  • iii-directory/src/config.rs
  • iii-directory/src/fs_source.rs
  • iii-directory/src/functions/agents.rs
  • iii-directory/src/functions/mod.rs
  • iii-directory/src/functions/skills.rs
  • iii-directory/src/functions/update.rs
  • iii-directory/src/main.rs
  • iii-directory/tests/common/workers.rs
  • iii-directory/tests/e2e/config.yaml
  • iii-directory/ui/src/function-trigger/parsers.ts
  • iii-directory/ui/src/page/agent-fields.tsx
  • iii-directory/ui/src/page/browser.tsx
  • iii-directory/ui/src/page/index.tsx
  • shell/src/code/functions/create_file.rs
  • shell/src/code/functions/mod.rs
  • shell/src/code/functions/update_file.rs
  • shell/src/exec/host.rs
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shell-worker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 68 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

…only lifecycle

The wire-schema goldens pick up the updated spawn and triggers.unregister
descriptions, and INT-017 arms its doomed wake with `expires_in_ms` — the
contract's only deadline form — instead of an absolute expansion token
(relative-to-registration also means a slow boot can never eat the window).

Claude-Session: https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
@andersonleal
andersonleal merged commit 88ddcb1 into main Aug 26, 2026
30 checks passed
@andersonleal
andersonleal deleted the fix/shell-worker branch August 26, 2026 23:21
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.

1 participant