@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,361 @@
1
+ /**
2
+ * Standalone session error-response handlers for AgentCoordinator.
3
+ *
4
+ * Extracted from agent.ts so the four related private methods live in their
5
+ * own testable module. The coordinator delegates to these functions by passing
6
+ * an object literal that satisfies `SessionErrorHandlerDeps`.
7
+ *
8
+ * Side-effect seal: this module contains NO direct IO (no node:fs, no HTTP
9
+ * calls, no Bun primitives). Every effectful operation is injected through
10
+ * the deps interface.
11
+ */
12
+
13
+ import type { TranscriptEntry } from "../shared/types"
14
+ import { AUTO_CONTINUE_EVENT_VERSION, type AutoContinueEvent } from "./auto-continue/events"
15
+ import type { LimitDetection, LimitDetector } from "./auto-continue/limit-detector"
16
+ import type { AuthErrorDetection } from "./auto-continue/auth-error-detector"
17
+ import { deriveChatSchedules } from "./auto-continue/read-model"
18
+ import { log } from "../shared/log"
19
+ import { type AnyValue } from "../shared/errors"
20
+ import type { OAuthTokenEntry } from "../shared/types"
21
+ import type { ActiveTurn, ClaudeSessionState } from "./claude-session-state"
22
+ import { timestamped } from "./claude-message-normalizer"
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Timing constants (same values as in agent.ts — moved here as the single
26
+ // source of truth for the error-handler subsystem).
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Milliseconds to wait before firing a token-rotation auto-continue. Gives
30
+ * the new token a moment to "warm up" before the next turn starts. */
31
+ export const TOKEN_ROTATION_SCHEDULE_DELAY_MS = 100
32
+
33
+ /** Additional stagger per concurrent detector in the same herd window, so
34
+ * PTY cold-boots do not all race to spawn at the same instant. */
35
+ export const TOKEN_ROTATION_HERD_STAGGER_MS = 250
36
+
37
+ /** Window during which multiple detectors for the same token ID are treated
38
+ * as a single rotation event (herd deduplication). */
39
+ export const TOKEN_ROTATION_DEDUPE_WINDOW_MS = 5_000
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Structural sub-interfaces — only the operations this module calls.
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Entry stored per-token in the deduplication map. */
46
+ export interface TokenRotationDedupeEntry {
47
+ firstSeenAt: number
48
+ staggerCount: number
49
+ }
50
+
51
+ /** Subset of OAuthTokenPool used by the error handlers. */
52
+ interface ErrorHandlerOAuthPool {
53
+ markLimited(id: string, resetAt: number): void
54
+ markError(id: string, message: string): void
55
+ pickActive(reservedFor?: string): OAuthTokenEntry | null
56
+ earliestUnlimit(): number | null
57
+ }
58
+
59
+ /** Subset of EventStore used by the error handlers. */
60
+ interface ErrorHandlerStore {
61
+ getAutoContinueEvents(chatId: string): AutoContinueEvent[]
62
+ appendAutoContinueEvent(event: AutoContinueEvent): Promise<void>
63
+ recordTurnFailed(chatId: string, error: string): Promise<void>
64
+ appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Dependency bundle injected by AgentCoordinator
69
+ // ---------------------------------------------------------------------------
70
+
71
+ export interface SessionErrorHandlerDeps {
72
+ /**
73
+ * Mutable map owned by AgentCoordinator tracking the first-seen time and
74
+ * stagger count per token ID across rotation detectors in the same herd
75
+ * window. Passed by reference so acquireRotationSlot can mutate it.
76
+ */
77
+ tokenRotationDedupe: Map<string, TokenRotationDedupeEntry>
78
+
79
+ /** The live claudeSessions map owned by the coordinator (read-only). */
80
+ claudeSessions: Pick<Map<string, ClaudeSessionState>, "get">
81
+
82
+ /** Active turns map — `.get()` to check existence, `.delete()` to remove. */
83
+ activeTurns: Pick<Map<string, ActiveTurn>, "get" | "delete">
84
+
85
+ /** OAuth token pool, or null when no pool is configured. */
86
+ oauthPool: ErrorHandlerOAuthPool | null
87
+
88
+ /** EventStore — subset used by the error handlers. */
89
+ store: ErrorHandlerStore
90
+
91
+ /** Returns true when the chat has auto-resume enabled. */
92
+ resolveAutoResumeFor(chatId: string): boolean
93
+
94
+ /**
95
+ * Appends an AutoContinueEvent to the store, notifies the schedule manager,
96
+ * and emits a state-change event for the chat.
97
+ */
98
+ emitAutoContinueEvent(event: AutoContinueEvent): Promise<void>
99
+
100
+ /**
101
+ * Tears down the given Claude session (stops the subprocess, releases the
102
+ * OAuth reservation unless keepReservation is set).
103
+ */
104
+ closeClaudeSession(
105
+ chatId: string,
106
+ session: ClaudeSessionState,
107
+ opts?: { keepReservation?: boolean },
108
+ ): void
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Exported standalone functions
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Acquire a rotation slot for the given token, returning an optional
117
+ * extra scheduling delay and whether this is the first detector (to avoid
118
+ * duplicate markLimited/markError calls).
119
+ *
120
+ * Multiple detectors for the same token ID within TOKEN_ROTATION_DEDUPE_WINDOW_MS
121
+ * reuse the same slot; each additional caller gets an increasing stagger delay
122
+ * so PTY cold-boots spread out instead of stampeding.
123
+ */
124
+ export function acquireRotationSlot(
125
+ deps: SessionErrorHandlerDeps,
126
+ tokenId: string | null,
127
+ ): { extraDelayMs: number; isFirst: boolean } {
128
+ if (!tokenId) return { extraDelayMs: 0, isFirst: true }
129
+ const now = Date.now()
130
+ const existing = deps.tokenRotationDedupe.get(tokenId)
131
+ if (!existing || now - existing.firstSeenAt > TOKEN_ROTATION_DEDUPE_WINDOW_MS) {
132
+ deps.tokenRotationDedupe.set(tokenId, { firstSeenAt: now, staggerCount: 0 })
133
+ return { extraDelayMs: 0, isFirst: true }
134
+ }
135
+ existing.staggerCount += 1
136
+ return { extraDelayMs: existing.staggerCount * TOKEN_ROTATION_HERD_STAGGER_MS, isFirst: false }
137
+ }
138
+
139
+ /**
140
+ * Thin wrapper: run the rate-limit detector against the raw error and, if a
141
+ * detection fires, delegate to handleLimitDetection.
142
+ *
143
+ * Returns true when the error was recognised as a rate-limit and handled.
144
+ */
145
+ export async function handleLimitError(
146
+ deps: SessionErrorHandlerDeps,
147
+ chatId: string,
148
+ detector: LimitDetector,
149
+ error: AnyValue,
150
+ ): Promise<boolean> {
151
+ const detection = detector.detect(chatId, error)
152
+ if (!detection) return false
153
+ return handleLimitDetection(deps, chatId, detection)
154
+ }
155
+
156
+ /**
157
+ * Core rate-limit response:
158
+ * 1. Guard against a duplicate: if a live schedule already exists, bail out.
159
+ * 2. Mark the current token as limited in the pool.
160
+ * 3. Pick a rotation target (another usable pool token).
161
+ * 4. Emit an AutoContinueEvent (rotation, auto-resume, or proposed).
162
+ * 5. When rotating: close the limited session so the next turn spawns with
163
+ * the new token; record the turn as failed.
164
+ * 6. When not rotating: append an `auto_continue_prompt` transcript entry
165
+ * so the UI can show the schedule card.
166
+ *
167
+ * Returns true (the error was handled, do not log the raw error).
168
+ */
169
+ export async function handleLimitDetection(
170
+ deps: SessionErrorHandlerDeps,
171
+ chatId: string,
172
+ detection: LimitDetection,
173
+ ): Promise<boolean> {
174
+ const live = deriveChatSchedules(deps.store.getAutoContinueEvents(chatId), chatId).liveScheduleId
175
+ if (live !== null) return true
176
+
177
+ const session = deps.claudeSessions.get(chatId)
178
+ const limitedTokenId = session?.activeTokenId ?? null
179
+ const slot = acquireRotationSlot(deps, limitedTokenId)
180
+ if (deps.oauthPool && limitedTokenId && slot.isFirst) {
181
+ deps.oauthPool.markLimited(limitedTokenId, detection.resetAt)
182
+ }
183
+ const rotationTarget = deps.oauthPool?.pickActive(chatId) ?? null
184
+ const canRotate = rotationTarget !== null
185
+ && (!limitedTokenId || rotationTarget.id !== limitedTokenId)
186
+
187
+ if (deps.oauthPool) {
188
+ log.info("[oauth-pool] rate-limit detected", {
189
+ chatId,
190
+ markedLimitedTokenId: limitedTokenId,
191
+ resetAt: new Date(detection.resetAt).toISOString(),
192
+ tz: detection.tz,
193
+ nextTokenId: rotationTarget?.id ?? null,
194
+ canRotate,
195
+ herdSlot: slot,
196
+ })
197
+ }
198
+
199
+ const now = Date.now()
200
+ const scheduleId = crypto.randomUUID()
201
+ const base = { v: AUTO_CONTINUE_EVENT_VERSION, timestamp: now, chatId, scheduleId }
202
+
203
+ // When no rotation is possible, "wait until rate-limit clears" means waiting
204
+ // for the earliest token in the pool to become available again — not just
205
+ // the current detection's resetAt, which would over-shoot if another pool
206
+ // token has an earlier limitedUntil.
207
+ const earliestPoolUnlimit = deps.oauthPool?.earliestUnlimit() ?? null
208
+ const waitUntil = earliestPoolUnlimit !== null
209
+ ? Math.min(detection.resetAt, earliestPoolUnlimit)
210
+ : detection.resetAt
211
+
212
+ let event: AutoContinueEvent
213
+ if (canRotate) {
214
+ event = {
215
+ ...base,
216
+ kind: "auto_continue_accepted",
217
+ scheduledAt: now + TOKEN_ROTATION_SCHEDULE_DELAY_MS + slot.extraDelayMs,
218
+ tz: detection.tz,
219
+ source: "token_rotation",
220
+ resetAt: detection.resetAt,
221
+ detectedAt: now,
222
+ }
223
+ } else if (deps.resolveAutoResumeFor(chatId)) {
224
+ event = {
225
+ ...base,
226
+ kind: "auto_continue_accepted",
227
+ scheduledAt: waitUntil,
228
+ tz: detection.tz,
229
+ source: "auto_setting",
230
+ resetAt: waitUntil,
231
+ detectedAt: now,
232
+ }
233
+ } else {
234
+ event = {
235
+ ...base,
236
+ kind: "auto_continue_proposed",
237
+ detectedAt: now,
238
+ resetAt: waitUntil,
239
+ tz: detection.tz,
240
+ }
241
+ }
242
+
243
+ await deps.emitAutoContinueEvent(event)
244
+ if (canRotate && session) {
245
+ // Tear down the session bound to the limited token so the next turn
246
+ // spawns a fresh subprocess with the rotated token's credentials.
247
+ // Without this, startClaudeTurn reuses the cached session and
248
+ // sendPrompt is routed to the still-limited token's subprocess.
249
+ // keepReservation: true — the `pickActive(chatId)` above already
250
+ // claimed `rotationTarget` under this chatId; the default `release`
251
+ // path would scan reservedBy for owner===chatId and drop it,
252
+ // leaking the rotation's reservation (audit #9d).
253
+ deps.closeClaudeSession(chatId, session, { keepReservation: true })
254
+ const active = deps.activeTurns.get(chatId)
255
+ if (active) {
256
+ await deps.store.recordTurnFailed(chatId, "rate_limit")
257
+ deps.activeTurns.delete(chatId)
258
+ }
259
+ }
260
+ if (!canRotate) {
261
+ await deps.store.appendMessage(chatId, timestamped({
262
+ kind: "auto_continue_prompt",
263
+ scheduleId,
264
+ }))
265
+ }
266
+
267
+ return true
268
+ }
269
+
270
+ /**
271
+ * Core auth-error response:
272
+ * 1. Guard against a duplicate: if a live schedule already exists, bail out.
273
+ * 2. Mark the current token as errored in the pool.
274
+ * 3. Pick a rotation target.
275
+ * 4. Emit an AutoContinueEvent (immediate rotation or proposed).
276
+ * 5. When rotating: close the dead session so the next turn spawns with the
277
+ * new token; record the turn as failed.
278
+ * 6. When not rotating: append an `auto_continue_prompt` transcript entry
279
+ * so the UI can prompt the user to fix their token pool.
280
+ *
281
+ * Returns true when the failure was handled (rotated or proposed),
282
+ * false otherwise (caller logs the raw error).
283
+ */
284
+ export async function handleAuthFailure(
285
+ deps: SessionErrorHandlerDeps,
286
+ session: ClaudeSessionState,
287
+ detection: AuthErrorDetection,
288
+ ): Promise<boolean> {
289
+ const chatId = session.chatId
290
+ const live = deriveChatSchedules(deps.store.getAutoContinueEvents(chatId), chatId).liveScheduleId
291
+ if (live !== null) return true
292
+
293
+ const erroredTokenId = session.activeTokenId
294
+ const slot = acquireRotationSlot(deps, erroredTokenId)
295
+ if (deps.oauthPool && erroredTokenId && slot.isFirst) {
296
+ deps.oauthPool.markError(erroredTokenId, detection.reason)
297
+ }
298
+ const rotationTarget = deps.oauthPool?.pickActive(chatId) ?? null
299
+ const canRotate = rotationTarget !== null
300
+ && (!erroredTokenId || rotationTarget.id !== erroredTokenId)
301
+
302
+ if (deps.oauthPool) {
303
+ log.info("[oauth-pool] auth-error detected", {
304
+ chatId,
305
+ markedErrorTokenId: erroredTokenId,
306
+ reason: detection.reason,
307
+ nextTokenId: rotationTarget?.id ?? null,
308
+ canRotate,
309
+ herdSlot: slot,
310
+ })
311
+ }
312
+
313
+ const now = Date.now()
314
+ const scheduleId = crypto.randomUUID()
315
+ const base = { v: AUTO_CONTINUE_EVENT_VERSION, timestamp: now, chatId, scheduleId }
316
+
317
+ // Auth errors mean the token is dead, not throttled — rotate
318
+ // immediately when possible, no wait window.
319
+ const event: AutoContinueEvent = canRotate
320
+ ? {
321
+ ...base,
322
+ kind: "auto_continue_accepted",
323
+ scheduledAt: now + TOKEN_ROTATION_SCHEDULE_DELAY_MS + slot.extraDelayMs,
324
+ tz: "system",
325
+ source: "token_rotation",
326
+ resetAt: now,
327
+ detectedAt: now,
328
+ }
329
+ : {
330
+ ...base,
331
+ kind: "auto_continue_proposed",
332
+ detectedAt: now,
333
+ resetAt: now,
334
+ tz: "system",
335
+ }
336
+
337
+ await deps.emitAutoContinueEvent(event)
338
+ if (canRotate) {
339
+ // Tear down the session bound to the dead token so the next turn
340
+ // spawns a fresh subprocess with the rotated token in env.
341
+ // keepReservation: true — `pickActive(chatId)` above already claimed
342
+ // the rotation target under this chatId. The previous inline close +
343
+ // delete pair sidestepped `closeClaudeSession` to avoid the
344
+ // accidental release; route through the helper now that release is
345
+ // opt-out, for symmetry with the rate-limit rotation path.
346
+ deps.closeClaudeSession(chatId, session, { keepReservation: true })
347
+ const active = deps.activeTurns.get(chatId)
348
+ if (active) {
349
+ await deps.store.recordTurnFailed(chatId, "auth_error")
350
+ deps.activeTurns.delete(chatId)
351
+ }
352
+ }
353
+ if (!canRotate) {
354
+ await deps.store.appendMessage(chatId, timestamped({
355
+ kind: "auto_continue_prompt",
356
+ scheduleId,
357
+ }))
358
+ }
359
+
360
+ return true
361
+ }