@cuongtran001/kanna 1.3.0 → 1.4.0
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-BHj370W7.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
- package/dist/client/assets/channel-BP9IMEvd.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
- package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.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.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/chat-op-log.test.ts +63 -0
- package/src/server/chat-op-log.ts +71 -0
- package/src/server/chat-ops-diff.test.ts +120 -0
- package/src/server/chat-ops-diff.ts +97 -0
- package/src/server/chat-ops-parity.test.ts +140 -0
- 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 +532 -0
- package/src/server/event-store-messages.adapter.ts +455 -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 +291 -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 +75 -0
- package/src/server/event-store.ts +257 -2285
- package/src/server/events.ts +1 -1
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/storage/backend.ts +7 -0
- package/src/server/storage/fs-storage.adapter.ts +21 -1
- package/src/server/storage/in-memory-storage.ts +12 -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 +560 -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 +360 -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 +238 -0
- package/src/server/ws-router.test.ts +90 -0
- package/src/server/ws-router.ts +340 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/chat-ops.test.ts +138 -0
- package/src/shared/chat-ops.ts +71 -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/protocol.ts +3 -2
- 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 +31 -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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone session-state query helpers and idle-reaper for AgentCoordinator.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from agent.ts so the query logic lives in its own testable module.
|
|
5
|
+
* The coordinator delegates to these functions by passing an object literal
|
|
6
|
+
* that satisfies `SessionStateQueryDeps`.
|
|
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, KannaStatus, PendingToolSnapshot } from "../shared/types"
|
|
14
|
+
import type { ActiveTurn, ClaudeSessionState } from "./claude-session-state"
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Deps interface
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface SessionStateQueryDeps {
|
|
21
|
+
/** Live turn state keyed by chatId. */
|
|
22
|
+
activeTurns: Map<string, ActiveTurn>
|
|
23
|
+
/** Live Claude session state keyed by chatId. */
|
|
24
|
+
claudeSessions: Map<string, ClaudeSessionState>
|
|
25
|
+
/** Streams currently draining (only `.keys()` is consumed). */
|
|
26
|
+
drainingStreams: { keys(): IterableIterator<string> }
|
|
27
|
+
/** chatIds with a slash-commands load in-flight. */
|
|
28
|
+
slashCommandsInFlight: ReadonlySet<string>
|
|
29
|
+
/** Returns true when the given provider is a Claude SDK provider. */
|
|
30
|
+
isClaudeSdkProvider: (provider: AgentProvider) => boolean
|
|
31
|
+
/** Returns true when the session has a pending background Bash task. */
|
|
32
|
+
hasPendingBackgroundTask: (session: ClaudeSessionState, now: number) => boolean
|
|
33
|
+
/** Resolves the effective idle timeout in milliseconds. */
|
|
34
|
+
resolveClaudeIdleMs: () => number
|
|
35
|
+
/** Returns true when the chat has an in-flight Workflow. */
|
|
36
|
+
hasLiveWorkflow: (chatId: string) => boolean
|
|
37
|
+
/** Tears down and cleans up a Claude session. */
|
|
38
|
+
closeClaudeSession: (chatId: string, session: ClaudeSessionState) => void
|
|
39
|
+
/** Notifies subscribers that state has changed for the given chat. */
|
|
40
|
+
emitStateChange: (chatId: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Public query functions
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a map of chatId → KannaStatus for all currently active turns.
|
|
49
|
+
*/
|
|
50
|
+
export function getActiveStatuses(deps: SessionStateQueryDeps): Map<string, KannaStatus> {
|
|
51
|
+
const statuses = new Map<string, KannaStatus>()
|
|
52
|
+
for (const [chatId, turn] of deps.activeTurns.entries()) {
|
|
53
|
+
statuses.set(chatId, turn.status)
|
|
54
|
+
}
|
|
55
|
+
return statuses
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns a map of chatId → waitStartedAt for turns that are currently
|
|
60
|
+
* waiting (i.e. have a non-null waitStartedAt).
|
|
61
|
+
*/
|
|
62
|
+
export function getWaitStartedAtByChatId(deps: SessionStateQueryDeps): Map<string, number> {
|
|
63
|
+
const out = new Map<string, number>()
|
|
64
|
+
for (const [chatId, turn] of deps.activeTurns.entries()) {
|
|
65
|
+
if (turn.waitStartedAt != null) out.set(chatId, turn.waitStartedAt)
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the pending tool snapshot for the active turn in the given chat,
|
|
72
|
+
* or null if there is no active turn or no pending tool.
|
|
73
|
+
*/
|
|
74
|
+
export function getPendingTool(
|
|
75
|
+
deps: SessionStateQueryDeps,
|
|
76
|
+
chatId: string,
|
|
77
|
+
): PendingToolSnapshot | null {
|
|
78
|
+
const pending = deps.activeTurns.get(chatId)?.pendingTool
|
|
79
|
+
if (!pending) return null
|
|
80
|
+
return { toolUseId: pending.toolUseId, toolKind: pending.tool.toolKind }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the set of chatIds whose streams are currently draining.
|
|
85
|
+
*/
|
|
86
|
+
export function getDrainingChatIds(deps: SessionStateQueryDeps): Set<string> {
|
|
87
|
+
return new Set(deps.drainingStreams.keys())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the set of chatIds that have a slash-commands load in-flight.
|
|
92
|
+
*/
|
|
93
|
+
export function getSlashCommandsLoadingChatIds(deps: SessionStateQueryDeps): Set<string> {
|
|
94
|
+
return new Set(deps.slashCommandsInFlight)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Snapshot of live Claude session states per chat. Used by the sidebar badge
|
|
99
|
+
* selector. Chats not present in the returned map are implicitly `cold`.
|
|
100
|
+
*/
|
|
101
|
+
export function getClaudeSessionStates(
|
|
102
|
+
deps: SessionStateQueryDeps,
|
|
103
|
+
): Map<string, "warming" | "active" | "idle"> {
|
|
104
|
+
const out = new Map<string, "warming" | "active" | "idle">()
|
|
105
|
+
const now = Date.now()
|
|
106
|
+
for (const [chatId, session] of deps.claudeSessions) {
|
|
107
|
+
const activeProv = deps.activeTurns.get(chatId)?.provider
|
|
108
|
+
if (activeProv !== undefined && deps.isClaudeSdkProvider(activeProv)) {
|
|
109
|
+
out.set(chatId, "active")
|
|
110
|
+
} else if (deps.hasPendingBackgroundTask(session, now)) {
|
|
111
|
+
// Held warm for a background Bash task — surface as "warming", not "idle".
|
|
112
|
+
out.set(chatId, "warming")
|
|
113
|
+
} else if (now - session.lastUsedAt >= deps.resolveClaudeIdleMs()) {
|
|
114
|
+
out.set(chatId, "idle")
|
|
115
|
+
} else {
|
|
116
|
+
out.set(chatId, "warming")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Idle-reaper helpers (private semantics preserved as package functions)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns true when the given Claude session has no live activity and the
|
|
128
|
+
* idle timeout has elapsed since it was last used.
|
|
129
|
+
*
|
|
130
|
+
* Mirrors the private `isClaudeSessionIdle` on AgentCoordinator.
|
|
131
|
+
*/
|
|
132
|
+
export function isClaudeSessionIdle(
|
|
133
|
+
deps: SessionStateQueryDeps,
|
|
134
|
+
chatId: string,
|
|
135
|
+
session: ClaudeSessionState,
|
|
136
|
+
now = Date.now(),
|
|
137
|
+
): boolean {
|
|
138
|
+
const activeProv = deps.activeTurns.get(chatId)?.provider
|
|
139
|
+
if (activeProv !== undefined && deps.isClaudeSdkProvider(activeProv)) return false
|
|
140
|
+
if (session.pendingPromptSeqs.length > 0) return false
|
|
141
|
+
if (deps.hasLiveWorkflow(chatId)) return false
|
|
142
|
+
if (deps.hasPendingBackgroundTask(session, now)) return false
|
|
143
|
+
return now - session.lastUsedAt >= deps.resolveClaudeIdleMs()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Iterates all live Claude sessions and closes any that are idle.
|
|
148
|
+
*
|
|
149
|
+
* Mirrors the private `sweepIdleClaudeSessions` on AgentCoordinator.
|
|
150
|
+
*/
|
|
151
|
+
export function sweepIdleClaudeSessions(
|
|
152
|
+
deps: SessionStateQueryDeps,
|
|
153
|
+
now = Date.now(),
|
|
154
|
+
): void {
|
|
155
|
+
for (const [chatId, session] of [...deps.claudeSessions.entries()]) {
|
|
156
|
+
if (!isClaudeSessionIdle(deps, chatId, session, now)) continue
|
|
157
|
+
deps.closeClaudeSession(chatId, session)
|
|
158
|
+
deps.emitStateChange(chatId)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core session and turn state types shared between AgentCoordinator (agent.ts)
|
|
3
|
+
* and the extracted runClaudeSession event-loop (claude-session-runner.ts).
|
|
4
|
+
*
|
|
5
|
+
* Keeping these in a dedicated module avoids a circular import: agent.ts imports
|
|
6
|
+
* values from claude-session-runner.ts, so claude-session-runner.ts must not
|
|
7
|
+
* import values from agent.ts. Both sides import these type-only definitions
|
|
8
|
+
* from this neutral file instead.
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentProvider, KannaStatus, NormalizedToolCall, SlashCommand } from "../shared/types"
|
|
11
|
+
import type { AnyValue } from "../shared/errors"
|
|
12
|
+
import type { ClaudeSessionHandle, HarnessTurn } from "./harness-types"
|
|
13
|
+
|
|
14
|
+
export interface PendingToolRequest {
|
|
15
|
+
toolUseId: string
|
|
16
|
+
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
17
|
+
resolve: (result: AnyValue) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ActiveTurn {
|
|
21
|
+
chatId: string
|
|
22
|
+
provider: AgentProvider
|
|
23
|
+
turn: HarnessTurn
|
|
24
|
+
claudePromptSeq?: number
|
|
25
|
+
model: string
|
|
26
|
+
effort?: string
|
|
27
|
+
serviceTier?: "fast"
|
|
28
|
+
planMode: boolean
|
|
29
|
+
status: KannaStatus
|
|
30
|
+
pendingTool: PendingToolRequest | null
|
|
31
|
+
postToolFollowUp: { content: string; planMode: boolean } | null
|
|
32
|
+
hasFinalResult: boolean
|
|
33
|
+
cancelRequested: boolean
|
|
34
|
+
cancelRecorded: boolean
|
|
35
|
+
clientTraceId?: string
|
|
36
|
+
profilingStartedAt?: number
|
|
37
|
+
waitStartedAt: number | null
|
|
38
|
+
// True when this turn was synthesised by Kanna to inject `/compact` before
|
|
39
|
+
// the user's real message. Used to update the per-chat compact circuit
|
|
40
|
+
// breaker on completion (reset on success, increment on failure).
|
|
41
|
+
proactiveCompactInjection?: boolean
|
|
42
|
+
// _id of the user_prompt entry that triggered this turn (when appended on
|
|
43
|
+
// this turn). Used to attribute main-Claude-initiated subagent runs to the
|
|
44
|
+
// originating user message.
|
|
45
|
+
userMessageId: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ClaudeSessionState {
|
|
49
|
+
id: string
|
|
50
|
+
chatId: string
|
|
51
|
+
session: ClaudeSessionHandle
|
|
52
|
+
localPath: string
|
|
53
|
+
additionalDirectories: string[]
|
|
54
|
+
model: string
|
|
55
|
+
effort?: string
|
|
56
|
+
planMode: boolean
|
|
57
|
+
sessionToken: string | null
|
|
58
|
+
accountInfoLoaded: boolean
|
|
59
|
+
nextPromptSeq: number
|
|
60
|
+
pendingPromptSeqs: number[]
|
|
61
|
+
activeTokenId: string | null
|
|
62
|
+
oauthKeyMasked: string | null
|
|
63
|
+
oauthLabel: string | null
|
|
64
|
+
// OpenRouter turns route through the SDK with ANTHROPIC_AUTH_TOKEN set to the
|
|
65
|
+
// OpenRouter key, so the SDK self-reports a misleading Anthropic source. Hold
|
|
66
|
+
// the OpenRouter identity here to surface it in the account_info entry.
|
|
67
|
+
openrouterKeyMasked: string | null
|
|
68
|
+
openrouterModel: string | null
|
|
69
|
+
lastUsedAt: number
|
|
70
|
+
// Claude-Code background Bash tasks (`Bash(run_in_background: true)`) run as
|
|
71
|
+
// children of this PTY process and notify completion via a `<task-notification>`
|
|
72
|
+
// transcript line that the continuous tail re-enters as a real turn — but ONLY
|
|
73
|
+
// if the process is still alive. Track launched task ids + a keep-alive
|
|
74
|
+
// deadline so the idle reaper / budget enforcer does not tear the process down
|
|
75
|
+
// mid-flight. See adr-20260604-pty-background-task-keepalive.
|
|
76
|
+
backgroundTaskIds: Set<string>
|
|
77
|
+
backgroundTaskDeadlineAt: number
|
|
78
|
+
// Armed-loop state captured at spawn. Both drivers bake the loop tool-block
|
|
79
|
+
// into the spawn (PTY: --disallowedTools CLI args; SDK: options.disallowedTools
|
|
80
|
+
// so the model never sees the blocked tools — Claude Code's filter-at-spawn
|
|
81
|
+
// pattern). When the armed state changes (setup_loop arms / stop_loop or
|
|
82
|
+
// user-send disarms) the session must be respawned at the next turn boundary
|
|
83
|
+
// or the block goes stale.
|
|
84
|
+
loopArmedAtSpawn: boolean
|
|
85
|
+
/** SDK only: set once the workflows dir has been registered for this session. */
|
|
86
|
+
workflowsDirRegistered?: boolean
|
|
87
|
+
// Number of cancelled turns awaiting their interrupt-induced tail `result`.
|
|
88
|
+
// The SDK's `interrupt()` resolves the query loop with a `result` whose
|
|
89
|
+
// subtype is `error_during_execution` (NOT `cancelled`) and empty text, which
|
|
90
|
+
// would otherwise render as "An unknown error occurred." after the
|
|
91
|
+
// `interrupted` entry. Set on cancel, consumed (and the tail suppressed) when
|
|
92
|
+
// that result arrives, reset on each new turn so a no-tail cancel can't leak
|
|
93
|
+
// suppression onto a later real error.
|
|
94
|
+
cancelledResultPending: number
|
|
95
|
+
// Set by clearClaudeSessionContext (/clear machinery: setup_loop, background
|
|
96
|
+
// delivery). Once the chat's context is declared cleared, any session_token
|
|
97
|
+
// this in-flight session still emits belongs to the OLD conversation and
|
|
98
|
+
// must never re-persist over the wipe. Fresh spawns start unsuppressed.
|
|
99
|
+
suppressSessionTokenPersist: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Re-export SlashCommand as a convenience so importers of this module can get
|
|
103
|
+
// the type without adding a separate shared/types import.
|
|
104
|
+
export type { SlashCommand }
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for claude-slash-commands.ts
|
|
3
|
+
*
|
|
4
|
+
* Covers the extracted ensureSlashCommandsLoaded and mergeLocalCatalog helpers.
|
|
5
|
+
* All IO is injected through the deps interface; no real sessions are spawned.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from "bun:test"
|
|
9
|
+
import { ensureSlashCommandsLoaded, mergeLocalCatalog, type SlashCommandsDeps } from "./claude-slash-commands"
|
|
10
|
+
import type { SlashCommand } from "../shared/types"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function makeSlashCommand(name: string, description = ""): SlashCommand {
|
|
17
|
+
return { name, description, argumentHint: "" }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeDeps(overrides: Partial<SlashCommandsDeps> = {}): SlashCommandsDeps {
|
|
21
|
+
return {
|
|
22
|
+
store: {
|
|
23
|
+
getChat: () => ({
|
|
24
|
+
provider: "claude" as const,
|
|
25
|
+
slashCommands: null,
|
|
26
|
+
planMode: false,
|
|
27
|
+
projectId: "proj-1",
|
|
28
|
+
sessionTokensByProvider: { claude: null },
|
|
29
|
+
}),
|
|
30
|
+
getProject: () => ({ id: "proj-1", localPath: "/tmp/proj" }),
|
|
31
|
+
recordSessionCommandsLoaded: async () => undefined,
|
|
32
|
+
},
|
|
33
|
+
claudeSessions: { get: () => undefined },
|
|
34
|
+
oauthPool: null,
|
|
35
|
+
slashCommandsInFlight: new Set<string>(),
|
|
36
|
+
emitStateChange: () => undefined,
|
|
37
|
+
resolveClaudeDriverPreference: () => "sdk",
|
|
38
|
+
startClaudeSessionPTY: async () => {
|
|
39
|
+
throw new Error("PTY not expected in these tests")
|
|
40
|
+
},
|
|
41
|
+
startClaudeSessionSDK: async () => {
|
|
42
|
+
throw new Error("SDK session not expected in these tests")
|
|
43
|
+
},
|
|
44
|
+
getSubagents: () => [],
|
|
45
|
+
getGlobalPromptAppend: () => undefined,
|
|
46
|
+
getEnabledCustomMcpServers: () => [],
|
|
47
|
+
claudePtyRegistry: null,
|
|
48
|
+
ptyInstanceRegistry: null,
|
|
49
|
+
workflowRegistry: null,
|
|
50
|
+
subagentTranscriptRegistry: null,
|
|
51
|
+
localCatalog: null,
|
|
52
|
+
...overrides,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// mergeLocalCatalog
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
describe("mergeLocalCatalog", () => {
|
|
61
|
+
test("returns commands unchanged when localCatalog is null", () => {
|
|
62
|
+
const commands = [makeSlashCommand("foo"), makeSlashCommand("bar")]
|
|
63
|
+
const deps = makeDeps({ localCatalog: null })
|
|
64
|
+
const result = mergeLocalCatalog(deps, commands, "/cwd")
|
|
65
|
+
expect(result).toEqual(commands)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("appends local commands that do not conflict with CLI commands", () => {
|
|
69
|
+
const cliCommands = [makeSlashCommand("help")]
|
|
70
|
+
const localCommands = [makeSlashCommand("mylocal")]
|
|
71
|
+
const deps = makeDeps({
|
|
72
|
+
localCatalog: { list: () => localCommands },
|
|
73
|
+
})
|
|
74
|
+
const result = mergeLocalCatalog(deps, cliCommands, "/cwd")
|
|
75
|
+
expect(result).toHaveLength(2)
|
|
76
|
+
expect(result[0]?.name).toBe("help")
|
|
77
|
+
expect(result[1]?.name).toBe("mylocal")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("filters out local commands that duplicate CLI commands (case-insensitive)", () => {
|
|
81
|
+
const cliCommands = [makeSlashCommand("Help")]
|
|
82
|
+
const localCommands = [makeSlashCommand("help"), makeSlashCommand("custom")]
|
|
83
|
+
const deps = makeDeps({
|
|
84
|
+
localCatalog: { list: () => localCommands },
|
|
85
|
+
})
|
|
86
|
+
const result = mergeLocalCatalog(deps, cliCommands, "/cwd")
|
|
87
|
+
expect(result).toHaveLength(2)
|
|
88
|
+
expect(result.map((c) => c.name)).toEqual(["Help", "custom"])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("returns commands unchanged when localCatalog.list throws", () => {
|
|
92
|
+
const cliCommands = [makeSlashCommand("foo")]
|
|
93
|
+
const deps = makeDeps({
|
|
94
|
+
localCatalog: {
|
|
95
|
+
list: () => {
|
|
96
|
+
throw new Error("scan failed")
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
const result = mergeLocalCatalog(deps, cliCommands, "/cwd")
|
|
101
|
+
expect(result).toEqual(cliCommands)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// ensureSlashCommandsLoaded
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe("ensureSlashCommandsLoaded", () => {
|
|
110
|
+
test("returns early when chat is not found", async () => {
|
|
111
|
+
const deps = makeDeps({ store: { ...makeDeps().store, getChat: () => null } })
|
|
112
|
+
await expect(ensureSlashCommandsLoaded(deps, "chat-1")).resolves.toBeUndefined()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test("returns early for codex provider", async () => {
|
|
116
|
+
const deps = makeDeps({
|
|
117
|
+
store: {
|
|
118
|
+
...makeDeps().store,
|
|
119
|
+
getChat: () => ({
|
|
120
|
+
provider: "codex" as const,
|
|
121
|
+
slashCommands: null,
|
|
122
|
+
planMode: false,
|
|
123
|
+
projectId: "proj-1",
|
|
124
|
+
sessionTokensByProvider: {},
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
await expect(ensureSlashCommandsLoaded(deps, "chat-1")).resolves.toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test("returns early when slash commands are already loaded", async () => {
|
|
132
|
+
const deps = makeDeps({
|
|
133
|
+
store: {
|
|
134
|
+
...makeDeps().store,
|
|
135
|
+
getChat: () => ({
|
|
136
|
+
provider: "claude" as const,
|
|
137
|
+
slashCommands: [makeSlashCommand("help")],
|
|
138
|
+
planMode: false,
|
|
139
|
+
projectId: "proj-1",
|
|
140
|
+
sessionTokensByProvider: { claude: null },
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
await expect(ensureSlashCommandsLoaded(deps, "chat-1")).resolves.toBeUndefined()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("returns early when load is already in-flight", async () => {
|
|
148
|
+
const inFlight = new Set(["chat-1"])
|
|
149
|
+
const deps = makeDeps({ slashCommandsInFlight: inFlight })
|
|
150
|
+
await expect(ensureSlashCommandsLoaded(deps, "chat-1")).resolves.toBeUndefined()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("loads commands from existing session and persists them", async () => {
|
|
154
|
+
const commands = [makeSlashCommand("clear"), makeSlashCommand("help")]
|
|
155
|
+
let recorded: SlashCommand[] = []
|
|
156
|
+
const deps = makeDeps({
|
|
157
|
+
claudeSessions: {
|
|
158
|
+
get: () => ({
|
|
159
|
+
session: { getSupportedCommands: async () => commands },
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
store: {
|
|
163
|
+
getChat: () => ({
|
|
164
|
+
provider: "claude" as const,
|
|
165
|
+
slashCommands: null,
|
|
166
|
+
planMode: false,
|
|
167
|
+
projectId: "proj-1",
|
|
168
|
+
sessionTokensByProvider: { claude: null },
|
|
169
|
+
}),
|
|
170
|
+
getProject: () => ({ id: "proj-1", localPath: "/tmp/proj" }),
|
|
171
|
+
recordSessionCommandsLoaded: async (_chatId, cmds) => {
|
|
172
|
+
recorded = cmds
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
await ensureSlashCommandsLoaded(deps, "chat-1")
|
|
177
|
+
expect(recorded).toEqual(commands)
|
|
178
|
+
expect(deps.slashCommandsInFlight.has("chat-1")).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test("clears in-flight flag on error from existing session", async () => {
|
|
182
|
+
const deps = makeDeps({
|
|
183
|
+
claudeSessions: {
|
|
184
|
+
get: () => ({
|
|
185
|
+
session: {
|
|
186
|
+
getSupportedCommands: async () => {
|
|
187
|
+
throw new Error("session died")
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
await ensureSlashCommandsLoaded(deps, "chat-1")
|
|
194
|
+
expect(deps.slashCommandsInFlight.has("chat-1")).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test("spawns ephemeral SDK session when no active session and no oauthPool", async () => {
|
|
198
|
+
const cliCommands = [makeSlashCommand("help")]
|
|
199
|
+
const closed = { value: false }
|
|
200
|
+
let recorded: SlashCommand[] = []
|
|
201
|
+
const noopAsync = async () => undefined
|
|
202
|
+
const deps = makeDeps({
|
|
203
|
+
startClaudeSessionSDK: async () => ({
|
|
204
|
+
provider: "claude" as const,
|
|
205
|
+
stream: (async function* () {})(),
|
|
206
|
+
interrupt: noopAsync,
|
|
207
|
+
close: () => { closed.value = true },
|
|
208
|
+
sendPrompt: noopAsync,
|
|
209
|
+
setModel: noopAsync,
|
|
210
|
+
setPermissionMode: noopAsync,
|
|
211
|
+
getSupportedCommands: async () => cliCommands,
|
|
212
|
+
}),
|
|
213
|
+
store: {
|
|
214
|
+
getChat: () => ({
|
|
215
|
+
provider: "claude" as const,
|
|
216
|
+
slashCommands: null,
|
|
217
|
+
planMode: false,
|
|
218
|
+
projectId: "proj-1",
|
|
219
|
+
sessionTokensByProvider: { claude: null },
|
|
220
|
+
}),
|
|
221
|
+
getProject: () => ({ id: "proj-1", localPath: "/tmp/proj" }),
|
|
222
|
+
recordSessionCommandsLoaded: async (_chatId, cmds) => {
|
|
223
|
+
recorded = cmds
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
await ensureSlashCommandsLoaded(deps, "chat-1")
|
|
228
|
+
expect(closed.value).toBe(true)
|
|
229
|
+
expect(recorded).toEqual(cliCommands)
|
|
230
|
+
expect(deps.slashCommandsInFlight.has("chat-1")).toBe(false)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test("merges local catalog commands into loaded commands", async () => {
|
|
234
|
+
const cliCommands = [makeSlashCommand("help")]
|
|
235
|
+
const localCommands = [makeSlashCommand("mylocal")]
|
|
236
|
+
let recorded: SlashCommand[] = []
|
|
237
|
+
const deps = makeDeps({
|
|
238
|
+
claudeSessions: {
|
|
239
|
+
get: () => ({
|
|
240
|
+
session: { getSupportedCommands: async () => cliCommands },
|
|
241
|
+
}),
|
|
242
|
+
},
|
|
243
|
+
localCatalog: { list: () => localCommands },
|
|
244
|
+
store: {
|
|
245
|
+
getChat: () => ({
|
|
246
|
+
provider: "claude" as const,
|
|
247
|
+
slashCommands: null,
|
|
248
|
+
planMode: false,
|
|
249
|
+
projectId: "proj-1",
|
|
250
|
+
sessionTokensByProvider: { claude: null },
|
|
251
|
+
}),
|
|
252
|
+
getProject: () => ({ id: "proj-1", localPath: "/tmp/proj" }),
|
|
253
|
+
recordSessionCommandsLoaded: async (_chatId, cmds) => {
|
|
254
|
+
recorded = cmds
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
await ensureSlashCommandsLoaded(deps, "chat-1")
|
|
259
|
+
expect(recorded).toHaveLength(2)
|
|
260
|
+
expect(recorded.map((c) => c.name)).toEqual(["help", "mylocal"])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test("skips ephemeral spawn when oauthPool has tokens but pickEphemeral returns null", async () => {
|
|
264
|
+
const deps = makeDeps({
|
|
265
|
+
oauthPool: {
|
|
266
|
+
pickEphemeral: () => null,
|
|
267
|
+
hasAnyToken: () => true,
|
|
268
|
+
markUsed: () => undefined,
|
|
269
|
+
},
|
|
270
|
+
startClaudeSessionSDK: async () => {
|
|
271
|
+
throw new Error("should not be called")
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
await expect(ensureSlashCommandsLoaded(deps, "chat-1")).resolves.toBeUndefined()
|
|
275
|
+
expect(deps.slashCommandsInFlight.has("chat-1")).toBe(false)
|
|
276
|
+
})
|
|
277
|
+
})
|