@cuongtran001/kanna 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
  2. package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
  3. package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
  4. package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
  5. package/dist/client/assets/channel-BEYTrUAm.js +1 -0
  6. package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
  7. package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
  8. package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
  9. package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
  10. package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
  11. package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
  12. package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
  13. package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
  14. package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
  15. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
  16. package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
  17. package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
  18. package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
  19. package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
  20. package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
  21. package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
  22. package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
  23. package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
  24. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
  25. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
  26. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
  27. package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
  28. package/dist/client/assets/index-CLsOWXff.css +32 -0
  29. package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
  30. package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
  34. package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
  35. package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
  36. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
  44. package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
  45. package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
  46. package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
  47. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
  48. package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
  49. package/dist/client/index.html +2 -2
  50. package/package.json +1 -1
  51. package/src/server/agent-coordinator-types.ts +158 -0
  52. package/src/server/agent-coordinator.ts +919 -0
  53. package/src/server/agent-deps-builders.ts +425 -0
  54. package/src/server/agent.oauth-rotation.test.ts +101 -0
  55. package/src/server/agent.test.ts +24 -8
  56. package/src/server/agent.ts +35 -4963
  57. package/src/server/auto-continue/read-model.test.ts +2 -2
  58. package/src/server/auto-continue/read-model.ts +4 -1
  59. package/src/server/claude-autocontinue-commands.test.ts +494 -0
  60. package/src/server/claude-autocontinue-commands.ts +256 -0
  61. package/src/server/claude-cancel-handler.test.ts +472 -0
  62. package/src/server/claude-cancel-handler.ts +241 -0
  63. package/src/server/claude-chat-management.test.ts +395 -0
  64. package/src/server/claude-chat-management.ts +251 -0
  65. package/src/server/claude-harness-stream.test.ts +273 -0
  66. package/src/server/claude-harness-stream.ts +158 -0
  67. package/src/server/claude-loop-orch-commands.test.ts +579 -0
  68. package/src/server/claude-loop-orch-commands.ts +466 -0
  69. package/src/server/claude-message-normalizer.test.ts +213 -0
  70. package/src/server/claude-message-normalizer.ts +436 -0
  71. package/src/server/claude-prompt-helpers.test.ts +248 -0
  72. package/src/server/claude-prompt-helpers.ts +139 -0
  73. package/src/server/claude-sdk-queue.test.ts +169 -0
  74. package/src/server/claude-sdk-queue.ts +82 -0
  75. package/src/server/claude-send-command.test.ts +414 -0
  76. package/src/server/claude-send-command.ts +420 -0
  77. package/src/server/claude-session-config-helpers.test.ts +279 -0
  78. package/src/server/claude-session-config-helpers.ts +151 -0
  79. package/src/server/claude-session-config.test.ts +250 -0
  80. package/src/server/claude-session-config.ts +159 -0
  81. package/src/server/claude-session-error-handler.test.ts +498 -0
  82. package/src/server/claude-session-error-handler.ts +361 -0
  83. package/src/server/claude-session-lifecycle.test.ts +669 -0
  84. package/src/server/claude-session-lifecycle.ts +283 -0
  85. package/src/server/claude-session-rebuild.test.ts +224 -0
  86. package/src/server/claude-session-rebuild.ts +101 -0
  87. package/src/server/claude-session-runner.test.ts +523 -0
  88. package/src/server/claude-session-runner.ts +521 -0
  89. package/src/server/claude-session-spawner.test.ts +401 -0
  90. package/src/server/claude-session-spawner.ts +401 -0
  91. package/src/server/claude-session-start.test.ts +205 -0
  92. package/src/server/claude-session-start.ts +277 -0
  93. package/src/server/claude-session-state-queries.test.ts +344 -0
  94. package/src/server/claude-session-state-queries.ts +160 -0
  95. package/src/server/claude-session-state.ts +104 -0
  96. package/src/server/claude-slash-commands.test.ts +277 -0
  97. package/src/server/claude-slash-commands.ts +234 -0
  98. package/src/server/claude-spawn-helpers.test.ts +221 -0
  99. package/src/server/claude-spawn-helpers.ts +179 -0
  100. package/src/server/claude-steer-log.test.ts +243 -0
  101. package/src/server/claude-steer-log.ts +58 -0
  102. package/src/server/claude-subagent-tool-response.test.ts +213 -0
  103. package/src/server/claude-subagent-tool-response.ts +165 -0
  104. package/src/server/claude-subagent-wiring.test.ts +263 -0
  105. package/src/server/claude-subagent-wiring.ts +324 -0
  106. package/src/server/claude-tool-respond.test.ts +321 -0
  107. package/src/server/claude-tool-respond.ts +142 -0
  108. package/src/server/claude-turn-runner.test.ts +310 -0
  109. package/src/server/claude-turn-runner.ts +228 -0
  110. package/src/server/claude-turn-starter.test.ts +266 -0
  111. package/src/server/claude-turn-starter.ts +566 -0
  112. package/src/server/claude-usage-math.test.ts +169 -0
  113. package/src/server/claude-usage-math.ts +132 -0
  114. package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
  115. package/src/server/diff-store-branch-ops.adapter.ts +602 -0
  116. package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
  117. package/src/server/diff-store-commit-ops.adapter.ts +258 -0
  118. package/src/server/diff-store-errors.ts +85 -0
  119. package/src/server/diff-store-file-ops.adapter.ts +212 -0
  120. package/src/server/diff-store-git-branch.adapter.ts +686 -0
  121. package/src/server/diff-store-io.adapter.ts +27 -0
  122. package/src/server/diff-store-parse.test.ts +212 -0
  123. package/src/server/diff-store-parse.ts +122 -0
  124. package/src/server/diff-store-state.ts +82 -0
  125. package/src/server/diff-store.ts +76 -1854
  126. package/src/server/event-store-apply.test.ts +280 -0
  127. package/src/server/event-store-apply.ts +132 -0
  128. package/src/server/event-store-chat-lifecycle.test.ts +408 -0
  129. package/src/server/event-store-chat-lifecycle.ts +441 -0
  130. package/src/server/event-store-entity-write.ts +356 -0
  131. package/src/server/event-store-helpers.test.ts +230 -0
  132. package/src/server/event-store-helpers.ts +235 -0
  133. package/src/server/event-store-init.ts +276 -0
  134. package/src/server/event-store-messages.adapter.test.ts +365 -0
  135. package/src/server/event-store-messages.adapter.ts +213 -0
  136. package/src/server/event-store-orch-subscription.ts +124 -0
  137. package/src/server/event-store-orch.test.ts +462 -0
  138. package/src/server/event-store-orch.ts +312 -0
  139. package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
  140. package/src/server/event-store-peripheral-events.adapter.ts +105 -0
  141. package/src/server/event-store-snapshot.test.ts +597 -0
  142. package/src/server/event-store-snapshot.ts +658 -0
  143. package/src/server/event-store-subagent.test.ts +408 -0
  144. package/src/server/event-store-subagent.ts +198 -0
  145. package/src/server/event-store-tool-requests.test.ts +210 -0
  146. package/src/server/event-store-tool-requests.ts +131 -0
  147. package/src/server/event-store-transcript-write.adapter.ts +285 -0
  148. package/src/server/event-store-write-ops.test.ts +554 -0
  149. package/src/server/event-store-write-ops.ts +529 -0
  150. package/src/server/event-store.test.ts +35 -0
  151. package/src/server/event-store.ts +251 -2284
  152. package/src/server/events.ts +3 -0
  153. package/src/server/harness-types.ts +15 -1
  154. package/src/server/oauth-errors.ts +19 -0
  155. package/src/server/read-models.ts +26 -1
  156. package/src/server/subagent-orchestrator.ts +3 -0
  157. package/src/server/ws-router-agent-ctrl.test.ts +187 -0
  158. package/src/server/ws-router-agent-ctrl.ts +137 -0
  159. package/src/server/ws-router-broadcast.ts +488 -0
  160. package/src/server/ws-router-chat.test.ts +461 -0
  161. package/src/server/ws-router-chat.ts +279 -0
  162. package/src/server/ws-router-defaults.test.ts +283 -0
  163. package/src/server/ws-router-defaults.ts +434 -0
  164. package/src/server/ws-router-diff.test.ts +167 -0
  165. package/src/server/ws-router-diff.ts +250 -0
  166. package/src/server/ws-router-envelope.ts +329 -0
  167. package/src/server/ws-router-misc.test.ts +422 -0
  168. package/src/server/ws-router-misc.ts +265 -0
  169. package/src/server/ws-router-orch.test.ts +190 -0
  170. package/src/server/ws-router-orch.ts +93 -0
  171. package/src/server/ws-router-project.test.ts +403 -0
  172. package/src/server/ws-router-project.ts +256 -0
  173. package/src/server/ws-router-push.test.ts +193 -0
  174. package/src/server/ws-router-push.ts +120 -0
  175. package/src/server/ws-router-settings.test.ts +173 -0
  176. package/src/server/ws-router-settings.ts +416 -0
  177. package/src/server/ws-router-skills.test.ts +194 -0
  178. package/src/server/ws-router-skills.ts +204 -0
  179. package/src/server/ws-router-utils.ts +224 -0
  180. package/src/server/ws-router.ts +339 -2254
  181. package/src/shared/app-settings-types.ts +365 -0
  182. package/src/shared/core-types.ts +27 -0
  183. package/src/shared/git-diff-types.ts +165 -0
  184. package/src/shared/loop-progress.test.ts +106 -0
  185. package/src/shared/loop-progress.ts +88 -0
  186. package/src/shared/mcp-types.ts +105 -0
  187. package/src/shared/provider-model-types.ts +437 -0
  188. package/src/shared/subagent-types.ts +198 -0
  189. package/src/shared/tool-call-types.ts +314 -0
  190. package/src/shared/transcript-types.ts +270 -0
  191. package/src/shared/types.ts +27 -1668
  192. package/dist/client/assets/channel-BMkhjiLI.js +0 -1
  193. package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
  194. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
  195. package/dist/client/assets/index-H6C-f2Vp.css +0 -32
  196. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Builder functions for all 18 *Deps objects used by AgentCoordinator.
