@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,277 @@
|
|
|
1
|
+
import { query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
+
import {
|
|
3
|
+
createKannaMcpServer,
|
|
4
|
+
type KannaMcpDelegationContext,
|
|
5
|
+
type SetupLoopHandlerResult,
|
|
6
|
+
} from "./kanna-mcp"
|
|
7
|
+
import type { LoopSetupInput } from "./loop-template"
|
|
8
|
+
import { KANNA_MCP_SERVER_NAME } from "../shared/tools"
|
|
9
|
+
import { homedir } from "node:os"
|
|
10
|
+
import type { McpServerConfig } from "../shared/types"
|
|
11
|
+
import { KANNA_SYSTEM_PROMPT_APPEND } from "../shared/kanna-system-prompt"
|
|
12
|
+
import {
|
|
13
|
+
buildCanUseTool,
|
|
14
|
+
buildClaudeEnv,
|
|
15
|
+
LOOP_BLOCKED_NATIVE_TOOLS,
|
|
16
|
+
} from "./claude-spawn-helpers"
|
|
17
|
+
import {
|
|
18
|
+
buildUserMcpServers,
|
|
19
|
+
CLAUDE_TOOLSET,
|
|
20
|
+
SDK_RESTRICTED_FS_NATIVE_TOOLS,
|
|
21
|
+
} from "./claude-session-config"
|
|
22
|
+
import { toSdkEffort } from "./claude-prompt-helpers"
|
|
23
|
+
import { AsyncMessageQueue, toClaudeMessageStream } from "./claude-sdk-queue"
|
|
24
|
+
import { createClaudeHarnessStream } from "./claude-harness-stream"
|
|
25
|
+
import { parseConfiguredContextWindowFromModelId } from "./claude-usage-math"
|
|
26
|
+
import { log } from "../shared/log"
|
|
27
|
+
import type { ClaudeSessionHandle, HarnessToolRequest } from "./harness-types"
|
|
28
|
+
import type { SubagentOrchestrator } from "./subagent-orchestrator"
|
|
29
|
+
import type { TunnelGateway } from "./cloudflare-tunnel/gateway"
|
|
30
|
+
import type { ToolCallbackService } from "./tool-callback"
|
|
31
|
+
import type { ChatPermissionPolicy } from "../shared/permission-policy"
|
|
32
|
+
import type { OrchRunDetail, OrchRunInput } from "../shared/orchestration-types"
|
|
33
|
+
import type { ModelPrice } from "../shared/token-pricing"
|
|
34
|
+
import type { AnyValue } from "../shared/errors"
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Injectable collaborators for startClaudeSession.
|
|
38
|
+
*
|
|
39
|
+
* Kept as a second parameter (with a production default via
|
|
40
|
+
* buildStartClaudeSessionDeps) so tests can supply fakes for the modules that
|
|
41
|
+
* would otherwise pollute Bun's global module registry via mock.module().
|
|
42
|
+
*/
|
|
43
|
+
export type StartClaudeSessionDeps = {
|
|
44
|
+
readonly buildCanUseTool: typeof buildCanUseTool
|
|
45
|
+
readonly buildClaudeEnv: typeof buildClaudeEnv
|
|
46
|
+
readonly loopBlockedNativeTools: readonly string[]
|
|
47
|
+
/** Structural constructor type so both AsyncMessageQueue and test fakes satisfy it. */
|
|
48
|
+
readonly AsyncMessageQueueCtor: { new <T>(): { push(value: T): void; close(): void } & AsyncIterable<T> }
|
|
49
|
+
readonly toClaudeMessageStream: typeof toClaudeMessageStream
|
|
50
|
+
readonly createClaudeHarnessStream: typeof createClaudeHarnessStream
|
|
51
|
+
readonly parseConfiguredContextWindowFromModelId: typeof parseConfiguredContextWindowFromModelId
|
|
52
|
+
readonly buildUserMcpServers: typeof buildUserMcpServers
|
|
53
|
+
readonly claudeToolset: readonly string[]
|
|
54
|
+
readonly sdkRestrictedFsNativeTools: readonly string[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns the real (production) dep implementations. */
|
|
58
|
+
export function buildStartClaudeSessionDeps(): StartClaudeSessionDeps {
|
|
59
|
+
return {
|
|
60
|
+
buildCanUseTool,
|
|
61
|
+
buildClaudeEnv,
|
|
62
|
+
loopBlockedNativeTools: LOOP_BLOCKED_NATIVE_TOOLS,
|
|
63
|
+
AsyncMessageQueueCtor: AsyncMessageQueue,
|
|
64
|
+
toClaudeMessageStream,
|
|
65
|
+
createClaudeHarnessStream,
|
|
66
|
+
parseConfiguredContextWindowFromModelId,
|
|
67
|
+
buildUserMcpServers,
|
|
68
|
+
claudeToolset: CLAUDE_TOOLSET,
|
|
69
|
+
sdkRestrictedFsNativeTools: SDK_RESTRICTED_FS_NATIVE_TOOLS,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function startClaudeSession(args: {
|
|
74
|
+
projectId: string
|
|
75
|
+
localPath: string
|
|
76
|
+
model: string
|
|
77
|
+
effort?: string
|
|
78
|
+
planMode: boolean
|
|
79
|
+
sessionToken: string | null
|
|
80
|
+
forkSession: boolean
|
|
81
|
+
oauthToken: string | null
|
|
82
|
+
/** When set, redirect the SDK to OpenRouter instead of Anthropic. */
|
|
83
|
+
openrouterApiKey?: string | null
|
|
84
|
+
additionalDirectories?: string[]
|
|
85
|
+
chatId?: string
|
|
86
|
+
tunnelGateway?: TunnelGateway | null
|
|
87
|
+
onToolRequest: (request: HarnessToolRequest) => Promise<AnyValue>
|
|
88
|
+
systemPromptAppend?: string
|
|
89
|
+
systemPromptOverride?: string
|
|
90
|
+
initialPrompt?: string
|
|
91
|
+
/** Routes AskUserQuestion/ExitPlanMode through tool-callback when KANNA_MCP_TOOL_CALLBACKS=1. */
|
|
92
|
+
toolCallback?: ToolCallbackService
|
|
93
|
+
/** Per-chat permission policy. Defaults to POLICY_DEFAULT if omitted. */
|
|
94
|
+
chatPolicy?: ChatPermissionPolicy
|
|
95
|
+
/** Orchestrator for delegate_subagent. Omit to hide the tool. */
|
|
96
|
+
subagentOrchestrator?: SubagentOrchestrator
|
|
97
|
+
/** Per-spawn delegation context (depth / ancestor chain / parentUserMessageId resolver). */
|
|
98
|
+
delegationContext?: KannaMcpDelegationContext
|
|
99
|
+
/** Enabled user MCP servers, merged into the SDK's mcpServers map. */
|
|
100
|
+
customMcpServers?: readonly McpServerConfig[]
|
|
101
|
+
/** Pre-resolved oauth bearer tokens keyed by server id (from ensureFreshMcpToken). */
|
|
102
|
+
oauthBearers?: ReadonlyMap<string, string>
|
|
103
|
+
/** Folder-restricted subagent: disallow native FS tools, allowlist mcp__kanna__*, per-run path-deny. */
|
|
104
|
+
restrictedAllowedPaths?: string[]
|
|
105
|
+
/** Backs the `setup_loop` MCP tool. Omit to hide the tool. */
|
|
106
|
+
setupLoop?: (input: LoopSetupInput) => Promise<SetupLoopHandlerResult>
|
|
107
|
+
/** Backs the `stop_loop` MCP tool. Omit to hide the tool. */
|
|
108
|
+
stopLoop?: () => Promise<void>
|
|
109
|
+
/** Live check: true while an autonomous loop is armed — blocks direct-edit native tools. */
|
|
110
|
+
isLoopArmed?: () => boolean
|
|
111
|
+
/** Backs the `orch_run` / `orch_run_status` / `orch_cancel_run` MCP tools. Main-chat only. */
|
|
112
|
+
runOrch?: (input: OrchRunInput) => Promise<{ ok: true; runId: string } | { ok: false; errors: string[] }>
|
|
113
|
+
cancelOrchRun?: (runId: string) => Promise<void>
|
|
114
|
+
getOrchRunStatus?: (runId: string) => OrchRunDetail | null
|
|
115
|
+
/**
|
|
116
|
+
* Agentic-turn bound passed natively to the SDK query() (Claude Code's
|
|
117
|
+
* per-agent frontmatter maxTurns analog): the SDK stops gracefully and
|
|
118
|
+
* keeps the accumulated output. Used by subagent spawns.
|
|
119
|
+
*/
|
|
120
|
+
maxTurns?: number
|
|
121
|
+
/** When true, leave the prompt queue open after initialPrompt and expose pushChannelPrompt on the handle. */
|
|
122
|
+
keepAlive?: boolean
|
|
123
|
+
/** Per-turn price for computing cost when the provider doesn't report it (OpenRouter). */
|
|
124
|
+
turnPrice?: ModelPrice | null
|
|
125
|
+
/** Overrides the configured context window (OpenRouter model contextLength). */
|
|
126
|
+
contextWindowOverride?: number
|
|
127
|
+
},
|
|
128
|
+
_deps: StartClaudeSessionDeps = buildStartClaudeSessionDeps(),
|
|
129
|
+
): Promise<ClaudeSessionHandle> {
|
|
130
|
+
const canUseTool = _deps.buildCanUseTool({
|
|
131
|
+
localPath: args.localPath,
|
|
132
|
+
chatId: args.chatId,
|
|
133
|
+
sessionToken: args.sessionToken,
|
|
134
|
+
onToolRequest: args.onToolRequest,
|
|
135
|
+
toolCallback: args.toolCallback,
|
|
136
|
+
chatPolicy: args.chatPolicy,
|
|
137
|
+
isLoopArmed: args.isLoopArmed,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const promptQueue = new _deps.AsyncMessageQueueCtor<SDKUserMessage>()
|
|
141
|
+
|
|
142
|
+
const q = query({
|
|
143
|
+
prompt: promptQueue,
|
|
144
|
+
options: {
|
|
145
|
+
cwd: args.localPath,
|
|
146
|
+
...(args.additionalDirectories && args.additionalDirectories.length > 0
|
|
147
|
+
? { additionalDirectories: args.additionalDirectories }
|
|
148
|
+
: {}),
|
|
149
|
+
model: args.model,
|
|
150
|
+
effort: toSdkEffort(args.effort),
|
|
151
|
+
resume: args.sessionToken ?? undefined,
|
|
152
|
+
forkSession: args.forkSession,
|
|
153
|
+
permissionMode: args.planMode ? "plan" : "acceptEdits",
|
|
154
|
+
canUseTool,
|
|
155
|
+
// Filter-at-spawn (Claude Code's filterToolsForAgent pattern): while a
|
|
156
|
+
// loop is armed the direct-edit tools are removed from the tool list the
|
|
157
|
+
// model sees, so it cannot even attempt them. The dynamic canUseTool
|
|
158
|
+
// deny stays as belt-and-suspenders; an armed-state flip respawns the
|
|
159
|
+
// session (see loopArmedAtSpawn in startClaudeTurn).
|
|
160
|
+
...(args.isLoopArmed?.() ? { disallowedTools: [..._deps.loopBlockedNativeTools] } : {}),
|
|
161
|
+
// Per-agent turn bound, threaded from Subagent.maxTurns. The SDK emits
|
|
162
|
+
// a graceful stop at the limit — accumulated output is preserved,
|
|
163
|
+
// matching Claude Code's max_turns_reached semantics.
|
|
164
|
+
...(args.maxTurns !== undefined ? { maxTurns: args.maxTurns } : {}),
|
|
165
|
+
tools: args.restrictedAllowedPaths && args.restrictedAllowedPaths.length > 0
|
|
166
|
+
? _deps.claudeToolset.filter((t) => !new Set<string>(_deps.sdkRestrictedFsNativeTools).has(t))
|
|
167
|
+
: [..._deps.claudeToolset],
|
|
168
|
+
mcpServers: {
|
|
169
|
+
[KANNA_MCP_SERVER_NAME]: createKannaMcpServer({
|
|
170
|
+
projectId: args.projectId,
|
|
171
|
+
localPath: args.localPath,
|
|
172
|
+
chatId: args.chatId,
|
|
173
|
+
sessionId: args.sessionToken ?? undefined,
|
|
174
|
+
tunnelGateway: args.tunnelGateway ?? null,
|
|
175
|
+
toolCallback: args.toolCallback,
|
|
176
|
+
chatPolicy: args.chatPolicy,
|
|
177
|
+
subagentOrchestrator: args.subagentOrchestrator,
|
|
178
|
+
delegationContext: args.delegationContext,
|
|
179
|
+
restrictedAllowedPaths: args.restrictedAllowedPaths,
|
|
180
|
+
setupLoop: args.setupLoop,
|
|
181
|
+
stopLoop: args.stopLoop,
|
|
182
|
+
runOrch: args.runOrch,
|
|
183
|
+
cancelOrchRun: args.cancelOrchRun,
|
|
184
|
+
getOrchRunStatus: args.getOrchRunStatus,
|
|
185
|
+
}),
|
|
186
|
+
..._deps.buildUserMcpServers(args.customMcpServers ?? [], args.oauthBearers),
|
|
187
|
+
},
|
|
188
|
+
systemPrompt: args.systemPromptOverride != null
|
|
189
|
+
? args.systemPromptOverride
|
|
190
|
+
: {
|
|
191
|
+
type: "preset",
|
|
192
|
+
preset: "claude_code",
|
|
193
|
+
append: args.systemPromptAppend ?? KANNA_SYSTEM_PROMPT_APPEND,
|
|
194
|
+
},
|
|
195
|
+
settingSources: ["user", "project", "local"],
|
|
196
|
+
pathToClaudeCodeExecutable: process.env.CLAUDE_EXECUTABLE?.replace(/^~(?=\/|$)/, homedir()) || undefined,
|
|
197
|
+
env: _deps.buildClaudeEnv(process.env, args.oauthToken, args.openrouterApiKey ? { apiKey: args.openrouterApiKey } : null),
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Follow-up turns (sendPrompt + keep-alive pushChannelPrompt) share one
|
|
202
|
+
// queue-push policy so the two transports cannot drift apart.
|
|
203
|
+
const enqueueUserPrompt = (content: string) => {
|
|
204
|
+
promptQueue.push({
|
|
205
|
+
type: "user",
|
|
206
|
+
message: { role: "user", content },
|
|
207
|
+
parent_tool_use_id: null,
|
|
208
|
+
session_id: args.sessionToken ?? "",
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (args.initialPrompt != null) {
|
|
213
|
+
promptQueue.push({
|
|
214
|
+
type: "user",
|
|
215
|
+
message: {
|
|
216
|
+
role: "user",
|
|
217
|
+
content: args.initialPrompt,
|
|
218
|
+
},
|
|
219
|
+
parent_tool_use_id: null,
|
|
220
|
+
session_id: args.sessionToken ?? undefined,
|
|
221
|
+
})
|
|
222
|
+
if (!args.keepAlive) {
|
|
223
|
+
promptQueue.close()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
provider: "claude",
|
|
229
|
+
stream: _deps.createClaudeHarnessStream(
|
|
230
|
+
_deps.toClaudeMessageStream(q),
|
|
231
|
+
args.contextWindowOverride ?? _deps.parseConfiguredContextWindowFromModelId(args.model),
|
|
232
|
+
args.turnPrice ? () => args.turnPrice ?? null : undefined,
|
|
233
|
+
),
|
|
234
|
+
getAccountInfo: async () => {
|
|
235
|
+
try {
|
|
236
|
+
return await q.accountInfo()
|
|
237
|
+
} catch {
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
interrupt: async () => {
|
|
242
|
+
await q.interrupt()
|
|
243
|
+
},
|
|
244
|
+
sendPrompt: async (content: string) => {
|
|
245
|
+
enqueueUserPrompt(content)
|
|
246
|
+
},
|
|
247
|
+
setModel: async (model: string) => {
|
|
248
|
+
await q.setModel(model)
|
|
249
|
+
},
|
|
250
|
+
setPermissionMode: async (planMode: boolean) => {
|
|
251
|
+
await q.setPermissionMode(planMode ? "plan" : "acceptEdits")
|
|
252
|
+
},
|
|
253
|
+
getSupportedCommands: async () => {
|
|
254
|
+
try {
|
|
255
|
+
return await q.supportedCommands()
|
|
256
|
+
} catch (error) {
|
|
257
|
+
log.warn("[kanna/claude] supportedCommands failed", String(error))
|
|
258
|
+
return []
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
...(args.keepAlive ? {
|
|
262
|
+
pushChannelPrompt: async (content: string) => {
|
|
263
|
+
enqueueUserPrompt(content)
|
|
264
|
+
},
|
|
265
|
+
} : {}),
|
|
266
|
+
close: () => {
|
|
267
|
+
promptQueue.close()
|
|
268
|
+
q.close()
|
|
269
|
+
// Do NOT cancel pending tool-callback records here. close() also fires
|
|
270
|
+
// on token rotation and idle-session sweep — both of which preserve
|
|
271
|
+
// the model's logical turn (it will resume / re-emit). Denying
|
|
272
|
+
// mid-turn used to mask the question prompt as a silent drop. Pending
|
|
273
|
+
// records are now reaped by the explicit chat.cancel / chat.delete
|
|
274
|
+
// paths in ws-router.ts and by recoverOnStartup on server boot.
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, it, expect, mock } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
getActiveStatuses,
|
|
4
|
+
getWaitStartedAtByChatId,
|
|
5
|
+
getPendingTool,
|
|
6
|
+
getDrainingChatIds,
|
|
7
|
+
getSlashCommandsLoadingChatIds,
|
|
8
|
+
getClaudeSessionStates,
|
|
9
|
+
isClaudeSessionIdle,
|
|
10
|
+
sweepIdleClaudeSessions,
|
|
11
|
+
type SessionStateQueryDeps,
|
|
12
|
+
} from "./claude-session-state-queries"
|
|
13
|
+
import type { ActiveTurn, ClaudeSessionState, PendingToolRequest } from "./claude-session-state"
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
function makeSession(overrides?: Partial<ClaudeSessionState>): ClaudeSessionState {
|
|
20
|
+
return {
|
|
21
|
+
id: "sess-1",
|
|
22
|
+
chatId: "chat-1",
|
|
23
|
+
session: {} as ClaudeSessionState["session"],
|
|
24
|
+
localPath: "/tmp/test",
|
|
25
|
+
additionalDirectories: [],
|
|
26
|
+
model: "claude-opus-4-5",
|
|
27
|
+
planMode: false,
|
|
28
|
+
sessionToken: null,
|
|
29
|
+
accountInfoLoaded: false,
|
|
30
|
+
nextPromptSeq: 0,
|
|
31
|
+
pendingPromptSeqs: [],
|
|
32
|
+
activeTokenId: null,
|
|
33
|
+
oauthKeyMasked: null,
|
|
34
|
+
oauthLabel: null,
|
|
35
|
+
openrouterKeyMasked: null,
|
|
36
|
+
openrouterModel: null,
|
|
37
|
+
lastUsedAt: Date.now(),
|
|
38
|
+
backgroundTaskIds: new Set(),
|
|
39
|
+
backgroundTaskDeadlineAt: 0,
|
|
40
|
+
loopArmedAtSpawn: false,
|
|
41
|
+
...overrides,
|
|
42
|
+
} as ClaudeSessionState
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeActiveTurn(overrides?: Partial<ActiveTurn>): ActiveTurn {
|
|
46
|
+
return {
|
|
47
|
+
chatId: "chat-1",
|
|
48
|
+
provider: "claude",
|
|
49
|
+
turn: {} as ActiveTurn["turn"],
|
|
50
|
+
model: "claude-opus-4-5",
|
|
51
|
+
planMode: false,
|
|
52
|
+
status: "running",
|
|
53
|
+
pendingTool: null,
|
|
54
|
+
postToolFollowUp: null,
|
|
55
|
+
hasFinalResult: false,
|
|
56
|
+
cancelRequested: false,
|
|
57
|
+
cancelRecorded: false,
|
|
58
|
+
waitStartedAt: null,
|
|
59
|
+
userMessageId: null,
|
|
60
|
+
...overrides,
|
|
61
|
+
} as ActiveTurn
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeDeps(overrides?: Partial<SessionStateQueryDeps>): SessionStateQueryDeps {
|
|
65
|
+
return {
|
|
66
|
+
activeTurns: new Map(),
|
|
67
|
+
claudeSessions: new Map(),
|
|
68
|
+
drainingStreams: new Map(),
|
|
69
|
+
slashCommandsInFlight: new Set(),
|
|
70
|
+
isClaudeSdkProvider: mock(() => false),
|
|
71
|
+
hasPendingBackgroundTask: mock(() => false),
|
|
72
|
+
resolveClaudeIdleMs: mock(() => 600_000),
|
|
73
|
+
hasLiveWorkflow: mock(() => false),
|
|
74
|
+
closeClaudeSession: mock(() => undefined),
|
|
75
|
+
emitStateChange: mock(() => undefined),
|
|
76
|
+
...overrides,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// getActiveStatuses
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
describe("getActiveStatuses", () => {
|
|
85
|
+
it("returns empty map when no active turns", () => {
|
|
86
|
+
const deps = makeDeps()
|
|
87
|
+
expect(getActiveStatuses(deps).size).toBe(0)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("maps chatId → status for each active turn", () => {
|
|
91
|
+
const deps = makeDeps({
|
|
92
|
+
activeTurns: new Map([
|
|
93
|
+
["chat-1", makeActiveTurn({ chatId: "chat-1", status: "running" })],
|
|
94
|
+
["chat-2", makeActiveTurn({ chatId: "chat-2", status: "waiting_for_user" })],
|
|
95
|
+
]),
|
|
96
|
+
})
|
|
97
|
+
const result = getActiveStatuses(deps)
|
|
98
|
+
expect(result.get("chat-1")).toBe("running")
|
|
99
|
+
expect(result.get("chat-2")).toBe("waiting_for_user")
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// getWaitStartedAtByChatId
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
describe("getWaitStartedAtByChatId", () => {
|
|
108
|
+
it("returns empty map when no turns are waiting", () => {
|
|
109
|
+
const deps = makeDeps({
|
|
110
|
+
activeTurns: new Map([["chat-1", makeActiveTurn({ waitStartedAt: null })]]),
|
|
111
|
+
})
|
|
112
|
+
expect(getWaitStartedAtByChatId(deps).size).toBe(0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("returns waitStartedAt only for turns with non-null value", () => {
|
|
116
|
+
const ts = 12345
|
|
117
|
+
const deps = makeDeps({
|
|
118
|
+
activeTurns: new Map([
|
|
119
|
+
["chat-1", makeActiveTurn({ waitStartedAt: ts })],
|
|
120
|
+
["chat-2", makeActiveTurn({ waitStartedAt: null })],
|
|
121
|
+
]),
|
|
122
|
+
})
|
|
123
|
+
const result = getWaitStartedAtByChatId(deps)
|
|
124
|
+
expect(result.get("chat-1")).toBe(ts)
|
|
125
|
+
expect(result.has("chat-2")).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// getPendingTool
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
describe("getPendingTool", () => {
|
|
134
|
+
it("returns null when chatId has no active turn", () => {
|
|
135
|
+
const deps = makeDeps()
|
|
136
|
+
expect(getPendingTool(deps, "chat-x")).toBeNull()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("returns null when active turn has no pending tool", () => {
|
|
140
|
+
const deps = makeDeps({
|
|
141
|
+
activeTurns: new Map([["chat-1", makeActiveTurn({ pendingTool: null })]]),
|
|
142
|
+
})
|
|
143
|
+
expect(getPendingTool(deps, "chat-1")).toBeNull()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("returns PendingToolSnapshot when pending tool is present", () => {
|
|
147
|
+
const deps = makeDeps({
|
|
148
|
+
activeTurns: new Map([
|
|
149
|
+
[
|
|
150
|
+
"chat-1",
|
|
151
|
+
makeActiveTurn({
|
|
152
|
+
pendingTool: {
|
|
153
|
+
toolUseId: "tool-123",
|
|
154
|
+
tool: { toolKind: "ask_user_question" } as PendingToolRequest["tool"],
|
|
155
|
+
resolve: () => undefined,
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
]),
|
|
160
|
+
})
|
|
161
|
+
const result = getPendingTool(deps, "chat-1")
|
|
162
|
+
expect(result).toEqual({ toolUseId: "tool-123", toolKind: "ask_user_question" })
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// getDrainingChatIds
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
describe("getDrainingChatIds", () => {
|
|
171
|
+
it("returns empty set when no draining streams", () => {
|
|
172
|
+
const deps = makeDeps()
|
|
173
|
+
expect(getDrainingChatIds(deps).size).toBe(0)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("returns chatIds of draining streams", () => {
|
|
177
|
+
const draining = new Map<string, unknown>([["chat-a", {}], ["chat-b", {}]])
|
|
178
|
+
const deps = makeDeps({ drainingStreams: draining as SessionStateQueryDeps["drainingStreams"] })
|
|
179
|
+
const result = getDrainingChatIds(deps)
|
|
180
|
+
expect(result.has("chat-a")).toBe(true)
|
|
181
|
+
expect(result.has("chat-b")).toBe(true)
|
|
182
|
+
expect(result.size).toBe(2)
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// getSlashCommandsLoadingChatIds
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
describe("getSlashCommandsLoadingChatIds", () => {
|
|
191
|
+
it("returns empty set when no slash commands in flight", () => {
|
|
192
|
+
const deps = makeDeps()
|
|
193
|
+
expect(getSlashCommandsLoadingChatIds(deps).size).toBe(0)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("mirrors the slashCommandsInFlight set", () => {
|
|
197
|
+
const deps = makeDeps({ slashCommandsInFlight: new Set(["chat-1", "chat-2"]) })
|
|
198
|
+
const result = getSlashCommandsLoadingChatIds(deps)
|
|
199
|
+
expect(result.has("chat-1")).toBe(true)
|
|
200
|
+
expect(result.has("chat-2")).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// getClaudeSessionStates
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
describe("getClaudeSessionStates", () => {
|
|
209
|
+
it("returns empty map when no sessions", () => {
|
|
210
|
+
const deps = makeDeps()
|
|
211
|
+
expect(getClaudeSessionStates(deps).size).toBe(0)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("returns 'active' for SDK providers", () => {
|
|
215
|
+
const session = makeSession({ chatId: "chat-1" })
|
|
216
|
+
const deps = makeDeps({
|
|
217
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
218
|
+
activeTurns: new Map([["chat-1", makeActiveTurn({ provider: "claude" })]]),
|
|
219
|
+
isClaudeSdkProvider: () => true,
|
|
220
|
+
})
|
|
221
|
+
expect(getClaudeSessionStates(deps).get("chat-1")).toBe("active")
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("returns 'warming' when background task pending", () => {
|
|
225
|
+
const session = makeSession({ chatId: "chat-1", lastUsedAt: 0 })
|
|
226
|
+
const deps = makeDeps({
|
|
227
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
228
|
+
hasPendingBackgroundTask: () => true,
|
|
229
|
+
resolveClaudeIdleMs: () => 100,
|
|
230
|
+
})
|
|
231
|
+
expect(getClaudeSessionStates(deps).get("chat-1")).toBe("warming")
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("returns 'idle' when idle timeout elapsed and no activity", () => {
|
|
235
|
+
const session = makeSession({ chatId: "chat-1", lastUsedAt: 0 })
|
|
236
|
+
const deps = makeDeps({
|
|
237
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
238
|
+
resolveClaudeIdleMs: () => 1, // 1 ms — already elapsed
|
|
239
|
+
})
|
|
240
|
+
expect(getClaudeSessionStates(deps).get("chat-1")).toBe("idle")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("returns 'warming' when recently used and no activity", () => {
|
|
244
|
+
const session = makeSession({ chatId: "chat-1", lastUsedAt: Date.now() })
|
|
245
|
+
const deps = makeDeps({
|
|
246
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
247
|
+
resolveClaudeIdleMs: () => 600_000,
|
|
248
|
+
})
|
|
249
|
+
expect(getClaudeSessionStates(deps).get("chat-1")).toBe("warming")
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// isClaudeSessionIdle
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
describe("isClaudeSessionIdle", () => {
|
|
258
|
+
it("returns false when provider is an SDK provider", () => {
|
|
259
|
+
const session = makeSession({ lastUsedAt: 0 })
|
|
260
|
+
const deps = makeDeps({
|
|
261
|
+
activeTurns: new Map([["chat-1", makeActiveTurn({ provider: "claude" })]]),
|
|
262
|
+
isClaudeSdkProvider: () => true,
|
|
263
|
+
resolveClaudeIdleMs: () => 1,
|
|
264
|
+
})
|
|
265
|
+
expect(isClaudeSessionIdle(deps, "chat-1", session, Date.now())).toBe(false)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("returns false when session has pending prompt seqs", () => {
|
|
269
|
+
const session = makeSession({ lastUsedAt: 0, pendingPromptSeqs: [1] })
|
|
270
|
+
const deps = makeDeps({ resolveClaudeIdleMs: () => 1 })
|
|
271
|
+
expect(isClaudeSessionIdle(deps, "chat-1", session, Date.now())).toBe(false)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it("returns false when chat has a live workflow", () => {
|
|
275
|
+
const session = makeSession({ lastUsedAt: 0 })
|
|
276
|
+
const deps = makeDeps({
|
|
277
|
+
hasLiveWorkflow: () => true,
|
|
278
|
+
resolveClaudeIdleMs: () => 1,
|
|
279
|
+
})
|
|
280
|
+
expect(isClaudeSessionIdle(deps, "chat-1", session, Date.now())).toBe(false)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it("returns false when background task is pending", () => {
|
|
284
|
+
const session = makeSession({ lastUsedAt: 0 })
|
|
285
|
+
const deps = makeDeps({
|
|
286
|
+
hasPendingBackgroundTask: () => true,
|
|
287
|
+
resolveClaudeIdleMs: () => 1,
|
|
288
|
+
})
|
|
289
|
+
expect(isClaudeSessionIdle(deps, "chat-1", session, Date.now())).toBe(false)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it("returns false when idle timeout has not elapsed", () => {
|
|
293
|
+
const session = makeSession({ lastUsedAt: Date.now() })
|
|
294
|
+
const deps = makeDeps({ resolveClaudeIdleMs: () => 600_000 })
|
|
295
|
+
expect(isClaudeSessionIdle(deps, "chat-1", session, Date.now())).toBe(false)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it("returns true when all idle conditions met", () => {
|
|
299
|
+
const session = makeSession({ lastUsedAt: 0, pendingPromptSeqs: [] })
|
|
300
|
+
const deps = makeDeps({ resolveClaudeIdleMs: () => 1 })
|
|
301
|
+
expect(isClaudeSessionIdle(deps, "chat-1", session, Date.now())).toBe(true)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// sweepIdleClaudeSessions
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
describe("sweepIdleClaudeSessions", () => {
|
|
310
|
+
it("does nothing when no sessions", () => {
|
|
311
|
+
const deps = makeDeps()
|
|
312
|
+
sweepIdleClaudeSessions(deps, Date.now())
|
|
313
|
+
expect((deps.closeClaudeSession as ReturnType<typeof mock>).mock.calls.length).toBe(0)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it("closes and emits state change for idle sessions", () => {
|
|
317
|
+
const session = makeSession({ chatId: "chat-1", lastUsedAt: 0, pendingPromptSeqs: [] })
|
|
318
|
+
const closeFn = mock<(chatId: string, session: ClaudeSessionState) => void>(() => undefined)
|
|
319
|
+
const emitFn = mock<(chatId: string) => void>(() => undefined)
|
|
320
|
+
const deps = makeDeps({
|
|
321
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
322
|
+
resolveClaudeIdleMs: () => 1,
|
|
323
|
+
closeClaudeSession: closeFn,
|
|
324
|
+
emitStateChange: emitFn,
|
|
325
|
+
})
|
|
326
|
+
sweepIdleClaudeSessions(deps, Date.now())
|
|
327
|
+
expect(closeFn.mock.calls.length).toBe(1)
|
|
328
|
+
expect(closeFn.mock.calls[0]?.[0]).toBe("chat-1")
|
|
329
|
+
expect(emitFn.mock.calls.length).toBe(1)
|
|
330
|
+
expect(emitFn.mock.calls[0]?.[0]).toBe("chat-1")
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it("does not close non-idle sessions", () => {
|
|
334
|
+
const session = makeSession({ chatId: "chat-1", lastUsedAt: Date.now(), pendingPromptSeqs: [] })
|
|
335
|
+
const closeFn = mock(() => undefined)
|
|
336
|
+
const deps = makeDeps({
|
|
337
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
338
|
+
resolveClaudeIdleMs: () => 600_000,
|
|
339
|
+
closeClaudeSession: closeFn,
|
|
340
|
+
})
|
|
341
|
+
sweepIdleClaudeSessions(deps, Date.now())
|
|
342
|
+
expect(closeFn.mock.calls.length).toBe(0)
|
|
343
|
+
})
|
|
344
|
+
})
|