@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.
- package/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.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-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/index-CLsOWXff.css +32 -0
- package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
- package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.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-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +2 -2
- 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.oauth-rotation.test.ts +101 -0
- package/src/server/agent.test.ts +24 -8
- package/src/server/agent.ts +35 -4963
- package/src/server/auto-continue/read-model.test.ts +2 -2
- package/src/server/auto-continue/read-model.ts +4 -1
- 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 -2284
- package/src/server/events.ts +3 -0
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/read-models.ts +26 -1
- package/src/server/subagent-orchestrator.ts +3 -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/loop-progress.test.ts +106 -0
- package/src/shared/loop-progress.ts +88 -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 +27 -1668
- package/dist/client/assets/channel-BMkhjiLI.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
- package/dist/client/assets/index-H6C-f2Vp.css +0 -32
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone chat management functions for AgentCoordinator.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from agent.ts so the chat lifecycle and steering logic lives in
|
|
5
|
+
* its own testable module. The coordinator delegates to these functions by
|
|
6
|
+
* passing an object literal that satisfies `ChatManagementDeps`.
|
|
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 { AgentProvider, QueuedChatMessage } from "../shared/types"
|
|
14
|
+
import type { ClientCommand } from "../shared/protocol"
|
|
15
|
+
import type { ClaudeSessionState } from "./claude-session-state"
|
|
16
|
+
import type { GenerateChatTitleResult } from "./generate-title"
|
|
17
|
+
import { logClaudeSteer } from "./claude-steer-log"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Structural sub-interfaces — only the slices each function calls.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Subset of the activeTurns map used by chat management. */
|
|
24
|
+
interface ActiveTurnsMap {
|
|
25
|
+
has(chatId: string): boolean
|
|
26
|
+
get(chatId: string): { proactiveCompactInjection?: boolean } | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Subset of the drainingStreams map used by chat management. */
|
|
30
|
+
interface DrainingStreamsMap {
|
|
31
|
+
get(chatId: string): { turn: { close(): void } } | undefined
|
|
32
|
+
has(chatId: string): boolean
|
|
33
|
+
delete(chatId: string): boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Subset of the claudeSessions map used by chat management. */
|
|
37
|
+
interface ClaudeSessionsMap {
|
|
38
|
+
get(chatId: string): ClaudeSessionState | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Subset of the autoResumeByChat map used by chat management. */
|
|
42
|
+
interface AutoResumeMap {
|
|
43
|
+
delete(chatId: string): boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Subset of EventStore used by the chat management functions. */
|
|
47
|
+
interface ChatManagementStore {
|
|
48
|
+
getQueuedMessage(chatId: string, queuedMessageId: string): QueuedChatMessage | null
|
|
49
|
+
removeQueuedMessage(chatId: string, queuedMessageId: string): Promise<void>
|
|
50
|
+
requireChat(chatId: string): {
|
|
51
|
+
title: string
|
|
52
|
+
provider: AgentProvider | null
|
|
53
|
+
sessionTokensByProvider: Partial<Record<AgentProvider, string | null>>
|
|
54
|
+
pendingForkSessionToken?: { provider: AgentProvider; token: string } | null
|
|
55
|
+
}
|
|
56
|
+
forkChat(chatId: string): Promise<{ id: string }>
|
|
57
|
+
renameChat(chatId: string, title: string): Promise<void>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Minimal analytics interface used by chat management. */
|
|
61
|
+
interface AnalyticsSubset {
|
|
62
|
+
track(eventName: string, properties?: Record<string, unknown>): void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Dependency bundle injected by AgentCoordinator
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export interface ChatManagementDeps {
|
|
70
|
+
/** Active turn map — read-only for management functions. */
|
|
71
|
+
activeTurns: ActiveTurnsMap
|
|
72
|
+
/** Draining streams map — management functions read and mutate it. */
|
|
73
|
+
drainingStreams: DrainingStreamsMap
|
|
74
|
+
/** Active Claude session map — used by closeChat to find the session. */
|
|
75
|
+
claudeSessions: ClaudeSessionsMap
|
|
76
|
+
/** Per-chat auto-resume flags — cleared on closeChat. */
|
|
77
|
+
autoResumeByChat: AutoResumeMap
|
|
78
|
+
/** Subset of EventStore needed by management functions. */
|
|
79
|
+
store: ChatManagementStore
|
|
80
|
+
/** Analytics reporter for tracking events. */
|
|
81
|
+
analytics: AnalyticsSubset
|
|
82
|
+
/** Cancel the active turn for a chat. */
|
|
83
|
+
cancel(chatId: string, options?: { hideInterrupted?: boolean; skipQueueDrain?: boolean }): Promise<void>
|
|
84
|
+
/** Close the Claude session for a chat, freeing its process slot. */
|
|
85
|
+
closeClaudeSession(chatId: string, session: ClaudeSessionState, opts?: { keepReservation?: boolean }): void
|
|
86
|
+
/** Notify listeners that the observable state for chatId has changed. */
|
|
87
|
+
emitStateChange(chatId: string): void
|
|
88
|
+
/** Generate a title for a chat message (AI-assisted). */
|
|
89
|
+
generateTitle(messageContent: string, cwd: string): Promise<GenerateChatTitleResult>
|
|
90
|
+
/** Report a non-fatal background error (e.g. title generation failure). Nullable for optional injection. */
|
|
91
|
+
reportBackgroundError: ((message: string) => void) | null
|
|
92
|
+
/** Dequeue a queued message and start processing it. */
|
|
93
|
+
dequeueAndStartQueuedMessage(
|
|
94
|
+
chatId: string,
|
|
95
|
+
queuedMessage: QueuedChatMessage,
|
|
96
|
+
options?: { steered?: boolean },
|
|
97
|
+
): Promise<void>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Extracted functions
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stop a draining stream for `chatId` if one is active.
|
|
106
|
+
* Closes the stream's turn, removes the entry, and notifies state listeners.
|
|
107
|
+
*/
|
|
108
|
+
export async function stopDraining(deps: ChatManagementDeps, chatId: string): Promise<void> {
|
|
109
|
+
const draining = deps.drainingStreams.get(chatId)
|
|
110
|
+
if (!draining) return
|
|
111
|
+
draining.turn.close()
|
|
112
|
+
deps.drainingStreams.delete(chatId)
|
|
113
|
+
deps.emitStateChange(chatId)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Fully close a chat: stop any draining stream, close the Claude session,
|
|
118
|
+
* clear the auto-resume flag, and notify state listeners.
|
|
119
|
+
*/
|
|
120
|
+
export async function closeChat(deps: ChatManagementDeps, chatId: string): Promise<void> {
|
|
121
|
+
await stopDraining(deps, chatId)
|
|
122
|
+
const claudeSession = deps.claudeSessions.get(chatId)
|
|
123
|
+
if (claudeSession) {
|
|
124
|
+
deps.closeClaudeSession(chatId, claudeSession)
|
|
125
|
+
}
|
|
126
|
+
deps.autoResumeByChat.delete(chatId)
|
|
127
|
+
deps.emitStateChange(chatId)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Steer a chat to a previously queued message: cancel the current active turn
|
|
132
|
+
* (if any) and immediately start the queued message.
|
|
133
|
+
*/
|
|
134
|
+
export async function steer(
|
|
135
|
+
deps: ChatManagementDeps,
|
|
136
|
+
command: Extract<ClientCommand, { type: "message.steer" }>,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
const queuedMessage = deps.store.getQueuedMessage(command.chatId, command.queuedMessageId)
|
|
139
|
+
if (!queuedMessage) {
|
|
140
|
+
throw new Error("Queued message not found")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logClaudeSteer("steer_requested", {
|
|
144
|
+
chatId: command.chatId,
|
|
145
|
+
queuedMessageId: command.queuedMessageId,
|
|
146
|
+
activeTurn: deps.activeTurns.has(command.chatId),
|
|
147
|
+
queuedMessagePreview: queuedMessage.content.slice(0, 160),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (deps.activeTurns.has(command.chatId)) {
|
|
151
|
+
await deps.cancel(command.chatId, { hideInterrupted: true, skipQueueDrain: true })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logClaudeSteer("steer_after_cancel", {
|
|
155
|
+
chatId: command.chatId,
|
|
156
|
+
stillActive: deps.activeTurns.has(command.chatId),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (deps.activeTurns.has(command.chatId)) {
|
|
160
|
+
throw new Error("Chat is still running")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await deps.dequeueAndStartQueuedMessage(command.chatId, queuedMessage, { steered: true })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Remove a queued message, refusing if a proactive compact turn is in flight
|
|
168
|
+
* (dropping the message mid-compact would silently lose user intent).
|
|
169
|
+
*/
|
|
170
|
+
export async function dequeue(
|
|
171
|
+
deps: ChatManagementDeps,
|
|
172
|
+
command: Extract<ClientCommand, { type: "message.dequeue" }>,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const queuedMessage = deps.store.getQueuedMessage(command.chatId, command.queuedMessageId)
|
|
175
|
+
if (!queuedMessage) {
|
|
176
|
+
throw new Error("Queued message not found")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Refuse to drop the queued message while a Kanna-injected `/compact`
|
|
180
|
+
// turn is running. The compact was triggered specifically to make room
|
|
181
|
+
// for this queued message; auto-draining it after compact completes
|
|
182
|
+
// would silently lose user intent and waste the compact spend.
|
|
183
|
+
const active = deps.activeTurns.get(command.chatId)
|
|
184
|
+
if (active?.proactiveCompactInjection) {
|
|
185
|
+
throw new Error("Cannot remove queued message while compact is running")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await deps.store.removeQueuedMessage(command.chatId, command.queuedMessageId)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Fork `chatId` into a new child chat, inheriting the parent's session token.
|
|
193
|
+
* Throws if the chat is active, has no provider, or has no session to fork.
|
|
194
|
+
*/
|
|
195
|
+
export async function forkChat(deps: ChatManagementDeps, chatId: string): Promise<{ chatId: string }> {
|
|
196
|
+
const chat = deps.store.requireChat(chatId)
|
|
197
|
+
if (deps.activeTurns.has(chatId) || deps.drainingStreams.has(chatId)) {
|
|
198
|
+
throw new Error("Chat must be idle before forking")
|
|
199
|
+
}
|
|
200
|
+
if (!chat.provider) {
|
|
201
|
+
throw new Error("Chat must have a provider before forking")
|
|
202
|
+
}
|
|
203
|
+
const currentProviderToken = chat.provider
|
|
204
|
+
? (chat.sessionTokensByProvider[chat.provider] ?? null)
|
|
205
|
+
: null
|
|
206
|
+
const pendingForkForProvider =
|
|
207
|
+
chat.pendingForkSessionToken?.provider === chat.provider
|
|
208
|
+
? chat.pendingForkSessionToken.token
|
|
209
|
+
: null
|
|
210
|
+
if (!currentProviderToken && !pendingForkForProvider) {
|
|
211
|
+
throw new Error("Chat has no session to fork")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const forked = await deps.store.forkChat(chatId)
|
|
215
|
+
deps.analytics.track("chat_created")
|
|
216
|
+
return { chatId: forked.id }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Generate a title for a chat message in the background, persisting it only
|
|
221
|
+
* if the chat's current title still matches `expectedCurrentTitle`.
|
|
222
|
+
* Errors are reported via `deps.reportBackgroundError` rather than thrown.
|
|
223
|
+
*/
|
|
224
|
+
export async function generateTitleInBackground(
|
|
225
|
+
deps: ChatManagementDeps,
|
|
226
|
+
chatId: string,
|
|
227
|
+
messageContent: string,
|
|
228
|
+
cwd: string,
|
|
229
|
+
expectedCurrentTitle: string,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
try {
|
|
232
|
+
const result = await deps.generateTitle(messageContent, cwd)
|
|
233
|
+
if (result.failureMessage) {
|
|
234
|
+
deps.reportBackgroundError?.(
|
|
235
|
+
`[title-generation] chat ${chatId} failed provider title generation: ${result.failureMessage}`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
if (!result.title || result.usedFallback) return
|
|
239
|
+
|
|
240
|
+
const chat = deps.store.requireChat(chatId)
|
|
241
|
+
if (chat.title !== expectedCurrentTitle) return
|
|
242
|
+
|
|
243
|
+
await deps.store.renameChat(chatId, result.title)
|
|
244
|
+
deps.emitStateChange(chatId)
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
247
|
+
deps.reportBackgroundError?.(
|
|
248
|
+
`[title-generation] chat ${chatId} failed background title generation: ${message}`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { createClaudeHarnessStream } from "./claude-harness-stream"
|
|
3
|
+
import type { ClaudeRawSdkMessage } from "./claude-message-normalizer"
|
|
4
|
+
import type { HarnessEvent } from "./harness-types"
|
|
5
|
+
import type { ModelPrice } from "../shared/token-pricing"
|
|
6
|
+
|
|
7
|
+
function fakeQuery(messages: unknown[]): AsyncGenerator<ClaudeRawSdkMessage> {
|
|
8
|
+
return (async function* () {
|
|
9
|
+
for (const m of messages) yield m as ClaudeRawSdkMessage
|
|
10
|
+
})()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function collect(
|
|
14
|
+
messages: unknown[],
|
|
15
|
+
configuredContextWindow?: number,
|
|
16
|
+
resolveTurnPrice?: () => ModelPrice | null,
|
|
17
|
+
): Promise<HarnessEvent[]> {
|
|
18
|
+
const events: HarnessEvent[] = []
|
|
19
|
+
for await (const ev of createClaudeHarnessStream(
|
|
20
|
+
fakeQuery(messages),
|
|
21
|
+
configuredContextWindow,
|
|
22
|
+
resolveTurnPrice,
|
|
23
|
+
)) {
|
|
24
|
+
events.push(ev)
|
|
25
|
+
}
|
|
26
|
+
return events
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("createClaudeHarnessStream", () => {
|
|
30
|
+
test("empty stream yields nothing", async () => {
|
|
31
|
+
const events = await collect([])
|
|
32
|
+
expect(events).toHaveLength(0)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("session_token event emitted when sdkMessage.session_id is present", async () => {
|
|
36
|
+
const events = await collect([
|
|
37
|
+
{
|
|
38
|
+
type: "assistant",
|
|
39
|
+
session_id: "sess-abc",
|
|
40
|
+
message: { id: "m1", role: "assistant", content: [] },
|
|
41
|
+
},
|
|
42
|
+
])
|
|
43
|
+
const tokens = events.filter((e) => e.type === "session_token")
|
|
44
|
+
expect(tokens).toHaveLength(1)
|
|
45
|
+
expect(tokens[0]).toEqual({ type: "session_token", sessionToken: "sess-abc" })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("no session_token event when session_id is absent", async () => {
|
|
49
|
+
const events = await collect([
|
|
50
|
+
{
|
|
51
|
+
type: "result",
|
|
52
|
+
subtype: "success",
|
|
53
|
+
is_error: false,
|
|
54
|
+
duration_ms: 0,
|
|
55
|
+
uuid: "r1",
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
const tokens = events.filter((e) => e.type === "session_token")
|
|
59
|
+
expect(tokens).toHaveLength(0)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("rate_limit_event with resetsAt produces rate_limit HarnessEvent", async () => {
|
|
63
|
+
const resetAt = 1_748_800_000
|
|
64
|
+
const events = await collect([
|
|
65
|
+
{
|
|
66
|
+
type: "rate_limit_event",
|
|
67
|
+
session_id: "sess-rl",
|
|
68
|
+
rate_limit_info: { status: "rejected", resetsAt: resetAt },
|
|
69
|
+
},
|
|
70
|
+
])
|
|
71
|
+
const rateLimits = events.filter((e) => e.type === "rate_limit")
|
|
72
|
+
expect(rateLimits).toHaveLength(1)
|
|
73
|
+
const ev = rateLimits[0]
|
|
74
|
+
expect(ev.type).toBe("rate_limit")
|
|
75
|
+
// SDK emits resetsAt as epoch seconds; limit-detector converts to ms
|
|
76
|
+
expect(ev.rateLimit?.resetAt).toBe(resetAt * 1000)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("assistant message emits context_window_updated transcript entry", async () => {
|
|
80
|
+
const events = await collect([
|
|
81
|
+
{
|
|
82
|
+
type: "assistant",
|
|
83
|
+
session_id: "sess-1",
|
|
84
|
+
message: { id: "m1", role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
85
|
+
usage: { input_tokens: 100, output_tokens: 30 },
|
|
86
|
+
},
|
|
87
|
+
])
|
|
88
|
+
const cwUpdated = events.filter(
|
|
89
|
+
(e) => e.type === "transcript" && e.entry?.kind === "context_window_updated",
|
|
90
|
+
)
|
|
91
|
+
expect(cwUpdated.length).toBeGreaterThanOrEqual(1)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("result message produces enriched result entry with usage", async () => {
|
|
95
|
+
const events = await collect([
|
|
96
|
+
{
|
|
97
|
+
type: "assistant",
|
|
98
|
+
session_id: "sess-u",
|
|
99
|
+
message: { id: "m1", role: "assistant", content: [] },
|
|
100
|
+
usage: { input_tokens: 50, output_tokens: 20 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "result",
|
|
104
|
+
subtype: "success",
|
|
105
|
+
session_id: "sess-u",
|
|
106
|
+
is_error: false,
|
|
107
|
+
duration_ms: 300,
|
|
108
|
+
usage: { input_tokens: 50, output_tokens: 20 },
|
|
109
|
+
uuid: "r1",
|
|
110
|
+
},
|
|
111
|
+
])
|
|
112
|
+
const resultEntries = events.flatMap((e) =>
|
|
113
|
+
e.type === "transcript" && e.entry?.kind === "result" ? [e.entry] : [],
|
|
114
|
+
)
|
|
115
|
+
expect(resultEntries).toHaveLength(1)
|
|
116
|
+
expect(resultEntries[0].usage).toBeDefined()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("api_error in turn scrubs result body (no duplicate text)", async () => {
|
|
120
|
+
const errorText = "You've hit your limit"
|
|
121
|
+
const events = await collect([
|
|
122
|
+
{
|
|
123
|
+
type: "assistant",
|
|
124
|
+
session_id: "sess-ae",
|
|
125
|
+
uuid: "msg-err",
|
|
126
|
+
isApiErrorMessage: true,
|
|
127
|
+
message: {
|
|
128
|
+
model: "<synthetic>",
|
|
129
|
+
content: [{ type: "text", text: errorText }],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: "result",
|
|
134
|
+
subtype: "error",
|
|
135
|
+
session_id: "sess-ae",
|
|
136
|
+
is_error: true,
|
|
137
|
+
result: errorText,
|
|
138
|
+
duration_ms: 1000,
|
|
139
|
+
uuid: "r-ae",
|
|
140
|
+
},
|
|
141
|
+
])
|
|
142
|
+
const apiErrorEntries = events.flatMap((e) =>
|
|
143
|
+
e.type === "transcript" && e.entry?.kind === "api_error" ? [e.entry] : [],
|
|
144
|
+
)
|
|
145
|
+
const resultEntries = events.flatMap((e) =>
|
|
146
|
+
e.type === "transcript" && e.entry?.kind === "result" ? [e.entry] : [],
|
|
147
|
+
)
|
|
148
|
+
expect(apiErrorEntries).toHaveLength(1)
|
|
149
|
+
expect(resultEntries).toHaveLength(1)
|
|
150
|
+
// Result body is scrubbed to "" to avoid duplicate display
|
|
151
|
+
expect(resultEntries[0].result).toBe("")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("normalizeClaudeStreamMessage entries are yielded as transcript events", async () => {
|
|
155
|
+
const events = await collect([
|
|
156
|
+
{
|
|
157
|
+
type: "result",
|
|
158
|
+
subtype: "success",
|
|
159
|
+
session_id: "sess-norm",
|
|
160
|
+
is_error: false,
|
|
161
|
+
result: "done",
|
|
162
|
+
duration_ms: 50,
|
|
163
|
+
uuid: "r-norm",
|
|
164
|
+
},
|
|
165
|
+
])
|
|
166
|
+
const transcriptEntries = events.filter((e) => e.type === "transcript")
|
|
167
|
+
expect(transcriptEntries.length).toBeGreaterThanOrEqual(1)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test("result with total_cost_usd attaches costUsd to result entry", async () => {
|
|
171
|
+
const events = await collect([
|
|
172
|
+
{
|
|
173
|
+
type: "result",
|
|
174
|
+
subtype: "success",
|
|
175
|
+
session_id: "sess-cost",
|
|
176
|
+
is_error: false,
|
|
177
|
+
duration_ms: 100,
|
|
178
|
+
total_cost_usd: 0.0042,
|
|
179
|
+
uuid: "r-cost",
|
|
180
|
+
},
|
|
181
|
+
])
|
|
182
|
+
const resultEntries = events.flatMap((e) =>
|
|
183
|
+
e.type === "transcript" && e.entry?.kind === "result" ? [e.entry] : [],
|
|
184
|
+
)
|
|
185
|
+
expect(resultEntries).toHaveLength(1)
|
|
186
|
+
expect(resultEntries[0].costUsd).toBe(0.0042)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test("resolveTurnPrice callback used when total_cost_usd absent", async () => {
|
|
190
|
+
const price: ModelPrice = { inputPerMTok: 3, outputPerMTok: 15, cachedInputPerMTok: 0.3 }
|
|
191
|
+
const events = await collect(
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
type: "assistant",
|
|
195
|
+
session_id: "sess-price",
|
|
196
|
+
message: { id: "m1", role: "assistant", content: [] },
|
|
197
|
+
usage: { input_tokens: 1000, output_tokens: 100 },
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
type: "result",
|
|
201
|
+
subtype: "success",
|
|
202
|
+
session_id: "sess-price",
|
|
203
|
+
is_error: false,
|
|
204
|
+
duration_ms: 100,
|
|
205
|
+
usage: { input_tokens: 1000, output_tokens: 100 },
|
|
206
|
+
uuid: "r-price",
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
undefined,
|
|
210
|
+
() => price,
|
|
211
|
+
)
|
|
212
|
+
const resultEntries = events.flatMap((e) =>
|
|
213
|
+
e.type === "transcript" && e.entry?.kind === "result" ? [e.entry] : [],
|
|
214
|
+
)
|
|
215
|
+
expect(resultEntries).toHaveLength(1)
|
|
216
|
+
expect(typeof resultEntries[0].costUsd).toBe("number")
|
|
217
|
+
expect((resultEntries[0].costUsd as number)).toBeGreaterThan(0)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test("duplicate assistant usage id is not double-counted", async () => {
|
|
221
|
+
const events = await collect([
|
|
222
|
+
{
|
|
223
|
+
type: "assistant",
|
|
224
|
+
session_id: "sess-dup",
|
|
225
|
+
message: { id: "dup-id", role: "assistant", content: [] },
|
|
226
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "assistant",
|
|
230
|
+
session_id: "sess-dup",
|
|
231
|
+
message: { id: "dup-id", role: "assistant", content: [] },
|
|
232
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
233
|
+
},
|
|
234
|
+
])
|
|
235
|
+
const cwUpdated = events.filter(
|
|
236
|
+
(e) => e.type === "transcript" && e.entry?.kind === "context_window_updated",
|
|
237
|
+
)
|
|
238
|
+
// Only one context_window_updated per unique usage id
|
|
239
|
+
expect(cwUpdated).toHaveLength(1)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test("configuredContextWindow floors the context window from SDK", async () => {
|
|
243
|
+
const events = await collect(
|
|
244
|
+
[
|
|
245
|
+
{
|
|
246
|
+
type: "assistant",
|
|
247
|
+
session_id: "sess-1m",
|
|
248
|
+
message: { id: "m1", role: "assistant", content: [] },
|
|
249
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
type: "result",
|
|
253
|
+
subtype: "success",
|
|
254
|
+
session_id: "sess-1m",
|
|
255
|
+
is_error: false,
|
|
256
|
+
duration_ms: 100,
|
|
257
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
258
|
+
modelUsage: { "claude-sonnet-4-6": { contextWindow: 200_000 } },
|
|
259
|
+
uuid: "r-1m",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
1_000_000,
|
|
263
|
+
)
|
|
264
|
+
const cwUpdated = events.flatMap((e) =>
|
|
265
|
+
e.type === "transcript" && e.entry?.kind === "context_window_updated" ? [e.entry] : [],
|
|
266
|
+
)
|
|
267
|
+
// The configuredContextWindow (1M) should prevail over modelUsage (200K)
|
|
268
|
+
const lastSnapshot = cwUpdated[cwUpdated.length - 1]
|
|
269
|
+
expect(lastSnapshot).toBeDefined()
|
|
270
|
+
// maxTokens in the snapshot should reflect the 1M floor, not the 200K from modelUsage
|
|
271
|
+
expect(lastSnapshot.usage.maxTokens).toBeGreaterThanOrEqual(1_000_000)
|
|
272
|
+
})
|
|
273
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ContextWindowUsageSnapshot, ProviderUsage } from "../shared/types"
|
|
2
|
+
import { computeCostUsd } from "../shared/token-pricing"
|
|
3
|
+
import type { ModelPrice } from "../shared/token-pricing"
|
|
4
|
+
import type { HarnessEvent } from "./harness-types"
|
|
5
|
+
import {
|
|
6
|
+
timestamped,
|
|
7
|
+
type ClaudeRawSdkMessage,
|
|
8
|
+
getClaudeAssistantMessageUsageId,
|
|
9
|
+
normalizeClaudeStreamMessage,
|
|
10
|
+
} from "./claude-message-normalizer"
|
|
11
|
+
import {
|
|
12
|
+
normalizeClaudeUsageSnapshot,
|
|
13
|
+
resolveFinalTurnUsage,
|
|
14
|
+
maxClaudeContextWindowFromModelUsage,
|
|
15
|
+
} from "./claude-usage-math"
|
|
16
|
+
import { ClaudeLimitDetector } from "./auto-continue/limit-detector"
|
|
17
|
+
|
|
18
|
+
export async function* createClaudeHarnessStream(
|
|
19
|
+
q: AsyncIterable<ClaudeRawSdkMessage>,
|
|
20
|
+
configuredContextWindow?: number,
|
|
21
|
+
resolveTurnPrice?: () => ModelPrice | null,
|
|
22
|
+
): AsyncGenerator<HarnessEvent> {
|
|
23
|
+
let seenAssistantUsageIds = new Set<string>()
|
|
24
|
+
let latestUsageSnapshot: ContextWindowUsageSnapshot | null = null
|
|
25
|
+
let lastKnownContextWindow: number | undefined = configuredContextWindow
|
|
26
|
+
const detector = new ClaudeLimitDetector()
|
|
27
|
+
// SDK rate-limit / api-error turns emit BOTH a synthetic assistant
|
|
28
|
+
// `isApiErrorMessage` (→ `api_error` entry, red card with text) AND a
|
|
29
|
+
// `type:"result"` whose `result` field repeats the same text (→ second
|
|
30
|
+
// red card + "Failed after Xs"). Track per-turn api_error emission so
|
|
31
|
+
// we can scrub the duplicate body off the trailing result entry; the
|
|
32
|
+
// duration footer still renders, the message renders once.
|
|
33
|
+
let apiErrorEmittedInTurn = false
|
|
34
|
+
|
|
35
|
+
// Per-turn billed token usage and cost to attach to the result entry.
|
|
36
|
+
// Set when the `type:"result"` SDK message is processed; cleared after
|
|
37
|
+
// the result entry is yielded so they don't leak to a subsequent turn.
|
|
38
|
+
let pendingResultUsage: ProviderUsage | undefined
|
|
39
|
+
let pendingResultCost: number | undefined
|
|
40
|
+
|
|
41
|
+
for await (const sdkMessage of q) {
|
|
42
|
+
const sessionToken = typeof sdkMessage.session_id === "string" ? sdkMessage.session_id : null
|
|
43
|
+
if (sessionToken) {
|
|
44
|
+
yield { type: "session_token", sessionToken }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sdkMessage?.type === "rate_limit_event") {
|
|
48
|
+
const detection = detector.detectFromSdkRateLimitInfo("", sdkMessage.rate_limit_info)
|
|
49
|
+
if (detection) {
|
|
50
|
+
yield { type: "rate_limit", rateLimit: { resetAt: detection.resetAt, tz: detection.tz } }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sdkMessage?.type === "assistant") {
|
|
55
|
+
const usageId = getClaudeAssistantMessageUsageId(sdkMessage)
|
|
56
|
+
const usageSnapshot = normalizeClaudeUsageSnapshot(sdkMessage.usage, lastKnownContextWindow)
|
|
57
|
+
if (usageId && usageSnapshot && !seenAssistantUsageIds.has(usageId)) {
|
|
58
|
+
seenAssistantUsageIds.add(usageId)
|
|
59
|
+
latestUsageSnapshot = usageSnapshot
|
|
60
|
+
yield {
|
|
61
|
+
type: "transcript",
|
|
62
|
+
entry: timestamped({
|
|
63
|
+
kind: "context_window_updated",
|
|
64
|
+
usage: usageSnapshot,
|
|
65
|
+
}),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (sdkMessage?.type === "result") {
|
|
71
|
+
const resultContextWindow = maxClaudeContextWindowFromModelUsage(sdkMessage.modelUsage)
|
|
72
|
+
// Never let SDK lower the configured window — see comment on
|
|
73
|
+
// parseConfiguredContextWindowFromModelId for the 1M beta footgun.
|
|
74
|
+
if (resultContextWindow !== undefined) {
|
|
75
|
+
lastKnownContextWindow = Math.max(lastKnownContextWindow ?? 0, resultContextWindow)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const accumulatedUsage = normalizeClaudeUsageSnapshot(
|
|
79
|
+
sdkMessage.usage,
|
|
80
|
+
lastKnownContextWindow,
|
|
81
|
+
)
|
|
82
|
+
const finalUsage = resolveFinalTurnUsage(
|
|
83
|
+
latestUsageSnapshot,
|
|
84
|
+
accumulatedUsage,
|
|
85
|
+
lastKnownContextWindow,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const providerCostUsd =
|
|
89
|
+
typeof sdkMessage.total_cost_usd === "number"
|
|
90
|
+
? sdkMessage.total_cost_usd
|
|
91
|
+
: undefined
|
|
92
|
+
|
|
93
|
+
let costUsd = providerCostUsd
|
|
94
|
+
if (costUsd === undefined && resolveTurnPrice && finalUsage) {
|
|
95
|
+
const price = resolveTurnPrice()
|
|
96
|
+
if (price) {
|
|
97
|
+
costUsd = computeCostUsd(
|
|
98
|
+
{
|
|
99
|
+
inputTokens: finalUsage.inputTokens,
|
|
100
|
+
cachedInputTokens: finalUsage.cachedInputTokens,
|
|
101
|
+
outputTokens: finalUsage.outputTokens,
|
|
102
|
+
},
|
|
103
|
+
price,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Stash billed token figures for the result entry (populated below
|
|
109
|
+
// in the entry loop). Prefer `accumulatedUsage` (the per-turn
|
|
110
|
+
// cumulative that the SDK computes) for tokens; fall back to
|
|
111
|
+
// `finalUsage` when accumulated is null.
|
|
112
|
+
const billed = accumulatedUsage ?? finalUsage
|
|
113
|
+
pendingResultUsage = billed
|
|
114
|
+
? {
|
|
115
|
+
...(billed.inputTokens !== undefined ? { inputTokens: billed.inputTokens } : {}),
|
|
116
|
+
...(billed.outputTokens !== undefined ? { outputTokens: billed.outputTokens } : {}),
|
|
117
|
+
...(billed.cachedInputTokens !== undefined ? { cachedInputTokens: billed.cachedInputTokens } : {}),
|
|
118
|
+
}
|
|
119
|
+
: undefined
|
|
120
|
+
pendingResultCost = costUsd
|
|
121
|
+
|
|
122
|
+
if (finalUsage) {
|
|
123
|
+
const usageWithCost = costUsd !== undefined ? { ...finalUsage, costUsd } : finalUsage
|
|
124
|
+
yield {
|
|
125
|
+
type: "transcript",
|
|
126
|
+
entry: timestamped({
|
|
127
|
+
kind: "context_window_updated",
|
|
128
|
+
usage: usageWithCost,
|
|
129
|
+
}),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
seenAssistantUsageIds = new Set<string>()
|
|
134
|
+
latestUsageSnapshot = null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const entry of normalizeClaudeStreamMessage(sdkMessage)) {
|
|
138
|
+
if (entry.kind === "api_error") {
|
|
139
|
+
apiErrorEmittedInTurn = true
|
|
140
|
+
} else if (entry.kind === "result") {
|
|
141
|
+
const scrubbed = entry.isError && apiErrorEmittedInTurn
|
|
142
|
+
? { ...entry, result: "" }
|
|
143
|
+
: entry
|
|
144
|
+
apiErrorEmittedInTurn = false
|
|
145
|
+
const enriched = {
|
|
146
|
+
...scrubbed,
|
|
147
|
+
...(pendingResultUsage !== undefined ? { usage: pendingResultUsage } : {}),
|
|
148
|
+
...(pendingResultCost !== undefined ? { costUsd: pendingResultCost } : {}),
|
|
149
|
+
}
|
|
150
|
+
pendingResultUsage = undefined
|
|
151
|
+
pendingResultCost = undefined
|
|
152
|
+
yield { type: "transcript", entry: enriched }
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
yield { type: "transcript", entry }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|