@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,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone event-loop runner for an active Claude session.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from AgentCoordinator.runClaudeSession in agent.ts so the ~414-line
|
|
5
|
+
* method can live in its own testable module. The coordinator delegates to this
|
|
6
|
+
* function by passing an object literal that satisfies `RunClaudeSessionDeps`.
|
|
7
|
+
*
|
|
8
|
+
* Side-effect seal: this file is pure logic — no direct IO. Any IO is injected
|
|
9
|
+
* through the deps interface.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { log } from "../shared/log"
|
|
13
|
+
import type { AnyValue } from "../shared/errors"
|
|
14
|
+
import type { AgentProvider, Subagent, TranscriptEntry } from "../shared/types"
|
|
15
|
+
import type { LimitDetector, LimitDetection } from "./auto-continue/limit-detector"
|
|
16
|
+
import type { AuthErrorDetection } from "./auto-continue/auth-error-detector"
|
|
17
|
+
import type { ClaudeDriverPreference } from "../shared/types"
|
|
18
|
+
import {
|
|
19
|
+
isPromptTooLongMessage,
|
|
20
|
+
isNoConversationFoundMessage,
|
|
21
|
+
backgroundTaskIdsFromToolResult,
|
|
22
|
+
} from "./claude-prompt-helpers"
|
|
23
|
+
import { timestamped } from "./claude-message-normalizer"
|
|
24
|
+
import { logClaudeSteer } from "./claude-steer-log"
|
|
25
|
+
import type { ClaudeSessionState, ActiveTurn, SlashCommand } from "./claude-session-state"
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Structural auth-error detector — only the methods called in this module.
|
|
29
|
+
// Using a structural interface instead of the concrete ClaudeAuthErrorDetector
|
|
30
|
+
// class keeps test deps minimal without requiring `as unknown as`.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
interface AuthErrorDetectable {
|
|
34
|
+
detect(chatId: string, error: AnyValue): AuthErrorDetection | null
|
|
35
|
+
detectFromResultText(chatId: string, text: string): AuthErrorDetection | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Minimal store interface — only the methods runClaudeSession actually calls.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
interface RunClaudeSessionStore {
|
|
43
|
+
appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
|
|
44
|
+
recordTurnFailed(chatId: string, reason: string): Promise<void>
|
|
45
|
+
setSessionTokenForProvider(chatId: string, provider: AgentProvider, token: string | null): Promise<void>
|
|
46
|
+
setPendingForkSessionToken(
|
|
47
|
+
chatId: string,
|
|
48
|
+
token: { provider: AgentProvider; token: string } | null,
|
|
49
|
+
): Promise<void>
|
|
50
|
+
recordTurnFinished(chatId: string): Promise<void>
|
|
51
|
+
setCompactFailureCount(chatId: string, count: number): Promise<void>
|
|
52
|
+
recordTurnCancelled(chatId: string): Promise<void>
|
|
53
|
+
getChat(chatId: string): { compactFailureCount?: number; pendingForkSessionToken?: { token: string } | null } | null | undefined
|
|
54
|
+
recordSessionCommandsLoaded(chatId: string, commands: SlashCommand[]): Promise<void>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Minimal oauth-pool interface — only the release() call used in this module.
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
interface OAuthPoolReleaseable {
|
|
62
|
+
release(chatId: string): void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Dependency bundle injected by AgentCoordinator
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export interface RunClaudeSessionDeps {
|
|
70
|
+
openrouterFirstEntryTimeoutMs: number
|
|
71
|
+
claudeSessions: Map<string, ClaudeSessionState>
|
|
72
|
+
activeTurns: Map<string, ActiveTurn>
|
|
73
|
+
oauthPool: OAuthPoolReleaseable | null
|
|
74
|
+
claudeLimitDetector: LimitDetector
|
|
75
|
+
claudeAuthErrorDetector: AuthErrorDetectable
|
|
76
|
+
throwOnClaudeSessionStart: boolean
|
|
77
|
+
store: RunClaudeSessionStore
|
|
78
|
+
emitStateChange(chatId?: string): void
|
|
79
|
+
handleLimitDetection(chatId: string, detection: LimitDetection): Promise<boolean>
|
|
80
|
+
maybeRegisterSdkWorkflowsDir(session: ClaudeSessionState): void
|
|
81
|
+
getSubagents(): Subagent[]
|
|
82
|
+
resolveBackgroundTaskMaxMs(): number
|
|
83
|
+
mergeLocalCatalog(commands: SlashCommand[], cwd: string): SlashCommand[]
|
|
84
|
+
handleLimitError(chatId: string, detector: LimitDetector, error: AnyValue): Promise<boolean>
|
|
85
|
+
handleAuthFailure(session: ClaudeSessionState, detection: AuthErrorDetection): Promise<boolean>
|
|
86
|
+
closeClaudeSession(chatId: string, session: ClaudeSessionState): void
|
|
87
|
+
maybeStartNextQueuedMessage(chatId: string): Promise<boolean | void>
|
|
88
|
+
resolveClaudeDriverPreference(): ClaudeDriverPreference
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Main function
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Consumes the HarnessEvent stream for a running Claude session and handles:
|
|
97
|
+
* - session_token persistence and workflow-dir registration
|
|
98
|
+
* - rate-limit / auth-failure detection and delegation
|
|
99
|
+
* - transcript entry appending and active-turn bookkeeping
|
|
100
|
+
* - background-task keep-alive tracking
|
|
101
|
+
* - compact-boundary finalization (PTY driver only)
|
|
102
|
+
* - fail-close on stream end without a terminal result
|
|
103
|
+
*
|
|
104
|
+
* Behavior is identical to the original private method on AgentCoordinator.
|
|
105
|
+
*/
|
|
106
|
+
export async function runClaudeSession(
|
|
107
|
+
deps: RunClaudeSessionDeps,
|
|
108
|
+
session: ClaudeSessionState,
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
// OpenRouter-only first-entry watchdog. OpenRouter routes through the
|
|
111
|
+
// Claude SDK; a stalled upstream emits the session-token handshake then
|
|
112
|
+
// goes silent — the stream stays open with no entry, so this for-await
|
|
113
|
+
// never returns or throws and the chat hangs "running" until restart. The
|
|
114
|
+
// existing catch/finally fail-close is claude-provider-gated and depends
|
|
115
|
+
// on an active turn that the openrouter path tears down early, so the
|
|
116
|
+
// watchdog records the failure itself, then interrupts + closes the
|
|
117
|
+
// session to end the stream. `firstEntrySeen` guards against a late real
|
|
118
|
+
// entry; close() prevents any further entry from being processed.
|
|
119
|
+
const isOpenRouterSession = session.openrouterModel !== null
|
|
120
|
+
let firstEntrySeen = false
|
|
121
|
+
let firstEntryWatchdog: ReturnType<typeof setTimeout> | null = null
|
|
122
|
+
const clearFirstEntryWatchdog = () => {
|
|
123
|
+
if (firstEntryWatchdog !== null) {
|
|
124
|
+
clearTimeout(firstEntryWatchdog)
|
|
125
|
+
firstEntryWatchdog = null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (isOpenRouterSession) {
|
|
129
|
+
firstEntryWatchdog = setTimeout(() => {
|
|
130
|
+
if (firstEntrySeen) return
|
|
131
|
+
if (deps.claudeSessions.get(session.chatId) !== session) return
|
|
132
|
+
firstEntrySeen = true
|
|
133
|
+
const message = `OpenRouter produced no response within ${deps.openrouterFirstEntryTimeoutMs}ms — the selected model may be invalid or the upstream stalled.`
|
|
134
|
+
log.warn("[kanna/agent] openrouter stream produced no entry within watchdog window — failing turn", {
|
|
135
|
+
chatId: session.chatId,
|
|
136
|
+
sessionId: session.id,
|
|
137
|
+
model: session.openrouterModel,
|
|
138
|
+
timeoutMs: deps.openrouterFirstEntryTimeoutMs,
|
|
139
|
+
})
|
|
140
|
+
void (async () => {
|
|
141
|
+
await deps.store.appendMessage(
|
|
142
|
+
session.chatId,
|
|
143
|
+
timestamped({
|
|
144
|
+
kind: "result",
|
|
145
|
+
subtype: "error",
|
|
146
|
+
isError: true,
|
|
147
|
+
durationMs: deps.openrouterFirstEntryTimeoutMs,
|
|
148
|
+
result: message,
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
await deps.store.recordTurnFailed(session.chatId, message)
|
|
152
|
+
const active = deps.activeTurns.get(session.chatId)
|
|
153
|
+
if (active) deps.activeTurns.delete(session.chatId)
|
|
154
|
+
deps.emitStateChange(session.chatId)
|
|
155
|
+
void session.session.interrupt().catch(() => {})
|
|
156
|
+
session.session.close()
|
|
157
|
+
})()
|
|
158
|
+
}, deps.openrouterFirstEntryTimeoutMs)
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
let simulateLimit = deps.throwOnClaudeSessionStart
|
|
162
|
+
for await (const event of session.session.stream) {
|
|
163
|
+
if (simulateLimit) {
|
|
164
|
+
simulateLimit = false
|
|
165
|
+
throw new Error("simulated rate limit")
|
|
166
|
+
}
|
|
167
|
+
if (event.type === "session_token" && event.sessionToken) {
|
|
168
|
+
session.sessionToken = event.sessionToken
|
|
169
|
+
// Persist only when this session is still current, no cancel is in
|
|
170
|
+
// flight, and no /clear suppressed it. A cancelled spawn can emit
|
|
171
|
+
// its session_token AFTER the user interrupted — the CLI may never
|
|
172
|
+
// persist that conversation, so storing the token would poison the
|
|
173
|
+
// next `--resume` ("No conversation found with session ID"). A
|
|
174
|
+
// /clear (setup_loop, background delivery) mid-stream must likewise
|
|
175
|
+
// not be resurrected by the old conversation's next token event.
|
|
176
|
+
const isCurrentSession = deps.claudeSessions.get(session.chatId) === session
|
|
177
|
+
if (
|
|
178
|
+
isCurrentSession
|
|
179
|
+
&& session.cancelledResultPending === 0
|
|
180
|
+
&& !session.suppressSessionTokenPersist
|
|
181
|
+
) {
|
|
182
|
+
await deps.store.setSessionTokenForProvider(session.chatId, "claude", event.sessionToken)
|
|
183
|
+
}
|
|
184
|
+
deps.maybeRegisterSdkWorkflowsDir(session)
|
|
185
|
+
deps.emitStateChange(session.chatId)
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (event.type === "rate_limit" && event.rateLimit) {
|
|
190
|
+
// Stale rate_limit events from a session that has already been
|
|
191
|
+
// rotated away must not trigger another rotation/continue.
|
|
192
|
+
if (deps.claudeSessions.get(session.chatId) !== session) continue
|
|
193
|
+
await deps.handleLimitDetection(session.chatId, {
|
|
194
|
+
chatId: session.chatId,
|
|
195
|
+
resetAt: event.rateLimit.resetAt,
|
|
196
|
+
tz: event.rateLimit.tz,
|
|
197
|
+
raw: event,
|
|
198
|
+
})
|
|
199
|
+
if (deps.claudeSessions.get(session.chatId) !== session) break
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!event.entry) continue
|
|
204
|
+
firstEntrySeen = true
|
|
205
|
+
clearFirstEntryWatchdog()
|
|
206
|
+
if (deps.claudeSessions.get(session.chatId) !== session) break
|
|
207
|
+
// Suppress the interrupt-induced tail `result` of a cancelled turn.
|
|
208
|
+
// cancel() already removed the active turn, recorded the cancellation,
|
|
209
|
+
// and appended the `interrupted` entry; the SDK then emits one error
|
|
210
|
+
// `result` (subtype error_during_execution, empty text) that would
|
|
211
|
+
// otherwise render as "An unknown error occurred." Drop it (and skip
|
|
212
|
+
// the seq shift — cancel() already spliced the cancelled seq).
|
|
213
|
+
if (
|
|
214
|
+
event.entry.kind === "result" &&
|
|
215
|
+
event.entry.isError &&
|
|
216
|
+
session.cancelledResultPending > 0
|
|
217
|
+
) {
|
|
218
|
+
session.cancelledResultPending -= 1
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (event.entry.kind === "system_init") {
|
|
222
|
+
const kannaNames = deps.getSubagents().map((s) => s.name)
|
|
223
|
+
if (kannaNames.length > 0) {
|
|
224
|
+
const entry = event.entry
|
|
225
|
+
const existing = new Set(entry.agents)
|
|
226
|
+
const extra = kannaNames.filter((n) => !existing.has(n))
|
|
227
|
+
if (extra.length > 0) {
|
|
228
|
+
entry.agents = [...entry.agents, ...extra]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
await deps.store.appendMessage(session.chatId, event.entry)
|
|
233
|
+
// Background-task keep-alive guard (SDK + PTY).
|
|
234
|
+
// On launch: add the task id and refresh the zombie-backstop deadline.
|
|
235
|
+
// On settle (task_notification): remove the id — gate primary signal is
|
|
236
|
+
// set size>0, not the clock. The deadline (default 4h) is refreshed on
|
|
237
|
+
// every launch and settle so it only fires when a notification is truly
|
|
238
|
+
// lost (SDK crash / dropped message), never during normal execution.
|
|
239
|
+
if (event.entry.kind === "tool_result") {
|
|
240
|
+
const launchedIds = backgroundTaskIdsFromToolResult(
|
|
241
|
+
event.entry.content,
|
|
242
|
+
)
|
|
243
|
+
if (launchedIds.length > 0) {
|
|
244
|
+
for (const id of launchedIds) session.backgroundTaskIds.add(id)
|
|
245
|
+
session.backgroundTaskDeadlineAt = Date.now() + deps.resolveBackgroundTaskMaxMs()
|
|
246
|
+
deps.emitStateChange(session.chatId)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (event.entry.kind === "status" && event.entry.backgroundTaskId) {
|
|
250
|
+
const settledId = event.entry.backgroundTaskId
|
|
251
|
+
session.backgroundTaskIds.delete(settledId)
|
|
252
|
+
if (session.backgroundTaskIds.size > 0) {
|
|
253
|
+
session.backgroundTaskDeadlineAt = Date.now() + deps.resolveBackgroundTaskMaxMs()
|
|
254
|
+
} else {
|
|
255
|
+
session.backgroundTaskDeadlineAt = 0
|
|
256
|
+
}
|
|
257
|
+
deps.emitStateChange(session.chatId)
|
|
258
|
+
}
|
|
259
|
+
const active = deps.activeTurns.get(session.chatId)
|
|
260
|
+
if (event.entry.kind === "system_init" && active) {
|
|
261
|
+
active.status = "running"
|
|
262
|
+
const chat = deps.store.getChat(session.chatId)
|
|
263
|
+
if (
|
|
264
|
+
chat?.pendingForkSessionToken
|
|
265
|
+
&& session.sessionToken
|
|
266
|
+
&& session.sessionToken !== chat.pendingForkSessionToken.token
|
|
267
|
+
) {
|
|
268
|
+
await deps.store.setPendingForkSessionToken(session.chatId, null)
|
|
269
|
+
}
|
|
270
|
+
// Refresh the chat's slashCommands from the live system_init list
|
|
271
|
+
// every spawn. The cold-start `getSupportedCommands()` call right
|
|
272
|
+
// after spawn often returns the static fallback because system_init
|
|
273
|
+
// hadn't arrived yet; this overwrites that with the canonical list
|
|
274
|
+
// (skills + plugins + built-ins, no `/` prefix).
|
|
275
|
+
if (Array.isArray(event.entry.slashCommands)) {
|
|
276
|
+
const names = event.entry.slashCommands
|
|
277
|
+
const commands: SlashCommand[] = names.map((name) => ({
|
|
278
|
+
name,
|
|
279
|
+
description: "",
|
|
280
|
+
argumentHint: "",
|
|
281
|
+
}))
|
|
282
|
+
const merged = deps.mergeLocalCatalog(commands, session.localPath)
|
|
283
|
+
await deps.store.recordSessionCommandsLoaded(session.chatId, merged)
|
|
284
|
+
}
|
|
285
|
+
logClaudeSteer("claude_event_system_init", {
|
|
286
|
+
chatId: session.chatId,
|
|
287
|
+
sessionId: session.id,
|
|
288
|
+
activePromptSeq: active.claudePromptSeq ?? null,
|
|
289
|
+
pendingPromptSeqs: [...session.pendingPromptSeqs],
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const completedClaudePromptSeq = event.entry.kind === "result" || event.entry.kind === "interrupted"
|
|
294
|
+
? (session.pendingPromptSeqs.shift() ?? null)
|
|
295
|
+
: null
|
|
296
|
+
if (completedClaudePromptSeq !== null) {
|
|
297
|
+
session.lastUsedAt = Date.now()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
logClaudeSteer("claude_event", {
|
|
301
|
+
chatId: session.chatId,
|
|
302
|
+
sessionId: session.id,
|
|
303
|
+
entryKind: event.entry.kind,
|
|
304
|
+
activePromptSeq: active?.claudePromptSeq ?? null,
|
|
305
|
+
completedPromptSeq: completedClaudePromptSeq,
|
|
306
|
+
activeStatus: active?.status ?? null,
|
|
307
|
+
pendingPromptSeqs: [...session.pendingPromptSeqs],
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// PTY-only: the Kanna-injected proactive `/compact` turn never emits a
|
|
311
|
+
// terminal `result`/`turn_duration` under the interactive TUI — it
|
|
312
|
+
// writes only a `system/compact_boundary` line (confirmed in the
|
|
313
|
+
// on-disk transcript). Without a result, the normal finalize path below
|
|
314
|
+
// (kind === "result") never runs, so the active turn and its
|
|
315
|
+
// `proactiveCompactInjection` flag linger forever — permanently wedging
|
|
316
|
+
// `dequeue()` ("Cannot remove queued message while compact is running")
|
|
317
|
+
// and the queued-message drain. Treat the boundary as the compact
|
|
318
|
+
// turn's completion: finalize like the SDK result path and drain the
|
|
319
|
+
// queued user message the compact made room for. The SDK driver is
|
|
320
|
+
// excluded because there a real `result` still follows; finalizing here
|
|
321
|
+
// would double-finalize and corrupt the trailing result's seq
|
|
322
|
+
// accounting. See adr-20260608-pty-compact-boundary-dequeue-finalize.
|
|
323
|
+
if (
|
|
324
|
+
event.entry.kind === "compact_boundary"
|
|
325
|
+
&& active?.proactiveCompactInjection
|
|
326
|
+
&& !active.cancelRequested
|
|
327
|
+
&& deps.resolveClaudeDriverPreference() === "pty"
|
|
328
|
+
) {
|
|
329
|
+
active.hasFinalResult = true
|
|
330
|
+
await deps.store.recordTurnFinished(session.chatId)
|
|
331
|
+
await deps.store.setCompactFailureCount(session.chatId, 0)
|
|
332
|
+
// The compact prompt's seq never gets shifted (no result event), so
|
|
333
|
+
// drop it here — otherwise the next real turn's result would shift
|
|
334
|
+
// this stale seq and FIFO-mismatch, wedging that turn. Mirrors
|
|
335
|
+
// cancel()'s pending-seq drain.
|
|
336
|
+
if (active.claudePromptSeq != null) {
|
|
337
|
+
const idx = session.pendingPromptSeqs.indexOf(active.claudePromptSeq)
|
|
338
|
+
if (idx >= 0) session.pendingPromptSeqs.splice(idx, 1)
|
|
339
|
+
}
|
|
340
|
+
deps.activeTurns.delete(session.chatId)
|
|
341
|
+
deps.oauthPool?.release(session.chatId)
|
|
342
|
+
await deps.maybeStartNextQueuedMessage(session.chatId)
|
|
343
|
+
deps.emitStateChange(session.chatId)
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (event.entry.kind === "result" && active && completedClaudePromptSeq === (active.claudePromptSeq ?? null)) {
|
|
348
|
+
active.hasFinalResult = true
|
|
349
|
+
// True once a rate-limit / auth-error was routed through
|
|
350
|
+
// handleLimitDetection / handleAuthFailure. Those paths already
|
|
351
|
+
// marked the failed token limited/errored (dropping its
|
|
352
|
+
// reservation) and, when a rotation target exists, pinned the
|
|
353
|
+
// replacement token under this chatId for the scheduled
|
|
354
|
+
// auto-continue to reuse. The turn-scoped release below MUST be
|
|
355
|
+
// skipped in that case — otherwise it drops the freshly-pinned
|
|
356
|
+
// rotation token and a concurrent chat can steal it before
|
|
357
|
+
// fireAutoContinue spawns the replacement session (audit #1).
|
|
358
|
+
let failureHandled = false
|
|
359
|
+
if (event.entry.isError) {
|
|
360
|
+
const resultText = event.entry.result || "Turn failed"
|
|
361
|
+
const debugRaw = event.entry.debugRaw ?? ""
|
|
362
|
+
const detection = deps.claudeLimitDetector.detectFromResultText?.(session.chatId, resultText) ?? null
|
|
363
|
+
const authDetection = deps.claudeAuthErrorDetector.detectFromResultText(session.chatId, resultText)
|
|
364
|
+
?? deps.claudeAuthErrorDetector.detectFromResultText(session.chatId, debugRaw)
|
|
365
|
+
let handled = false
|
|
366
|
+
if (detection) {
|
|
367
|
+
handled = await deps.handleLimitDetection(session.chatId, detection)
|
|
368
|
+
} else if (authDetection) {
|
|
369
|
+
handled = await deps.handleAuthFailure(session, authDetection)
|
|
370
|
+
}
|
|
371
|
+
failureHandled = handled
|
|
372
|
+
if (handled) {
|
|
373
|
+
await deps.store.recordTurnFailed(session.chatId, detection ? "rate_limit" : "auth_error")
|
|
374
|
+
} else if (
|
|
375
|
+
isPromptTooLongMessage(resultText)
|
|
376
|
+
|| isNoConversationFoundMessage(resultText)
|
|
377
|
+
|| isNoConversationFoundMessage(debugRaw)
|
|
378
|
+
) {
|
|
379
|
+
await deps.store.recordTurnFailed(session.chatId, resultText)
|
|
380
|
+
deps.closeClaudeSession(session.chatId, session)
|
|
381
|
+
await deps.store.setSessionTokenForProvider(session.chatId, "claude", null)
|
|
382
|
+
} else {
|
|
383
|
+
await deps.store.recordTurnFailed(session.chatId, resultText)
|
|
384
|
+
}
|
|
385
|
+
if (active.proactiveCompactInjection) {
|
|
386
|
+
const prev = deps.store.getChat(session.chatId)?.compactFailureCount ?? 0
|
|
387
|
+
await deps.store.setCompactFailureCount(session.chatId, prev + 1)
|
|
388
|
+
}
|
|
389
|
+
} else if (!active.cancelRequested) {
|
|
390
|
+
await deps.store.recordTurnFinished(session.chatId)
|
|
391
|
+
if (active.proactiveCompactInjection) {
|
|
392
|
+
await deps.store.setCompactFailureCount(session.chatId, 0)
|
|
393
|
+
}
|
|
394
|
+
// Note: pending-workflow harvest wake removed — workflow-completion
|
|
395
|
+
// notification is a follow-up ADR. Model can delegate a status-check
|
|
396
|
+
// subagent if it needs event-driven workflow wake.
|
|
397
|
+
}
|
|
398
|
+
deps.activeTurns.delete(session.chatId)
|
|
399
|
+
// Turn-scoped reservation: release on turn end so other chats can
|
|
400
|
+
// claim the same token while this chat is idle. The next turn for
|
|
401
|
+
// this chat reuses the same claude session (no re-pick); the
|
|
402
|
+
// rotation race between in-flight turns is still serialized via
|
|
403
|
+
// markLimited/markError (both drop the reservation) and the
|
|
404
|
+
// atomic single-threaded pickActive(chatId) calls.
|
|
405
|
+
//
|
|
406
|
+
// Skip when a rotation handled the failure: the rotation already
|
|
407
|
+
// pinned the replacement token under this chatId and the
|
|
408
|
+
// scheduled auto-continue (TOKEN_ROTATION_SCHEDULE_DELAY_MS later)
|
|
409
|
+
// depends on that pin still being held.
|
|
410
|
+
if (!failureHandled) {
|
|
411
|
+
deps.oauthPool?.release(session.chatId)
|
|
412
|
+
}
|
|
413
|
+
if (!active.cancelRequested) {
|
|
414
|
+
await deps.maybeStartNextQueuedMessage(session.chatId)
|
|
415
|
+
}
|
|
416
|
+
} else if (event.entry.kind === "result" && event.entry.isError) {
|
|
417
|
+
// Fallback: an errored result carrying a recognizable rate-limit /
|
|
418
|
+
// auth signature must NOT be silently dropped just because its
|
|
419
|
+
// prompt-seq did not line up with the active turn. Observed as a 9h
|
|
420
|
+
// autonomous-loop stall: a synthetic 429 result on an auto-continue
|
|
421
|
+
// wake turn arrived with the pending prompt-seq queue already drained
|
|
422
|
+
// (so the seq gate above missed), and the loop died with no resume
|
|
423
|
+
// schedule — no proposal, no accept — until a human manually resumed.
|
|
424
|
+
// handleLimitDetection is idempotent (dedupes on a live schedule) and
|
|
425
|
+
// handleAuthFailure only rotates/proposes, so re-driving detection
|
|
426
|
+
// here only ever adds a missing resume, never a duplicate. The
|
|
427
|
+
// auto-resume setting is still honoured inside handleLimitDetection.
|
|
428
|
+
const resultText = event.entry.result || ""
|
|
429
|
+
const debugRaw = event.entry.debugRaw ?? ""
|
|
430
|
+
const detection = deps.claudeLimitDetector.detectFromResultText?.(session.chatId, resultText) ?? null
|
|
431
|
+
const authDetection = detection
|
|
432
|
+
? null
|
|
433
|
+
: deps.claudeAuthErrorDetector.detectFromResultText(session.chatId, resultText)
|
|
434
|
+
?? deps.claudeAuthErrorDetector.detectFromResultText(session.chatId, debugRaw)
|
|
435
|
+
if (detection) {
|
|
436
|
+
await deps.handleLimitDetection(session.chatId, detection)
|
|
437
|
+
} else if (authDetection) {
|
|
438
|
+
await deps.handleAuthFailure(session, authDetection)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
deps.emitStateChange(session.chatId)
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
const active = deps.activeTurns.get(session.chatId)
|
|
446
|
+
if (active && !active.cancelRequested) {
|
|
447
|
+
const limitHandled = await deps.handleLimitError(session.chatId, deps.claudeLimitDetector, error)
|
|
448
|
+
const authDetection = limitHandled
|
|
449
|
+
? null
|
|
450
|
+
: deps.claudeAuthErrorDetector.detect(session.chatId, error)
|
|
451
|
+
const authHandled = authDetection
|
|
452
|
+
? await deps.handleAuthFailure(session, authDetection)
|
|
453
|
+
: false
|
|
454
|
+
const handled = limitHandled || authHandled
|
|
455
|
+
if (!handled) {
|
|
456
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
457
|
+
await deps.store.appendMessage(
|
|
458
|
+
session.chatId,
|
|
459
|
+
timestamped({
|
|
460
|
+
kind: "result",
|
|
461
|
+
subtype: "error",
|
|
462
|
+
isError: true,
|
|
463
|
+
durationMs: 0,
|
|
464
|
+
result: message,
|
|
465
|
+
})
|
|
466
|
+
)
|
|
467
|
+
await deps.store.recordTurnFailed(session.chatId, message)
|
|
468
|
+
if (isPromptTooLongMessage(message) || isNoConversationFoundMessage(message)) {
|
|
469
|
+
deps.closeClaudeSession(session.chatId, session)
|
|
470
|
+
await deps.store.setSessionTokenForProvider(session.chatId, "claude", null)
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
await deps.store.recordTurnFailed(session.chatId, limitHandled ? "rate_limit" : "auth_error")
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} finally {
|
|
477
|
+
clearFirstEntryWatchdog()
|
|
478
|
+
const active = deps.activeTurns.get(session.chatId)
|
|
479
|
+
const isCurrentSession = deps.claudeSessions.get(session.chatId) === session
|
|
480
|
+
// Trace point: stream-end-without-final-result is the hang signature.
|
|
481
|
+
// If `hasActiveTurn=true` && `hasFinalResult=false` && this fires,
|
|
482
|
+
// the user will see "still running" forever unless we fail-close.
|
|
483
|
+
log.info("[kanna/agent] runClaudeSession stream ended", {
|
|
484
|
+
chatId: session.chatId,
|
|
485
|
+
sessionId: session.id,
|
|
486
|
+
sessionToken: session.sessionToken,
|
|
487
|
+
isCurrentSession,
|
|
488
|
+
hasActiveTurn: Boolean(active),
|
|
489
|
+
activeStatus: active?.status,
|
|
490
|
+
cancelRequested: active?.cancelRequested,
|
|
491
|
+
hasFinalResult: active?.hasFinalResult,
|
|
492
|
+
})
|
|
493
|
+
// Only clear chat state if it still points at us. A cancel-then-steer,
|
|
494
|
+
// or an oauth-pool rotation that closes this session and schedules an
|
|
495
|
+
// auto-continue, can install a fresh session (and activeTurn) under
|
|
496
|
+
// the same chatId before this finally runs; wiping either
|
|
497
|
+
// unconditionally would break the fresh session's bookkeeping and
|
|
498
|
+
// leave its stream running headless (no isError branch fires →
|
|
499
|
+
// sessionToken never cleared → next turn loops with the same
|
|
500
|
+
// too-large --resume context).
|
|
501
|
+
if (isCurrentSession) {
|
|
502
|
+
deps.claudeSessions.delete(session.chatId)
|
|
503
|
+
deps.oauthPool?.release(session.chatId)
|
|
504
|
+
if (active?.provider === "claude") {
|
|
505
|
+
if (active.cancelRequested && !active.cancelRecorded) {
|
|
506
|
+
await deps.store.recordTurnCancelled(session.chatId)
|
|
507
|
+
} else if (!active.hasFinalResult) {
|
|
508
|
+
// Stream ended without any terminal result event (PTY died,
|
|
509
|
+
// SDK transport dropped, etc). Fail-close the turn so the UI
|
|
510
|
+
// stops showing "running" forever. Without this the chat is
|
|
511
|
+
// wedged until the user manually clicks Stop or reloads.
|
|
512
|
+
log.warn("[kanna/agent] stream ended with no final result — recording turn failure", { chatId: session.chatId, sessionId: session.id })
|
|
513
|
+
await deps.store.recordTurnFailed(session.chatId, "session stream ended without a result")
|
|
514
|
+
}
|
|
515
|
+
deps.activeTurns.delete(session.chatId)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
session.session.close()
|
|
519
|
+
deps.emitStateChange(session.chatId)
|
|
520
|
+
}
|
|
521
|
+
}
|