Skip to content

feat(approval-gate): iii-native pause-and-resume with reactive wakes - #152

Merged
sergiofilhowz merged 7 commits into
feat/harness-nodefrom
feat/iii-native-approval-resume
May 18, 2026
Merged

sergiofilhowz merged 7 commits into
feat/harness-nodefrom
feat/iii-native-approval-resume

Conversation

@ytallo

@ytallo ytallo commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Native iii approval flow: the gate replies allow / deny / pending directly on the hook reply; the turn FSM owns waiting through an explicit function_awaiting_approval state; reactive iii state triggers (not durable publishes) wake the turn when a decision is written. Console gets approve / deny buttons and survives multi-turn approvals on a single stream.

How it works

Gate (subscriber on policy::permission_check)

policy::approval_gate consults policy::check_permissions for each incoming function call and writes its decision on the reply stream:

  • allow → { block: false }.
  • deny → { block: true, reason, denial }; emits function_call_denied on agent::events.
  • needs_approval → { block: true, status: 'pending' }; emits approval_requested. The gate persists nothing — the orchestrator owns waiting.

Orchestrator pause: function_awaiting_approval

When handleExecute finds a prepared call whose hook reply is pending, the FSM transitions to function_awaiting_approval. The turn record carries the awaited function-call ids. There is no polling, no scheduled timeout, no placeholder transcript message.

Reactive wakes (replace the durable turn::step_requested self-publish)

Three iii state triggers drive the turn forward. Each uses a server-side condition_function_id so the engine drops irrelevant writes (deletes, unrelated keys) before invoking the adapter.

Trigger Scope Wakes on
turn::on_record_written agent turn_state writes with non-terminal, non-awaiting state
approval::on_decision_written approvals writes carrying a decision field
turn::on_abort_signal agent abort_signal = true writes

Saving the next turn record IS the wake. Writing an approval decision IS the wake.

Resolve and abort write decisions directly

  • approval::resolve writes approvals/<session>/<call> = { decision, reason } and emits approval_resolved. The state trigger handles the wake.
  • Aborting a paused turn writes aborted decisions inline for each awaited call. The same trigger picks them up.

approval::consume, approval::list_pending, approval::sweep_session, run::resume, and the placeholder-resurrect helpers are removed — the paused FSM state is the lifecycle.

Console / web

  • Approve / Deny call approval::resolve via the iii client and show a submitting state while the turn resumes.
  • ui::session::event stays subscribed for the chat lifetime instead of exiting on the first agent_end, so a second approval inside the same chat still reaches the UI.
  • fcall-start dedupes on function_call_id (the same id fires for function_execution_start and approval_requested) — the second event patches the existing message instead of spawning a duplicate.
  • MessageList auto-scrolls to a newly arrived pending approval.
  • The streaming pill flips to READY on agent_end and re-arms on the next activity event so a multi-turn resume looks live.

Test plan

  • pnpm --filter harness-node typecheck && lint && test
  • pnpm --filter chat-app typecheck && lint
  • Console E2E: hit the gate, approve, watch the call run, send a second prompt, approve again — both modals appear and resume.
  • Deny path: the prefilled denial reaches the transcript and the assistant continues.
  • Abort during pending: awaited calls resolve as aborted and the UI clears.

ytallo added 3 commits May 18, 2026 06:57
Replace the hardcoded `ModelId` union and four-entry `MODELS` constant
with composite `provider::<catalog_model_id>` keys sourced from the
harness-node models-catalog worker.

- `lib/catalog-model-key.ts`: parse/format `provider::id` (uses `::`
  separator constant `CATALOG_MODEL_KEY_SEP`).
- `lib/models-catalog.ts`: client wrapper around `models::list` /
  `models::get` for the picker.
- `hooks/use-model-picker-source.ts`: live-updating picker source with a
  static fallback for when the engine is down or the mock backend is in
  use.
- `types/chat.ts`: `ModelId` widened to `string`; `STATIC_MODEL_OPTIONS`
  + `DEFAULT_MODEL` replace the old enum-shaped `MODELS`.
- `components/chat/ModelPicker.tsx`: accept `options` + `loading` props;
  fall back to `{id, label}` from the current value if the catalog is
  empty so the picker is never blank.
- `components/chat/Composer.tsx`, `components/chat/ChatView.tsx`,
  `pages/Chat.tsx`, `hooks/use-conversations.ts`,
  `pages/Examples/sections/*`, `pages/Playground/index.tsx`: thread the
  catalog-shaped model ids through; legacy heuristic ids without `::`
  still get the old `claude*` / `gemini*` provider guess in
  `resolveRunParams`.
- `lib/backend/real.ts`: `resolveRunParams` parses `provider::id` and
  falls back to the heuristic for legacy ids; doc comment updated.
This commit removes the debug instrumentation code that was previously used to log session events to a local ingestion endpoint. The removal streamlines the event handling process in the `realStream` function, improving code clarity and maintainability.
…ring

Port the fail-closed approval-gate redesign from the Rust workspace to
harness-node and finish wiring the console/web UI so chat sessions can
approve/deny intercepted tool calls without polling.

Backend:
- intercept-at-gate: subscriber returns block_reply with status
  pending|denied; resolved decisions are drained later via
  approval::consume; pending entries can be force-resolved via
  approval::sweep_session
- run::resume rebuilds terminal TurnStateRecord (preserving turn_count
  + max_turns) and publishes a fresh turn::step_requested
- handle_resolve emits approval_resolved + asks the orchestrator to
  resume via a single iii::durable::publish on turn::step_requested;
  no polling loop and no explicit timeout (the bus default is the
  right backstop)
- subscriber detects terminal records and rebuilds via
  build_resume_plan before stepping
- hook-fanout merge_first_block_wins preserves the full blocking reply
  (status, function_call_id, subscriber, approval_gate, denial)
- publish-collect emits publish.{ok,error} + publish_failed so the
  orchestrator can fail closed on bus unavailability
- handle_awaiting + tearing_down each call approval::consume first;
  on non-empty result they stage the resolved entries as prepared
  calls and transition (or resurrect) into FunctionExecute
- router::abort composes [state::set abort, approval::sweep_session]
- session-tree sorts entries by (timestamp, id) so replies always
  appear in causal order

Console/web:
- approve/deny buttons call approval::resolve via the iii client
- the ui::session::event stream stays subscribed for the chat
  lifetime (no auto-close on first agent_end) so multi-turn
  approvals continue to flow
- approval prompts auto-scroll into view; submitting state shows
  disabled buttons + visible feedback while waiting for the agent
  to resume
- fcall-start events deduplicate on functionCallId; pending_approval
  placeholders suppress their fcall-end until the real result lands

Tests: TDD'd helpers, intercept/consume/sweep handlers, resume
plumbing, the orchestrator integration points, and an end-to-end
approval-resume.e2e covering intercept -> resolve -> publish ->
subscriber wake -> rebuild -> consume -> execute.
@vercel

vercel Bot commented May 18, 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, Comment May 18, 2026 8:15pm

Request Review

@coderabbitai

coderabbitai Bot commented May 18, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53bb1781-13f4-4e99-8444-57dd1122a02b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/iii-native-approval-resume

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 and usage tips.

…-native-approval-resume

# Conflicts:
#	console/web/src/components/chat/ModelPicker.tsx
#	console/web/src/hooks/use-conversations.ts
#	console/web/src/lib/backend/real.ts
#	console/web/src/pages/Chat.tsx
@github-actions

github-actions Bot commented May 18, 2026 •

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 26 skipped (no docs/).

Layer Result
structure ✓
vale ✓
ai ✓

Three for three. Nicely done.

…iggers

Replaces the polling mechanism for session updates with a reactive approach using state triggers. The new implementation filters state events for session creation and directly pushes updates to subscribers without polling. This change enhances efficiency and reduces latency in session updates, streamlining the fanout process. Key components include condition and handler functions for session creation events, and a direct trigger registration for state changes.
* Make approval pause explicit in the turn state machine

The iii-native approval flow needs a non-terminal state where the orchestrator can wait without tearing down the turn or rebuilding from provisioning. This adds the state value and locks its non-terminal behavior in the state test.

Constraint: Plan task 1 targets harness-node TypeScript only; Rust workers remain untouched.

Rejected: Keep using stopped/resume as the pause marker | later tasks remove that detour.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Not-tested: Full harness-node suite deferred until the batch verification step

* Let the turn record own approval waits

The pause-in-place design needs the orchestrator record to carry the function calls that are waiting on human decisions. This adds the awaiting_approval entry shape without changing any behavior yet.

Constraint: Field remains optional so fresh and existing records continue to deserialize without migration.

Rejected: Store pending calls only in approval-gate state | the simplification moves ownership into the orchestrator FSM.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until the batch verification step

* Make prepared function calls extensible for approvals

The upcoming pause-in-place approval handler needs to persist per-call metadata without overloading tuple positions. This converts prepared calls to an object shape and adds the optional pre_approved flag while preserving the current execution behavior.

Constraint: Existing staged values may use function_call or legacy tool_call keys, so the loader keeps both forms.

