@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,263 @@
1
+ /**
2
+ * Colocated unit tests for claude-subagent-wiring.ts.
3
+ *
4
+ * Tests cover the two extracted functions without a real AgentCoordinator:
5
+ * - buildClaudeSubagentStarter — PTY vs SDK dispatch logic
6
+ * - buildSubagentProviderRunForChat — ProviderRunStart construction
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test"
10
+ import {
11
+ buildClaudeSubagentStarter,
12
+ buildSubagentProviderRunForChat,
13
+ type SubagentWiringDeps,
14
+ type BuildSubagentProviderRunForChatArgs,
15
+ } from "./claude-subagent-wiring"
16
+ import type { ProviderRunStart } from "./subagent-orchestrator"
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Minimal stubs — only what the functions call
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const NOOP_PROMISE = () => Promise.resolve(null as unknown as never)
23
+
24
+ function makeDeps(overrides: Partial<SubagentWiringDeps> = {}): SubagentWiringDeps {
25
+ const pendingResolvers = new Map<
26
+ string,
27
+ { resolve: (v: unknown) => void; reject: (e: Error) => void }
28
+ >()
29
+
30
+ const fakeSession = { interrupt: () => {}, getAccountInfo: async () => null, close: () => {}, getSupportedCommands: async () => [], setModel: async () => {}, setPermissionMode: async () => {}, sendPrompt: NOOP_PROMISE, pushChannelPrompt: NOOP_PROMISE }
31
+
32
+ return {
33
+ store: {
34
+ requireChat: (chatId) => ({
35
+ id: chatId,
36
+ projectId: "proj-1",
37
+ provider: "claude",
38
+ model: "claude-opus-4-5",
39
+ effort: undefined,
40
+ serviceTier: undefined,
41
+ planMode: false,
42
+ title: "Test chat",
43
+ createdAt: 0,
44
+ updatedAt: 0,
45
+ tokenCount: 0,
46
+ compactFailureCount: 0,
47
+ policyOverride: undefined,
48
+ stackProjectIds: [],
49
+ } as never),
50
+ getProject: (_id) => ({
51
+ id: "proj-1",
52
+ localPath: "/tmp/test-project",
53
+ title: "Test Project",
54
+ createdAt: 0,
55
+ updatedAt: 0,
56
+ } as never),
57
+ appendSubagentEvent: async () => {},
58
+ },
59
+ startClaudeSessionFn: async (_a) => fakeSession as never,
60
+ startClaudeSessionPTYFn: async (_a) => fakeSession as never,
61
+ toolCallback: null,
62
+ tunnelGateway: null,
63
+ claudePtyRegistry: null,
64
+ ptyInstanceRegistry: null,
65
+ workflowRegistry: null,
66
+ subagentOrchestrator: {
67
+ notifySubagentToolPending: () => {},
68
+ } as never,
69
+ codexManager: {} as never,
70
+ oauthPool: null,
71
+ subagentPendingResolvers: pendingResolvers as never,
72
+ realpath: (p) => p,
73
+ resolveClaudeDriverPreference: () => "sdk",
74
+ getEnabledCustomMcpServers: () => [],
75
+ buildOAuthBearers: async () => new Map(),
76
+ resolveChatPolicy: () => ({ mode: "acceptEdits" } as never),
77
+ emitStateChange: () => {},
78
+ buildPoolUnavailableMessage: () => "no token available",
79
+ getAppSettingsSnapshot: () => ({}),
80
+ readLlmProvider: async () => ({ provider: "claude", apiKey: null } as never),
81
+ subagentPendingKey: (chatId, runId, toolUseId) => `${chatId}::${runId}::${toolUseId}`,
82
+ ...overrides,
83
+ }
84
+ }
85
+
86
+ const BASE_SUBAGENT = {
87
+ id: "subagent-1",
88
+ name: "Test Agent",
89
+ description: "A test agent",
90
+ provider: "claude" as const,
91
+ model: "claude-opus-4-5",
92
+ modelOptions: { reasoningEffort: "low" as const, contextWindow: "200k" as const },
93
+ systemPrompt: "You are a test agent",
94
+ contextScope: "full-transcript" as const,
95
+ triggerMode: "manual" as const,
96
+ workingDir: undefined,
97
+ allowedPaths: undefined,
98
+ maxTurns: undefined,
99
+ updatedAt: 0,
100
+ createdAt: 0,
101
+ }
102
+
103
+ const BASE_ARGS: BuildSubagentProviderRunForChatArgs = {
104
+ subagent: BASE_SUBAGENT,
105
+ chatId: "chat-1",
106
+ primer: null,
107
+ userInstruction: "do something",
108
+ runId: "run-1",
109
+ abortSignal: new AbortController().signal,
110
+ depth: 0,
111
+ ancestorSubagentIds: [],
112
+ parentUserMessageId: "msg-1",
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // buildClaudeSubagentStarter
117
+ // ---------------------------------------------------------------------------
118
+
119
+ describe("buildClaudeSubagentStarter", () => {
120
+ test("returns a function", () => {
121
+ const starter = buildClaudeSubagentStarter(makeDeps())
122
+ expect(typeof starter).toBe("function")
123
+ })
124
+
125
+ test("SDK preference — calls startClaudeSessionFn with merged mcpServers", async () => {
126
+ let capturedArgs: unknown = null
127
+ const deps = makeDeps({
128
+ resolveClaudeDriverPreference: () => "sdk",
129
+ startClaudeSessionFn: async (a) => {
130
+ capturedArgs = a
131
+ return {} as never
132
+ },
133
+ getEnabledCustomMcpServers: () => [{ id: "s1", name: "server1", transport: "stdio", command: "npx", args: [], env: {}, disabled: false }] as never,
134
+ buildOAuthBearers: async () => new Map([["s1", "bearer-token"]]),
135
+ })
136
+ const starter = buildClaudeSubagentStarter(deps)
137
+ await starter({
138
+ projectId: "proj",
139
+ localPath: "/tmp/x",
140
+ model: "claude-opus-4-5",
141
+ effort: undefined,
142
+ planMode: false,
143
+ sessionToken: null,
144
+ forkSession: false,
145
+ oauthToken: null,
146
+ onToolRequest: async () => null,
147
+ } as never)
148
+
149
+ expect(capturedArgs).not.toBeNull()
150
+ const args = capturedArgs as Record<string, unknown>
151
+ expect(Array.isArray(args.customMcpServers)).toBe(true)
152
+ expect((args.customMcpServers as unknown[]).length).toBe(1)
153
+ expect(args.oauthBearers).toBeInstanceOf(Map)
154
+ })
155
+
156
+ test("PTY preference — calls startClaudeSessionPTYFn with oneShot: true", async () => {
157
+ let ptyCalled = false
158
+ let capturedPtyArgs: unknown = null
159
+ const deps = makeDeps({
160
+ resolveClaudeDriverPreference: () => "pty",
161
+ startClaudeSessionPTYFn: async (a) => {
162
+ ptyCalled = true
163
+ capturedPtyArgs = a
164
+ return {} as never
165
+ },
166
+ })
167
+ const starter = buildClaudeSubagentStarter(deps)
168
+ await starter({
169
+ chatId: "chat-x",
170
+ projectId: "proj",
171
+ localPath: "/tmp/x",
172
+ model: "claude-opus-4-5",
173
+ effort: undefined,
174
+ planMode: false,
175
+ sessionToken: null,
176
+ forkSession: false,
177
+ oauthToken: null,
178
+ onToolRequest: async () => null,
179
+ } as never)
180
+
181
+ expect(ptyCalled).toBe(true)
182
+ const ptyArgs = capturedPtyArgs as Record<string, unknown>
183
+ expect(ptyArgs.oneShot).toBe(true)
184
+ })
185
+ })
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // buildSubagentProviderRunForChat
189
+ // ---------------------------------------------------------------------------
190
+
191
+ describe("buildSubagentProviderRunForChat", () => {
192
+ test("returns a ProviderRunStart with a start function", () => {
193
+ const result: ProviderRunStart = buildSubagentProviderRunForChat(makeDeps(), BASE_ARGS)
194
+ expect(typeof result.start).toBe("function")
195
+ })
196
+
197
+ test("throws when project is not found", () => {
198
+ const deps = makeDeps({
199
+ store: {
200
+ requireChat: (_chatId) => ({ id: _chatId, projectId: "proj-missing" } as never),
201
+ getProject: (_id) => undefined,
202
+ appendSubagentEvent: async () => {},
203
+ },
204
+ })
205
+ expect(() => buildSubagentProviderRunForChat(deps, BASE_ARGS)).toThrow(
206
+ "Project proj-missing not found for chat chat-1",
207
+ )
208
+ })
209
+
210
+ test("cwdOverride suppresses restriction and additionalDirectories", () => {
211
+ const capturedStartArgs: unknown[] = []
212
+ const deps = makeDeps({
213
+ startClaudeSessionFn: async (a) => {
214
+ capturedStartArgs.push(a)
215
+ return {} as never
216
+ },
217
+ })
218
+ const args: BuildSubagentProviderRunForChatArgs = {
219
+ ...BASE_ARGS,
220
+ cwdOverride: "/tmp/worktree",
221
+ }
222
+ const result = buildSubagentProviderRunForChat(deps, args)
223
+ // The ProviderRunStart is constructed — just verify it's valid
224
+ expect(typeof result.start).toBe("function")
225
+ })
226
+
227
+ test("delegation context increments depth and chains ancestorSubagentIds", () => {
228
+ // We verify the ProviderRunStart is created correctly with depth 0+1=1
229
+ // by checking the function doesn't throw with proper args
230
+ const args: BuildSubagentProviderRunForChatArgs = {
231
+ ...BASE_ARGS,
232
+ depth: 0,
233
+ ancestorSubagentIds: [],
234
+ }
235
+ const result = buildSubagentProviderRunForChat(makeDeps(), args)
236
+ expect(typeof result.start).toBe("function")
237
+ })
238
+
239
+ test("subagentPendingKey is called when onToolRequest receives ask_user_question", async () => {
240
+ let keyBuilt: string | null = null
241
+ const deps = makeDeps({
242
+ subagentPendingKey: (chatId, runId, toolUseId) => {
243
+ keyBuilt = `${chatId}::${runId}::${toolUseId}`
244
+ return keyBuilt
245
+ },
246
+ store: {
247
+ requireChat: (_chatId) =>
248
+ ({ id: _chatId, projectId: "proj-1", stackProjectIds: [] } as never),
249
+ getProject: (_id) =>
250
+ ({ id: "proj-1", localPath: "/tmp/test-project", title: "Test" } as never),
251
+ appendSubagentEvent: async () => {},
252
+ },
253
+ })
254
+
255
+ // We can't easily invoke onToolRequest without running start(), but we can
256
+ // confirm the ProviderRunStart builds without error, which exercises the
257
+ // closure that captures subagentPendingKey.
258
+ const result = buildSubagentProviderRunForChat(deps, BASE_ARGS)
259
+ expect(typeof result.start).toBe("function")
260
+ // The key builder will be called lazily when onToolRequest fires during a run.
261
+ expect(keyBuilt).toBeNull() // not yet called
262
+ })
263
+ })
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Subagent provider-run wiring — standalone extraction from AgentCoordinator.
3
+ *
4
+ * Responsibilities:
5
+ * - buildClaudeSubagentStarter — builds the startClaudeSession callback for
6
+ * subagent runs: PTY/SDK dispatch, OAuth, MCP config, pool wiring.
7
+ * - buildSubagentProviderRunForChat — constructs the full ProviderRunStart
8
+ * bundle for a subagent run bound to a specific chat.
9
+ *
10
+ * Side-effect seal: this module contains NO direct IO (no node:fs, no HTTP
11
+ * calls, no Bun primitives). Every effectful operation is injected through
12
+ * SubagentWiringDeps so the module remains testable without real drivers.
13
+ * The `realpath` function is injected as a dep (RealpathFn) rather than
14
+ * imported from `paths-fs.adapter` directly.
15
+ */
16
+
17
+ import type { AnyValue } from "../shared/errors"
18
+ import type {
19
+ ClaudeDriverPreference,
20
+ LlmProviderSnapshot,
21
+ McpServerConfig,
22
+ Subagent,
23
+ } from "../shared/types"
24
+ import type { HarnessToolRequest } from "./harness-types"
25
+ import type { ClaudeSessionHandle } from "./harness-types"
26
+ import type { KannaMcpDelegationContext } from "./kanna-mcp"
27
+ import type { ChatRecord, ProjectRecord, SubagentRunEvent } from "./events"
28
+ import type { ProviderRunStart, SubagentOrchestrator } from "./subagent-orchestrator"
29
+ import type { BuildSubagentProviderRunArgs } from "./subagent-provider-run"
30
+ import { buildSubagentProviderRun } from "./subagent-provider-run"
31
+ import type { StartClaudeSessionPtyArgs } from "./claude-pty/driver"
32
+ import type { ChatPermissionPolicy } from "../shared/permission-policy"
33
+ import type { ToolCallbackService } from "./tool-callback"
34
+ import type { TunnelGateway } from "./cloudflare-tunnel/gateway"
35
+ import type { ClaudePtyRegistry } from "./claude-pty/pid-registry.adapter"
36
+ import type { PtyInstanceRegistry } from "./claude-pty/pty-instance-registry"
37
+ import type { WorkflowRegistry } from "./workflow-registry"
38
+ import type { CodexAppServerManager } from "./codex-app-server"
39
+ import type { RealpathFn } from "./paths"
40
+ import { resolveSubagentRoots } from "./paths"
41
+ import { resolveSpawnPaths, resolveStackProjects } from "./claude-session-config"
42
+ import { openrouterAuthReady } from "./provider-catalog"
43
+ import { OAuthPoolUnavailableError } from "./oauth-errors"
44
+ // Type-only import — no IO is pulled in from the SDK session start module.
45
+ import type { startClaudeSession as StartClaudeSessionFn } from "./claude-session-start"
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Structural sub-interfaces — only the operations this module calls.
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /** Subset of EventStore used by subagent wiring. */
52
+ interface SubagentWiringStore {
53
+ requireChat(chatId: string): ChatRecord
54
+ getProject(id: string): ProjectRecord | null | undefined
55
+ appendSubagentEvent(event: SubagentRunEvent): Promise<void>
56
+ }
57
+
58
+ /** Subset of OAuthTokenPool used by subagent wiring. */
59
+ interface SubagentWiringOAuthPool {
60
+ hasUsable(reservedFor?: string): boolean
61
+ pickActive(chatId: string): { id: string; token: string; label: string } | null | undefined
62
+ markUsed(tokenId: string): void
63
+ hasAnyToken(): boolean
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Dependency bundle
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * All AgentCoordinator fields and methods accessed by the subagent wiring
72
+ * functions. Passed as a single deps object so both functions stay testable
73
+ * without a real coordinator.
74
+ */
75
+ export interface SubagentWiringDeps {
76
+ // Store (structural subset — no concrete EventStore import)
77
+ store: SubagentWiringStore
78
+
79
+ // Session-start function references (injected so tests can stub them)
80
+ startClaudeSessionFn: typeof StartClaudeSessionFn
81
+ startClaudeSessionPTYFn: (args: StartClaudeSessionPtyArgs) => Promise<ClaudeSessionHandle>
82
+
83
+ // Opaque state passed through to the start functions
84
+ toolCallback: ToolCallbackService | null
85
+ tunnelGateway: TunnelGateway | null
86
+ claudePtyRegistry: ClaudePtyRegistry | null
87
+ ptyInstanceRegistry: PtyInstanceRegistry | null
88
+ workflowRegistry: WorkflowRegistry | null
89
+ subagentOrchestrator: SubagentOrchestrator
90
+ codexManager: CodexAppServerManager
91
+ oauthPool: SubagentWiringOAuthPool | null
92
+
93
+ // Per-run pending resolver registry (shared with the coordinator's cancel paths)
94
+ subagentPendingResolvers: Map<string, { resolve: (v: AnyValue) => void; reject: (e: Error) => void }>
95
+
96
+ // IO injection — realpath wraps realpathSync but is IO; inject via deps
97
+ realpath: RealpathFn
98
+
99
+ // Method references for private AgentCoordinator helpers
100
+ resolveClaudeDriverPreference: () => ClaudeDriverPreference
101
+ getEnabledCustomMcpServers: () => readonly McpServerConfig[]
102
+ buildOAuthBearers: (servers: readonly McpServerConfig[]) => Promise<Map<string, string>>
103
+ resolveChatPolicy: (chatId: string) => ChatPermissionPolicy
104
+ emitStateChange: (chatId: string) => void
105
+ buildPoolUnavailableMessage: (reservedFor: string, scopeSuffix: string) => string
106
+ getAppSettingsSnapshot: () => {
107
+ globalPromptAppend?: string
108
+ claudeAuth?: { authenticated?: boolean } | null
109
+ }
110
+ readLlmProvider: () => Promise<LlmProviderSnapshot>
111
+ subagentPendingKey: (chatId: string, runId: string, toolUseId: string) => string
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Exported args type for buildSubagentProviderRunForChat
116
+ // ---------------------------------------------------------------------------
117
+
118
+ export interface BuildSubagentProviderRunForChatArgs {
119
+ subagent: Subagent
120
+ chatId: string
121
+ primer: string | null
122
+ userInstruction: string | null
123
+ runId: string
124
+ abortSignal: AbortSignal
125
+ depth: number
126
+ ancestorSubagentIds: string[]
127
+ parentUserMessageId: string
128
+ /**
129
+ * Orchestration workers run in an isolated git worktree, not the chat cwd.
130
+ * When set, this overrides the resolved cwd, disables workingDir/allowedPaths
131
+ * restriction, and drops additional dirs / stack labelling (the worktree is
132
+ * a self-contained checkout).
133
+ */
134
+ cwdOverride?: string
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Core functions
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * D6 — subagent Claude starter. When `KANNA_CLAUDE_DRIVER=pty` the subagent
143
+ * turn runs through the PTY driver (subscription billing) instead of always
144
+ * falling back to the SDK (API billing). Adapts the SDK-shaped
145
+ * `startClaudeSession` arg to `StartClaudeSessionPtyArgs`, injecting the
146
+ * coordinator-owned preflight / toolCallback / tunnel / policy context and
147
+ * `oneShot: true` so the REPL closes after the single subagent turn.
148
+ *
149
+ * Extracted from AgentCoordinator.buildClaudeSubagentStarter.
150
+ */
151
+ export function buildClaudeSubagentStarter(
152
+ deps: SubagentWiringDeps,
153
+ ): NonNullable<BuildSubagentProviderRunArgs["startClaudeSession"]> {
154
+ return async (a) => {
155
+ const enabledMcpServers = deps.getEnabledCustomMcpServers()
156
+ const oauthBearers = await deps.buildOAuthBearers(enabledMcpServers)
157
+ if (deps.resolveClaudeDriverPreference() === "pty") {
158
+ return deps.startClaudeSessionPTYFn({
159
+ chatId: a.chatId ?? "",
160
+ projectId: a.projectId,
161
+ localPath: a.localPath,
162
+ model: a.model,
163
+ effort: a.effort,
164
+ planMode: a.planMode,
165
+ sessionToken: a.sessionToken,
166
+ forkSession: a.forkSession,
167
+ oauthToken: a.oauthToken,
168
+ additionalDirectories: a.additionalDirectories,
169
+ onToolRequest: a.onToolRequest,
170
+ systemPromptOverride: a.systemPromptOverride,
171
+ initialPrompt: a.initialPrompt,
172
+ subagentOrchestrator: a.subagentOrchestrator,
173
+ delegationContext: a.delegationContext,
174
+ toolCallback: deps.toolCallback ?? undefined,
175
+ tunnelGateway: deps.tunnelGateway,
176
+ chatPolicy: a.chatId ? deps.resolveChatPolicy(a.chatId) : undefined,
177
+ oneShot: true,
178
+ ptyRegistry: deps.claudePtyRegistry ?? undefined,
179
+ ptyInstanceRegistry: deps.ptyInstanceRegistry ?? undefined,
180
+ workflowRegistry: deps.workflowRegistry ?? undefined,
181
+ customMcpServers: enabledMcpServers,
182
+ oauthBearers,
183
+ restrictedAllowedPaths: a.restrictedAllowedPaths,
184
+ keepAlive: a.keepAlive,
185
+ })
186
+ }
187
+ return deps.startClaudeSessionFn({ ...a, customMcpServers: enabledMcpServers, oauthBearers })
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Constructs the full ProviderRunStart bundle for a subagent run bound to a
193
+ * specific chat. Resolves the cwd, builds the delegation context, wires up the
194
+ * interactive tool-request forwarder, and delegates to `buildSubagentProviderRun`.
195
+ *
196
+ * Extracted from AgentCoordinator.buildSubagentProviderRunForChat.
197
+ */
198
+ export function buildSubagentProviderRunForChat(
199
+ deps: SubagentWiringDeps,
200
+ args: BuildSubagentProviderRunForChatArgs,
201
+ ): ProviderRunStart {
202
+ const chat = deps.store.requireChat(args.chatId)
203
+ const project = deps.store.getProject(chat.projectId)
204
+ if (!project) throw new Error(`Project ${chat.projectId} not found for chat ${args.chatId}`)
205
+ const spawn = resolveSpawnPaths(chat, project.localPath)
206
+ const restriction =
207
+ args.cwdOverride === undefined &&
208
+ (args.subagent.workingDir !== undefined || args.subagent.allowedPaths !== undefined)
209
+ ? resolveSubagentRoots(
210
+ spawn.cwd,
211
+ args.subagent.workingDir,
212
+ args.subagent.allowedPaths,
213
+ deps.realpath,
214
+ )
215
+ : null
216
+
217
+ const onToolRequest = async (request: HarnessToolRequest): Promise<AnyValue> => {
218
+ if (
219
+ request.tool.toolKind !== "ask_user_question" &&
220
+ request.tool.toolKind !== "exit_plan_mode"
221
+ ) {
222
+ // Non-interactive tools (bash, read, write, ...) — SDK handles
223
+ // them via canUseTool wrapper. No forwarding needed.
224
+ return null
225
+ }
226
+ const toolUseId = request.tool.toolId
227
+ const key = deps.subagentPendingKey(args.chatId, args.runId, toolUseId)
228
+ await deps.store.appendSubagentEvent({
229
+ v: 3,
230
+ type: "subagent_tool_pending",
231
+ timestamp: Date.now(),
232
+ chatId: args.chatId,
233
+ runId: args.runId,
234
+ toolUseId,
235
+ toolKind: request.tool.toolKind,
236
+ input: request.tool.input,
237
+ })
238
+ deps.emitStateChange(args.chatId)
239
+ deps.subagentOrchestrator.notifySubagentToolPending(args.runId)
240
+ return await new Promise<AnyValue>((resolve, reject) => {
241
+ // Defensive: if `canUseTool` somehow fires twice for the same
242
+ // (chatId, runId, toolUseId) — e.g. SDK retry — reject the previous
243
+ // resolver before overwriting so its Promise doesn't leak.
244
+ const existing = deps.subagentPendingResolvers.get(key)
245
+ if (existing) {
246
+ existing.reject(new Error("superseded by retry"))
247
+ }
248
+ deps.subagentPendingResolvers.set(key, { resolve, reject })
249
+ })
250
+ }
251
+
252
+ const delegationContext: KannaMcpDelegationContext = {
253
+ parentSubagentId: args.subagent.id,
254
+ parentRunId: args.runId,
255
+ ancestorSubagentIds: [...args.ancestorSubagentIds, args.subagent.id],
256
+ depth: args.depth + 1,
257
+ // For sub-spawn-sub, the parent_user_message_id stays anchored to the
258
+ // chat turn that started the whole chain — that's the attribution the
259
+ // run_started events use, and the orchestrator's depth/cycle checks
260
+ // protect against runaway chains.
261
+ getParentUserMessageId: () => args.parentUserMessageId,
262
+ // Subagents cannot inherit the user's @-mention authority: manual-trigger
263
+ // gates are enforced only at the top-level turn where the user typed the mention.
264
+ getMentionedSubagentIds: () => [],
265
+ }
266
+
267
+ return buildSubagentProviderRun({
268
+ subagent: args.subagent,
269
+ chatId: args.chatId,
270
+ primer: args.primer,
271
+ userInstruction: args.userInstruction,
272
+ runId: args.runId,
273
+ abortSignal: args.abortSignal,
274
+ cwd: args.cwdOverride ?? restriction?.cwd ?? spawn.cwd,
275
+ additionalDirectories: args.cwdOverride ? [] : spawn.additionalDirectories,
276
+ // Only label stack projects for unrestricted runs — a path-restricted
277
+ // subagent cannot reach every root, so listing them all would mislead.
278
+ stackProjects:
279
+ args.cwdOverride || restriction
280
+ ? []
281
+ : resolveStackProjects(chat, (id) => deps.store.getProject(id)?.title),
282
+ allowedPaths: restriction?.allowedPaths,
283
+ projectId: project.id,
284
+ startClaudeSession: buildClaudeSubagentStarter(deps),
285
+ // PTY claude has no native maxTurns (interactive CLI) — the orchestrator
286
+ // applies a host-side tool-call-count backstop for PTY + Codex runs.
287
+ claudeDriverIsPty: deps.resolveClaudeDriverPreference() === "pty",
288
+ subagentOrchestrator: deps.subagentOrchestrator,
289
+ delegationContext,
290
+ codexManager: deps.codexManager,
291
+ onToolRequest,
292
+ globalPromptAppend: deps.getAppSettingsSnapshot().globalPromptAppend,
293
+ authReady: async (provider) => {
294
+ if (provider === "openrouter") {
295
+ return openrouterAuthReady(await deps.readLlmProvider())
296
+ }
297
+ if (provider === "claude") {
298
+ const settings = deps.getAppSettingsSnapshot()
299
+ // Pass parent chat id so a token already reserved by the parent
300
+ // counts as usable. Subagent runs are sequential under the parent
301
+ // (parent's turn is paused), so sharing the parent's reservation
302
+ // is correct — see oauth-token-pool isEligible.
303
+ return Boolean(
304
+ settings.claudeAuth?.authenticated || deps.oauthPool?.hasUsable(args.chatId),
305
+ )
306
+ }
307
+ return true
308
+ },
309
+ pickOauthToken: () => {
310
+ // Subagent inherits the parent chat's reservation by re-picking under
311
+ // the same chatId. pickActive treats the parent's reservation as
312
+ // owned-by-self (drops + re-binds to chatId), so the lifecycle stays
313
+ // bound to the parent's close path — no separate subagent release.
314
+ const picked = deps.oauthPool?.pickActive(args.chatId) ?? null
315
+ if (deps.oauthPool && deps.oauthPool.hasAnyToken() && !picked) {
316
+ throw new OAuthPoolUnavailableError(
317
+ deps.buildPoolUnavailableMessage(args.chatId, " for subagent run"),
318
+ )
319
+ }
320
+ if (picked) deps.oauthPool!.markUsed(picked.id)
321
+ return picked?.token ?? null
322
+ },
323
+ })
324
+ }