@cuongtran001/kanna 1.3.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.
- package/dist/client/assets/{arc-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +365 -0
- package/src/server/event-store-messages.adapter.ts +213 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +285 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +35 -0
- package/src/server/event-store.ts +251 -2285
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +488 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +329 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +224 -0
- package/src/server/ws-router.ts +339 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +25 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.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
|
+
}
|