Rejected: Add a parallel pre-approved lookup table | it would split one prepared-call concern across two state keys.

Confidence: medium

Scope-risk: moderate

Directive: Keep PreparedEntry as the single persisted shape for prepared-call execution metadata.

Tested: pnpm test tests/turn-orchestrator/persistence-prepared.test.ts tests/turn-orchestrator/functions.test.ts tests/turn-orchestrator/assistant.test.ts tests/turn-orchestrator/tearing-down.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm test

Tested: pnpm typecheck

Tested: pnpm lint

* Define dispatch outcomes before approval cutover

The approval pause cutover needs dispatchWithHook to distinguish successful execution, hard denial, and pending approval. This commit introduces the discriminated result type before changing runtime dispatch behavior.

Constraint: Task 4 is type-only; dispatchWithHook still returns FunctionResult until the later refactor task.

Rejected: Change dispatch behavior now | the plan keeps the behavioral cutover staged.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/agent-call.test.ts

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Resume approved calls from an explicit wait state

The new approval pause state needs a handler that stays idle until every awaited decision is present, then updates prepared calls with either pre_approved metadata or a denial result before returning to function execution.

Constraint: Decisions are read from the approvals state scope using the existing session/function-call key shape.

Rejected: Poll approval::consume from generic assistant or teardown states | the simplification makes awaiting approval an explicit FSM state.

Confidence: medium

Scope-risk: moderate

Directive: Keep missing decisions as a no-op so durable turn::step_requested wakeups remain idempotent.

Tested: pnpm test tests/turn-orchestrator/awaiting-approval.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Route approval waits through the turn transition switch

The FSM can now enter function_awaiting_approval, so the central step dispatcher must route that state to the new handler instead of silently doing nothing.

Constraint: This only wires the state; handleExecute has not yet been cut over to produce it.

Rejected: Depend on fallthrough/no-op behavior | the wait state needs explicit wake handling from turn::step_requested.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Normalize awaiting-approval files for Biome

The new wait-state handler passed behavior and type checks, but Biome required import ordering and line wrapping before the batch could be called clean.

Constraint: Formatting-only follow-up after Task 5 and Task 6 verification.

Confidence: high

Scope-risk: narrow

Tested: pnpm test

Tested: pnpm typecheck

Tested: pnpm lint

* Return structured outcomes from hooked dispatch

The approval cutover needs dispatchWithHook to report whether a call ran, was denied, or is waiting for human approval. This changes the dispatcher API to return DispatchResult and keeps the LLM-facing agent::call wrapper unwrapped as a FunctionResult.

Constraint: handleExecute still consumes the old return shape until the next cutover task.

Rejected: Keep pending as a placeholder FunctionResult | the new FSM state owns approval waiting explicitly.

Confidence: medium

Scope-risk: moderate

Directive: Callers must branch on DispatchResult.kind before treating the value as a FunctionResult.

Tested: pnpm test tests/turn-orchestrator/agent-call.test.ts tests/turn-orchestrator/functions.test.ts

Not-tested: pnpm typecheck fails until handleExecute is cut over in Task 9

* Pause function execution in the approval wait state

handleExecute now treats pending approval as a real FSM pause instead of manufacturing a placeholder function result. It also resumes pre-approved calls without re-entering the hook and emits denial results without dispatching blocked calls.

Constraint: Old placeholder helper tests are skipped until the later cleanup task removes that machinery.

Rejected: Keep pending approval as a terminating FunctionResult | that leaks an artificial result into the transcript and keeps the resume detour alive.

Confidence: medium

Scope-risk: moderate

Directive: Preserve the existing executed-call check before dispatching so retrying a step does not duplicate approved function execution.

Tested: pnpm test tests/turn-orchestrator/functions.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Tested: pnpm test tests/integration/approval-resume.e2e.test.ts

Tested: pnpm test

Tested: pnpm lint

* Keep approval gate pending state in the orchestrator

The gate no longer writes a pending state-bus record when policy asks for human approval. It emits approval_requested and returns a pending hook reply; the turn orchestrator owns the pending-call list on its state record.

Constraint: The approvals state bus now stores resolved decisions only.

Rejected: Keep handleIntercept in the gate path | it preserves the obsolete pending-record lifecycle and duplicates orchestrator state.

Confidence: medium

Scope-risk: moderate

Directive: Do not add state-bus pending writes back to policy::approval_gate; pending ownership belongs to TurnStateRecord.awaiting_approval.

Tested: pnpm test tests/approval-gate/gate-subscriber.test.ts tests/approval-gate/events.test.ts

Tested: pnpm typecheck

* Drop approval request expiry from event types

Approval prompts no longer time out through the gate, so the shared event types should not promise an expires_at timestamp. The console typecheck also needed the currently-unused mode parameter marked intentionally unused.

Constraint: No timeout sweep behavior remains in the target approval-request event contract.

Rejected: Keep expires_at as optional | it would preserve dead UI expectations around automatic expiry.

Confidence: high

Scope-risk: narrow

Tested: pnpm typecheck in harness-node

Tested: pnpm typecheck in console/web

* Remove obsolete approval intercept lifecycle

The gate-subscriber now handles needs_approval directly and the orchestrator owns pending approval state, so the old intercept helper and its state-writing tests are dead. The old approval-resume integration is paused until the new pause-in-place integration lands later in the plan.

Constraint: Task 26 is responsible for replacing the legacy integration coverage.

Rejected: Keep intercept.ts as compatibility shim | no production caller remains and it would preserve obsolete pending-record semantics.

Confidence: medium

Scope-risk: moderate

Directive: Do not reintroduce approval-gate/intercept; route policy::approval_gate pending replies through gate-subscriber directly.

Tested: pnpm typecheck in harness-node

Tested: pnpm test tests/approval-gate tests/integration/approval-resume.e2e.test.ts

* Normalize console backend imports

Biome sorted the touched console backend imports after the approval event type cleanup. This keeps the batch verification clean without changing runtime behavior.

Constraint: Formatting-only follow-up from console/web verification.

Confidence: high

Scope-risk: narrow

Tested: pnpm typecheck in console/web

Tested: pnpm exec biome check src/lib/backend/real.ts src/types/iii-agent-event.ts in console/web

* Let approval resolution overwrite the wait record

The orchestrator now pauses in a native awaiting-approval state and reads the final decision directly from the state bus, so resolve no longer needs to validate or mutate a pending lifecycle record. It writes the compact decision payload and wakes the turn subscriber once the write succeeds.

Constraint: Keep approval::list_pending export until its registration is removed in the later registry cleanup task.

Rejected: Preserve pending/status/resolved_at transitions | the paused turn only needs decision and reason, and stale lifecycle fields recreate the old consume path.

Confidence: high

Scope-risk: narrow

Directive: Do not reintroduce preexisting pending-record checks here; the paused turn owns waiting semantics.

Tested: pnpm --filter harness-node test tests/approval-gate/pending.test.ts

* Retire wake-failed approval events

Resolve now writes a decision and publishes the turn wake directly. There is no retry wrapper that converts wake errors into stream events, so keeping approval_wake_failed advertised a lifecycle branch that no longer exists.

Constraint: Preserve approval_requested and approval_resolved event envelopes unchanged for existing consumers.

Rejected: Keep an unused event helper for compatibility | no caller remains and the event would only describe obsolete resume failure handling.

Confidence: high

Scope-risk: narrow

Directive: Add a new lifecycle event only when a real runtime branch emits it.

Tested: rg -n "emitApprovalWakeFailed|approval_wake_failed" harness-node/src harness-node/tests

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/approval-gate

* Remove approval placeholder resurrection helpers

Approval waits now stay inside the function FSM, so the old placeholder, consume, and resurrection helpers no longer describe the runtime path. Finalize appends real function results directly, awaiting-assistant no longer polls approval::consume, and teardown no longer resurrects terminal turns.

Constraint: Keep the paused-turn approval path in function_awaiting_approval; resolved decisions are read from state during function execution resumption.

Rejected: Leave consume helpers until a later pass | deleting the helpers without their callers would break typecheck, and keeping them would preserve dead behavior.

Confidence: high

Scope-risk: moderate

Directive: Do not add transcript placeholder replacement back unless function_awaiting_approval is removed or the transcript again stores synthetic pending results.

Tested: pnpm --filter harness-node test tests/turn-orchestrator

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Drop obsolete approval list and consume APIs

Approval decisions are now read directly by the paused function state, so the separate list_pending and consume endpoints are dead runtime surface. Removing them keeps the approval gate contract to resolve, sweep-on-abort, and the policy subscriber.

Constraint: Keep approval::sweep_session registered until abort cleanup is converted to inline state cleanup.

Rejected: Remove sweep in the same pass | router::abort still calls approval::sweep_session and that cleanup belongs to the abort task.

Confidence: high

Scope-risk: narrow

Directive: Do not re-add list_pending or consume for resume; function_awaiting_approval is the resume boundary.

Tested: pnpm --filter harness-node test tests/approval-gate tests/harness/policy.test.ts tests/turn-orchestrator/assistant.test.ts tests/turn-orchestrator/tearing-down.test.ts

