@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,566 @@
1
+ /**
2
+ * Turn spawning pipeline for AgentCoordinator.
3
+ *
4
+ * Extracted from agent.ts to reduce file size. Contains the two adjacent
5
+ * private methods that form the "spawn and route" logic:
6
+ * - startTurnForChat — validates state, appends user prompt, records turn_started
7
+ * - startTurnAfterTurnStarted — picks provider, spawns session, routes to codec
8
+ *
9
+ * All IO is delegated through the StartTurnDeps interface; this file is pure
10
+ * orchestration and therefore does NOT need an `.adapter.ts` suffix.
11
+ */
12
+ import type {
13
+ AgentProvider,
14
+ ChatAttachment,
15
+ ResolvedStackBinding,
16
+ Subagent,
17
+ TranscriptEntry,
18
+ ClaudeDriverPreference,
19
+ } from "../shared/types"
20
+ import { isCodexReasoningEffort, providerUsesSdkSession } from "../shared/types"
21
+ import { isClaudeSdkProvider } from "./provider-catalog"
22
+ import type { ChatRecord, ProjectRecord } from "./events"
23
+ import type { ActiveTurn, ClaudeSessionState } from "./claude-session-state"
24
+ import type { AnyValue } from "../shared/errors"
25
+ import type { HarnessTurn, HarnessToolRequest } from "./harness-types"
26
+ import type { EventStore } from "./event-store"
27
+ import type { CodexAppServerManager } from "./codex-app-server"
28
+ import type { SubagentOrchestrator } from "./subagent-orchestrator"
29
+ import { OAuthPoolUnavailableError } from "./oauth-errors"
30
+ import { buildPromptText } from "./claude-prompt-helpers"
31
+ import { buildHistoryPrimer, shouldInjectPrimer } from "./history-primer"
32
+ import { fallbackTitleFromMessage } from "./generate-title"
33
+ import { parseMentions, type ParsedMention } from "./mention-parser"
34
+ import { resolveSpawnPaths, resolveStackProjects } from "./claude-session-config"
35
+ import { timestamped } from "./claude-message-normalizer"
36
+ import {
37
+ logClaudeSteer,
38
+ logSendToStartingProfile,
39
+ type SendToStartingProfile,
40
+ } from "./claude-steer-log"
41
+ import { log } from "../shared/log"
42
+ import { LOG_PREFIX } from "../shared/branding"
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Dep types
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /** Args for the inner startClaudeTurn dep — mirrors the private method signature. */
49
+ export interface StartClaudeTurnArgs {
50
+ chatId: string
51
+ projectId: string
52
+ localPath: string
53
+ additionalDirectories?: string[]
54
+ stackProjects?: ResolvedStackBinding[]
55
+ model: string
56
+ effort?: string
57
+ planMode: boolean
58
+ sessionToken: string | null
59
+ forkSession: boolean
60
+ onToolRequest: (request: HarnessToolRequest) => Promise<AnyValue>
61
+ provider: AgentProvider
62
+ }
63
+
64
+ /** Args for recreateActiveTurnFromSession dep — mirrors the private method signature. */
65
+ export interface RecreateActiveTurnArgs {
66
+ chatId: string
67
+ provider: AgentProvider
68
+ model: string
69
+ effort?: string
70
+ serviceTier?: "fast"
71
+ planMode: boolean
72
+ clientTraceId?: string
73
+ }
74
+
75
+ /** AppSettings snapshot fields consumed by this module. */
76
+ export interface StartTurnAppSettings {
77
+ globalPromptAppend?: string
78
+ }
79
+
80
+ /**
81
+ * All AgentCoordinator fields / methods accessed by the turn spawning pipeline.
82
+ * Passed as a single deps argument to the two extracted functions.
83
+ */
84
+ export interface StartTurnDeps {
85
+ // Maps (mutable — methods read and write these)
86
+ activeTurns: Map<string, ActiveTurn>
87
+ claudeSessions: Map<string, ClaudeSessionState>
88
+ drainingStreams: Map<string, { turn: HarnessTurn }>
89
+ mentionedSubagentIdsByChat: Map<string, string[]>
90
+
91
+ // Service objects
92
+ store: EventStore
93
+ codexManager: CodexAppServerManager
94
+ subagentOrchestrator: Pick<SubagentOrchestrator, "clearChatCancellation">
95
+
96
+ // Callbacks for private AgentCoordinator methods
97
+ clearDrainingStream: (chatId: string) => void
98
+ emitStateChange: (chatId: string, options?: { immediate?: boolean }) => void
99
+ resolveClaudeDriverPreference: () => ClaudeDriverPreference
100
+ getSubagents: () => Subagent[]
101
+ getAppSettingsSnapshot: () => StartTurnAppSettings
102
+ /** Fired in background (return value discarded). */
103
+ generateTitleInBackground: (chatId: string, content: string, localPath: string, optimisticTitle: string) => Promise<void>
104
+ recreateActiveTurnFromSession: (args: RecreateActiveTurnArgs) => ActiveTurn | undefined
105
+ startClaudeTurn: (args: StartClaudeTurnArgs) => Promise<HarnessTurn>
106
+ findLastUserMessageId: (chatId: string) => string | null
107
+ /** Fires the runTurn loop (return value discarded). */
108
+ runTurn: (active: ActiveTurn) => void
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Arg types (mirror the private method signatures)
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export interface StartTurnForChatArgs {
116
+ chatId: string
117
+ provider: AgentProvider
118
+ content: string
119
+ attachments: ChatAttachment[]
120
+ model: string
121
+ effort?: string
122
+ serviceTier?: "fast"
123
+ planMode: boolean
124
+ appendUserPrompt: boolean
125
+ steered?: boolean
126
+ autoContinue?: { scheduleId: string }
127
+ userClearedContext?: boolean
128
+ profile?: SendToStartingProfile | null
129
+ }
130
+
131
+ interface StartTurnAfterTurnStartedCtx {
132
+ args: StartTurnForChatArgs
133
+ chat: ChatRecord
134
+ project: ProjectRecord
135
+ existingMessages: TranscriptEntry[]
136
+ shouldGenerateTitle: boolean
137
+ optimisticTitle: string | null
138
+ appendedUserMessageId: string | null
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Extracted functions
143
+ // ---------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Extracted from AgentCoordinator.startTurnForChat.
147
+ *
148
+ * Validates pre-conditions, appends the user prompt, records turn_started,
149
+ * then delegates to startTurnAfterTurnStarted.
150
+ */
151
+ export async function startTurnForChat(
152
+ deps: StartTurnDeps,
153
+ args: StartTurnForChatArgs,
154
+ ): Promise<void> {
155
+ logSendToStartingProfile(args.profile, "start_turn.begin", {
156
+ chatId: args.chatId,
157
+ provider: args.provider,
158
+ appendUserPrompt: args.appendUserPrompt,
159
+ planMode: args.planMode,
160
+ })
161
+
162
+ // Close any lingering draining stream before starting a new turn.
163
+ const draining = deps.drainingStreams.get(args.chatId)
164
+ if (draining) {
165
+ draining.turn.close()
166
+ deps.clearDrainingStream(args.chatId)
167
+ }
168
+
169
+ // A new user turn implicitly clears any prior cancellation marker —
170
+ // otherwise a Stop-then-resend cycle wedges every delegate_subagent
171
+ // call in this chat with "Chat cancelled before run started" until
172
+ // process restart. Mirrors the clear already done by
173
+ // runMentionsForUserMessage for the @mention path.
174
+ deps.subagentOrchestrator.clearChatCancellation(args.chatId)
175
+
176
+ const chat = deps.store.requireChat(args.chatId)
177
+ if (deps.activeTurns.has(args.chatId)) {
178
+ throw new Error("Chat is already running")
179
+ }
180
+
181
+ if (chat.provider !== args.provider) {
182
+ await deps.store.setChatProvider(args.chatId, args.provider)
183
+ logSendToStartingProfile(args.profile, "start_turn.provider_set", {
184
+ chatId: args.chatId,
185
+ provider: args.provider,
186
+ })
187
+ }
188
+ await deps.store.setPlanMode(args.chatId, args.planMode)
189
+ logSendToStartingProfile(args.profile, "start_turn.plan_mode_set", {
190
+ chatId: args.chatId,
191
+ planMode: args.planMode,
192
+ })
193
+
194
+ const existingMessages = deps.store.getMessages(args.chatId)
195
+ const shouldGenerateTitle = args.appendUserPrompt && chat.title === "New Chat" && existingMessages.length === 0
196
+ const optimisticTitle = shouldGenerateTitle ? fallbackTitleFromMessage(args.content) : null
197
+
198
+ if (optimisticTitle) {
199
+ await deps.store.renameChat(args.chatId, optimisticTitle)
200
+ logSendToStartingProfile(args.profile, "start_turn.optimistic_title_set", {
201
+ chatId: args.chatId,
202
+ title: optimisticTitle,
203
+ })
204
+ }
205
+
206
+ const project = deps.store.getProject(chat.projectId)
207
+ if (!project) {
208
+ throw new Error("Project not found")
209
+ }
210
+
211
+ let appendedUserMessageId: string | null = null
212
+ if (args.appendUserPrompt) {
213
+ const parsedMentions = parseMentions(args.content, deps.getSubagents())
214
+ const subagentMentions = parsedMentions
215
+ .filter((mention): mention is Extract<ParsedMention, { kind: "subagent" }> => mention.kind === "subagent")
216
+ .map((mention) => ({ subagentId: mention.subagentId, raw: mention.raw }))
217
+ deps.mentionedSubagentIdsByChat.set(
218
+ args.chatId,
219
+ subagentMentions.map((m) => m.subagentId),
220
+ )
221
+ const unknownSubagentMentions = parsedMentions
222
+ .filter((mention): mention is Extract<ParsedMention, { kind: "unknown-subagent" }> => mention.kind === "unknown-subagent")
223
+ .map((mention) => ({ name: mention.name, raw: mention.raw }))
224
+ const userPromptEntry = timestamped(
225
+ {
226
+ kind: "user_prompt",
227
+ content: args.content,
228
+ attachments: args.attachments,
229
+ steered: args.steered,
230
+ autoContinue: args.autoContinue,
231
+ ...(subagentMentions.length > 0 ? { subagentMentions } : {}),
232
+ ...(unknownSubagentMentions.length > 0 ? { unknownSubagentMentions } : {}),
233
+ },
234
+ Date.now()
235
+ )
236
+ await deps.store.appendMessage(args.chatId, userPromptEntry)
237
+ appendedUserMessageId = userPromptEntry._id
238
+ logSendToStartingProfile(args.profile, "start_turn.user_prompt_appended", {
239
+ chatId: args.chatId,
240
+ entryId: userPromptEntry._id,
241
+ })
242
+ }
243
+ await deps.store.recordTurnStarted(args.chatId, {
244
+ provider: args.provider,
245
+ model: args.model,
246
+ ...(args.effort !== undefined ? { effort: args.effort } : {}),
247
+ ...(args.serviceTier !== undefined ? { serviceTier: args.serviceTier } : {}),
248
+ planMode: args.planMode,
249
+ driver: deps.resolveClaudeDriverPreference(),
250
+ })
251
+ logSendToStartingProfile(args.profile, "start_turn.turn_started_recorded", {
252
+ chatId: args.chatId,
253
+ })
254
+
255
+ try {
256
+ await startTurnAfterTurnStarted(deps, {
257
+ args,
258
+ chat,
259
+ project,
260
+ existingMessages,
261
+ shouldGenerateTitle,
262
+ optimisticTitle,
263
+ appendedUserMessageId,
264
+ })
265
+ } catch (error) {
266
+ const message = error instanceof Error ? error.message : String(error)
267
+ const isOAuthRefusal = error instanceof OAuthPoolUnavailableError
268
+ log.error(`${LOG_PREFIX} startTurnForChat failed after turn_started`, {
269
+ chatId: args.chatId,
270
+ provider: args.provider,
271
+ model: args.model,
272
+ planMode: args.planMode,
273
+ error: message,
274
+ stack: error instanceof Error ? error.stack : undefined,
275
+ kind: isOAuthRefusal ? "oauth_pool_unavailable" : "unknown",
276
+ })
277
+ // OAuth-pool refusal: persist the formatted refusal (with chat-link
278
+ // markdown produced by `buildPoolUnavailableMessage`) as a `result`
279
+ // transcript entry so the UI's transcript renders it inline and
280
+ // durably, instead of relying on the ephemeral commandError banner
281
+ // that gets wiped by the next chat snapshot tick.
282
+ if (isOAuthRefusal) {
283
+ try {
284
+ await deps.store.appendMessage(
285
+ args.chatId,
286
+ timestamped({
287
+ kind: "result",
288
+ subtype: "error",
289
+ isError: true,
290
+ durationMs: 0,
291
+ result: message,
292
+ })
293
+ )
294
+ } catch (appendErr) {
295
+ log.error(`${LOG_PREFIX} append refusal result entry failed`, {
296
+ chatId: args.chatId,
297
+ appendErr: appendErr instanceof Error ? appendErr.message : String(appendErr),
298
+ })
299
+ }
300
+ }
301
+ try {
302
+ await deps.store.recordTurnFailed(args.chatId, message)
303
+ } catch (recordErr) {
304
+ log.error(`${LOG_PREFIX} recordTurnFailed also failed`, {
305
+ chatId: args.chatId,
306
+ recordErr: recordErr instanceof Error ? recordErr.message : String(recordErr),
307
+ })
308
+ }
309
+ deps.activeTurns.delete(args.chatId)
310
+ deps.emitStateChange(args.chatId, { immediate: true })
311
+ // Swallow refusals — the transcript entry above is the user-facing
312
+ // signal. Re-throwing would surface a transient commandError banner
313
+ // that races with snapshot ticks and visibly flickers (see #235).
314
+ if (isOAuthRefusal) {
315
+ return
316
+ }
317
+ throw error
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Extracted from AgentCoordinator.startTurnAfterTurnStarted.
323
+ *
324
+ * Picks provider, resolves session tokens / priming, spawns the SDK/PTY
325
+ * claude session or Codex turn, registers the ActiveTurn, and routes to
326
+ * the codec (runTurn vs sendPrompt on the SDK queue).
327
+ */
328
+ async function startTurnAfterTurnStarted(
329
+ deps: StartTurnDeps,
330
+ ctx: StartTurnAfterTurnStartedCtx,
331
+ ): Promise<void> {
332
+ const { args, chat, project, existingMessages, shouldGenerateTitle, optimisticTitle, appendedUserMessageId } = ctx
333
+ if (shouldGenerateTitle) {
334
+ void deps.generateTitleInBackground(args.chatId, args.content, project.localPath, optimisticTitle ?? "New Chat")
335
+ }
336
+
337
+ const onToolRequest = async (request: HarnessToolRequest): Promise<AnyValue> => {
338
+ let active = deps.activeTurns.get(args.chatId)
339
+ if (!active) {
340
+ // The prior turn's `result` event already deleted the activeTurn, but
341
+ // the Claude SDK fired another `canUseTool` — happens when the SDK
342
+ // self-resumes after a background task notification. Re-promote a
343
+ // minimal activeTurn from the live session so the question renders
344
+ // instead of failing with "Chat turn ended unexpectedly".
345
+ active = deps.recreateActiveTurnFromSession(args)
346
+ if (!active) {
347
+ throw new Error("Chat turn ended unexpectedly")
348
+ }
349
+ }
350
+
351
+ active.status = "waiting_for_user"
352
+ active.waitStartedAt = Date.now()
353
+ deps.emitStateChange(args.chatId)
354
+
355
+ // Capture in a const to give TypeScript a stable narrowed reference inside
356
+ // the Promise executor (re-assignment to `active` after the if-block could
357
+ // theoretically happen but doesn't in practice; const makes the intent clear).
358
+ const narrowedActive = active
359
+ return await new Promise<AnyValue>((resolve) => {
360
+ narrowedActive.pendingTool = {
361
+ toolUseId: request.tool.toolId,
362
+ tool: request.tool,
363
+ resolve,
364
+ }
365
+ })
366
+ }
367
+
368
+ const targetProvider: AgentProvider = args.provider
369
+ const existingToken = chat.sessionTokensByProvider[targetProvider] ?? null
370
+ const pendingForkToken = chat.pendingForkSessionToken?.provider === targetProvider
371
+ ? chat.pendingForkSessionToken.token
372
+ : null
373
+ const shouldPrime = shouldInjectPrimer(
374
+ chat.sessionTokensByProvider,
375
+ targetProvider,
376
+ Boolean(args.userClearedContext),
377
+ )
378
+ const userPromptText = buildPromptText(args.content, args.attachments)
379
+ const primer = shouldPrime
380
+ ? buildHistoryPrimer(existingMessages, targetProvider, userPromptText)
381
+ : null
382
+ const promptContent = primer ?? userPromptText
383
+
384
+ let turn: HarnessTurn
385
+ if (isClaudeSdkProvider(args.provider)) {
386
+ logSendToStartingProfile(args.profile, "start_turn.provider_boot.begin", {
387
+ chatId: args.chatId,
388
+ provider: args.provider,
389
+ model: args.model,
390
+ })
391
+ const spawn = resolveSpawnPaths(chat, project.localPath)
392
+ turn = await deps.startClaudeTurn({
393
+ chatId: args.chatId,
394
+ projectId: project.id,
395
+ localPath: spawn.cwd,
396
+ additionalDirectories: spawn.additionalDirectories,
397
+ stackProjects: resolveStackProjects(chat, (id) => deps.store.getProject(id)?.title),
398
+ model: args.model,
399
+ effort: args.effort,
400
+ planMode: args.planMode,
401
+ sessionToken: pendingForkToken ?? existingToken,
402
+ forkSession: pendingForkToken != null,
403
+ onToolRequest,
404
+ provider: args.provider,
405
+ })
406
+ logSendToStartingProfile(args.profile, "start_turn.provider_boot.ready", {
407
+ chatId: args.chatId,
408
+ provider: args.provider,
409
+ model: args.model,
410
+ })
411
+ } else {
412
+ logSendToStartingProfile(args.profile, "start_turn.provider_boot.begin", {
413
+ chatId: args.chatId,
414
+ provider: args.provider,
415
+ model: args.model,
416
+ })
417
+ // Codex single-cwd: peer worktrees not passed to startSession. Cross-root writes use grantRoot.
418
+ const sessionToken = await deps.codexManager.startSession({
419
+ chatId: args.chatId,
420
+ cwd: resolveSpawnPaths(chat, project.localPath).cwd,
421
+ projectId: project.id,
422
+ model: args.model,
423
+ serviceTier: args.serviceTier,
424
+ sessionToken: existingToken,
425
+ pendingForkSessionToken: pendingForkToken,
426
+ })
427
+ if (pendingForkToken && sessionToken) {
428
+ await deps.store.setPendingForkSessionToken(args.chatId, null)
429
+ }
430
+ logSendToStartingProfile(args.profile, "start_turn.session_ready", {
431
+ chatId: args.chatId,
432
+ provider: args.provider,
433
+ model: args.model,
434
+ })
435
+ turn = await deps.codexManager.startTurn({
436
+ chatId: args.chatId,
437
+ content: promptContent,
438
+ model: args.model,
439
+ effort: isCodexReasoningEffort(args.effort) ? args.effort : undefined,
440
+ serviceTier: args.serviceTier,
441
+ planMode: args.planMode,
442
+ onToolRequest,
443
+ developerInstructions: deps.getAppSettingsSnapshot().globalPromptAppend,
444
+ })
445
+ logSendToStartingProfile(args.profile, "start_turn.provider_boot.ready", {
446
+ chatId: args.chatId,
447
+ provider: args.provider,
448
+ model: args.model,
449
+ })
450
+ }
451
+
452
+ const active: ActiveTurn = {
453
+ chatId: args.chatId,
454
+ provider: args.provider,
455
+ turn,
456
+ model: args.model,
457
+ effort: args.effort,
458
+ serviceTier: args.serviceTier,
459
+ planMode: args.planMode,
460
+ status: args.provider === "claude" ? "running" : "starting",
461
+ pendingTool: null,
462
+ postToolFollowUp: null,
463
+ hasFinalResult: false,
464
+ cancelRequested: false,
465
+ cancelRecorded: false,
466
+ clientTraceId: args.profile?.traceId,
467
+ profilingStartedAt: args.profile?.startedAt,
468
+ waitStartedAt: null,
469
+ userMessageId: appendedUserMessageId ?? deps.findLastUserMessageId(args.chatId),
470
+ }
471
+ deps.activeTurns.set(args.chatId, active)
472
+ logSendToStartingProfile(args.profile, "start_turn.active_turn_registered", {
473
+ chatId: args.chatId,
474
+ status: active.status,
475
+ })
476
+ deps.emitStateChange(args.chatId, { immediate: active.status === "starting" })
477
+ logSendToStartingProfile(args.profile, "start_turn.state_change_emitted", {
478
+ chatId: args.chatId,
479
+ status: active.status,
480
+ })
481
+
482
+ if (turn.getAccountInfo) {
483
+ void turn.getAccountInfo()
484
+ .then(async (accountInfo) => {
485
+ const session = deps.claudeSessions.get(args.chatId)
486
+ if (args.provider === "openrouter") {
487
+ // OpenRouter routes through the SDK with ANTHROPIC_AUTH_TOKEN set to
488
+ // the OpenRouter key, so the SDK self-reports tokenSource
489
+ // "ANTHROPIC_AUTH_TOKEN" with no account — mislabeling the chat as
490
+ // Anthropic. Override with the OpenRouter identity instead.
491
+ if (!session) return
492
+ if (session.accountInfoLoaded) return
493
+ session.accountInfoLoaded = true
494
+ await deps.store.appendMessage(args.chatId, timestamped({
495
+ kind: "account_info",
496
+ accountInfo: {
497
+ tokenSource: "openrouter",
498
+ ...(session.openrouterKeyMasked ? { oauthKeyMasked: session.openrouterKeyMasked } : {}),
499
+ ...(session.openrouterModel ? { organization: session.openrouterModel } : {}),
500
+ },
501
+ }))
502
+ deps.emitStateChange(args.chatId)
503
+ } else {
504
+ if (!accountInfo) return
505
+ let augmented = accountInfo
506
+ if (args.provider === "claude") {
507
+ if (!session) return
508
+ if (session.accountInfoLoaded) return
509
+ session.accountInfoLoaded = true
510
+ // Mirror the PTY driver's deriveAccountInfoFromOauth: when the
511
+ // turn was started with a kanna OAuth-pool token, surface its
512
+ // name as organization and tag the source so the UI renders
513
+ // "Pool token" identically across drivers. SDK-reported extras
514
+ // (email, subscriptionType) are preserved.
515
+ if (session.activeTokenId) {
516
+ augmented = {
517
+ ...accountInfo,
518
+ tokenSource: "kanna-oauth-pool",
519
+ ...(session.oauthLabel ? { organization: session.oauthLabel } : {}),
520
+ ...(session.oauthKeyMasked ? { oauthKeyMasked: session.oauthKeyMasked } : {}),
521
+ }
522
+ } else if (session.oauthKeyMasked && !accountInfo.oauthKeyMasked) {
523
+ augmented = { ...accountInfo, oauthKeyMasked: session.oauthKeyMasked }
524
+ }
525
+ }
526
+ await deps.store.appendMessage(args.chatId, timestamped({ kind: "account_info", accountInfo: augmented }))
527
+ deps.emitStateChange(args.chatId)
528
+ }
529
+ })
530
+ .catch(() => undefined)
531
+ }
532
+
533
+ if (providerUsesSdkSession(args.provider)) {
534
+ // claude and openrouter both deliver their prompt through the SDK
535
+ // session queue; gating this on `=== "claude"` is what left openrouter's
536
+ // prompt undelivered, hanging every openrouter turn until the watchdog.
537
+ const session = deps.claudeSessions.get(args.chatId)
538
+ if (!session) {
539
+ throw new Error("SDK session was not initialized")
540
+ }
541
+ const promptSeq = session.nextPromptSeq + 1
542
+ session.nextPromptSeq = promptSeq
543
+ session.pendingPromptSeqs.push(promptSeq)
544
+ // A new turn starts: clear any stale cancellation marker so a previous
545
+ // cancel that never produced a tail result can't suppress this turn's
546
+ // real result.
547
+ session.cancelledResultPending = 0
548
+ active.claudePromptSeq = promptSeq
549
+ logClaudeSteer("claude_prompt_sent", {
550
+ chatId: args.chatId,
551
+ sessionId: session.id,
552
+ promptSeq,
553
+ activeStatus: active.status,
554
+ contentPreview: args.content.slice(0, 160),
555
+ pendingPromptSeqs: [...session.pendingPromptSeqs],
556
+ })
557
+ await session.session.sendPrompt(promptContent)
558
+ session.lastUsedAt = Date.now()
559
+ logSendToStartingProfile(args.profile, "start_turn.claude_prompt_sent", {
560
+ chatId: args.chatId,
561
+ })
562
+ return
563
+ }
564
+
565
+ void deps.runTurn(active)
566
+ }