3
+ *
4
+ * Each function takes the coordinator instance and returns the typed *Deps
5
+ * object that the corresponding functional module expects. Using
6
+ * `import type { AgentCoordinator }` avoids a circular runtime dependency
7
+ * (the import is erased at compile time).
8
+ *
9
+ * **Visibility requirement:** All AgentCoordinator fields and methods accessed
10
+ * here must be declared `readonly` (not `private readonly`) so TypeScript
11
+ * allows external access.
12
+ */
13
+
14
+ import type { AgentCoordinator } from "./agent-coordinator"
15
+ import { ensureFreshMcpToken } from "./mcp-oauth.adapter"
16
+ import { realpathAdapter } from "./paths-fs.adapter"
17
+ import { ensureTrackingFile } from "./loop-template-io.adapter"
18
+ import { homedir } from "node:os"
19
+ import { providerUsesSdkSession } from "../shared/types"
20
+ import { isClaudeSdkProvider } from "./provider-catalog"
21
+
22
+ import type { ClaudeSessionConfigHelpersDeps } from "./claude-session-config-helpers"
23
+ import type { SessionLifecycleDeps } from "./claude-session-lifecycle"
24
+ import type { SessionErrorHandlerDeps } from "./claude-session-error-handler"
25
+ import type { AutoContinueCommandDeps } from "./claude-autocontinue-commands"
26
+ import type { LoopOrchCommandDeps } from "./claude-loop-orch-commands"
27
+ import type { CancelHandlerDeps } from "./claude-cancel-handler"
28
+ import type { ChatManagementDeps } from "./claude-chat-management"
29
+ import type { SendCommandDeps } from "./claude-send-command"
30
+ import type { SubagentWiringDeps } from "./claude-subagent-wiring"
31
+ import type { SlashCommandsDeps } from "./claude-slash-commands"
32
+ import type { SubagentToolResponseDeps } from "./claude-subagent-tool-response"
33
+ import type { ToolRespondDeps } from "./claude-tool-respond"
34
+ import type { SessionStateQueryDeps } from "./claude-session-state-queries"
35
+ import type { StartTurnDeps } from "./claude-turn-starter"
36
+ import type { SessionRebuildDeps } from "./claude-session-rebuild"
37
+ import type { SpawnClaudeTurnDeps } from "./claude-session-spawner"
38
+ import type { RunClaudeSessionDeps } from "./claude-session-runner"
39
+ import type { RunTurnDeps } from "./claude-turn-runner"
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // 1. Session config helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export function buildClaudeSessionConfigHelpersDeps(agent: AgentCoordinator): ClaudeSessionConfigHelpersDeps {
46
+ return {
47
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
48
+ chatPolicy: agent.chatPolicy,
49
+ store: agent.store,
50
+ ptyInstanceRegistry: agent.ptyInstanceRegistry,
51
+ ensureFreshToken: (server, opts) => ensureFreshMcpToken(server, opts),
52
+ persistOAuthState: agent.persistOAuthStateFn,
53
+ killProcessTree: async (pid) => {
54
+ const { killProcessTree } = await import("./claude-pty/pid-registry.adapter")
55
+ await killProcessTree(pid)
56
+ },
57
+ }
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // 2. Session lifecycle
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export function buildSessionLifecycleDeps(agent: AgentCoordinator): SessionLifecycleDeps {
65
+ return {
66
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
67
+ defaultIdleMs: agent.claudeSessionLifecycle.idleMs,
68
+ defaultMaxResidentSessions: agent.claudeSessionLifecycle.maxResidentSessions,
69
+ claudeSessions: agent.claudeSessions,
70
+ activeTurns: agent.activeTurns,
71
+ oauthPool: agent.oauthPool,
72
+ workflowRegistry: agent.workflowRegistry,
73
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
74
+ emitStateChange: (chatId: string) => { agent.emitStateChange(chatId) },
75
+ store: agent.store,
76
+ homeDir: homedir(),
77
+ }
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // 3. Session error handler
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export function buildSessionErrorHandlerDeps(agent: AgentCoordinator): SessionErrorHandlerDeps {
85
+ return {
86
+ tokenRotationDedupe: agent.tokenRotationDedupe,
87
+ claudeSessions: agent.claudeSessions,
88
+ activeTurns: agent.activeTurns,
89
+ oauthPool: agent.oauthPool,
90
+ store: agent.store,
91
+ resolveAutoResumeFor: (chatId: string) => agent.resolveAutoResumeFor(chatId),
92
+ emitAutoContinueEvent: (event) => agent.emitAutoContinueEvent(event),
93
+ closeClaudeSession: (chatId, session, opts?) =>
94
+ agent.closeClaudeSession(chatId, session, opts),
95
+ }
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // 4. Auto-continue commands
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export function buildAutoContinueCommandDeps(agent: AgentCoordinator): AutoContinueCommandDeps {
103
+ return {
104
+ autoResumeByChat: agent.autoResumeByChat,
105
+ getAutoResumePreference: () => agent.getAutoResumePreference(),
106
+ store: agent.store,
107
+ scheduleManager: agent.scheduleManager,
108
+ emitStateChange: (chatId: string) => { agent.emitStateChange(chatId) },
109
+ enqueueMessage: (chatId, content, attachments, options) =>
110
+ agent.enqueueMessage(chatId, content, attachments, options),
111
+ maybeStartNextQueuedMessage: (chatId) => agent.maybeStartNextQueuedMessage(chatId),
112
+ }
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // 5. Loop / orch commands
117
+ // ---------------------------------------------------------------------------
118
+
119
+ export function buildLoopOrchCommandDeps(agent: AgentCoordinator): LoopOrchCommandDeps {
120
+ return {
121
+ store: agent.store,
122
+ orchestrationQueue: agent.getOrchestrationQueue(),
123
+ claudeSessions: agent.claudeSessions,
124
+ activeTurns: agent.activeTurns,
125
+ getSubagents: () => agent.getSubagents(),
126
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
127
+ buildSubagentProviderRunForChat: (args) => agent.buildSubagentProviderRunForChat(args),
128
+ closeClaudeSession: (chatId, session) => agent.closeClaudeSession(chatId, session),
129
+ emitAutoContinueEvent: (event) => agent.emitAutoContinueEvent(event),
130
+ ensureTrackingFile,
131
+ isLoopArmed: (chatId) => agent.isLoopArmed(chatId),
132
+ }
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // 6. Cancel handler
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export function buildCancelHandlerDeps(agent: AgentCoordinator): CancelHandlerDeps {
140
+ return {
141
+ drainingStreams: agent.drainingStreams,
142
+ rejectPendingResolversForChat: (chatId) => agent.rejectPendingResolversForChat(chatId),
143
+ cancelChatInOrchestrator: (chatId) => agent.getSubagentOrchestrator().cancelChat(chatId),
144
+ activeTurns: agent.activeTurns,
145
+ store: agent.store,
146
+ claudeSessions: agent.claudeSessions,
147
+ emitStateChange: (chatId) => agent.emitStateChange(chatId),
148
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
149
+ closeClaudeSession: (chatId, session) => agent.closeClaudeSession(chatId, session),
150
+ maybeStartNextQueuedMessage: async (chatId) => { await agent.maybeStartNextQueuedMessage(chatId) },
151
+ }
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // 7. Chat management
156
+ // ---------------------------------------------------------------------------
157
+
158
+ export function buildChatManagementDeps(agent: AgentCoordinator): ChatManagementDeps {
159
+ return {
160
+ activeTurns: agent.activeTurns,
161
+ drainingStreams: agent.drainingStreams,
162
+ claudeSessions: agent.claudeSessions,
163
+ autoResumeByChat: agent.autoResumeByChat,
164
+ store: agent.store,
165
+ analytics: agent.analytics,
166
+ cancel: (chatId, options) => agent.cancel(chatId, options),
167
+ closeClaudeSession: (chatId, session, opts) => agent.closeClaudeSession(chatId, session, opts),
168
+ emitStateChange: (chatId) => agent.emitStateChange(chatId),
169
+ generateTitle: (messageContent, cwd) => agent.generateTitle(messageContent, cwd),
170
+ reportBackgroundError: agent.reportBackgroundError,
171
+ dequeueAndStartQueuedMessage: (chatId, queuedMessage, options) =>
172
+ agent.dequeueAndStartQueuedMessage(chatId, queuedMessage, options),
173
+ }
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // 8. Send command
178
+ // ---------------------------------------------------------------------------
179
+
180
+ export function buildSendCommandDeps(agent: AgentCoordinator): SendCommandDeps {
181
+ return {
182
+ store: agent.store,
183
+ activeTurns: agent.activeTurns,
184
+ claudeSessions: agent.claudeSessions,
185
+ autoResumeByChat: agent.autoResumeByChat,
186
+ analytics: agent.analytics,
187
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
188
+ stopLoop: (chatId, reason) => agent.stopLoop(chatId, reason),
189
+ emitStateChange: (chatId) => agent.emitStateChange(chatId),
190
+ startTurnForChat: (args) => agent.startTurnForChat(args),
191
+ }
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // 9. Subagent wiring
196
+ // ---------------------------------------------------------------------------
197
+
198
+ export function buildSubagentWiringDeps(agent: AgentCoordinator): SubagentWiringDeps {
199
+ return {
200
+ store: agent.store,
201
+ startClaudeSessionFn: agent.startClaudeSessionFn,
202
+ startClaudeSessionPTYFn: agent.startClaudeSessionPTYFn,
203
+ toolCallback: agent.toolCallback,
204
+ tunnelGateway: agent.tunnelGateway,
205
+ claudePtyRegistry: agent.claudePtyRegistry,
206
+ ptyInstanceRegistry: agent.ptyInstanceRegistry,
207
+ workflowRegistry: agent.workflowRegistry,
208
+ subagentOrchestrator: agent.getSubagentOrchestrator(),
209
+ codexManager: agent.codexManager,
210
+ oauthPool: agent.oauthPool,
211
+ subagentPendingResolvers: agent.subagentPendingResolvers,
212
+ realpath: realpathAdapter,
213
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
214
+ getEnabledCustomMcpServers: () => agent.getEnabledCustomMcpServers(),
215
+ buildOAuthBearers: (servers) => agent.buildOAuthBearers(servers),
216
+ resolveChatPolicy: (chatId) => agent.resolveChatPolicy(chatId),
217
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
218
+ buildPoolUnavailableMessage: (reservedFor, scopeSuffix) =>
219
+ agent.buildPoolUnavailableMessage(reservedFor, scopeSuffix),
220
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
221
+ readLlmProvider: () => agent.readLlmProvider(),
222
+ subagentPendingKey: (chatId, runId, toolUseId) =>
223
+ agent.subagentPendingKey(chatId, runId, toolUseId),
224
+ }
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // 10. Slash commands
229
+ // ---------------------------------------------------------------------------
230
+
231
+ export function buildSlashCommandsDeps(agent: AgentCoordinator): SlashCommandsDeps {
232
+ return {
233
+ store: agent.store,
234
+ claudeSessions: agent.claudeSessions,
235
+ oauthPool: agent.oauthPool,
236
+ slashCommandsInFlight: agent.slashCommandsInFlight,
237
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
238
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
239
+ startClaudeSessionPTY: agent.startClaudeSessionPTYFn,
240
+ startClaudeSessionSDK: agent.startClaudeSessionFn,
241
+ getSubagents: () => agent.getSubagents(),
242
+ getGlobalPromptAppend: () => agent.getAppSettingsSnapshot().globalPromptAppend,
243
+ getEnabledCustomMcpServers: () => agent.getEnabledCustomMcpServers(),
244
+ claudePtyRegistry: agent.claudePtyRegistry,
245
+ ptyInstanceRegistry: agent.ptyInstanceRegistry,
246
+ workflowRegistry: agent.workflowRegistry,
247
+ subagentTranscriptRegistry: agent.subagentTranscriptRegistry,
248
+ localCatalog: agent.localCatalog,
249
+ }
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // 11. Subagent tool response
254
+ // ---------------------------------------------------------------------------
255
+
256
+ export function buildSubagentToolResponseDeps(agent: AgentCoordinator): SubagentToolResponseDeps {
257
+ return {
258
+ subagentPendingResolvers: agent.subagentPendingResolvers,
259
+ store: agent.store,
260
+ subagentOrchestrator: agent.getSubagentOrchestrator(),
261
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
262
+ }
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // 12. Tool respond
267
+ // ---------------------------------------------------------------------------
268
+
269
+ export function buildToolRespondDeps(agent: AgentCoordinator): ToolRespondDeps {
270
+ return {
271
+ activeTurns: agent.activeTurns,
272
+ store: agent.store,
273
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
274
+ }
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // 13. Session state queries
279
+ // ---------------------------------------------------------------------------
280
+
281
+ export function buildSessionStateQueryDeps(agent: AgentCoordinator): SessionStateQueryDeps {
282
+ return {
283
+ activeTurns: agent.activeTurns,
284
+ claudeSessions: agent.claudeSessions,
285
+ drainingStreams: agent.drainingStreams,
286
+ slashCommandsInFlight: agent.slashCommandsInFlight,
287
+ isClaudeSdkProvider: (provider) => isClaudeSdkProvider(provider),
288
+ hasPendingBackgroundTask: (session, now) => agent.hasPendingBackgroundTask(session, now),
289
+ resolveClaudeIdleMs: () => agent.resolveClaudeIdleMs(),
290
+ hasLiveWorkflow: (chatId) => agent.hasLiveWorkflow(chatId),
291
+ closeClaudeSession: (chatId, session) => { agent.closeClaudeSession(chatId, session) },
292
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
293
+ }
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // 14. Start turn
298
+ // ---------------------------------------------------------------------------
299
+
300
+ export function buildStartTurnDeps(agent: AgentCoordinator): StartTurnDeps {
301
+ return {
302
+ activeTurns: agent.activeTurns,
303
+ claudeSessions: agent.claudeSessions,
304
+ drainingStreams: agent.drainingStreams,
305
+ mentionedSubagentIdsByChat: agent.mentionedSubagentIdsByChat,
306
+ store: agent.store,
307
+ codexManager: agent.codexManager,
308
+ subagentOrchestrator: agent.getSubagentOrchestrator(),
309
+ clearDrainingStream: (chatId) => agent.clearDrainingStream(chatId),
310
+ emitStateChange: (chatId, opts) => agent.emitStateChange(chatId, opts),
311
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
312
+ getSubagents: () => agent.getSubagents(),
313
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
314
+ generateTitleInBackground: (chatId, content, localPath, optimisticTitle) =>
315
+ agent.generateTitleInBackground(chatId, content, localPath, optimisticTitle),
316
+ recreateActiveTurnFromSession: (args) => agent.recreateActiveTurnFromSession(args),
317
+ startClaudeTurn: (args) => agent.startClaudeTurn(args),
318
+ findLastUserMessageId: (chatId) => agent.findLastUserMessageId(chatId),
319
+ runTurn: (active) => agent.runTurn(active),
320
+ }
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // 15. Session rebuild
325
+ // ---------------------------------------------------------------------------
326
+
327
+ export function buildSessionRebuildDeps(agent: AgentCoordinator): SessionRebuildDeps {
328
+ return {
329
+ claudeSessions: agent.claudeSessions,
330
+ activeTurns: agent.activeTurns,
331
+ providerUsesSdkSession: (provider) => providerUsesSdkSession(provider),
332
+ getMessages: (chatId) => agent.store.getMessages(chatId),
333
+ }
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // 16. Spawn Claude turn
338
+ // ---------------------------------------------------------------------------
339
+
340
+ export function buildSpawnClaudeTurnDeps(agent: AgentCoordinator): SpawnClaudeTurnDeps {
341
+ return {
342
+ claudeSessions: agent.claudeSessions,
343
+ activeTurns: agent.activeTurns,
344
+ mentionedSubagentIdsByChat: agent.mentionedSubagentIdsByChat,
345
+ oauthPool: agent.oauthPool,
346
+ store: agent.store,
347
+ startClaudeSessionFn: agent.startClaudeSessionFn,
348
+ startClaudeSessionPTYFn: agent.startClaudeSessionPTYFn,
349
+ subagentOrchestrator: agent.getSubagentOrchestrator(),
350
+ toolCallback: agent.toolCallback,
351
+ tunnelGateway: agent.tunnelGateway,
352
+ claudePtyRegistry: agent.claudePtyRegistry,
353
+ ptyInstanceRegistry: agent.ptyInstanceRegistry,
354
+ workflowRegistry: agent.workflowRegistry,
355
+ subagentTranscriptRegistry: agent.subagentTranscriptRegistry,
356
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
357
+ isLoopArmed: (chatId) => agent.isLoopArmed(chatId),
358
+ closeClaudeSession: (chatId, session) => agent.closeClaudeSession(chatId, session),
359
+ enforceClaudeSessionBudget: (protectedChatId?) => agent.enforceClaudeSessionBudget(protectedChatId),
360
+ readLlmProvider: () => agent.readLlmProvider(),
361
+ buildPoolUnavailableMessage: (reservedFor, scopeSuffix) =>
362
+ agent.buildPoolUnavailableMessage(reservedFor, scopeSuffix),
363
+ listOpenRouterModelsFn: agent.listOpenRouterModelsFn,
364
+ getSubagents: () => agent.getSubagents(),
365
+ getAppSettingsSnapshot: () => agent.getAppSettingsSnapshot(),
366
+ getEnabledCustomMcpServers: () => agent.getEnabledCustomMcpServers(),
367
+ buildOAuthBearers: (servers) => agent.buildOAuthBearers(servers),
368
+ setupLoop: (chatId, input) => agent.setupLoop({ chatId, input }),
369
+ stopLoop: (chatId, reason) => agent.stopLoop(chatId, reason),
370
+ runOrchestration: (chatId, input) => agent.runOrchestration(chatId, input),
371
+ cancelOrchRun: (runId) => agent.cancelOrchRun(runId),
372
+ getOrchRunDetail: (runId) => agent.getOrchRunDetail(runId),
373
+ resolveChatPolicy: (chatId) => agent.resolveChatPolicy(chatId),
374
+ runClaudeSession: (session) => { void agent.runClaudeSession(session) },
375
+ mergeLocalCatalog: (commands, cwd) => agent.mergeLocalCatalog(commands, cwd),
376
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
377
+ }
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // 17. Run Claude session
382
+ // ---------------------------------------------------------------------------
383
+
384
+ export function buildRunClaudeSessionDeps(agent: AgentCoordinator): RunClaudeSessionDeps {
385
+ return {
386
+ openrouterFirstEntryTimeoutMs: agent.openrouterFirstEntryTimeoutMs,
387
+ claudeSessions: agent.claudeSessions,
388
+ activeTurns: agent.activeTurns,
389
+ oauthPool: agent.oauthPool,
390
+ claudeLimitDetector: agent.claudeLimitDetector,
391
+ claudeAuthErrorDetector: agent.claudeAuthErrorDetector,
392
+ throwOnClaudeSessionStart: agent.throwOnClaudeSessionStart,
393
+ store: agent.store,
394
+ emitStateChange: (chatId?) => { agent.emitStateChange(chatId) },
395
+ handleLimitDetection: (chatId, detection) => agent.handleLimitDetection(chatId, detection),
396
+ maybeRegisterSdkWorkflowsDir: (session) => { agent.maybeRegisterSdkWorkflowsDir(session) },
397
+ getSubagents: () => agent.getSubagents(),
398
+ resolveBackgroundTaskMaxMs: () => agent.resolveBackgroundTaskMaxMs(),
399
+ mergeLocalCatalog: (commands, cwd) => agent.mergeLocalCatalog(commands, cwd),
400
+ handleLimitError: (chatId, detector, error) => agent.handleLimitError(chatId, detector, error),
401
+ handleAuthFailure: (session, detection) => agent.handleAuthFailure(session, detection),
402
+ closeClaudeSession: (chatId, session) => { agent.closeClaudeSession(chatId, session) },
403
+ maybeStartNextQueuedMessage: (chatId) => agent.maybeStartNextQueuedMessage(chatId),
404
+ resolveClaudeDriverPreference: () => agent.resolveClaudeDriverPreference(),
405
+ }
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // 18. Run turn
410
+ // ---------------------------------------------------------------------------
411
+
412
+ export function buildRunTurnDeps(agent: AgentCoordinator): RunTurnDeps {
413
+ return {
414
+ store: agent.store,
415
+ activeTurns: agent.activeTurns,
416
+ drainingStreams: agent.drainingStreams,
417
+ oauthPool: agent.oauthPool,
418
+ codexLimitDetector: agent.codexLimitDetector,
419
+ handleLimitError: (chatId, detector, error) => agent.handleLimitError(chatId, detector, error),
420
+ emitStateChange: (chatId) => { agent.emitStateChange(chatId) },
421
+ clearDrainingStream: (chatId) => { agent.clearDrainingStream(chatId) },
422
+ startTurnForChat: (args) => agent.startTurnForChat(args),
423
+ maybeStartNextQueuedMessage: (chatId) => agent.maybeStartNextQueuedMessage(chatId),
424
+ }
425
+ }
@@ -928,4 +928,105 @@ describe("AgentCoordinator OAuth rotation", () => {
928
928
  },
929
929
  10_000,
930
930
  )
931
+
932
+ test(
933
+ "errored rate-limit RESULT with no matching active turn still schedules a resume (loop silent-death regression)",
934
+ async () => {
935
+ // Regression for the 9h autonomous-loop stall: a synthetic 429 `result`
936
+ // arriving when the pending prompt-seq queue is already drained (observed
937
+ // on an auto-continue wake turn) fails the
938
+ // `completedClaudePromptSeq === active.claudePromptSeq` gate, so the entire
939
+ // error-handling block — INCLUDING limit detection — was skipped and the
940
+ // loop died with NO resume schedule (no proposal, no accept). It only
941
+ // recovered when the user manually typed "resume" hours later. A
942
+ // recognizable rate-limit result must schedule a resume even when the seq
943
+ // gate misses. Idempotent: handleLimitDetection dedupes on a live schedule.
944
+ let tokens: OAuthTokenEntry[] = [makeToken("a")] // single token → no rotation target
945
+ const pool = new OAuthTokenPool(
946
+ () => tokens,
947
+ (id, patch) => {
948
+ tokens = tokens.map((t) => (t.id === id ? { ...t, ...patch } : t))
949
+ },
950
+ )
951
+
952
+ const events = new AsyncEventQueue<HarnessEvent>()
953
+ const store = createFakeStore()
954
+ const coordinator = new AgentCoordinator({
955
+ store: store as never,
956
+ onStateChange: () => {},
957
+ oauthPool: pool,
958
+ // auto-resume preference defaults to OFF → the resume is a *proposal*,
959
+ // respecting the user's setting (not a silent forced accept).
960
+ startClaudeSession: async () => ({
961
+ provider: "claude",
962
+ stream: events,
963
+ getAccountInfo: async () => null,
964
+ interrupt: async () => {},
965
+ close: () => {},
966
+ setModel: async () => {},
967
+ setPermissionMode: async () => {},
968
+ getSupportedCommands: async () => [],
969
+ sendPrompt: async () => {
970
+ // Turn 1 finalizes normally, draining the pending prompt-seq…
971
+ events.push({
972
+ type: "transcript",
973
+ entry: {
974
+ _id: "result-ok",
975
+ createdAt: Date.now(),
976
+ kind: "result",
977
+ subtype: "success",
978
+ isError: false,
979
+ durationMs: 10,
980
+ result: "",
981
+ } as never,
982
+ })
983
+ // …then a synthetic 429 result arrives with the seq queue empty and
984
+ // no active turn — the gate misses. Pre-fix this was silently dropped.
985
+ events.push({
986
+ type: "transcript",
987
+ entry: {
988
+ _id: "result-429",
989
+ createdAt: Date.now(),
990
+ kind: "result",
991
+ subtype: "error",
992
+ isError: true,
993
+ durationMs: 0,
994
+ result: "You've hit your limit · resets 4:10am (Asia/Saigon)",
995
+ } as never,
996
+ })
997
+ events.close()
998
+ },
999
+ }),
1000
+ })
1001
+
1002
+ await coordinator.send({
1003
+ type: "chat.send",
1004
+ chatId: "chat-1",
1005
+ provider: "claude",
1006
+ content: "test",
1007
+ model: "claude-opus-4-7",
1008
+ })
1009
+
1010
+ await waitFor(
1011
+ () =>
1012
+ store
1013
+ .getAutoContinueEvents("chat-1")
1014
+ .some(
1015
+ (e) => e.kind === "auto_continue_proposed" || e.kind === "auto_continue_accepted",
1016
+ ),
1017
+ 4000,
1018
+ "unmatched rate-limit result schedules a resume instead of silent death",
1019
+ )
1020
+
1021
+ const schedule = store
1022
+ .getAutoContinueEvents("chat-1")
1023
+ .find(
1024
+ (e) => e.kind === "auto_continue_proposed" || e.kind === "auto_continue_accepted",
1025
+ )
1026
+ expect(schedule).toBeDefined()
1027
+ // Single-token pool → no rotation; auto-resume off → a proposal (respects setting).
1028
+ expect(schedule!.kind).toBe("auto_continue_proposed")
1029
+ },
1030
+ 10_000,
1031
+ )
931
1032
  })
@@ -23,6 +23,7 @@ import type { ChatPermissionPolicy } from "../shared/permission-policy"
23
23
  import { POLICY_DEFAULT } from "../shared/permission-policy"
24
24
  import type { HarnessTurn } from "./harness-types"
25
25
  import type { ChatAttachment, McpServerConfig, SlashCommand, TranscriptEntry } from "../shared/types"
26
+ import { AUTO_CONTINUE_EVENT_VERSION } from "./auto-continue/events"
26
27
  import type { AutoContinueEvent } from "./auto-continue/events"
27
28
  import type { WorkflowRegistry } from "./workflow-registry"
28
29
  import { AsyncEventQueue } from "./test-helpers/async-event-queue"
@@ -1213,13 +1214,19 @@ describe("AgentCoordinator codex integration", () => {
1213
1214
  content: "run something with a background task",
1214
1215
  })
1215
1216
 
1216
- // Wait for the result message to be persisted
1217
- await waitFor(() => store.messages.some((entry) => entry.kind === "result"))
1217
+ // Wait for the active turn to be removed — this happens synchronously
1218
+ // right after recordTurnFinished resolves, which is itself after
1219
+ // appendMessage(result). Using this as the waitFor condition avoids a
1220
+ // microtask-gap race: waiting for messages.some("result") fires one await
1221
+ // before activeTurns.delete, so the assertion would see the turn still set.
1222
+ await waitFor(() => !coordinator.getActiveStatuses().has("chat-1"))
1218
1223
 
1219
1224
  // The active turn should be removed even though the stream is still open.
1220
1225
  // This is the key assertion: the UI should show idle (not "Running...")
1221
1226
  // so the user can send new messages without hitting stop.
1222
1227
  expect(coordinator.getActiveStatuses().has("chat-1")).toBe(false)
1228
+ // Result must have been appended (that's what triggers the removal).
1229
+ expect(store.messages.some((entry) => entry.kind === "result")).toBe(true)
1223
1230
  expect(store.turnFinishedCount).toBe(1)
1224
1231
 
1225
1232
  // The stream is still open, so it should be draining
@@ -3600,7 +3607,7 @@ describe("AgentCoordinator claude integration", () => {
3600
3607
 
3601
3608
  // Pin the armed state to the test toggle — the send() takeover path would
3602
3609
  // otherwise disarm before the spawn check ever sees an armed loop.
3603
- coordinator.isLoopArmed = () => (armed ? { subagentId: "sa-1", prompt: "loop prompt" } : null)
3610
+ coordinator.isLoopArmed = () => (armed ? { subagentId: "sa-1", prompt: "loop prompt", armedAt: 0 } : null)
3604
3611
 
3605
3612
  const sendTurn = async (content: string, expectedFinished: number) => {
3606
3613
  await coordinator.send({
@@ -3681,7 +3688,7 @@ describe("AgentCoordinator claude integration", () => {
3681
3688
  },
3682
3689
  })
3683
3690
 
3684
- coordinator.isLoopArmed = () => (armed ? { subagentId: "sa-1", prompt: "loop prompt" } : null)
3691
+ coordinator.isLoopArmed = () => (armed ? { subagentId: "sa-1", prompt: "loop prompt", armedAt: 0 } : null)
3685
3692
 
3686
3693
  const sendTurn = async (content: string, expectedFinished: number) => {
3687
3694
  await coordinator.send({
@@ -4369,12 +4376,21 @@ describe("AgentCoordinator.deliverSubagentToMain (notification-driven /clear)",
4369
4376
 
4370
4377
  test("armed loop: notification omits <result> (PROGRESS.md is the contract) and the full loop prompt follows", async () => {
4371
4378
  const store = createFakeStore()
4379
+ // Pre-arm the loop via store events so isLoopArmed() returns the armed state.
4380
+ store.autoContinueEvents.push({
4381
+ v: AUTO_CONTINUE_EVENT_VERSION,
4382
+ timestamp: 0,
4383
+ chatId: "chat-1",
4384
+ scheduleId: "loop-schedule-1",
4385
+ kind: "loop_armed",
4386
+ subagentId: "sa-loop",
4387
+ prompt: "LOOP DISCIPLINE PROMPT",
4388
+ })
4372
4389
  const coordinator = new AgentCoordinator({
4373
4390
  store: store as never,
4374
4391
  onStateChange: () => {},
4375
4392
  startClaudeSession: async () => { throw new Error("not needed") },
4376
4393
  })
4377
- coordinator.isLoopArmed = () => ({ subagentId: "sa-loop", prompt: "LOOP DISCIPLINE PROMPT" })
4378
4394
 
4379
4395
  await (coordinator as unknown as { deliverSubagentToMain: DeliverFn }).deliverSubagentToMain(
4380
4396
  "chat-1",
@@ -4382,9 +4398,9 @@ describe("AgentCoordinator.deliverSubagentToMain (notification-driven /clear)",
4382
4398
  { status: "completed", runId: "run-loop", text: "iteration output that must not leak" },
4383
4399
  )
4384
4400
 
4385
- const events = store.getAutoContinueEvents("chat-1")
4386
- expect(events).toHaveLength(1)
4387
- const ev = events[0]
4401
+ const acEvents = store.getAutoContinueEvents("chat-1").filter((e) => e.kind === "auto_continue_accepted")
4402
+ expect(acEvents).toHaveLength(1)
4403
+ const ev = acEvents[0]
4388
4404
  expect(ev.kind).toBe("auto_continue_accepted")
4389
4405
  if (ev.kind === "auto_continue_accepted") {
4390
4406
  expect(ev.prompt).toContain("<task-notification>")