feat: fix strategy execution gap with execution_phase + executionMode (by Wren)#416
Conversation
…cution gap Three fixes for the strategy execution gap where start_strategy marks tasks active but no session spins up: 1. execution_phase column on task_groups tracks real execution state: idle → pending_trigger → worker_active → paused → completed (vs just status: 'active' which is ambiguous) 2. executionMode param on start_strategy: 'spawn' (default) forces a new session bypassing CLI-attached check; 'inline' returns the prompt for the calling agent to loop in the current session 3. Strategy triggers bypass shouldSkipSpawn — they are self-addressed (agent triggers itself) and the channel plugin's self-message filter silently drops them, causing dead letters The trigger handler stamps execution_phase=worker_active when a session actually spawns, giving real-time visibility into strategy progress. Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
Changes requested.
Thanks, Wren. I found a blocker in the trigger bypass path.
The new server.ts logic depends on payload.metadata.strategyTrigger, but the actual strategy path is triggerOwnerAgent() → handleSendToInbox(... threadKey ...) → thread-trigger dispatch. That thread dispatch builds AgentTriggerPayload without forwarding the message metadata, so payload.metadata is undefined in the default trigger handler. Result: isStrategyTrigger stays false, self-addressed strategy triggers still go through shouldSkipSpawn, and the original CLI-attached dead-letter path remains. The same omission also prevents taskGroupId, sandboxContainerName, and the worker_active stamp from working for strategy triggers.
Please propagate/re-hydrate thread message metadata before the skip decision (and add a regression for a self-addressed thread strategy trigger with a CLI-attached routed session). Also update the strategy cancel test: the targeted suite currently fails because the expected update payload doesn’t include execution_phase: 'idle'.
Validation run:
git diff --check origin/main...HEAD✅- changed-file NUL scan ✅
yarn workspace @inklabs/api test src/services/sessions/trigger-delivery.test.ts✅ 14/14yarn workspace @inklabs/api test src/services/strategy.service.test.ts src/mcp/tools/thread-handlers.test.ts src/mcp/tools/task-handlers.test.ts❌ 1 failure:cancelStrategytest expectation missingexecution_phase: 'idle'yarn workspace @inklabs/api type-check❌ known existingchannels/gateway.tsJson andmcp/server.ts thiserrors only
handleSendToInbox was storing metadata in DB rows but dropping it when building AgentTriggerPayload for dispatch. Strategy triggers (strategyTrigger, groupId, etc.) never reached the server.ts handler, so the shouldSkipSpawn bypass and execution_phase stamping were dead code. Also fixes cancelStrategy test expectation for execution_phase: 'idle' and adds regression test for metadata propagation. Co-Authored-By: Wren <noreply@anthropic.com>
conoremclaughlin
left a comment
There was a problem hiding this comment.
LGTM on re-review.
The original blocker is addressed: handleSendToInbox now forwards caller metadata into the AgentTriggerPayload in both the thread and legacy paths, so payload.metadata.strategyTrigger/groupId/sandboxContainerName are available to the default trigger handler before shouldSkipSpawn. The cancelStrategy test expectation is updated, and the targeted suites pass locally.
Validation run:
git diff --check origin/main...HEAD✅- changed-file NUL scan ✅
yarn workspace @inklabs/api test src/services/strategy.service.test.ts src/mcp/tools/inbox-handlers.test.ts src/services/sessions/trigger-delivery.test.ts src/mcp/tools/task-handlers.test.ts✅ 202/202yarn workspace @inklabs/api type-check❌ known baseline only: existingchannels/gateway.tsJson typings andmcp/server.tsthiserror
…n) (#417) ## Summary - **Root cause**: `resolveTriggeredAgents` had an early return for self-threads (1 participant) that returned `[]` unless `selfStudioTarget` was set. Strategy triggers (wren→wren `session_resume`) hit this path because the group had no `studioId`, so the trigger was silently swallowed — `dispatchTrigger` was never called. - **Fix**: In the self-thread case, also trigger self for actionable message types (`session_resume`, `task_request`). These are inherently "wake me up" signals and should not be dropped. - **Context**: This is the second layer of the strategy execution gap fix. PR #416 fixed metadata propagation (so the server handler *could* bypass `shouldSkipSpawn`), but the trigger wasn't even dispatched. Both fixes are needed for strategy self-triggers to work end-to-end. ## Files changed - `packages/api/src/mcp/tools/thread-handlers.ts` — self-thread trigger resolution now allows actionable types - `packages/api/src/mcp/tools/thread-handlers.test.ts` — 3 new test cases (session_resume, task_request, notification) for self-thread behavior ## Test plan - [x] Existing `resolveTriggeredAgents` test updated: plain self-messages still return `[]` - [x] New test: `session_resume` in self-thread returns `[senderAgentId]` - [x] New test: `task_request` in self-thread returns `[senderAgentId]` - [x] New test: `notification` in self-thread still returns `[]` - [x] Full suite passes: thread-handlers (20), inbox-handlers (38), strategy.service (59), trigger-delivery (14) — 152/152 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… (by Wren) (#416) ## Summary Fixes the strategy execution gap where `start_strategy` marks tasks active but no autonomous session spins up (`sessionsUsed: 0`). **Root cause**: Strategy triggers are self-addressed (wren → wren, same studio). The trigger handler sees the session is CLI-attached and skips spawn, expecting the channel plugin to deliver. But the channel plugin's self-message filter silently drops wren→wren messages. Dead letter — nobody processes the work. ### Changes - **`execution_phase` column** on `task_groups` — tracks real execution state (`idle` → `pending_trigger` → `worker_active` → `paused` → `completed`) instead of just `status: 'active'` which is ambiguous about whether work is actually running - **`executionMode` parameter** on `start_strategy` — `'spawn'` (default) force-spawns a new session bypassing the CLI-attached check; `'inline'` returns the prompt so the calling agent loops in the current session. Spawn is for autonomous task groups; inline is for explicit current-session looping - **Strategy triggers bypass `shouldSkipSpawn`** — when `metadata.strategyTrigger` is set, the trigger handler always spawns instead of deferring to channel plugin delivery - **`execution_phase` stamped on session spawn** — the trigger handler sets `worker_active` when a session actually starts, giving real-time visibility into strategy progress - **Phase tracking at all transitions** — pause, resume, cancel, complete, approval gates all update `execution_phase` - **`executionPhase` in API responses** — `get_strategy_status`, `list_task_groups`, `create_task_group`, `update_task_group` all surface the new field ### Files changed | File | What | |------|------| | `supabase/migrations/20260617204931_*.sql` | `execution_phase` column + partial index | | `task-groups.repository.ts` | `ExecutionPhase` type, interface + update method | | `strategy.service.ts` | `ExecutionMode` type, phase transitions at all lifecycle points | | `strategy-handlers.ts` | `executionMode` schema param | | `task-handlers.ts` | `executionPhase` in group responses | | `server.ts` | Strategy trigger bypass + worker_active stamp | | `types.ts` | Supabase generated types | ### Notes The channel plugin self-message filter (dropping sender === recipient messages from the same studio) is a separate issue that could affect other self-addressed scenarios. This PR fixes the strategy path specifically via the force-spawn bypass. The filter itself may warrant a separate fix for non-strategy self-messages. ## Test plan - [x] Type-check passes (no new errors) - [x] Existing task-handler tests pass (91/91) - [x] Trigger delivery tests pass (14/14) - [x] Migration applied locally - [ ] Manual test: `start_strategy` with default `executionMode` → verify new session spawns - [ ] Manual test: `start_strategy` with `executionMode: 'inline'` → verify no trigger, prompt returned - [ ] Verify `get_strategy_status` shows `executionPhase: 'worker_active'` after spawn - [ ] Verify `list_task_groups` includes `executionPhase` in response 🤖 Generated with [Claude Code](https://claude.com/claude-code) — Wren
Summary
Fixes the strategy execution gap where
start_strategymarks tasks active but no autonomous session spins up (sessionsUsed: 0).Root cause: Strategy triggers are self-addressed (wren → wren, same studio). The trigger handler sees the session is CLI-attached and skips spawn, expecting the channel plugin to deliver. But the channel plugin's self-message filter silently drops wren→wren messages. Dead letter — nobody processes the work.
Changes
execution_phasecolumn ontask_groups— tracks real execution state (idle→pending_trigger→worker_active→paused→completed) instead of juststatus: 'active'which is ambiguous about whether work is actually runningexecutionModeparameter onstart_strategy—'spawn'(default) force-spawns a new session bypassing the CLI-attached check;'inline'returns the prompt so the calling agent loops in the current session. Spawn is for autonomous task groups; inline is for explicit current-session loopingshouldSkipSpawn— whenmetadata.strategyTriggeris set, the trigger handler always spawns instead of deferring to channel plugin deliveryexecution_phasestamped on session spawn — the trigger handler setsworker_activewhen a session actually starts, giving real-time visibility into strategy progressexecution_phaseexecutionPhasein API responses —get_strategy_status,list_task_groups,create_task_group,update_task_groupall surface the new fieldFiles changed
supabase/migrations/20260617204931_*.sqlexecution_phasecolumn + partial indextask-groups.repository.tsExecutionPhasetype, interface + update methodstrategy.service.tsExecutionModetype, phase transitions at all lifecycle pointsstrategy-handlers.tsexecutionModeschema paramtask-handlers.tsexecutionPhasein group responsesserver.tstypes.tsNotes
The channel plugin self-message filter (dropping sender === recipient messages from the same studio) is a separate issue that could affect other self-addressed scenarios. This PR fixes the strategy path specifically via the force-spawn bypass. The filter itself may warrant a separate fix for non-strategy self-messages.
Test plan
start_strategywith defaultexecutionMode→ verify new session spawnsstart_strategywithexecutionMode: 'inline'→ verify no trigger, prompt returnedget_strategy_statusshowsexecutionPhase: 'worker_active'after spawnlist_task_groupsincludesexecutionPhasein response🤖 Generated with Claude Code
— Wren