Tested: pnpm --filter harness-node lint && pnpm --filter harness-node typecheck && pnpm --filter harness-node test

* Wake turns from approval state writes

Approval decisions are now intended to be reactive: the state write is the event that wakes the orchestrator. This adds the adapter that turns an approvals-scope state trigger into the existing turn::step_requested durable publish.

Constraint: State trigger payload keys use the approval key shape <session_id>/<function_call_id>.

Rejected: Debounce approval decision writes | repeated wakes are harmless and debouncing would add state to a pure adapter.

Confidence: high

Scope-risk: narrow

Directive: Keep this adapter thin; orchestration policy belongs in function_awaiting_approval.

Tested: pnpm --filter harness-node test tests/approval-gate/on-decision-written.test.ts

* Register approval decision state trigger

The approval gate now exposes the on-decision adapter as a state trigger on the approvals scope. Any write to an approval decision key can wake the turn orchestrator without the writer knowing about turn::step_requested.

Constraint: Register with scope only and no key filter so every approvals/<session>/<call> decision write is observed.

Rejected: Keep wake-up only in approval::resolve | abort and future override writers would still need duplicated publish logic.

Confidence: high

Scope-risk: narrow

Directive: Keep the trigger scope tied to loadApprovalGateConfig so custom approval scopes keep working.

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node test tests/approval-gate

* Let approval resolve rely on reactive wakeups

The approvals state trigger now owns turn wake-up, so approval::resolve should only persist the decision and emit approval_resolved. Removing resumeSession eliminates the duplicated explicit durable publish from the resolve path.

Constraint: approval::on_decision_written is registered on the approvals scope and publishes turn::step_requested after the state write.

Rejected: Keep resolve publishing as a fallback | it would double-wake sessions once the state trigger is active and blur responsibility for the wake-up.

Confidence: high

Scope-risk: narrow

Directive: Writers to the approvals scope should not publish turn::step_requested directly; the state trigger is the boundary.

Tested: pnpm --filter harness-node test tests/approval-gate/pending.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/approval-gate && pnpm --filter harness-node lint

* Abort approvals through state decisions

The abort path now writes aborted approval decisions directly for turns paused in function_awaiting_approval. Those writes are enough to wake the orchestrator through the approvals state trigger, so abort side effects no longer call sweep_session or publish turn::step_requested.

Constraint: This branch did not yet have performAbortSideEffects, so this applies the missing inline cleanup and the reactive no-publish behavior together.

Rejected: Keep approval::sweep_session in abort side effects | it preserves the old lifecycle endpoint and bypasses the decision-write trigger boundary.

Confidence: high

Scope-risk: moderate

Directive: Abort approval cleanup should write approvals/<session>/<call> decisions and let the state trigger wake the FSM.

Tested: pnpm --filter harness-node test tests/turn-orchestrator/abort.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/turn-orchestrator/abort.test.ts && pnpm --filter harness-node lint

* Cover reactive approval wake integration

The integration fixture now simulates the engine firing a state trigger after approvals-scope writes. A direct decision write proves the adapter publishes turn::step_requested without approval::resolve doing it explicitly.

Constraint: The fake state trigger runs on a microtask to match the engine's async trigger behavior closely enough for this unit-level integration test.

Rejected: Restore the skipped legacy approval-resume contract | it tested the old explicit resume path rather than the reactive state-trigger boundary.

Confidence: high

Scope-risk: narrow

Directive: Approval resume integration should model approvals state writes as the wake source.

Tested: pnpm --filter harness-node test tests/integration/approval-resume.e2e.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/integration/approval-resume.e2e.test.ts && pnpm --filter harness-node lint

* Remove terminal resume path from turn stepping

Approval waits now pause inside function_awaiting_approval and wake through approvals state writes. Terminal-session resurrection via run::resume and buildResumePlan is dead behavior, so turn::step now treats terminal records as terminal and no longer republishes while waiting for approval.

Constraint: Keep publishStep for the normal FSM pump after non-terminal transitions.

Rejected: Preserve run::resume for compatibility | no active approval path calls it and it would revive the old stopped-session model.

Confidence: high

Scope-risk: moderate

Directive: Do not reintroduce terminal resurrection unless approval waiting leaves the FSM again.

Tested: pnpm --filter harness-node test tests/turn-orchestrator

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Drop approval sweep and timeout remnants

The gate no longer owns pending-record lifecycles or timeouts. Abort writes aborted decisions inline and approval decisions wake through the state trigger, so approval::sweep_session, buildPendingRecord, and approval_gate.default_timeout_ms are dead surface.

Constraint: Leave unrelated hook-fanout and turn-orchestrator timeout config intact.

Rejected: Keep sweep as an internal fallback | no caller remains and it depends on the deleted pending record shape.

Confidence: high

Scope-risk: narrow

Directive: Approval cleanup should write decision records directly; do not restore pending/resolved sweep state.

Tested: pnpm --filter harness-node test tests/approval-gate tests/turn-orchestrator/abort.test.ts

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Remove stale approval placeholder branches

The approval wait path no longer emits synthetic pending function results, so console translation does not need to suppress pending_approval function_execution_end events. The hook comment now describes the actual pending dispatch result instead of the removed placeholder flow.

Constraint: Preserve approval_requested and approval_resolved UI event translation.

Rejected: Keep placeholder suppression defensively | it preserves a deleted wire shape and hides unexpected function results.

Confidence: high

Scope-risk: narrow

Directive: Pending approval UI should be driven by approval_requested, not synthetic function_result placeholders.

Tested: pnpm --filter chat-app typecheck

Tested: pnpm --filter chat-app exec biome check src/lib/backend/translate.ts

Tested: pnpm --filter harness-node test tests/turn-orchestrator/agent-call.test.ts && pnpm --filter harness-node lint

* Clean console approval lint drift

Repo-wide lint surfaced small console issues around approval resolve handlers and import ordering. Removing non-null assertions and stale suppressions keeps the cleanup branch lintable without changing approval UI behavior.

Constraint: Keep existing approval resolve callback contract unchanged.

Rejected: Leave console lint failures as unrelated | the approval callback warnings are on this feature surface and are cheap to make safe.

Confidence: high

Scope-risk: narrow

Directive: Keep approval IDs narrowed before invoking resolve callbacks instead of using non-null assertions.

Tested: pnpm --filter chat-app lint && pnpm --filter chat-app typecheck

Tested: pnpm -r lint

* fix(approval-gate): match engine state:* event_type values

* test(approval-gate): use engine state:* event_type values

* test(integration): fake-iii emits engine-correct event_type for state writes

* feat(approval-gate): filter approvals state trigger with condition function

Add isDecisionWrite/CONDITION_FN_ID to on-decision-written.ts and wire
condition_function_id into the state trigger config in register.ts so the
iii engine skips the handler on state:deleted and writes lacking a decision
field, without touching the in-handler guard (removed in Task 5).

* refactor(approval-gate): handler relies on engine-side condition for filtering

* feat(turn-orchestrator): adapter + condition for reactive abort wake

* feat(turn-orchestrator): register reactive abort_signal state trigger

* test(integration): abort_signal write triggers reactive wake

* chore: final cleanup after reactive trigger follow-up

Fix biome import-order lint: move `import type { ISdk }` before the
value imports in on-abort-signal.test.ts.
This commit deletes the `events.ts` file, which contained the `emitApprovalRequested` and `emitApprovalResolved` functions, as their functionality has been integrated directly into the `gate-subscriber` and `pending` modules. Corresponding test cases in `events.test.ts` have also been removed to reflect this change. The new implementation uses `streamSet` directly for emitting approval events, streamlining the approval gate flow.
@ytallo ytallo changed the title feat(approval-gate): port PR #150 with iii-native resume + console wiring feat(approval-gate): iii-native pause-and-resume with reactive wakes May 18, 2026
@sergiofilhowz
sergiofilhowz merged commit b3a5368 into feat/harness-node May 18, 2026
4 checks passed
ytallo added a commit that referenced this pull request May 18, 2026
…152)

* feat(console): drive model picker from harness-node models-catalog

Replace the hardcoded `ModelId` union and four-entry `MODELS` constant
with composite `provider::<catalog_model_id>` keys sourced from the
harness-node models-catalog worker.

- `lib/catalog-model-key.ts`: parse/format `provider::id` (uses `::`
  separator constant `CATALOG_MODEL_KEY_SEP`).
- `lib/models-catalog.ts`: client wrapper around `models::list` /
  `models::get` for the picker.
- `hooks/use-model-picker-source.ts`: live-updating picker source with a
  static fallback for when the engine is down or the mock backend is in
  use.
- `types/chat.ts`: `ModelId` widened to `string`; `STATIC_MODEL_OPTIONS`
  + `DEFAULT_MODEL` replace the old enum-shaped `MODELS`.
- `components/chat/ModelPicker.tsx`: accept `options` + `loading` props;
  fall back to `{id, label}` from the current value if the catalog is
  empty so the picker is never blank.
