Skip to content

feat(space): route handoff hooks through WorkflowHookEngine + wire handoff tool [#939] - #2472

Closed
lsm wants to merge 11 commits into
devfrom
space/handoff-executor-deep-hook-engine-integration-tool-wiring
Closed

lsm wants to merge 11 commits into
devfrom
space/handoff-executor-deep-hook-engine-integration-tool-wiring

Conversation

@lsm

@lsm lsm commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Follow-up to the runtime handoff transition (#923 / PR #2466) addressing the five review points that were deferred when the executor landed. This branch is stacked on top of #2466's branch (which is still open), so the diff includes #2466 — the follow-up itself is the single commit feat(space): route handoff hooks through WorkflowHookEngine + wire handoff tool [#939]. If #2466 merges first, this PR collapses to just that commit.

Transition-hook execution now routes through WorkflowHookEngine (which already owns patch/state/connector-auth/task-phase) instead of HookExecutor directly, and the agent-facing handoff tool is wired in. The five points: (1) handoff({ target, summary, data? }) tool in node-agent-tools with TaskAgentManager constructing+injecting the HandoffExecutor; (2) node-scoped delivery via node/slot pairs (activation, live-session lookup, enqueue + idempotency key, and auto-resume all carry the resolved node id) so a shared slot name across two nodes can't leak; (3) a successful patch_params result is applied to the payload before delivery; (4) taskStatus/frozenPrUrl/persisted hookLocalState now flow via the engine context builder; (5) permittedExternalLookups is derived from the hook validator so authenticated validation works on the handoff path. Engine additions: runDeclaredHook, persistHookOutcome, and the pr_ready identity-stamp / target-patch-strip gates generalized to the handoff method.

Daemon typecheck/lint/knip green; runtime-a/runtime-b/agent-other shards pass (639 + 1074 + 1172). Note: bun run check is red on the base for an unrelated pre-existing test-quality issue in provider-registry.test.ts (not touched here).

lsm added 9 commits August 11, 2026 21:18
Add HandoffExecutor — the reusable runtime operation for formal workflow
ownership transfer via the handoff contract (#922). One operation:

  1. validates task/run state (terminal runs reject handoffs),
  2. resolves the sender node + declared transition (resolveHandoffTransition),
  3. resolves target node(s)/slot(s) from the transition target,
  4. authorizes the declared channel topology (ChannelResolver; open topology
     permits all),
  5. enforces cyclic transition maxCycles,
  6. authorizes + commits the transition's declared gate fields
     (GateDataRepository + evaluateGate), rejecting data keys outside the gate's
     writable shape,
  7. runs the transition's hook validator (HookExecutor),
  8. activates/reuses the target worker session (existing activateTargetSession)
     and delivers the existing peer-message envelope (formatAgentMessage),
     queueing when the target is not yet live,
  9. returns a structured delivered/queued/blocked/failed result.

Reuses ChannelRouter/ChannelResolver, gate evaluation, the hook engine, and
existing target-activation code rather than duplicating transition logic.
Authoritative fresh-turn packet construction (and the sender round-completion
that accompanies a delivered handoff) is a later task; this does not alter
generic send_message semantics.

Adds a handoff_cycles table (migration 187) + HandoffCycleRepository mirroring
channel_cycles, keyed by (run_id, transition_key). Handoff cycle caps reset
alongside channel cycles on human touch. Registers the table in the db-query
space scope and the schema-parity allowlist.

Focused runtime tests cover authorization, gate/hook failure + success, cycle
limits, queued activation, terminal-run rejection, broadcast, and the pure
cyclicity/target-resolution helpers.
P1#1 (cycle burned on failed delivery): switch from post-delivery
increment to reserve-before-delivery — the atomic increment both checks
the cap and reserves in one step (closing the check-then-increment
TOCTOU two concurrent handoffs had), and a failed delivery refunds the
reservation so only TAKEN handoffs (delivered||queued) count. Adds
HandoffCycleRepository.decrement. New cyclic+failed-delivery test
asserts the cap stays intact across failed attempts and a later
successful handoff still proceeds.

P1#2 (execute() never-throws): wrap the throw-prone paths (gate
merge/eval, hook validator, target-session lookup) with a typed
HandoffExecutionError(stage) caught at the top level of execute(), so
a DB/script/validator throw maps to a structured `failed` result
instead of escaping to the caller.

P2: hoist listByWorkflowRun in deliver (one read up front + one per
activation, not 2× per slot). Surface partial-delivery diagnostics on
the delivered branch. Add coverage for cancelled run state, gate
rateLimited propagation, hook retryable_block propagation, partial
delivery, autonomy-path gate write (sufficient + insufficient), and
the throwing-validator never-throws path.

P3: drop the redundant gate-row re-read (use the merge return's
updatedAt).

31 focused runtime tests pass; typecheck/lint/knip/parity and the
runtime shards green.
Resolve migration-number collision: dev shipped M187/M188/M189 for
delivery (#2463), so renumber the handoff_cycles migration 187→190
(function, registration, index export, test-helper comment). All other
files auto-merged cleanly. typecheck/lint/knop/parity green; migration
shards and handoff tests pass.
Addresses the self-contained points from the inline review:

- Cap-before-side-effects (P1): read-only isCapReached check now runs
  BEFORE gate/hook, so a capped cyclic transition no longer persists
  gate data or runs hook validators on a guaranteed cycle_limit. The
  atomic reservation still runs right before delivery (TOCTOU-safe).
- Dedupe cycle (P2): a DEDUPED enqueue (message already pending from a
  prior attempt) no longer charges a cycle; only new deliveries/enqueues
  count as taken.
- Self-target rejection (P1): a transition that resolves to the sender's
  own node is rejected — queueing it back into the sender's live session
  would violate the round-boundary contract.
- Run-state recheck (P2): reload run state immediately before delivery so
  a cancellation during async gate/hook validation doesn't deliver into a
  terminal run.
- Hook workspace (P1): carry config.workspacePath into hook context so
  validators/script hooks resolve the task worktree, not the daemon cwd.
- Hook rawParams (P1): populate rawParams with the full operation payload
  so built-in validators reading (rawParams ?? params).data see the
  supplied fields (e.g. pr_url).
- Hook authorization (P1): enforce enabled/humanOnly/sourceNode/targetNode/
  authorizedCallers before executing, mirroring WorkflowHookEngine.

Two new tests (hook-auth block, self-target rejection); 33 pass.

Batch 3 (node-scoped delivery, cyclic resetOnCycle gate reset, gate-data
publication, stale-execution filtering, and routing handoff hooks through
WorkflowHookEngine for patch_params/task-phase/frozen-identity/external-
lookups, plus wiring the handoff tool) is tracked with the reviewer.
- Stale executions (P2): exclude pending/cancelled node-executions from
  live-session lookup so a retained dead agentSessionId (e.g. after a
  spawn retry) is not selected over activation. Mirrors the production
  activation path's status filter.
- Data without declared shape (P2): reject `data` keys on a transition
  that declares neither a gate nor a hook — the contract requires keys
  to come from the bound gate fields or hook template fields.

Two new tests (stale-execution skip, ungated/unhooked data rejection);
35 pass.
Adds the non-blocking P3 from the approving review: enqueue the same
handoff twice and assert the 2nd (deduped) outcome is still `queued`
while the cycle counter stays at 1 (the dedupe path is refunded, not
charged). 36 tests pass.
…#923]

dev removed the legacy gate subsystem entirely (M190 drops gate_data /
gate_open_state / space_workflows.gates; gate-data-repository, gate-
evaluator, gate-features, gate-open-state-repository deleted;
HandoffTransition.gateId removed — handoffs now authorize via hooks
only, matching dev's new direction). Reconcile:

- Renumber the handoff_cycles migration 190→191 (dev shipped M190 for
  the gate removal). Registered in runMigrations; index.ts kept at dev's
  export set (no per-migration re-export needed).
- Strip the gate path from HandoffExecutor entirely: drop commitGate,
  GateDataRepository/evaluateGate/getEffectiveGate/scriptExecutor deps,
  gateOutcome from the result, and the gate-commit tests. Authorization
  is now hook-only (transition.hookId); data keys require a bound hook.
- scope-config.test / parity / space-test-db: drop gate_data &
  gate_open_state, keep handoff_cycles (dev + this PR).

29 handoff tests pass; typecheck/lint/knip/parity green; migration,
handlers, and runtime shards green (one unrelated flaky git-worktree
test in space-worktree-manager, stable in isolation).
…eness) [#923]

- Hook template data (P2): pass `hook.templateData` into the validator
  context so hooks with template-defined values see them.
- Delimiter-safe cycle keys (P2): URI-encode the node/transition ids in
  the cycle-counter key so a literal '/' in an id can't collide.
- Stricter live-session filter (P1): only `in_progress`/`idle` executions
  count as having a usable session — terminal (done) / blocked /
  waiting_rebind rows can retain a dead session id and must not be
  selected over activation. Extends the prior pending/cancelled guard.

29 tests pass; typecheck/lint green.
…ndoff tool [#939]

Follow-up to the runtime handoff transition (#923, PR #2466) addressing the
five deferred review points. Handoffs are hook-only after dev's gate-subsystem
removal, so transition-hook execution now routes through WorkflowHookEngine
(which already owns patch/state/connector-auth/task-phase) instead of
HookExecutor directly, and the agent-facing handoff tool is wired in.

1. Wire a handoff({ target, summary, data? }) tool into node-agent-tools
   (HandoffSchema + handler + conditional registration). TaskAgentManager
   constructs the HandoffExecutor and injects it alongside agentMessageRouter.
2. Node-scoped delivery: resolve node/slot pairs and scope activation,
   live-session lookup, enqueue (idempotency key + persisted workflowNodeId),
   and auto-resume by the resolved node id so a shared slot name across two
   nodes can no longer leak.
3. Apply a successful patch_params result to the handoff payload before
   delivery (e.g. pr_ready discovering data.pr_url).
4. Populate taskStatus / frozenPrUrl / persisted hookLocalState via the engine
   context builder (post_approval_only / pr_ready post-approval branches).
5. Derive permittedExternalLookups from the hook validator (connector auth) so
   authenticated validation works on the handoff path.

Engine additions: runDeclaredHook (run a single transition-bound hook by id
through the shared pipeline), persistHookOutcome (the post-action persist, now
shared by the MCP wrapper and the executor), and generalized the pr_ready
identity-stamp + target-patch-strip gates to also cover the handoff method.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5424fb456d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/tools/node-agent-tools.ts
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/workflow-hook-engine.ts
Comment thread packages/daemon/src/lib/space/runtime/workflow-hook-engine.ts

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1[1m] (HyperNeo)

Model: glm-5.1[1m] | Client: HyperNeo | Provider: z.ai

Reviewing the #939 follow-up (commit 5424fb456) — routing transition-hook execution through WorkflowHookEngine instead of HookExecutor direct, plus the agent-facing handoff tool and node-scoped delivery. I read the full follow-up diff, the integrated state of the new files, traced the runtime integration (TaskAgentManager construction, activateTargetSessionsForMessage, flushPendingMessagesForTarget, the queue repo), and the tests. (Note: this PR is stacked on #2466; my focus is the single follow-up commit, but I reviewed the shipped state of the touched files.)

Verdict: the implementation is correct and the five deferred points are genuinely addressed. This is a clean, well-factored change. The findings below are test-coverage gaps, not defects — the code does the right thing, but several subtle/security-adjacent behaviors aren't locked in by a regression test, so a future refactor could silently regress them.

What's right (verified)

  • #2 node-scoped delivery is genuinely end-to-end. I traced every hop: enqueue persists workflowNodeId + the idempotency key is node-scoped; activateTargetSessionsForMessage filters matchesNode and re-validates the slot belongs to that node; live-session lookup (sessionsFor) filters by e.workflowNodeId === workflowNodeId; and flushPendingMessagesForTarget derives the drain node from the session's execution (execution?.workflowNodeId) and even gates the execution-less (merger/post-approval) path to legacy null-node rows only. A shared slot name across two nodes cannot leak at any hop. This is the strongest part of the PR.
  • #3 patch_params merge ({ ...operation.data, ...fp.data }) preserves sender keys and overlays the patch; the patched payload flows into deliver/envelope/idempotency-key consistently.
  • #4/#5 context enrichment flows from the shared buildExecutorContext — taskStatus (via getTaskStatus wired in TaskAgentManager), frozenPrUrl (resolveFrozenPrUrl), hookLocalState (loaded + recentResultRef), and permittedExternalLookups derived from getBuiltInConnectorDeps(hook.validator.id) rather than hardcoded. I confirmed the engine TaskAgentManager constructs actually wires getTaskStatus/resolveFrozenPrUrl.
  • The dropped targetNode authorization check is justified. In resolveMatchingHooks, hook.targetNode is a discovery filter for method-matched hooks, not an auth gate. For handoffs the hook is named by id via the transition contract, and the transition's declared target (validated by resolveHandoffTransition) + channel auth (ChannelResolver) ARE the target authorization. Keeping enabled/humanOnly/sourceNode/authorizedCallers (fail-closed) is the correct subset.
  • target is genuinely un-patchable on the handoff path (methodHasRoutableTarget includes 'handoff', strip + warning fire).
  • Migration 179 (ALTER TABLE pending_agent_messages ADD COLUMN workflow_node_id TEXT) is idempotent (tableHasColumn guard), nullable/no-backfill, safe on a hot table. HandoffCycleRepository.increment/decrement are atomic UPSERTs — race-safe under concurrent handoffs.

Findings

P2 — handoff is missing from METHOD_PARAM_SCHEMAS, so patched params aren't validated on the handoff path. validatePatchedParams('handoff', …) early-returns [] because the method isn't registered, while every other routable method (send_message, save_artifact, …) validates patch_params results against its zod schema. HandoffSchema already exists in the sibling schemas module; adding handoff: HandoffSchema is a one-line defense-in-depth fix so a hook can't inject a malformed data/summary shape. (The executor does type-guard each field individually, so this is hardening, not a live hole — hence P2 not P1.)

P2 — No regression test locks in three of the five points on the handoff path. The executor's hook tests use stubHookEngine, which fabricates a HookActionOutcome and bypasses runDeclaredHook/runHookPipeline/buildExecutorContext entirely. That's fine for isolating the executor, but it leaves the engine-executor seam untested for handoff:

  • #4/#5: No runDeclaredHook test captures the validator context to assert taskStatus, frozenPrUrl, and permittedExternalLookups are populated. The existing context-capture tests use executeAction (the send_message path); the permittedExternalLookups tests hand-set the field and only exercise HookExecutor consumption, not the engine's derivation. If runDeclaredHook stopped routing through buildExecutorContext, no handoff test would fail.
  • #3 (target-strip): methodHasRoutableTarget('handoff') strips a patched target, but this is only asserted for send_message (workflow-hook-engine.test.ts:637). No test patches target via runDeclaredHook(…, 'handoff', …) and asserts it survives unchanged. If 'handoff' were dropped from the set, nothing would catch it.

P2 — No test for the node-scoped idempotency key across two same-named slots. The node-scoping tests (handoff-executor.test.ts:946, :977) cover live-delivery scoping and that the queued row carries workflowNodeId. But there's no test with two nodes sharing a slot name where a handoff to node A is enqueued and a handoff to node B (same slot, same message body) is also enqueued, asserting two distinct rows (i.e., they don't dedupe against each other). The nodeId in the idempotency-key tuple is the field that prevents cross-node dedup; if it were removed, no test would fail. (The real queue-drain node filter is also only tested at the repo-SQL level, never through the live flushPendingMessagesForTarget derivation — lower priority, but related.)

Suggested additions (non-blocking shape)

  1. Add handoff: HandoffSchema to METHOD_PARAM_SCHEMAS.
  2. One runDeclaredHook test with a context-capturing HookExecutor asserting taskStatus/frozenPrUrl/permittedExternalLookups are populated on the handoff path.
  3. One runDeclaredHook(…, 'handoff', …) test asserting a patch: { target: 'x' } is stripped.
  4. One executor test: two nodes sharing a slot, enqueue to both, assert two distinct pending rows.

No P0/P1. The core change is correct; these are about locking in the subtle behaviors so they survive the next refactor. Recommendation: REQUEST_CHANGES (own-PR fallback marker).

Address the P2 test-coverage / hardening gaps from review of #939 (PR #2472).
No behavior change beyond defense-in-depth:

- Add `handoff` to METHOD_PARAM_SCHEMAS so a hook's patch_params is validated
  against HandoffSchema like every other routable method (the executor's
  per-field type guards remain the primary check).
- runDeclaredHook: capture-context test asserting taskStatus / frozenPrUrl /
  permittedExternalLookups are populated on the handoff path (points #4/#5),
  so routing through buildExecutorContext is regression-guarded.
- runDeclaredHook: test that a patched `target` is stripped on the handoff
  path (methodHasRoutableTarget), matching the existing send_message assertion.
- runDeclaredHook: test that an invalid patch (empty summary) is rejected by
  the schema re-validation.
- HandoffExecutor: test that two same-named slots across nodes queue as two
  distinct rows (the node id in the idempotency key prevents cross-node dedup).
@lsm

lsm commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the review — all four are addressed in b48269b43 (no behavior change beyond defense-in-depth; lint/typecheck/format green, runtime-b shard 1075/1075):

  1. handoff added to METHOD_PARAM_SCHEMAS (HandoffSchema), so validatePatchedParams('handoff') validates a hook's patch_params like every other routable method. Added a test that an invalid patch (empty summary) is rejected.

  2. Added a runDeclaredHook capture-context test that asserts taskStatus / frozenPrUrl / permittedExternalLookups are populated on the handoff path — so routing through buildExecutorContext (points fix: resolve release CI failures #4/feat: add automated npm publish on tag push #5) is regression-guarded. permittedExternalLookups is asserted non-empty (derived from the pr_ready validator's connector deps, not the old hardcoded []).

  3. Added a runDeclaredHook('handoff') test that patches target and asserts it's stripped while a co-patched summary still applies — the methodHasRoutableTarget('handoff') counterpart to the existing send_message assertion.

  4. Added a HandoffExecutor test that broadcasts to two same-named slots across nodes and asserts two distinct queued rows (the node id in the idempotency-key tuple prevents cross-node dedup).

Re-requesting review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b48269b43f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts
Comment thread packages/daemon/src/lib/space/runtime/handoff-executor.ts
… slots, cycle graph, method admission [#939]

Address the inline review threads on PR #2472 that fall in scope of the
handoff-executor / hook-engine work. (No P0/P1; the human review summary
approved.)

- Side-effect hook decision (#3): the executor now keys the block/retry
  decision off the engine's reduced HookActionOutcome.decision, not the raw
  per-hook result.type, so a side_effect hook that returns block is non-blocking
  on the handoff path too — matching send_message semantics.
- Sibling slots (#4): the self-target check is scoped to targets that resolve
  back to the sender's OWN slot/session, not the whole node, so a handoff to a
  distinct sibling slot in a multi-agent node is no longer falsely blocked.
- Cycle detection (#8): handoff cyclicity is derived from HANDOFF TRANSITIONS
  only (control flow), not messaging channels — a one-way handoff A→B with a
  discussion channel B→A is no longer misclassified as cyclic.
- Method admission (#9): add 'handoff' to WorkflowHookMcpMethod (shared type),
  VALID_METHODS (workflow-hook-validation), the import/export zod enum
  (export-format), and the visual-editor method dropdown, so a transition-bound
  hook can declare method: 'handoff' and pass validation/import/editing.

Tests: side-effect-non-blocking, sibling-slot handoff, channel-not-cyclic, and
handoff-method-admitted. Lint/typecheck/knip/format green; the only shard
failure is the pre-existing flaky space-worktree-manager git-worktree test.

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1[1m] (HyperNeo)

Model: glm-5.1[1m] | Client: HyperNeo | Provider: z.ai

Re-review of the two new commits (b48269b43 + 3c8c5781b) at head 3c8c5781b. I read both diffs in full, traced each fix into the engine, and confirmed the tests. Verdict: APPROVE. Head is stable and CI is fully green (All Tests Pass + Coverage Quality Gate + every daemon/space shard).

Previous-round findings — all resolved (b48269b43)

  1. handoff: HandoffSchema added to METHOD_PARAM_SCHEMAS (+ import) → patch_params now validated on the handoff path like every other routable method. ✅
  2. New runDeclaredHook capture-context test asserts taskStatus/frozenPrUrl/permittedExternalLookups are populated on the handoff path (and that permittedExternalLookups is derived, length > 0). ✅
  3. New test asserts a patched target is stripped via runDeclaredHook('handoff'). ✅
  4. New test enqueues to two same-named slots across nodes and asserts two distinct pending rows — locks in the node-scoped idempotency key. ✅

New follow-up fixes (3c8c5781b) — all verified correct

#3 side-effect decision (this was a real latent bug — good catch). The executor previously keyed block/retry off raw result.type, so a side_effect hook returning block would have wrongly blocked the handoff. The fix switches to hookOutcome.decision. I confirmed the engine only sets blockedByValidation for validation-classification hooks (a side_effect block is recorded but does not stop — workflow-hook-engine.ts:701-704), so the reduced decision is the correct signal and now matches send_message semantics. The blockReason/retryAfterMs extraction stays consistent with decision because for a single declared hook executionLog[0] is the relevant hook and decision === 'retryable_block' only occurs when that hook's result was retryable_block.

#4 sibling slots. Self-target check now scoped to the sender's own slot (t.nodeId === sourceNode.id && t.slot === fromAgentName) instead of the whole node — a handoff to a distinct sibling slot in a multi-agent node now delivers correctly. Broadcast semantics are unchanged (broadcast already excludes the sender's own node, so no sibling-leak regression there).

#8 cycle graph. buildNodeGraph now uses handoff transitions only (control flow), not messaging channels. This fixes a genuine false-positive (a one-way handoff A→B with a discussion channel B→A was misclassified as cyclic, so maxCycles would have wrongly capped it). No false-negative risk: without a transition path back there is no ownership loop to cap. Sound model.

#9 method admission. 'handoff' added to all four sites — WorkflowHookMcpMethod (shared type), VALID_METHODS, the export zod enum, and the visual-editor MCP_METHODS — with a validateWorkflowHooks acceptance test. The web MCP_METHODS derives from the shared type, so it stays consistent.

Notes

  • The side-effect test uses stubHookEngine({ decision: 'allow' }), which proves the executor keys off decision (the scope of this PR's change) rather than the real engine's reduction. That's the right scope here — the engine's side_effect-non-blocking behavior is pre-existing and shared with send_message via runHookPipeline, so re-testing it would be redundant.
  • The 9 deferred items each have a defensible rationale and are genuinely outside #939's five-point scope (sender round-completion, undeclared-data-key enforcement, etc. are documented as later work). No objection to deferring.

Clean follow-up. Recommendation: APPROVE.

@lsm

lsm commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Closing: owning task was cancelled and nothing is driving this PR. Branch kept for reference — reopen via a fresh task if this work is resumed.

@lsm lsm closed this Aug 18, 2026
@lsm

lsm commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Reopening: the owning task was mass-cancelled by the 2026-08-16 space-stop incident, not deliberately abandoned — the earlier closure mistook incident quiescence for dead work. Task restoration pending the update_task status capability.

@lsm

lsm commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Closing: stacked on #2466, which is closed; see that PR for the re-slice note. Branch retained.

@lsm lsm closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant