@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,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure event-builder functions for EventStore write operations.
|
|
3
|
+
*
|
|
4
|
+
* Each function validates its inputs (throwing on error), then constructs and
|
|
5
|
+
* returns the event object that the EventStore should persist. Returning `null`
|
|
6
|
+
* signals a no-op (state is already the desired value, so no event is needed).
|
|
7
|
+
*
|
|
8
|
+
* Extracted from event-store.ts to reduce file size. All functions are pure —
|
|
9
|
+
* no IO, no side effects (side-effect seal: IO lives in *.adapter.ts files).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from "node:path"
|
|
13
|
+
import type { AgentProvider, QueuedChatMessage, SlashCommand, StackBinding } from "../shared/types"
|
|
14
|
+
import { STORE_VERSION } from "../shared/types"
|
|
15
|
+
import type { ChatPermissionPolicyOverride, ToolRequest, ToolRequestDecision, ToolRequestStatus } from "../shared/permission-policy"
|
|
16
|
+
import type {
|
|
17
|
+
ChatEvent,
|
|
18
|
+
ChatRecord,
|
|
19
|
+
ProjectEvent,
|
|
20
|
+
ProjectRecord,
|
|
21
|
+
QueuedMessageEvent,
|
|
22
|
+
StackEvent,
|
|
23
|
+
StackRecord,
|
|
24
|
+
StoreState,
|
|
25
|
+
ToolRequestEvent,
|
|
26
|
+
TurnEvent,
|
|
27
|
+
TurnRunConfig,
|
|
28
|
+
} from "./events"
|
|
29
|
+
import { resolveLocalPath } from "./paths"
|
|
30
|
+
import { slashCommandsEqual } from "./event-store-helpers"
|
|
31
|
+
|
|
32
|
+
// ─── Internal helper ───────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function requireChat(chatsById: Map<string, ChatRecord>, chatId: string): ChatRecord {
|
|
35
|
+
const chat = chatsById.get(chatId)
|
|
36
|
+
if (!chat || chat.deletedAt) throw new Error("Chat not found")
|
|
37
|
+
return chat
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Project builders ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export type OpenProjectResult =
|
|
43
|
+
| { kind: "existing"; project: ProjectRecord }
|
|
44
|
+
| { kind: "new"; event: Extract<ProjectEvent, { type: "project_opened" }> }
|
|
45
|
+
|
|
46
|
+
/** Resolves an existing project or builds the `project_opened` event for a new one. */
|
|
47
|
+
export function buildOpenProjectResult(
|
|
48
|
+
state: Pick<StoreState, "projectsById" | "projectIdsByPath">,
|
|
49
|
+
localPath: string,
|
|
50
|
+
title?: string,
|
|
51
|
+
): OpenProjectResult {
|
|
52
|
+
const normalized = resolveLocalPath(localPath)
|
|
53
|
+
const existingId = state.projectIdsByPath.get(normalized)
|
|
54
|
+
if (existingId) {
|
|
55
|
+
const existing = state.projectsById.get(existingId)
|
|
56
|
+
if (existing && !existing.deletedAt) return { kind: "existing", project: existing }
|
|
57
|
+
}
|
|
58
|
+
const hiddenProject = [...state.projectsById.values()]
|
|
59
|
+
.find((p) => p.localPath === normalized && p.deletedAt)
|
|
60
|
+
const projectId = hiddenProject?.id ?? crypto.randomUUID()
|
|
61
|
+
const event: Extract<ProjectEvent, { type: "project_opened" }> = {
|
|
62
|
+
v: STORE_VERSION,
|
|
63
|
+
type: "project_opened",
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
projectId,
|
|
66
|
+
localPath: normalized,
|
|
67
|
+
title: title?.trim() || path.basename(normalized) || normalized,
|
|
68
|
+
}
|
|
69
|
+
return { kind: "new", event }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Builds the `project_removed` event. Throws if the project is not found. */
|
|
73
|
+
export function buildRemoveProjectEvent(
|
|
74
|
+
projectsById: Map<string, { id: string; deletedAt?: number }>,
|
|
75
|
+
projectId: string,
|
|
76
|
+
): ProjectEvent {
|
|
77
|
+
const project = projectsById.get(projectId)
|
|
78
|
+
if (!project || project.deletedAt) throw new Error("Project not found")
|
|
79
|
+
return { v: STORE_VERSION, type: "project_removed", timestamp: Date.now(), projectId }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Builds the `project_star_set` event. Throws if the project is not found. */
|
|
83
|
+
export function buildSetProjectStarEvent(
|
|
84
|
+
projectsById: Map<string, { id: string; deletedAt?: number }>,
|
|
85
|
+
projectId: string,
|
|
86
|
+
starred: boolean,
|
|
87
|
+
): ProjectEvent {
|
|
88
|
+
const project = projectsById.get(projectId)
|
|
89
|
+
if (!project || project.deletedAt) throw new Error("Project not found")
|
|
90
|
+
const now = Date.now()
|
|
91
|
+
return {
|
|
92
|
+
v: STORE_VERSION,
|
|
93
|
+
type: "project_star_set",
|
|
94
|
+
timestamp: now,
|
|
95
|
+
projectId,
|
|
96
|
+
starredAt: starred ? now : null,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Stack builders ───────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** Builds the `stack_added` event. Throws on invalid inputs. */
|
|
103
|
+
export function buildCreateStackEvent(
|
|
104
|
+
state: Pick<StoreState, "projectsById" | "stacksById">,
|
|
105
|
+
title: string,
|
|
106
|
+
projectIds: string[],
|
|
107
|
+
): StackEvent & { stackId: string } {
|
|
108
|
+
const trimmed = title.trim()
|
|
109
|
+
if (trimmed === "") throw new Error("Stack title cannot be empty")
|
|
110
|
+
if (projectIds.length < 2) throw new Error("Stack requires at least 2 projects")
|
|
111
|
+
if (new Set(projectIds).size !== projectIds.length) throw new Error("Stack projectIds contain duplicates")
|
|
112
|
+
for (const id of projectIds) {
|
|
113
|
+
const project = state.projectsById.get(id)
|
|
114
|
+
if (!project || project.deletedAt) throw new Error(`Project not found: ${id}`)
|
|
115
|
+
}
|
|
116
|
+
const stackId = crypto.randomUUID()
|
|
117
|
+
return {
|
|
118
|
+
v: STORE_VERSION,
|
|
119
|
+
type: "stack_added",
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
stackId,
|
|
122
|
+
title: trimmed,
|
|
123
|
+
projectIds: [...projectIds],
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Builds the `stack_renamed` event, or `null` if the title is unchanged. Throws if not found. */
|
|
128
|
+
export function buildRenameStackEvent(
|
|
129
|
+
stacksById: Map<string, StackRecord>,
|
|
130
|
+
stackId: string,
|
|
131
|
+
title: string,
|
|
132
|
+
): StackEvent | null {
|
|
133
|
+
const stack = stacksById.get(stackId)
|
|
134
|
+
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
135
|
+
const trimmed = title.trim()
|
|
136
|
+
if (trimmed === "") throw new Error("Stack title cannot be empty")
|
|
137
|
+
if (trimmed === stack.title) return null
|
|
138
|
+
return { v: STORE_VERSION, type: "stack_renamed", timestamp: Date.now(), stackId, title: trimmed }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Builds the `stack_removed` event, or `null` if already deleted. Throws if not found. */
|
|
142
|
+
export function buildRemoveStackEvent(
|
|
143
|
+
stacksById: Map<string, StackRecord>,
|
|
144
|
+
stackId: string,
|
|
145
|
+
): StackEvent | null {
|
|
146
|
+
const stack = stacksById.get(stackId)
|
|
147
|
+
if (!stack) throw new Error("Stack not found")
|
|
148
|
+
if (stack.deletedAt) return null
|
|
149
|
+
return { v: STORE_VERSION, type: "stack_removed", timestamp: Date.now(), stackId }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Builds the `stack_project_added` event, or `null` if already a member. Throws if not found. */
|
|
153
|
+
export function buildAddProjectToStackEvent(
|
|
154
|
+
state: Pick<StoreState, "projectsById" | "stacksById">,
|
|
155
|
+
stackId: string,
|
|
156
|
+
projectId: string,
|
|
157
|
+
): StackEvent | null {
|
|
158
|
+
const stack = state.stacksById.get(stackId)
|
|
159
|
+
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
160
|
+
const project = state.projectsById.get(projectId)
|
|
161
|
+
if (!project || project.deletedAt) throw new Error("Project not found")
|
|
162
|
+
if (stack.projectIds.includes(projectId)) return null
|
|
163
|
+
return { v: STORE_VERSION, type: "stack_project_added", timestamp: Date.now(), stackId, projectId }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Builds the `stack_project_removed` event, or `null` if not a member. Throws on constraint violations. */
|
|
167
|
+
export function buildRemoveProjectFromStackEvent(
|
|
168
|
+
stacksById: Map<string, StackRecord>,
|
|
169
|
+
stackId: string,
|
|
170
|
+
projectId: string,
|
|
171
|
+
): StackEvent | null {
|
|
172
|
+
const stack = stacksById.get(stackId)
|
|
173
|
+
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
174
|
+
if (!stack.projectIds.includes(projectId)) return null
|
|
175
|
+
if (stack.projectIds.length <= 2) {
|
|
176
|
+
throw new Error("Stack must keep at least 2 projects. Delete the stack instead.")
|
|
177
|
+
}
|
|
178
|
+
return { v: STORE_VERSION, type: "stack_project_removed", timestamp: Date.now(), stackId, projectId }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── Sidebar order ────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Computes the new de-duplicated + validated sidebar order.
|
|
185
|
+
* Returns `null` if the order is unchanged (caller can skip the write).
|
|
186
|
+
*/
|
|
187
|
+
export function computeNewSidebarOrder(
|
|
188
|
+
projectsById: Map<string, { deletedAt?: number }>,
|
|
189
|
+
currentOrder: string[],
|
|
190
|
+
requestedIds: string[],
|
|
191
|
+
): string[] | null {
|
|
192
|
+
const valid = requestedIds.filter((id) => {
|
|
193
|
+
const p = projectsById.get(id)
|
|
194
|
+
return Boolean(p && !p.deletedAt)
|
|
195
|
+
})
|
|
196
|
+
const unique = [...new Set(valid)]
|
|
197
|
+
if (
|
|
198
|
+
unique.length === currentOrder.length
|
|
199
|
+
&& unique.every((id, i) => currentOrder[i] === id)
|
|
200
|
+
) {
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
return unique
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Chat lifecycle builders ───────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/** Builds the `chat_created` event after full validation. Throws on invalid inputs. */
|
|
209
|
+
export function buildCreateChatEvent(
|
|
210
|
+
state: Pick<StoreState, "projectsById" | "stacksById">,
|
|
211
|
+
projectId: string,
|
|
212
|
+
options?: { stackId?: string; stackBindings?: StackBinding[] },
|
|
213
|
+
): ChatEvent & { chatId: string } {
|
|
214
|
+
const project = state.projectsById.get(projectId)
|
|
215
|
+
if (!project || project.deletedAt) throw new Error("Project not found")
|
|
216
|
+
|
|
217
|
+
if (options?.stackId !== undefined || options?.stackBindings !== undefined) {
|
|
218
|
+
if (options.stackId === undefined || options.stackBindings === undefined) {
|
|
219
|
+
throw new Error("stackId and stackBindings must be provided together")
|
|
220
|
+
}
|
|
221
|
+
const stack = state.stacksById.get(options.stackId)
|
|
222
|
+
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
223
|
+
if (options.stackBindings.length === 0) throw new Error("stackBindings cannot be empty")
|
|
224
|
+
const primaries = options.stackBindings.filter((b) => b.role === "primary")
|
|
225
|
+
if (primaries.length !== 1) throw new Error("Exactly one primary binding required")
|
|
226
|
+
const seenProjects = new Set<string>()
|
|
227
|
+
for (const binding of options.stackBindings) {
|
|
228
|
+
if (seenProjects.has(binding.projectId)) throw new Error("Duplicate projectId in stackBindings")
|
|
229
|
+
seenProjects.add(binding.projectId)
|
|
230
|
+
if (!stack.projectIds.includes(binding.projectId)) {
|
|
231
|
+
throw new Error(`Binding projectId not a member of stack: ${binding.projectId}`)
|
|
232
|
+
}
|
|
233
|
+
const peer = state.projectsById.get(binding.projectId)
|
|
234
|
+
if (!peer || peer.deletedAt) throw new Error(`Project not found: ${binding.projectId}`)
|
|
235
|
+
if (typeof binding.worktreePath !== "string" || binding.worktreePath.trim() === "") {
|
|
236
|
+
throw new Error("worktreePath must be a non-empty string")
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (primaries[0].projectId !== projectId) {
|
|
240
|
+
throw new Error("Primary binding projectId must match createChat projectId")
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const chatId = crypto.randomUUID()
|
|
245
|
+
return {
|
|
246
|
+
v: STORE_VERSION,
|
|
247
|
+
type: "chat_created",
|
|
248
|
+
timestamp: Date.now(),
|
|
249
|
+
chatId,
|
|
250
|
+
projectId,
|
|
251
|
+
title: "New Chat",
|
|
252
|
+
...(options?.stackId !== undefined ? { stackId: options.stackId } : {}),
|
|
253
|
+
...(options?.stackBindings !== undefined
|
|
254
|
+
? { stackBindings: options.stackBindings.map((b) => ({ ...b })) }
|
|
255
|
+
: {}),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Builds the `chat_renamed` event, or `null` if the title is unchanged / empty. */
|
|
260
|
+
export function buildRenameChatEvent(
|
|
261
|
+
chatsById: Map<string, ChatRecord>,
|
|
262
|
+
chatId: string,
|
|
263
|
+
title: string,
|
|
264
|
+
): ChatEvent | null {
|
|
265
|
+
const trimmed = title.trim()
|
|
266
|
+
if (!trimmed) return null
|
|
267
|
+
const chat = requireChat(chatsById, chatId)
|
|
268
|
+
if (chat.title === trimmed) return null
|
|
269
|
+
return { v: STORE_VERSION, type: "chat_renamed", timestamp: Date.now(), chatId, title: trimmed }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Builds the `chat_archived` event. Throws if the chat is not found. */
|
|
273
|
+
export function buildArchiveChatEvent(chatsById: Map<string, ChatRecord>, chatId: string): ChatEvent {
|
|
274
|
+
requireChat(chatsById, chatId)
|
|
275
|
+
return { v: STORE_VERSION, type: "chat_archived", timestamp: Date.now(), chatId }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Builds the `chat_unarchived` event. Throws if the chat is not found. */
|
|
279
|
+
export function buildUnarchiveChatEvent(chatsById: Map<string, ChatRecord>, chatId: string): ChatEvent {
|
|
280
|
+
requireChat(chatsById, chatId)
|
|
281
|
+
return { v: STORE_VERSION, type: "chat_unarchived", timestamp: Date.now(), chatId }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Chat state-setter builders ────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/** Builds the `chat_provider_set` event, or `null` if the provider is unchanged. */
|
|
287
|
+
export function buildChatProviderEvent(
|
|
288
|
+
chatsById: Map<string, ChatRecord>,
|
|
289
|
+
chatId: string,
|
|
290
|
+
provider: AgentProvider,
|
|
291
|
+
): ChatEvent | null {
|
|
292
|
+
const chat = requireChat(chatsById, chatId)
|
|
293
|
+
if (chat.provider === provider) return null
|
|
294
|
+
return { v: STORE_VERSION, type: "chat_provider_set", timestamp: Date.now(), chatId, provider }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Builds the `chat_plan_mode_set` event, or `null` if unchanged. */
|
|
298
|
+
export function buildPlanModeEvent(
|
|
299
|
+
chatsById: Map<string, ChatRecord>,
|
|
300
|
+
chatId: string,
|
|
301
|
+
planMode: boolean,
|
|
302
|
+
): ChatEvent | null {
|
|
303
|
+
const chat = requireChat(chatsById, chatId)
|
|
304
|
+
if (chat.planMode === planMode) return null
|
|
305
|
+
return { v: STORE_VERSION, type: "chat_plan_mode_set", timestamp: Date.now(), chatId, planMode }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Builds the `chat_compact_failures_set` event, or `null` if unchanged. */
|
|
309
|
+
export function buildCompactFailuresEvent(
|
|
310
|
+
chatsById: Map<string, ChatRecord>,
|
|
311
|
+
chatId: string,
|
|
312
|
+
compactFailureCount: number,
|
|
313
|
+
): ChatEvent | null {
|
|
314
|
+
const chat = requireChat(chatsById, chatId)
|
|
315
|
+
if ((chat.compactFailureCount ?? 0) === compactFailureCount) return null
|
|
316
|
+
return {
|
|
317
|
+
v: STORE_VERSION, type: "chat_compact_failures_set", timestamp: Date.now(),
|
|
318
|
+
chatId, compactFailureCount,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Builds the `chat_read_state_set` event, or `null` if unchanged. */
|
|
323
|
+
export function buildChatReadStateEvent(
|
|
324
|
+
chatsById: Map<string, ChatRecord>,
|
|
325
|
+
chatId: string,
|
|
326
|
+
unread: boolean,
|
|
327
|
+
): ChatEvent | null {
|
|
328
|
+
const chat = requireChat(chatsById, chatId)
|
|
329
|
+
if (chat.unread === unread) return null
|
|
330
|
+
return { v: STORE_VERSION, type: "chat_read_state_set", timestamp: Date.now(), chatId, unread }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Builds the `chat_policy_override_set` event. Throws if the chat is not found. */
|
|
334
|
+
export function buildChatPolicyOverrideEvent(
|
|
335
|
+
chatsById: Map<string, ChatRecord>,
|
|
336
|
+
chatId: string,
|
|
337
|
+
policyOverride: ChatPermissionPolicyOverride | null,
|
|
338
|
+
): ChatEvent {
|
|
339
|
+
requireChat(chatsById, chatId)
|
|
340
|
+
return {
|
|
341
|
+
v: STORE_VERSION, type: "chat_policy_override_set", timestamp: Date.now(),
|
|
342
|
+
chatId, policyOverride,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Builds the `chat_source_hash_set` event, or `null` if unchanged. */
|
|
347
|
+
export function buildChatSourceHashEvent(
|
|
348
|
+
chatsById: Map<string, ChatRecord>,
|
|
349
|
+
chatId: string,
|
|
350
|
+
sourceHash: string | null,
|
|
351
|
+
): ChatEvent | null {
|
|
352
|
+
const chat = requireChat(chatsById, chatId)
|
|
353
|
+
if (chat.sourceHash === sourceHash) return null
|
|
354
|
+
return { v: STORE_VERSION, type: "chat_source_hash_set", timestamp: Date.now(), chatId, sourceHash }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── Queued-message builders ───────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/** Builds the `queued_message_enqueued` event and the resolved QueuedChatMessage. */
|
|
360
|
+
export function buildEnqueueMessageResult(
|
|
361
|
+
chatsById: Map<string, ChatRecord>,
|
|
362
|
+
chatId: string,
|
|
363
|
+
message: Omit<QueuedChatMessage, "id" | "createdAt"> & Partial<Pick<QueuedChatMessage, "id" | "createdAt">>,
|
|
364
|
+
): { event: QueuedMessageEvent; queuedMessage: QueuedChatMessage } {
|
|
365
|
+
requireChat(chatsById, chatId)
|
|
366
|
+
const queuedMessage: QueuedChatMessage = {
|
|
367
|
+
id: message.id ?? crypto.randomUUID(),
|
|
368
|
+
content: message.content,
|
|
369
|
+
attachments: [...(message.attachments ?? [])],
|
|
370
|
+
createdAt: message.createdAt ?? Date.now(),
|
|
371
|
+
provider: message.provider,
|
|
372
|
+
model: message.model,
|
|
373
|
+
modelOptions: message.modelOptions,
|
|
374
|
+
planMode: message.planMode,
|
|
375
|
+
autoContinue: message.autoContinue,
|
|
376
|
+
}
|
|
377
|
+
const event: QueuedMessageEvent = {
|
|
378
|
+
v: STORE_VERSION,
|
|
379
|
+
type: "queued_message_enqueued",
|
|
380
|
+
timestamp: queuedMessage.createdAt,
|
|
381
|
+
chatId,
|
|
382
|
+
message: queuedMessage,
|
|
383
|
+
}
|
|
384
|
+
return { event, queuedMessage }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Builds the `queued_message_removed` event. Throws if not found. */
|
|
388
|
+
export function buildRemoveQueuedMessageEvent(
|
|
389
|
+
chatsById: Map<string, ChatRecord>,
|
|
390
|
+
queuedMessagesByChatId: Map<string, QueuedChatMessage[]>,
|
|
391
|
+
chatId: string,
|
|
392
|
+
queuedMessageId: string,
|
|
393
|
+
): QueuedMessageEvent {
|
|
394
|
+
requireChat(chatsById, chatId)
|
|
395
|
+
const existing = queuedMessagesByChatId.get(chatId) ?? []
|
|
396
|
+
if (!existing.some((e) => e.id === queuedMessageId)) throw new Error("Queued message not found")
|
|
397
|
+
return {
|
|
398
|
+
v: STORE_VERSION, type: "queued_message_removed",
|
|
399
|
+
timestamp: Date.now(), chatId, queuedMessageId,
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ─── Turn / session builders ───────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
/** Builds the `turn_started` event. Throws if the chat is not found. */
|
|
406
|
+
export function buildTurnStartedEvent(
|
|
407
|
+
chatsById: Map<string, ChatRecord>,
|
|
408
|
+
chatId: string,
|
|
409
|
+
runConfig?: TurnRunConfig,
|
|
410
|
+
): TurnEvent {
|
|
411
|
+
requireChat(chatsById, chatId)
|
|
412
|
+
return {
|
|
413
|
+
v: STORE_VERSION, type: "turn_started", timestamp: Date.now(),
|
|
414
|
+
chatId, ...(runConfig ? { runConfig } : {}),
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Builds the `turn_finished` event. Throws if the chat is not found. */
|
|
419
|
+
export function buildTurnFinishedEvent(chatsById: Map<string, ChatRecord>, chatId: string): TurnEvent {
|
|
420
|
+
requireChat(chatsById, chatId)
|
|
421
|
+
return { v: STORE_VERSION, type: "turn_finished", timestamp: Date.now(), chatId }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Builds the `turn_failed` event. Throws if the chat is not found. */
|
|
425
|
+
export function buildTurnFailedEvent(
|
|
426
|
+
chatsById: Map<string, ChatRecord>,
|
|
427
|
+
chatId: string,
|
|
428
|
+
error: string,
|
|
429
|
+
): TurnEvent {
|
|
430
|
+
requireChat(chatsById, chatId)
|
|
431
|
+
return { v: STORE_VERSION, type: "turn_failed", timestamp: Date.now(), chatId, error }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Builds the `turn_cancelled` event. Throws if the chat is not found. */
|
|
435
|
+
export function buildTurnCancelledEvent(chatsById: Map<string, ChatRecord>, chatId: string): TurnEvent {
|
|
436
|
+
requireChat(chatsById, chatId)
|
|
437
|
+
return { v: STORE_VERSION, type: "turn_cancelled", timestamp: Date.now(), chatId }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Builds the `session_token_set` event, or `null` if unchanged. */
|
|
441
|
+
export function buildSessionTokenEvent(
|
|
442
|
+
chatsById: Map<string, ChatRecord>,
|
|
443
|
+
chatId: string,
|
|
444
|
+
provider: AgentProvider,
|
|
445
|
+
sessionToken: string | null,
|
|
446
|
+
): TurnEvent | null {
|
|
447
|
+
const chat = requireChat(chatsById, chatId)
|
|
448
|
+
if ((chat.sessionTokensByProvider[provider] ?? null) === sessionToken) return null
|
|
449
|
+
return {
|
|
450
|
+
v: STORE_VERSION, type: "session_token_set", timestamp: Date.now(),
|
|
451
|
+
chatId, sessionToken, provider,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Builds the `session_commands_loaded` event, or `null` if commands are unchanged. */
|
|
456
|
+
export function buildSessionCommandsEvent(
|
|
457
|
+
chatsById: Map<string, ChatRecord>,
|
|
458
|
+
chatId: string,
|
|
459
|
+
commands: SlashCommand[],
|
|
460
|
+
): TurnEvent | null {
|
|
461
|
+
const chat = requireChat(chatsById, chatId)
|
|
462
|
+
const normalized = commands.map((c) => ({
|
|
463
|
+
name: c.name,
|
|
464
|
+
description: c.description,
|
|
465
|
+
argumentHint: c.argumentHint,
|
|
466
|
+
}))
|
|
467
|
+
if (chat.slashCommands && slashCommandsEqual(chat.slashCommands, normalized)) return null
|
|
468
|
+
return {
|
|
469
|
+
v: STORE_VERSION, type: "session_commands_loaded", timestamp: Date.now(),
|
|
470
|
+
chatId, commands: normalized,
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Builds the `pending_fork_session_token_set` event, or `null` if unchanged. */
|
|
475
|
+
export function buildPendingForkSessionTokenEvent(
|
|
476
|
+
chatsById: Map<string, ChatRecord>,
|
|
477
|
+
chatId: string,
|
|
478
|
+
value: { provider: AgentProvider; token: string } | null,
|
|
479
|
+
): TurnEvent | null {
|
|
480
|
+
const chat = requireChat(chatsById, chatId)
|
|
481
|
+
const current = chat.pendingForkSessionToken ?? null
|
|
482
|
+
const same =
|
|
483
|
+
(current == null && value == null)
|
|
484
|
+
|| (current != null && value != null
|
|
485
|
+
&& current.provider === value.provider
|
|
486
|
+
&& current.token === value.token)
|
|
487
|
+
if (same) return null
|
|
488
|
+
return {
|
|
489
|
+
v: STORE_VERSION, type: "pending_fork_session_token_set", timestamp: Date.now(),
|
|
490
|
+
chatId,
|
|
491
|
+
pendingForkSessionToken: value?.token ?? null,
|
|
492
|
+
provider: value?.provider,
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ─── Tool-request builders ────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
/** Builds the `tool_request_put` event. */
|
|
499
|
+
export function buildPutToolRequestEvent(req: ToolRequest): ToolRequestEvent {
|
|
500
|
+
return { v: 3, type: "tool_request_put", timestamp: Date.now(), request: req }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Builds the `tool_request_resolved` event. Throws if the request id is unknown. */
|
|
504
|
+
export function buildResolveToolRequestEvent(
|
|
505
|
+
toolRequestsById: Map<string, ToolRequest>,
|
|
506
|
+
id: string,
|
|
507
|
+
args: {
|
|
508
|
+
status: ToolRequestStatus
|
|
509
|
+
decision?: ToolRequestDecision
|
|
510
|
+
resolvedAt: number
|
|
511
|
+
mismatchReason?: string
|
|
512
|
+
},
|
|
513
|
+
): ToolRequestEvent {
|
|
514
|
+
if (!toolRequestsById.has(id)) throw new Error(`resolveToolRequest: unknown id ${id}`)
|
|
515
|
+
return {
|
|
516
|
+
v: 3,
|
|
517
|
+
type: "tool_request_resolved",
|
|
518
|
+
timestamp: Date.now(),
|
|
519
|
+
id,
|
|
520
|
+
status: args.status,
|
|
521
|
+
decision: args.decision,
|
|
522
|
+
resolvedAt: args.resolvedAt,
|
|
523
|
+
mismatchReason: args.mismatchReason,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ─── Re-export ProjectRecord for callers of OpenProjectResult ─────────────
|
|
528
|
+
|
|
529
|
+
export type { ProjectRecord } from "./events"
|
|
@@ -853,6 +853,41 @@ describe("EventStore subagent runs", () => {
|
|
|
853
853
|
expect(runs[runId].finalText).toBe("hello world")
|
|
854
854
|
})
|
|
855
855
|
|
|
856
|
+
test("subagent runs are tracked for chats restored from snapshot.json", async () => {
|
|
857
|
+
const dataDir = await createTempDataDir()
|
|
858
|
+
const store = new EventStore(dataDir)
|
|
859
|
+
await store.initialize()
|
|
860
|
+
const project = await store.openProject("/tmp/p-sa-snap")
|
|
861
|
+
const chat = await store.createChat(project.id)
|
|
862
|
+
|
|
863
|
+
// Fold chat_created into snapshot.json and truncate the event logs —
|
|
864
|
+
// the same state a server reboot leaves behind for pre-existing chats.
|
|
865
|
+
await store.snapshotAndTruncateLogs()
|
|
866
|
+
|
|
867
|
+
const rebooted = new EventStore(dataDir)
|
|
868
|
+
await rebooted.initialize()
|
|
869
|
+
|
|
870
|
+
const runId = "r-after-reboot"
|
|
871
|
+
await rebooted.appendSubagentEvent({
|
|
872
|
+
v: 3,
|
|
873
|
+
type: "subagent_run_started",
|
|
874
|
+
timestamp: chat.createdAt + 1,
|
|
875
|
+
chatId: chat.id,
|
|
876
|
+
runId,
|
|
877
|
+
subagentId: "s1",
|
|
878
|
+
subagentName: "alpha",
|
|
879
|
+
provider: "claude",
|
|
880
|
+
model: "claude-opus-4-7",
|
|
881
|
+
parentUserMessageId: "u1",
|
|
882
|
+
parentRunId: null,
|
|
883
|
+
depth: 0,
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
const runs = rebooted.getSubagentRuns(chat.id)
|
|
887
|
+
expect(runs[runId]).toBeDefined()
|
|
888
|
+
expect(runs[runId].status).toBe("running")
|
|
889
|
+
})
|
|
890
|
+
|
|
856
891
|
test("subagent_message_delta accumulates into finalText; run_completed sets canonical", async () => {
|
|
857
892
|
const dataDir = await createTempDataDir()
|
|
858
893
|
const store = new EventStore(dataDir)
|