- `components/chat/Composer.tsx`, `components/chat/ChatView.tsx`,
  `pages/Chat.tsx`, `hooks/use-conversations.ts`,
  `pages/Examples/sections/*`, `pages/Playground/index.tsx`: thread the
  catalog-shaped model ids through; legacy heuristic ids without `::`
  still get the old `claude*` / `gemini*` provider guess in
  `resolveRunParams`.
- `lib/backend/real.ts`: `resolveRunParams` parses `provider::id` and
  falls back to the heuristic for legacy ids; doc comment updated.

* refactor(real): remove debug instrumentation from session event handling

This commit removes the debug instrumentation code that was previously used to log session events to a local ingestion endpoint. The removal streamlines the event handling process in the `realStream` function, improving code clarity and maintainability.

* feat(approval-gate): port PR #150 with iii-native resume + console wiring

Port the fail-closed approval-gate redesign from the Rust workspace to
harness-node and finish wiring the console/web UI so chat sessions can
approve/deny intercepted tool calls without polling.

Backend:
- intercept-at-gate: subscriber returns block_reply with status
  pending|denied; resolved decisions are drained later via
  approval::consume; pending entries can be force-resolved via
  approval::sweep_session
- run::resume rebuilds terminal TurnStateRecord (preserving turn_count
  + max_turns) and publishes a fresh turn::step_requested
- handle_resolve emits approval_resolved + asks the orchestrator to
  resume via a single iii::durable::publish on turn::step_requested;
  no polling loop and no explicit timeout (the bus default is the
  right backstop)
- subscriber detects terminal records and rebuilds via
  build_resume_plan before stepping
- hook-fanout merge_first_block_wins preserves the full blocking reply
  (status, function_call_id, subscriber, approval_gate, denial)
- publish-collect emits publish.{ok,error} + publish_failed so the
  orchestrator can fail closed on bus unavailability
- handle_awaiting + tearing_down each call approval::consume first;
  on non-empty result they stage the resolved entries as prepared
  calls and transition (or resurrect) into FunctionExecute
- router::abort composes [state::set abort, approval::sweep_session]
- session-tree sorts entries by (timestamp, id) so replies always
  appear in causal order

Console/web:
- approve/deny buttons call approval::resolve via the iii client
- the ui::session::event stream stays subscribed for the chat
  lifetime (no auto-close on first agent_end) so multi-turn
  approvals continue to flow
- approval prompts auto-scroll into view; submitting state shows
  disabled buttons + visible feedback while waiting for the agent
  to resume
- fcall-start events deduplicate on functionCallId; pending_approval
  placeholders suppress their fcall-end until the real result lands

Tests: TDD'd helpers, intercept/consume/sweep handlers, resume
plumbing, the orchestrator integration points, and an end-to-end
approval-resume.e2e covering intercept -> resolve -> publish ->
subscriber wake -> rebuild -> consume -> execute.

* feat(sessions-poll): implement reactive sessions fanout with state triggers

Replaces the polling mechanism for session updates with a reactive approach using state triggers. The new implementation filters state events for session creation and directly pushes updates to subscribers without polling. This change enhances efficiency and reduces latency in session updates, streamlining the fanout process. Key components include condition and handler functions for session creation events, and a direct trigger registration for state changes.

