(MOT-4590) fix(harness): namespace-correct trigger wakes and agent-contract hardening - #953
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reachedNext included review available in 43 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (44)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Contributor
skill-check — worker0 verified, 68 skipped (no docs/).
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 atfires: 0and the pipeline silently died after the planning stage.Root cause: the harness registered binding triggers by dispatching the
engine::register_triggerfunction, which pins the trigger's provider, home, and target namespaces todefault. In a namespaced compose stack the provider lookup misses the realstateprovider (the binding parks as PENDING forever, invisible toengine::registered-triggers::*), and a fire would resolveharness::trigger::deliverindefault, 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
sdk:<binding-id>id torn down through the kept SDK handle (an engine-side delete alone would be resurrected by the reconnect replay).harness — agent-contract hardening (from auditing every error across six live pipeline runs)
harness::triggers::unregisteracceptsidas an alias forsubscription_id(the sibling engine contract usesid; models carry it over).expires_in_ms, resolved server-side after the idempotency key. Absoluteexpires_atis retired — models repeatedly guessed months-stale epochs — anddeny_unknown_fieldsnames the retired field instead of silently dropping a deadline.delegates_toprofile gating removed everywhere (harness gate, directory field/UI, INT-026 rewritten asagent_identity): which agent a spawn names is the prompt's decision.iii-directory
global_agents_skills_folder(~/.agents/skills, read-only) andglobal_agents_folder(~/.iii/agents, profiles edited in place — it is iii's own directory; create stays anchored to the project root).{code, message}rejections instead of[object Object].shell
coder::create-file/update-fileaccept a flat single{path, content}as a one-entry batch (hand-rolledDeserializeso the published schema the goldens pin is unchanged); garbage payloads get the contract named back.shell::execspawn failures name the missing program and hint at PATH.console
Expires: in 2hforexpires_in_ms(legacyexpires_atrendering kept for old transcripts).Verification
cargo testgreen per worker: harness 444 (+ integration fixtures), iii-directory 398 + 33 UI tests, shell full suite incl. golden schemas; clippy-D warningsand fmt clean on all three; consoletsc -bclean (the one failing console test is a pre-existingTriggerActivityCardhover flake untouched by this PR).verify-wake-fix-1..6): wakes fired 100% since the channel registration, agent discovery worked with nodelegates_to,expires_in_mswas 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_triggershould default namespaces from the calling connection;engine::triggers::infodefaults todefaultand returns a misleading NOT_FOUND; pending triggers are invisible toregistered-triggers::list/info; the ~10s engine-wide stall (suspect: trace-store sqlite maintenance) and the ineffectiveauto_vacuum=INCREMENTALpragma on existing DBs.https://claude.ai/code/session_01WSwsJrtXnbEo5QzYcm5JdC
Summary by CodeRabbit