* Simplify iii-native approval gate flow (#154)

* Make approval pause explicit in the turn state machine

The iii-native approval flow needs a non-terminal state where the orchestrator can wait without tearing down the turn or rebuilding from provisioning. This adds the state value and locks its non-terminal behavior in the state test.

Constraint: Plan task 1 targets harness-node TypeScript only; Rust workers remain untouched.

Rejected: Keep using stopped/resume as the pause marker | later tasks remove that detour.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Not-tested: Full harness-node suite deferred until the batch verification step

* Let the turn record own approval waits

The pause-in-place design needs the orchestrator record to carry the function calls that are waiting on human decisions. This adds the awaiting_approval entry shape without changing any behavior yet.

Constraint: Field remains optional so fresh and existing records continue to deserialize without migration.

Rejected: Store pending calls only in approval-gate state | the simplification moves ownership into the orchestrator FSM.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until the batch verification step

* Make prepared function calls extensible for approvals

The upcoming pause-in-place approval handler needs to persist per-call metadata without overloading tuple positions. This converts prepared calls to an object shape and adds the optional pre_approved flag while preserving the current execution behavior.

Constraint: Existing staged values may use function_call or legacy tool_call keys, so the loader keeps both forms.

Rejected: Add a parallel pre-approved lookup table | it would split one prepared-call concern across two state keys.

Confidence: medium

Scope-risk: moderate

Directive: Keep PreparedEntry as the single persisted shape for prepared-call execution metadata.

Tested: pnpm test tests/turn-orchestrator/persistence-prepared.test.ts tests/turn-orchestrator/functions.test.ts tests/turn-orchestrator/assistant.test.ts tests/turn-orchestrator/tearing-down.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm test

Tested: pnpm typecheck

Tested: pnpm lint

* Define dispatch outcomes before approval cutover

The approval pause cutover needs dispatchWithHook to distinguish successful execution, hard denial, and pending approval. This commit introduces the discriminated result type before changing runtime dispatch behavior.

Constraint: Task 4 is type-only; dispatchWithHook still returns FunctionResult until the later refactor task.

Rejected: Change dispatch behavior now | the plan keeps the behavioral cutover staged.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/agent-call.test.ts

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Resume approved calls from an explicit wait state

The new approval pause state needs a handler that stays idle until every awaited decision is present, then updates prepared calls with either pre_approved metadata or a denial result before returning to function execution.

Constraint: Decisions are read from the approvals state scope using the existing session/function-call key shape.

Rejected: Poll approval::consume from generic assistant or teardown states | the simplification makes awaiting approval an explicit FSM state.

Confidence: medium

Scope-risk: moderate

Directive: Keep missing decisions as a no-op so durable turn::step_requested wakeups remain idempotent.

Tested: pnpm test tests/turn-orchestrator/awaiting-approval.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Route approval waits through the turn transition switch

The FSM can now enter function_awaiting_approval, so the central step dispatcher must route that state to the new handler instead of silently doing nothing.

Constraint: This only wires the state; handleExecute has not yet been cut over to produce it.

Rejected: Depend on fallthrough/no-op behavior | the wait state needs explicit wake handling from turn::step_requested.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Normalize awaiting-approval files for Biome

The new wait-state handler passed behavior and type checks, but Biome required import ordering and line wrapping before the batch could be called clean.

Constraint: Formatting-only follow-up after Task 5 and Task 6 verification.

Confidence: high

Scope-risk: narrow

Tested: pnpm test

Tested: pnpm typecheck

Tested: pnpm lint

* Return structured outcomes from hooked dispatch

The approval cutover needs dispatchWithHook to report whether a call ran, was denied, or is waiting for human approval. This changes the dispatcher API to return DispatchResult and keeps the LLM-facing agent::call wrapper unwrapped as a FunctionResult.

Constraint: handleExecute still consumes the old return shape until the next cutover task.

Rejected: Keep pending as a placeholder FunctionResult | the new FSM state owns approval waiting explicitly.

Confidence: medium

Scope-risk: moderate

Directive: Callers must branch on DispatchResult.kind before treating the value as a FunctionResult.

Tested: pnpm test tests/turn-orchestrator/agent-call.test.ts tests/turn-orchestrator/functions.test.ts

Not-tested: pnpm typecheck fails until handleExecute is cut over in Task 9

* Pause function execution in the approval wait state

handleExecute now treats pending approval as a real FSM pause instead of manufacturing a placeholder function result. It also resumes pre-approved calls without re-entering the hook and emits denial results without dispatching blocked calls.

Constraint: Old placeholder helper tests are skipped until the later cleanup task removes that machinery.

Rejected: Keep pending approval as a terminating FunctionResult | that leaks an artificial result into the transcript and keeps the resume detour alive.

Confidence: medium

Scope-risk: moderate

Directive: Preserve the existing executed-call check before dispatching so retrying a step does not duplicate approved function execution.

Tested: pnpm test tests/turn-orchestrator/functions.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Tested: pnpm test tests/integration/approval-resume.e2e.test.ts

Tested: pnpm test

Tested: pnpm lint

* Keep approval gate pending state in the orchestrator

The gate no longer writes a pending state-bus record when policy asks for human approval. It emits approval_requested and returns a pending hook reply; the turn orchestrator owns the pending-call list on its state record.

Constraint: The approvals state bus now stores resolved decisions only.

Rejected: Keep handleIntercept in the gate path | it preserves the obsolete pending-record lifecycle and duplicates orchestrator state.

Confidence: medium

Scope-risk: moderate

Directive: Do not add state-bus pending writes back to policy::approval_gate; pending ownership belongs to TurnStateRecord.awaiting_approval.

Tested: pnpm test tests/approval-gate/gate-subscriber.test.ts tests/approval-gate/events.test.ts

Tested: pnpm typecheck

* Drop approval request expiry from event types

Approval prompts no longer time out through the gate, so the shared event types should not promise an expires_at timestamp. The console typecheck also needed the currently-unused mode parameter marked intentionally unused.

Constraint: No timeout sweep behavior remains in the target approval-request event contract.

Rejected: Keep expires_at as optional | it would preserve dead UI expectations around automatic expiry.

Confidence: high

Scope-risk: narrow

Tested: pnpm typecheck in harness-node

Tested: pnpm typecheck in console/web

* Remove obsolete approval intercept lifecycle

The gate-subscriber now handles needs_approval directly and the orchestrator owns pending approval state, so the old intercept helper and its state-writing tests are dead. The old approval-resume integration is paused until the new pause-in-place integration lands later in the plan.

Constraint: Task 26 is responsible for replacing the legacy integration coverage.

Rejected: Keep intercept.ts as compatibility shim | no production caller remains and it would preserve obsolete pending-record semantics.

Confidence: medium

Scope-risk: moderate

Directive: Do not reintroduce approval-gate/intercept; route policy::approval_gate pending replies through gate-subscriber directly.

Tested: pnpm typecheck in harness-node

Tested: pnpm test tests/approval-gate tests/integration/approval-resume.e2e.test.ts

* Normalize console backend imports

Biome sorted the touched console backend imports after the approval event type cleanup. This keeps the batch verification clean without changing runtime behavior.

Constraint: Formatting-only follow-up from console/web verification.

Confidence: high

Scope-risk: narrow

Tested: pnpm typecheck in console/web

Tested: pnpm exec biome check src/lib/backend/real.ts src/types/iii-agent-event.ts in console/web

* Let approval resolution overwrite the wait record

The orchestrator now pauses in a native awaiting-approval state and reads the final decision directly from the state bus, so resolve no longer needs to validate or mutate a pending lifecycle record. It writes the compact decision payload and wakes the turn subscriber once the write succeeds.

Constraint: Keep approval::list_pending export until its registration is removed in the later registry cleanup task.

Rejected: Preserve pending/status/resolved_at transitions | the paused turn only needs decision and reason, and stale lifecycle fields recreate the old consume path.

Confidence: high

Scope-risk: narrow

Directive: Do not reintroduce preexisting pending-record checks here; the paused turn owns waiting semantics.

Tested: pnpm --filter harness-node test tests/approval-gate/pending.test.ts

* Retire wake-failed approval events

Resolve now writes a decision and publishes the turn wake directly. There is no retry wrapper that converts wake errors into stream events, so keeping approval_wake_failed advertised a lifecycle branch that no longer exists.

Constraint: Preserve approval_requested and approval_resolved event envelopes unchanged for existing consumers.

Rejected: Keep an unused event helper for compatibility | no caller remains and the event would only describe obsolete resume failure handling.

Confidence: high

Scope-risk: narrow

Directive: Add a new lifecycle event only when a real runtime branch emits it.

Tested: rg -n "emitApprovalWakeFailed|approval_wake_failed" harness-node/src harness-node/tests

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/approval-gate

* Remove approval placeholder resurrection helpers

Approval waits now stay inside the function FSM, so the old placeholder, consume, and resurrection helpers no longer describe the runtime path. Finalize appends real function results directly, awaiting-assistant no longer polls approval::consume, and teardown no longer resurrects terminal turns.

Constraint: Keep the paused-turn approval path in function_awaiting_approval; resolved decisions are read from state during function execution resumption.

Rejected: Leave consume helpers until a later pass | deleting the helpers without their callers would break typecheck, and keeping them would preserve dead behavior.

Confidence: high

Scope-risk: moderate

Directive: Do not add transcript placeholder replacement back unless function_awaiting_approval is removed or the transcript again stores synthetic pending results.

Tested: pnpm --filter harness-node test tests/turn-orchestrator

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Drop obsolete approval list and consume APIs

Approval decisions are now read directly by the paused function state, so the separate list_pending and consume endpoints are dead runtime surface. Removing them keeps the approval gate contract to resolve, sweep-on-abort, and the policy subscriber.

Constraint: Keep approval::sweep_session registered until abort cleanup is converted to inline state cleanup.

Rejected: Remove sweep in the same pass | router::abort still calls approval::sweep_session and that cleanup belongs to the abort task.

Confidence: high

Scope-risk: narrow

Directive: Do not re-add list_pending or consume for resume; function_awaiting_approval is the resume boundary.

Tested: pnpm --filter harness-node test tests/approval-gate tests/harness/policy.test.ts tests/turn-orchestrator/assistant.test.ts tests/turn-orchestrator/tearing-down.test.ts

Tested: pnpm --filter harness-node lint && pnpm --filter harness-node typecheck && pnpm --filter harness-node test

* Wake turns from approval state writes

Approval decisions are now intended to be reactive: the state write is the event that wakes the orchestrator. This adds the adapter that turns an approvals-scope state trigger into the existing turn::step_requested durable publish.

Constraint: State trigger payload keys use the approval key shape <session_id>/<function_call_id>.

Rejected: Debounce approval decision writes | repeated wakes are harmless and debouncing would add state to a pure adapter.

Confidence: high

Scope-risk: narrow

Directive: Keep this adapter thin; orchestration policy belongs in function_awaiting_approval.

Tested: pnpm --filter harness-node test tests/approval-gate/on-decision-written.test.ts

* Register approval decision state trigger

The approval gate now exposes the on-decision adapter as a state trigger on the approvals scope. Any write to an approval decision key can wake the turn orchestrator without the writer knowing about turn::step_requested.

Constraint: Register with scope only and no key filter so every approvals/<session>/<call> decision write is observed.

Rejected: Keep wake-up only in approval::resolve | abort and future override writers would still need duplicated publish logic.

Confidence: high

Scope-risk: narrow

Directive: Keep the trigger scope tied to loadApprovalGateConfig so custom approval scopes keep working.

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node test tests/approval-gate

* Let approval resolve rely on reactive wakeups

The approvals state trigger now owns turn wake-up, so approval::resolve should only persist the decision and emit approval_resolved. Removing resumeSession eliminates the duplicated explicit durable publish from the resolve path.

Constraint: approval::on_decision_written is registered on the approvals scope and publishes turn::step_requested after the state write.

Rejected: Keep resolve publishing as a fallback | it would double-wake sessions once the state trigger is active and blur responsibility for the wake-up.

Confidence: high

Scope-risk: narrow

Directive: Writers to the approvals scope should not publish turn::step_requested directly; the state trigger is the boundary.

Tested: pnpm --filter harness-node test tests/approval-gate/pending.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/approval-gate && pnpm --filter harness-node lint

* Abort approvals through state decisions

The abort path now writes aborted approval decisions directly for turns paused in function_awaiting_approval. Those writes are enough to wake the orchestrator through the approvals state trigger, so abort side effects no longer call sweep_session or publish turn::step_requested.

Constraint: This branch did not yet have performAbortSideEffects, so this applies the missing inline cleanup and the reactive no-publish behavior together.

Rejected: Keep approval::sweep_session in abort side effects | it preserves the old lifecycle endpoint and bypasses the decision-write trigger boundary.

Confidence: high

Scope-risk: moderate

Directive: Abort approval cleanup should write approvals/<session>/<call> decisions and let the state trigger wake the FSM.

Tested: pnpm --filter harness-node test tests/turn-orchestrator/abort.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/turn-orchestrator/abort.test.ts && pnpm --filter harness-node lint

* Cover reactive approval wake integration

The integration fixture now simulates the engine firing a state trigger after approvals-scope writes. A direct decision write proves the adapter publishes turn::step_requested without approval::resolve doing it explicitly.

Constraint: The fake state trigger runs on a microtask to match the engine's async trigger behavior closely enough for this unit-level integration test.

Rejected: Restore the skipped legacy approval-resume contract | it tested the old explicit resume path rather than the reactive state-trigger boundary.

Confidence: high

Scope-risk: narrow

Directive: Approval resume integration should model approvals state writes as the wake source.

Tested: pnpm --filter harness-node test tests/integration/approval-resume.e2e.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/integration/approval-resume.e2e.test.ts && pnpm --filter harness-node lint

* Remove terminal resume path from turn stepping

Approval waits now pause inside function_awaiting_approval and wake through approvals state writes. Terminal-session resurrection via run::resume and buildResumePlan is dead behavior, so turn::step now treats terminal records as terminal and no longer republishes while waiting for approval.

Constraint: Keep publishStep for the normal FSM pump after non-terminal transitions.

Rejected: Preserve run::resume for compatibility | no active approval path calls it and it would revive the old stopped-session model.

Confidence: high

Scope-risk: moderate

Directive: Do not reintroduce terminal resurrection unless approval waiting leaves the FSM again.

Tested: pnpm --filter harness-node test tests/turn-orchestrator

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Drop approval sweep and timeout remnants

The gate no longer owns pending-record lifecycles or timeouts. Abort writes aborted decisions inline and approval decisions wake through the state trigger, so approval::sweep_session, buildPendingRecord, and approval_gate.default_timeout_ms are dead surface.

Constraint: Leave unrelated hook-fanout and turn-orchestrator timeout config intact.

Rejected: Keep sweep as an internal fallback | no caller remains and it depends on the deleted pending record shape.

Confidence: high

Scope-risk: narrow

Directive: Approval cleanup should write decision records directly; do not restore pending/resolved sweep state.

Tested: pnpm --filter harness-node test tests/approval-gate tests/turn-orchestrator/abort.test.ts

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Remove stale approval placeholder branches

The approval wait path no longer emits synthetic pending function results, so console translation does not need to suppress pending_approval function_execution_end events. The hook comment now describes the actual pending dispatch result instead of the removed placeholder flow.

Constraint: Preserve approval_requested and approval_resolved UI event translation.

Rejected: Keep placeholder suppression defensively | it preserves a deleted wire shape and hides unexpected function results.

Confidence: high

Scope-risk: narrow

Directive: Pending approval UI should be driven by approval_requested, not synthetic function_result placeholders.

Tested: pnpm --filter chat-app typecheck

Tested: pnpm --filter chat-app exec biome check src/lib/backend/translate.ts

Tested: pnpm --filter harness-node test tests/turn-orchestrator/agent-call.test.ts && pnpm --filter harness-node lint

* Clean console approval lint drift

Repo-wide lint surfaced small console issues around approval resolve handlers and import ordering. Removing non-null assertions and stale suppressions keeps the cleanup branch lintable without changing approval UI behavior.

Constraint: Keep existing approval resolve callback contract unchanged.

Rejected: Leave console lint failures as unrelated | the approval callback warnings are on this feature surface and are cheap to make safe.

Confidence: high

Scope-risk: narrow

Directive: Keep approval IDs narrowed before invoking resolve callbacks instead of using non-null assertions.

Tested: pnpm --filter chat-app lint && pnpm --filter chat-app typecheck

Tested: pnpm -r lint

* fix(approval-gate): match engine state:* event_type values

* test(approval-gate): use engine state:* event_type values

* test(integration): fake-iii emits engine-correct event_type for state writes

* feat(approval-gate): filter approvals state trigger with condition function

Add isDecisionWrite/CONDITION_FN_ID to on-decision-written.ts and wire
condition_function_id into the state trigger config in register.ts so the
iii engine skips the handler on state:deleted and writes lacking a decision
field, without touching the in-handler guard (removed in Task 5).

* refactor(approval-gate): handler relies on engine-side condition for filtering

* feat(turn-orchestrator): adapter + condition for reactive abort wake

* feat(turn-orchestrator): register reactive abort_signal state trigger

* test(integration): abort_signal write triggers reactive wake

* chore: final cleanup after reactive trigger follow-up

Fix biome import-order lint: move `import type { ISdk }` before the
value imports in on-abort-signal.test.ts.

* Remove obsolete approval event handling and tests

This commit deletes the `events.ts` file, which contained the `emitApprovalRequested` and `emitApprovalResolved` functions, as their functionality has been integrated directly into the `gate-subscriber` and `pending` modules. Corresponding test cases in `events.test.ts` have also been removed to reflect this change. The new implementation uses `streamSet` directly for emitting approval events, streamlining the approval gate flow.
ytallo added a commit that referenced this pull request May 18, 2026
…156)

* feat(approval-gate): iii-native pause-and-resume with reactive wakes (#152)

* feat(console): drive model picker from harness-node models-catalog

Replace the hardcoded `ModelId` union and four-entry `MODELS` constant
with composite `provider::<catalog_model_id>` keys sourced from the
harness-node models-catalog worker.

- `lib/catalog-model-key.ts`: parse/format `provider::id` (uses `::`
  separator constant `CATALOG_MODEL_KEY_SEP`).
- `lib/models-catalog.ts`: client wrapper around `models::list` /
  `models::get` for the picker.
- `hooks/use-model-picker-source.ts`: live-updating picker source with a
  static fallback for when the engine is down or the mock backend is in
  use.
- `types/chat.ts`: `ModelId` widened to `string`; `STATIC_MODEL_OPTIONS`
  + `DEFAULT_MODEL` replace the old enum-shaped `MODELS`.
- `components/chat/ModelPicker.tsx`: accept `options` + `loading` props;
  fall back to `{id, label}` from the current value if the catalog is
  empty so the picker is never blank.
- `components/chat/Composer.tsx`, `components/chat/ChatView.tsx`,
  `pages/Chat.tsx`, `hooks/use-conversations.ts`,
  `pages/Examples/sections/*`, `pages/Playground/index.tsx`: thread the
  catalog-shaped model ids through; legacy heuristic ids without `::`
  still get the old `claude*` / `gemini*` provider guess in
  `resolveRunParams`.
- `lib/backend/real.ts`: `resolveRunParams` parses `provider::id` and
  falls back to the heuristic for legacy ids; doc comment updated.

* refactor(real): remove debug instrumentation from session event handling

This commit removes the debug instrumentation code that was previously used to log session events to a local ingestion endpoint. The removal streamlines the event handling process in the `realStream` function, improving code clarity and maintainability.

* feat(approval-gate): port PR #150 with iii-native resume + console wiring

Port the fail-closed approval-gate redesign from the Rust workspace to
harness-node and finish wiring the console/web UI so chat sessions can
approve/deny intercepted tool calls without polling.

Backend:
- intercept-at-gate: subscriber returns block_reply with status
  pending|denied; resolved decisions are drained later via
  approval::consume; pending entries can be force-resolved via
  approval::sweep_session
- run::resume rebuilds terminal TurnStateRecord (preserving turn_count
  + max_turns) and publishes a fresh turn::step_requested
- handle_resolve emits approval_resolved + asks the orchestrator to
  resume via a single iii::durable::publish on turn::step_requested;
  no polling loop and no explicit timeout (the bus default is the
  right backstop)
- subscriber detects terminal records and rebuilds via
  build_resume_plan before stepping
- hook-fanout merge_first_block_wins preserves the full blocking reply
  (status, function_call_id, subscriber, approval_gate, denial)
- publish-collect emits publish.{ok,error} + publish_failed so the
  orchestrator can fail closed on bus unavailability
- handle_awaiting + tearing_down each call approval::consume first;
  on non-empty result they stage the resolved entries as prepared
  calls and transition (or resurrect) into FunctionExecute
- router::abort composes [state::set abort, approval::sweep_session]
- session-tree sorts entries by (timestamp, id) so replies always
  appear in causal order

Console/web:
- approve/deny buttons call approval::resolve via the iii client
- the ui::session::event stream stays subscribed for the chat
  lifetime (no auto-close on first agent_end) so multi-turn
  approvals continue to flow
- approval prompts auto-scroll into view; submitting state shows
  disabled buttons + visible feedback while waiting for the agent
  to resume
- fcall-start events deduplicate on functionCallId; pending_approval
  placeholders suppress their fcall-end until the real result lands

Tests: TDD'd helpers, intercept/consume/sweep handlers, resume
plumbing, the orchestrator integration points, and an end-to-end
approval-resume.e2e covering intercept -> resolve -> publish ->
subscriber wake -> rebuild -> consume -> execute.

* feat(sessions-poll): implement reactive sessions fanout with state triggers

Replaces the polling mechanism for session updates with a reactive approach using state triggers. The new implementation filters state events for session creation and directly pushes updates to subscribers without polling. This change enhances efficiency and reduces latency in session updates, streamlining the fanout process. Key components include condition and handler functions for session creation events, and a direct trigger registration for state changes.

* Simplify iii-native approval gate flow (#154)

* Make approval pause explicit in the turn state machine

The iii-native approval flow needs a non-terminal state where the orchestrator can wait without tearing down the turn or rebuilding from provisioning. This adds the state value and locks its non-terminal behavior in the state test.

Constraint: Plan task 1 targets harness-node TypeScript only; Rust workers remain untouched.

Rejected: Keep using stopped/resume as the pause marker | later tasks remove that detour.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Not-tested: Full harness-node suite deferred until the batch verification step

* Let the turn record own approval waits

The pause-in-place design needs the orchestrator record to carry the function calls that are waiting on human decisions. This adds the awaiting_approval entry shape without changing any behavior yet.

Constraint: Field remains optional so fresh and existing records continue to deserialize without migration.

Rejected: Store pending calls only in approval-gate state | the simplification moves ownership into the orchestrator FSM.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until the batch verification step

* Make prepared function calls extensible for approvals

The upcoming pause-in-place approval handler needs to persist per-call metadata without overloading tuple positions. This converts prepared calls to an object shape and adds the optional pre_approved flag while preserving the current execution behavior.

Constraint: Existing staged values may use function_call or legacy tool_call keys, so the loader keeps both forms.

Rejected: Add a parallel pre-approved lookup table | it would split one prepared-call concern across two state keys.

Confidence: medium

Scope-risk: moderate

Directive: Keep PreparedEntry as the single persisted shape for prepared-call execution metadata.

Tested: pnpm test tests/turn-orchestrator/persistence-prepared.test.ts tests/turn-orchestrator/functions.test.ts tests/turn-orchestrator/assistant.test.ts tests/turn-orchestrator/tearing-down.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm test

Tested: pnpm typecheck

Tested: pnpm lint

* Define dispatch outcomes before approval cutover

The approval pause cutover needs dispatchWithHook to distinguish successful execution, hard denial, and pending approval. This commit introduces the discriminated result type before changing runtime dispatch behavior.

Constraint: Task 4 is type-only; dispatchWithHook still returns FunctionResult until the later refactor task.

Rejected: Change dispatch behavior now | the plan keeps the behavioral cutover staged.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/agent-call.test.ts

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Resume approved calls from an explicit wait state

The new approval pause state needs a handler that stays idle until every awaited decision is present, then updates prepared calls with either pre_approved metadata or a denial result before returning to function execution.

Constraint: Decisions are read from the approvals state scope using the existing session/function-call key shape.

Rejected: Poll approval::consume from generic assistant or teardown states | the simplification makes awaiting approval an explicit FSM state.

Confidence: medium

Scope-risk: moderate

Directive: Keep missing decisions as a no-op so durable turn::step_requested wakeups remain idempotent.

Tested: pnpm test tests/turn-orchestrator/awaiting-approval.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Route approval waits through the turn transition switch

The FSM can now enter function_awaiting_approval, so the central step dispatcher must route that state to the new handler instead of silently doing nothing.

Constraint: This only wires the state; handleExecute has not yet been cut over to produce it.

Rejected: Depend on fallthrough/no-op behavior | the wait state needs explicit wake handling from turn::step_requested.

Confidence: high

Scope-risk: narrow

Tested: pnpm test tests/turn-orchestrator/state.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Not-tested: Full harness-node suite deferred until batch verification

* Normalize awaiting-approval files for Biome

The new wait-state handler passed behavior and type checks, but Biome required import ordering and line wrapping before the batch could be called clean.

Constraint: Formatting-only follow-up after Task 5 and Task 6 verification.

Confidence: high

Scope-risk: narrow

Tested: pnpm test

Tested: pnpm typecheck

Tested: pnpm lint

* Return structured outcomes from hooked dispatch

The approval cutover needs dispatchWithHook to report whether a call ran, was denied, or is waiting for human approval. This changes the dispatcher API to return DispatchResult and keeps the LLM-facing agent::call wrapper unwrapped as a FunctionResult.

Constraint: handleExecute still consumes the old return shape until the next cutover task.

Rejected: Keep pending as a placeholder FunctionResult | the new FSM state owns approval waiting explicitly.

Confidence: medium

Scope-risk: moderate

Directive: Callers must branch on DispatchResult.kind before treating the value as a FunctionResult.

Tested: pnpm test tests/turn-orchestrator/agent-call.test.ts tests/turn-orchestrator/functions.test.ts

Not-tested: pnpm typecheck fails until handleExecute is cut over in Task 9

* Pause function execution in the approval wait state

handleExecute now treats pending approval as a real FSM pause instead of manufacturing a placeholder function result. It also resumes pre-approved calls without re-entering the hook and emits denial results without dispatching blocked calls.

Constraint: Old placeholder helper tests are skipped until the later cleanup task removes that machinery.

Rejected: Keep pending approval as a terminating FunctionResult | that leaks an artificial result into the transcript and keeps the resume detour alive.

Confidence: medium

Scope-risk: moderate

Directive: Preserve the existing executed-call check before dispatching so retrying a step does not duplicate approved function execution.

Tested: pnpm test tests/turn-orchestrator/functions.test.ts

Tested: pnpm test tests/turn-orchestrator

Tested: pnpm typecheck

Tested: pnpm test tests/integration/approval-resume.e2e.test.ts

Tested: pnpm test

Tested: pnpm lint

* Keep approval gate pending state in the orchestrator

The gate no longer writes a pending state-bus record when policy asks for human approval. It emits approval_requested and returns a pending hook reply; the turn orchestrator owns the pending-call list on its state record.

Constraint: The approvals state bus now stores resolved decisions only.

Rejected: Keep handleIntercept in the gate path | it preserves the obsolete pending-record lifecycle and duplicates orchestrator state.

Confidence: medium

Scope-risk: moderate

Directive: Do not add state-bus pending writes back to policy::approval_gate; pending ownership belongs to TurnStateRecord.awaiting_approval.

Tested: pnpm test tests/approval-gate/gate-subscriber.test.ts tests/approval-gate/events.test.ts

Tested: pnpm typecheck

* Drop approval request expiry from event types

Approval prompts no longer time out through the gate, so the shared event types should not promise an expires_at timestamp. The console typecheck also needed the currently-unused mode parameter marked intentionally unused.

Constraint: No timeout sweep behavior remains in the target approval-request event contract.

Rejected: Keep expires_at as optional | it would preserve dead UI expectations around automatic expiry.

Confidence: high

Scope-risk: narrow

Tested: pnpm typecheck in harness-node

Tested: pnpm typecheck in console/web

* Remove obsolete approval intercept lifecycle

The gate-subscriber now handles needs_approval directly and the orchestrator owns pending approval state, so the old intercept helper and its state-writing tests are dead. The old approval-resume integration is paused until the new pause-in-place integration lands later in the plan.

Constraint: Task 26 is responsible for replacing the legacy integration coverage.

Rejected: Keep intercept.ts as compatibility shim | no production caller remains and it would preserve obsolete pending-record semantics.

Confidence: medium

Scope-risk: moderate

Directive: Do not reintroduce approval-gate/intercept; route policy::approval_gate pending replies through gate-subscriber directly.

Tested: pnpm typecheck in harness-node

Tested: pnpm test tests/approval-gate tests/integration/approval-resume.e2e.test.ts

* Normalize console backend imports

Biome sorted the touched console backend imports after the approval event type cleanup. This keeps the batch verification clean without changing runtime behavior.

Constraint: Formatting-only follow-up from console/web verification.

Confidence: high

Scope-risk: narrow

Tested: pnpm typecheck in console/web

Tested: pnpm exec biome check src/lib/backend/real.ts src/types/iii-agent-event.ts in console/web

* Let approval resolution overwrite the wait record

The orchestrator now pauses in a native awaiting-approval state and reads the final decision directly from the state bus, so resolve no longer needs to validate or mutate a pending lifecycle record. It writes the compact decision payload and wakes the turn subscriber once the write succeeds.

Constraint: Keep approval::list_pending export until its registration is removed in the later registry cleanup task.

Rejected: Preserve pending/status/resolved_at transitions | the paused turn only needs decision and reason, and stale lifecycle fields recreate the old consume path.

Confidence: high

Scope-risk: narrow

Directive: Do not reintroduce preexisting pending-record checks here; the paused turn owns waiting semantics.

Tested: pnpm --filter harness-node test tests/approval-gate/pending.test.ts

* Retire wake-failed approval events

Resolve now writes a decision and publishes the turn wake directly. There is no retry wrapper that converts wake errors into stream events, so keeping approval_wake_failed advertised a lifecycle branch that no longer exists.

Constraint: Preserve approval_requested and approval_resolved event envelopes unchanged for existing consumers.

Rejected: Keep an unused event helper for compatibility | no caller remains and the event would only describe obsolete resume failure handling.

Confidence: high

Scope-risk: narrow

Directive: Add a new lifecycle event only when a real runtime branch emits it.

Tested: rg -n "emitApprovalWakeFailed|approval_wake_failed" harness-node/src harness-node/tests

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/approval-gate

* Remove approval placeholder resurrection helpers

Approval waits now stay inside the function FSM, so the old placeholder, consume, and resurrection helpers no longer describe the runtime path. Finalize appends real function results directly, awaiting-assistant no longer polls approval::consume, and teardown no longer resurrects terminal turns.

Constraint: Keep the paused-turn approval path in function_awaiting_approval; resolved decisions are read from state during function execution resumption.

Rejected: Leave consume helpers until a later pass | deleting the helpers without their callers would break typecheck, and keeping them would preserve dead behavior.

Confidence: high

Scope-risk: moderate

Directive: Do not add transcript placeholder replacement back unless function_awaiting_approval is removed or the transcript again stores synthetic pending results.

Tested: pnpm --filter harness-node test tests/turn-orchestrator

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Drop obsolete approval list and consume APIs

Approval decisions are now read directly by the paused function state, so the separate list_pending and consume endpoints are dead runtime surface. Removing them keeps the approval gate contract to resolve, sweep-on-abort, and the policy subscriber.

Constraint: Keep approval::sweep_session registered until abort cleanup is converted to inline state cleanup.

Rejected: Remove sweep in the same pass | router::abort still calls approval::sweep_session and that cleanup belongs to the abort task.

Confidence: high

Scope-risk: narrow

Directive: Do not re-add list_pending or consume for resume; function_awaiting_approval is the resume boundary.

Tested: pnpm --filter harness-node test tests/approval-gate tests/harness/policy.test.ts tests/turn-orchestrator/assistant.test.ts tests/turn-orchestrator/tearing-down.test.ts

Tested: pnpm --filter harness-node lint && pnpm --filter harness-node typecheck && pnpm --filter harness-node test

* Wake turns from approval state writes

Approval decisions are now intended to be reactive: the state write is the event that wakes the orchestrator. This adds the adapter that turns an approvals-scope state trigger into the existing turn::step_requested durable publish.

Constraint: State trigger payload keys use the approval key shape <session_id>/<function_call_id>.

Rejected: Debounce approval decision writes | repeated wakes are harmless and debouncing would add state to a pure adapter.

Confidence: high

Scope-risk: narrow

Directive: Keep this adapter thin; orchestration policy belongs in function_awaiting_approval.

Tested: pnpm --filter harness-node test tests/approval-gate/on-decision-written.test.ts

* Register approval decision state trigger

The approval gate now exposes the on-decision adapter as a state trigger on the approvals scope. Any write to an approval decision key can wake the turn orchestrator without the writer knowing about turn::step_requested.

Constraint: Register with scope only and no key filter so every approvals/<session>/<call> decision write is observed.

Rejected: Keep wake-up only in approval::resolve | abort and future override writers would still need duplicated publish logic.

Confidence: high

Scope-risk: narrow

Directive: Keep the trigger scope tied to loadApprovalGateConfig so custom approval scopes keep working.

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node test tests/approval-gate

* Let approval resolve rely on reactive wakeups

The approvals state trigger now owns turn wake-up, so approval::resolve should only persist the decision and emit approval_resolved. Removing resumeSession eliminates the duplicated explicit durable publish from the resolve path.

Constraint: approval::on_decision_written is registered on the approvals scope and publishes turn::step_requested after the state write.

Rejected: Keep resolve publishing as a fallback | it would double-wake sessions once the state trigger is active and blur responsibility for the wake-up.

Confidence: high

Scope-risk: narrow

Directive: Writers to the approvals scope should not publish turn::step_requested directly; the state trigger is the boundary.

Tested: pnpm --filter harness-node test tests/approval-gate/pending.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/approval-gate && pnpm --filter harness-node lint

* Abort approvals through state decisions

The abort path now writes aborted approval decisions directly for turns paused in function_awaiting_approval. Those writes are enough to wake the orchestrator through the approvals state trigger, so abort side effects no longer call sweep_session or publish turn::step_requested.

Constraint: This branch did not yet have performAbortSideEffects, so this applies the missing inline cleanup and the reactive no-publish behavior together.

Rejected: Keep approval::sweep_session in abort side effects | it preserves the old lifecycle endpoint and bypasses the decision-write trigger boundary.

Confidence: high

Scope-risk: moderate

Directive: Abort approval cleanup should write approvals/<session>/<call> decisions and let the state trigger wake the FSM.

Tested: pnpm --filter harness-node test tests/turn-orchestrator/abort.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/turn-orchestrator/abort.test.ts && pnpm --filter harness-node lint

* Cover reactive approval wake integration

The integration fixture now simulates the engine firing a state trigger after approvals-scope writes. A direct decision write proves the adapter publishes turn::step_requested without approval::resolve doing it explicitly.

Constraint: The fake state trigger runs on a microtask to match the engine's async trigger behavior closely enough for this unit-level integration test.

Rejected: Restore the skipped legacy approval-resume contract | it tested the old explicit resume path rather than the reactive state-trigger boundary.

Confidence: high

Scope-risk: narrow

Directive: Approval resume integration should model approvals state writes as the wake source.

Tested: pnpm --filter harness-node test tests/integration/approval-resume.e2e.test.ts

Tested: pnpm --filter harness-node typecheck && pnpm --filter harness-node test tests/integration/approval-resume.e2e.test.ts && pnpm --filter harness-node lint

* Remove terminal resume path from turn stepping

Approval waits now pause inside function_awaiting_approval and wake through approvals state writes. Terminal-session resurrection via run::resume and buildResumePlan is dead behavior, so turn::step now treats terminal records as terminal and no longer republishes while waiting for approval.

Constraint: Keep publishStep for the normal FSM pump after non-terminal transitions.

Rejected: Preserve run::resume for compatibility | no active approval path calls it and it would revive the old stopped-session model.

Confidence: high

Scope-risk: moderate

Directive: Do not reintroduce terminal resurrection unless approval waiting leaves the FSM again.

Tested: pnpm --filter harness-node test tests/turn-orchestrator

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Drop approval sweep and timeout remnants

The gate no longer owns pending-record lifecycles or timeouts. Abort writes aborted decisions inline and approval decisions wake through the state trigger, so approval::sweep_session, buildPendingRecord, and approval_gate.default_timeout_ms are dead surface.

Constraint: Leave unrelated hook-fanout and turn-orchestrator timeout config intact.

Rejected: Keep sweep as an internal fallback | no caller remains and it depends on the deleted pending record shape.

Confidence: high

Scope-risk: narrow

Directive: Approval cleanup should write decision records directly; do not restore pending/resolved sweep state.

Tested: pnpm --filter harness-node test tests/approval-gate tests/turn-orchestrator/abort.test.ts

Tested: pnpm --filter harness-node typecheck

Tested: pnpm --filter harness-node lint

* Remove stale approval placeholder branches

The approval wait path no longer emits synthetic pending function results, so console translation does not need to suppress pending_approval function_execution_end events. The hook comment now describes the actual pending dispatch result instead of the removed placeholder flow.

Constraint: Preserve approval_requested and approval_resolved UI event translation.

Rejected: Keep placeholder suppression defensively | it preserves a deleted wire shape and hides unexpected function results.

Confidence: high

Scope-risk: narrow

Directive: Pending approval UI should be driven by approval_requested, not synthetic function_result placeholders.

Tested: pnpm --filter chat-app typecheck

Tested: pnpm --filter chat-app exec biome check src/lib/backend/translate.ts

Tested: pnpm --filter harness-node test tests/turn-orchestrator/agent-call.test.ts && pnpm --filter harness-node lint

* Clean console approval lint drift

Repo-wide lint surfaced small console issues around approval resolve handlers and import ordering. Removing non-null assertions and stale suppressions keeps the cleanup branch lintable without changing approval UI behavior.

Constraint: Keep existing approval resolve callback contract unchanged.

Rejected: Leave console lint failures as unrelated | the approval callback warnings are on this feature surface and are cheap to make safe.

Confidence: high

Scope-risk: narrow

Directive: Keep approval IDs narrowed before invoking resolve callbacks instead of using non-null assertions.

Tested: pnpm --filter chat-app lint && pnpm --filter chat-app typecheck

Tested: pnpm -r lint

* fix(approval-gate): match engine state:* event_type values

* test(approval-gate): use engine state:* event_type values

* test(integration): fake-iii emits engine-correct event_type for state writes

* feat(approval-gate): filter approvals state trigger with condition function

Add isDecisionWrite/CONDITION_FN_ID to on-decision-written.ts and wire
condition_function_id into the state trigger config in register.ts so the
iii engine skips the handler on state:deleted and writes lacking a decision
field, without touching the in-handler guard (removed in Task 5).

* refactor(approval-gate): handler relies on engine-side condition for filtering

* feat(turn-orchestrator): adapter + condition for reactive abort wake

* feat(turn-orchestrator): register reactive abort_signal state trigger

* test(integration): abort_signal write triggers reactive wake

* chore: final cleanup after reactive trigger follow-up

Fix biome import-order lint: move `import type { ISdk }` before the
value imports in on-abort-signal.test.ts.

* Remove obsolete approval event handling and tests

This commit deletes the `events.ts` file, which contained the `emitApprovalRequested` and `emitApprovalResolved` functions, as their functionality has been integrated directly into the `gate-subscriber` and `pending` modules. Corresponding test cases in `events.test.ts` have also been removed to reflect this change. The new implementation uses `streamSet` directly for emitting approval events, streamlining the approval gate flow.

* docs(harness-node): refresh architecture for reactive approval resume

Sync the harness-node docs introduced on main with this branch's
pause-and-resume refactor: 11-state FSM with function_awaiting_approval,
state-trigger wakes on the approvals and agent scopes, hook-fanout's
stream-trigger reply path, and the harness sessions fanout moving from a
1 Hz state::list diff to a state trigger.

Also drop poll_interval_ms from PublishCollectConfig — it's leftover from
the old polling implementation and unused in the reactive path.

* feat(turn-orchestrator): reactive run::start_and_wait via terminal state trigger

executeSync now installs an in-process waiter keyed by session_id, kicks
the run, and races the waiter against sync_default_timeout_ms instead of
polling persistence every sync_poll_interval_ms. The waiter is resolved
by a new `state` trigger on scope=agent gated by
turn::is_terminal_state_write, which fires the moment the FSM lands
`session/<sid>/turn_state` with state=stopped.

Drops sync_poll_interval_ms from the config type, default loader, and
config.yaml — the only consumer is gone.

* feat(console): wire real backend to approval::resolve

- Add realResolveApproval calling approval::resolve over the iii bus and
  expose it via ChatBackend.resolveApproval so the chat UI can approve or
  deny pending function calls.
- Thread sessionId into translateAgentEvent so approval-related stream
  events carry their session context.
- Drop the stale "Phase 3 adds approve/deny buttons" comment now that the
  flow is live end-to-end.

This branch was successfully deployed

1 active deployment
Preview — 6758f4d0 Deployed May 18, 2026 by vercel[bot]
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.

2 participants