@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,210 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { ToolRequest } from "../shared/permission-policy"
|
|
3
|
+
import type { ToolRequestEvent } from "./events"
|
|
4
|
+
import {
|
|
5
|
+
applyToolRequestEvent,
|
|
6
|
+
deleteToolRequestsForChat,
|
|
7
|
+
getToolRequest,
|
|
8
|
+
listPendingToolRequests,
|
|
9
|
+
scanAllToolRequests,
|
|
10
|
+
} from "./event-store-tool-requests"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const TS = 1_700_000_000_000
|
|
17
|
+
|
|
18
|
+
function makeRequest(overrides: Partial<ToolRequest> = {}): ToolRequest {
|
|
19
|
+
return {
|
|
20
|
+
id: "req-1",
|
|
21
|
+
chatId: "chat-1",
|
|
22
|
+
sessionId: "session-1",
|
|
23
|
+
toolUseId: "tool-use-1",
|
|
24
|
+
toolName: "Bash",
|
|
25
|
+
arguments: { command: "ls" },
|
|
26
|
+
canonicalArgsHash: "abc123",
|
|
27
|
+
policyVerdict: "ask",
|
|
28
|
+
status: "pending",
|
|
29
|
+
createdAt: TS,
|
|
30
|
+
expiresAt: TS + 600_000,
|
|
31
|
+
...overrides,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function putEvent(req: ToolRequest): Extract<ToolRequestEvent, { type: "tool_request_put" }> {
|
|
36
|
+
return { v: 3, type: "tool_request_put", timestamp: TS, request: req }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolvedEvent(
|
|
40
|
+
id: string,
|
|
41
|
+
overrides: Partial<Extract<ToolRequestEvent, { type: "tool_request_resolved" }>> = {},
|
|
42
|
+
): Extract<ToolRequestEvent, { type: "tool_request_resolved" }> {
|
|
43
|
+
return {
|
|
44
|
+
v: 3,
|
|
45
|
+
type: "tool_request_resolved",
|
|
46
|
+
timestamp: TS + 1000,
|
|
47
|
+
id,
|
|
48
|
+
status: "answered",
|
|
49
|
+
resolvedAt: TS + 1000,
|
|
50
|
+
...overrides,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// applyToolRequestEvent — tool_request_put
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
describe("applyToolRequestEvent / tool_request_put", () => {
|
|
59
|
+
test("inserts a defensive copy into the map", () => {
|
|
60
|
+
const map = new Map<string, ToolRequest>()
|
|
61
|
+
const req = makeRequest()
|
|
62
|
+
applyToolRequestEvent(map, putEvent(req))
|
|
63
|
+
expect(map.size).toBe(1)
|
|
64
|
+
expect(map.get("req-1")).toEqual(req)
|
|
65
|
+
// defensive copy — mutation of the original must not affect the stored value
|
|
66
|
+
req.status = "canceled"
|
|
67
|
+
expect(map.get("req-1")?.status).toBe("pending")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("overwrites an existing entry for the same id", () => {
|
|
71
|
+
const map = new Map<string, ToolRequest>()
|
|
72
|
+
const first = makeRequest({ id: "req-1", toolName: "Read" })
|
|
73
|
+
const second = makeRequest({ id: "req-1", toolName: "Bash" })
|
|
74
|
+
applyToolRequestEvent(map, putEvent(first))
|
|
75
|
+
applyToolRequestEvent(map, putEvent(second))
|
|
76
|
+
expect(map.get("req-1")?.toolName).toBe("Bash")
|
|
77
|
+
expect(map.size).toBe(1)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// applyToolRequestEvent — tool_request_resolved
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
describe("applyToolRequestEvent / tool_request_resolved", () => {
|
|
86
|
+
test("updates status, decision, resolvedAt, and mismatchReason", () => {
|
|
87
|
+
const map = new Map<string, ToolRequest>()
|
|
88
|
+
applyToolRequestEvent(map, putEvent(makeRequest()))
|
|
89
|
+
applyToolRequestEvent(map, resolvedEvent("req-1", {
|
|
90
|
+
status: "answered",
|
|
91
|
+
decision: { kind: "allow" },
|
|
92
|
+
resolvedAt: TS + 5000,
|
|
93
|
+
mismatchReason: undefined,
|
|
94
|
+
}))
|
|
95
|
+
const stored = map.get("req-1")
|
|
96
|
+
expect(stored?.status).toBe("answered")
|
|
97
|
+
expect(stored?.decision).toEqual({ kind: "allow" })
|
|
98
|
+
expect(stored?.resolvedAt).toBe(TS + 5000)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test("is a no-op when id is unknown", () => {
|
|
102
|
+
const map = new Map<string, ToolRequest>()
|
|
103
|
+
applyToolRequestEvent(map, resolvedEvent("nonexistent"))
|
|
104
|
+
expect(map.size).toBe(0)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test("preserves existing decision when event omits it", () => {
|
|
108
|
+
const map = new Map<string, ToolRequest>()
|
|
109
|
+
const req = makeRequest({ decision: { kind: "deny", reason: "blocked" } })
|
|
110
|
+
applyToolRequestEvent(map, putEvent(req))
|
|
111
|
+
applyToolRequestEvent(map, resolvedEvent("req-1", { status: "timeout", decision: undefined }))
|
|
112
|
+
expect(map.get("req-1")?.decision).toEqual({ kind: "deny", reason: "blocked" })
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// getToolRequest
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
describe("getToolRequest", () => {
|
|
121
|
+
test("returns a defensive copy when found", () => {
|
|
122
|
+
const map = new Map<string, ToolRequest>()
|
|
123
|
+
const req = makeRequest()
|
|
124
|
+
map.set("req-1", req)
|
|
125
|
+
const result = getToolRequest(map, "req-1")
|
|
126
|
+
expect(result).toEqual(req)
|
|
127
|
+
// defensive copy
|
|
128
|
+
result!.status = "canceled"
|
|
129
|
+
expect(map.get("req-1")?.status).toBe("pending")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("returns null when not found", () => {
|
|
133
|
+
const map = new Map<string, ToolRequest>()
|
|
134
|
+
expect(getToolRequest(map, "missing")).toBeNull()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// listPendingToolRequests
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
describe("listPendingToolRequests", () => {
|
|
143
|
+
test("returns only pending requests for the given chatId", () => {
|
|
144
|
+
const map = new Map<string, ToolRequest>()
|
|
145
|
+
map.set("r1", makeRequest({ id: "r1", chatId: "chat-1", status: "pending" }))
|
|
146
|
+
map.set("r2", makeRequest({ id: "r2", chatId: "chat-1", status: "answered" }))
|
|
147
|
+
map.set("r3", makeRequest({ id: "r3", chatId: "chat-2", status: "pending" }))
|
|
148
|
+
const result = listPendingToolRequests(map, "chat-1")
|
|
149
|
+
expect(result).toHaveLength(1)
|
|
150
|
+
expect(result[0].id).toBe("r1")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("returns empty array when no pending requests exist for chat", () => {
|
|
154
|
+
const map = new Map<string, ToolRequest>()
|
|
155
|
+
map.set("r1", makeRequest({ id: "r1", chatId: "chat-1", status: "answered" }))
|
|
156
|
+
expect(listPendingToolRequests(map, "chat-1")).toEqual([])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("returns defensive copies", () => {
|
|
160
|
+
const map = new Map<string, ToolRequest>()
|
|
161
|
+
map.set("r1", makeRequest({ id: "r1", chatId: "chat-1", status: "pending" }))
|
|
162
|
+
const results = listPendingToolRequests(map, "chat-1")
|
|
163
|
+
results[0].status = "canceled"
|
|
164
|
+
expect(map.get("r1")?.status).toBe("pending")
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// scanAllToolRequests
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
describe("scanAllToolRequests", () => {
|
|
173
|
+
test("returns defensive copies of every entry in the map", () => {
|
|
174
|
+
const map = new Map<string, ToolRequest>()
|
|
175
|
+
map.set("r1", makeRequest({ id: "r1" }))
|
|
176
|
+
map.set("r2", makeRequest({ id: "r2", chatId: "chat-2", status: "answered" }))
|
|
177
|
+
const all = scanAllToolRequests(map)
|
|
178
|
+
expect(all).toHaveLength(2)
|
|
179
|
+
// defensive copy
|
|
180
|
+
all[0].status = "canceled"
|
|
181
|
+
expect(map.get("r1")?.status).toBe("pending")
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("returns empty array for an empty map", () => {
|
|
185
|
+
expect(scanAllToolRequests(new Map())).toEqual([])
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// deleteToolRequestsForChat
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
describe("deleteToolRequestsForChat", () => {
|
|
194
|
+
test("removes all entries belonging to the chat", () => {
|
|
195
|
+
const map = new Map<string, ToolRequest>()
|
|
196
|
+
map.set("r1", makeRequest({ id: "r1", chatId: "chat-1" }))
|
|
197
|
+
map.set("r2", makeRequest({ id: "r2", chatId: "chat-1" }))
|
|
198
|
+
map.set("r3", makeRequest({ id: "r3", chatId: "chat-2" }))
|
|
199
|
+
deleteToolRequestsForChat(map, "chat-1")
|
|
200
|
+
expect(map.size).toBe(1)
|
|
201
|
+
expect(map.has("r3")).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test("leaves the map unchanged when no entries belong to the chat", () => {
|
|
205
|
+
const map = new Map<string, ToolRequest>()
|
|
206
|
+
map.set("r1", makeRequest({ id: "r1", chatId: "chat-2" }))
|
|
207
|
+
deleteToolRequestsForChat(map, "chat-1")
|
|
208
|
+
expect(map.size).toBe(1)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-request read-model and write-path layers extracted from event-store.ts.
|
|
3
|
+
*
|
|
4
|
+
* All read functions are pure folds over the in-memory `toolRequestsById` map.
|
|
5
|
+
* Write functions (putToolRequest, resolveToolRequest) accept a ToolRequestWriteDeps
|
|
6
|
+
* so they can append to the event log without importing disk IO directly.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolRequest, ToolRequestDecision, ToolRequestStatus } from "../shared/permission-policy"
|
|
9
|
+
import type { StoreEvent, ToolRequestEvent } from "./events"
|
|
10
|
+
import {
|
|
11
|
+
buildPutToolRequestEvent,
|
|
12
|
+
buildResolveToolRequestEvent,
|
|
13
|
+
} from "./event-store-write-ops"
|
|
14
|
+
|
|
15
|
+
// ─── Write-path deps ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface ToolRequestWriteDeps {
|
|
18
|
+
readonly toolRequestsById: Map<string, ToolRequest>
|
|
19
|
+
readonly toolRequestsLogPath: string
|
|
20
|
+
append: (filePath: string, event: StoreEvent) => Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply a single `ToolRequestEvent` to the in-memory `toolRequestsById` map
|
|
25
|
+
* (mutates in place).
|
|
26
|
+
*
|
|
27
|
+
* Mirrors the two `case "tool_request_*"` branches that previously lived
|
|
28
|
+
* inside `EventStore.applyEvent`.
|
|
29
|
+
*/
|
|
30
|
+
export function applyToolRequestEvent(
|
|
31
|
+
toolRequestsById: Map<string, ToolRequest>,
|
|
32
|
+
event: ToolRequestEvent,
|
|
33
|
+
): void {
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case "tool_request_put": {
|
|
36
|
+
toolRequestsById.set(event.request.id, { ...event.request })
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
case "tool_request_resolved": {
|
|
40
|
+
const existing = toolRequestsById.get(event.id)
|
|
41
|
+
if (!existing) break
|
|
42
|
+
toolRequestsById.set(event.id, {
|
|
43
|
+
...existing,
|
|
44
|
+
status: event.status,
|
|
45
|
+
decision: event.decision ?? existing.decision,
|
|
46
|
+
resolvedAt: event.resolvedAt,
|
|
47
|
+
mismatchReason: event.mismatchReason,
|
|
48
|
+
})
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return a defensive copy of the tool request with the given id, or `null`
|
|
56
|
+
* if not found.
|
|
57
|
+
*/
|
|
58
|
+
export function getToolRequest(
|
|
59
|
+
toolRequestsById: Map<string, ToolRequest>,
|
|
60
|
+
id: string,
|
|
61
|
+
): ToolRequest | null {
|
|
62
|
+
const req = toolRequestsById.get(id)
|
|
63
|
+
return req ? { ...req } : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return defensive copies of all pending tool requests for the given chat.
|
|
68
|
+
*/
|
|
69
|
+
export function listPendingToolRequests(
|
|
70
|
+
toolRequestsById: Map<string, ToolRequest>,
|
|
71
|
+
chatId: string,
|
|
72
|
+
): ToolRequest[] {
|
|
73
|
+
const out: ToolRequest[] = []
|
|
74
|
+
for (const req of toolRequestsById.values()) {
|
|
75
|
+
if (req.chatId !== chatId) continue
|
|
76
|
+
if (req.status !== "pending") continue
|
|
77
|
+
out.push({ ...req })
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Return defensive copies of every tool request in the map.
|
|
84
|
+
*/
|
|
85
|
+
export function scanAllToolRequests(
|
|
86
|
+
toolRequestsById: Map<string, ToolRequest>,
|
|
87
|
+
): ToolRequest[] {
|
|
88
|
+
return [...toolRequestsById.values()].map((req) => ({ ...req }))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Remove every tool request belonging to the given chat from the map
|
|
93
|
+
* (mutates in place). Called when a chat is deleted.
|
|
94
|
+
*/
|
|
95
|
+
export function deleteToolRequestsForChat(
|
|
96
|
+
toolRequestsById: Map<string, ToolRequest>,
|
|
97
|
+
chatId: string,
|
|
98
|
+
): void {
|
|
99
|
+
for (const [id, req] of toolRequestsById) {
|
|
100
|
+
if (req.chatId === chatId) {
|
|
101
|
+
toolRequestsById.delete(id)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Write-path functions ──────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** Persist + apply a new tool request. */
|
|
109
|
+
export async function putToolRequest(
|
|
110
|
+
deps: ToolRequestWriteDeps,
|
|
111
|
+
req: ToolRequest,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const event = buildPutToolRequestEvent(req)
|
|
114
|
+
applyToolRequestEvent(deps.toolRequestsById, event)
|
|
115
|
+
await deps.append(deps.toolRequestsLogPath, event)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Persist + apply a tool request resolution. */
|
|
119
|
+
export async function resolveToolRequest(
|
|
120
|
+
deps: ToolRequestWriteDeps,
|
|
121
|
+
id: string,
|
|
122
|
+
args: { status: ToolRequestStatus; decision?: ToolRequestDecision; resolvedAt: number; mismatchReason?: string },
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const event = buildResolveToolRequestEvent(deps.toolRequestsById, id, args)
|
|
125
|
+
applyToolRequestEvent(deps.toolRequestsById, event)
|
|
126
|
+
await deps.append(deps.toolRequestsLogPath, event)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Re-export types consumed by event-store.ts method signatures so callers
|
|
130
|
+
// can import them from one place.
|
|
131
|
+
export type { ToolRequest, ToolRequestDecision, ToolRequestStatus }
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complex chat-transcript write operations extracted from event-store.ts.
|
|
3
|
+
*
|
|
4
|
+
* Contains methods that directly manipulate the transcript JSONL file or
|
|
5
|
+
* perform multi-step chat mutations (fork, delete, prune, appendMessage).
|
|
6
|
+
* All IO is performed through the injected StorageBackend.
|
|
7
|
+
*
|
|
8
|
+
* This module is an adapter (.adapter.ts) because it performs disk IO.
|
|
9
|
+
* It must NOT import from event-store.ts (no circular deps).
|
|
10
|
+
*/
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
import { LOG_PREFIX } from "../shared/branding"
|
|
13
|
+
import { log } from "../shared/log"
|
|
14
|
+
import { STORE_VERSION } from "../shared/types"
|
|
15
|
+
import type { TranscriptEntry } from "../shared/types"
|
|
16
|
+
import type { ToolRequest } from "../shared/permission-policy"
|
|
17
|
+
import type { ChatEvent, ChatRecord, StoreEvent } from "./events"
|
|
18
|
+
import { cloneTranscriptEntries } from "./events"
|
|
19
|
+
import type { StorageBackend } from "./storage/backend"
|
|
20
|
+
import { getForkedChatTitle, logSendToStartingProfile } from "./event-store-helpers"
|
|
21
|
+
import {
|
|
22
|
+
buildArchiveChatEvent,
|
|
23
|
+
buildChatProviderEvent,
|
|
24
|
+
buildPendingForkSessionTokenEvent,
|
|
25
|
+
buildPlanModeEvent,
|
|
26
|
+
buildUnarchiveChatEvent,
|
|
27
|
+
} from "./event-store-write-ops"
|
|
28
|
+
import { deleteToolRequestsForChat } from "./event-store-tool-requests"
|
|
29
|
+
import { applyChatMessageMetadata } from "./event-store-chat-lifecycle"
|
|
30
|
+
import type { TranscriptCache } from "./event-store-messages.adapter"
|
|
31
|
+
import type { ChatOp } from "../shared/chat-ops"
|
|
32
|
+
|
|
33
|
+
const STALE_EMPTY_CHAT_MAX_AGE_MS = 30 * 60 * 1000
|
|
34
|
+
|
|
35
|
+
// ─── Deps interface ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface ChatTranscriptWriteDeps {
|
|
38
|
+
readonly storage: StorageBackend
|
|
39
|
+
readonly transcriptsDir: string
|
|
40
|
+
readonly dataDir: string
|
|
41
|
+
readonly transcriptCache: TranscriptCache
|
|
42
|
+
readonly seenMessageIdsByChatId: Map<string, Set<string>>
|
|
43
|
+
readonly chatsById: Map<string, ChatRecord>
|
|
44
|
+
readonly toolRequestsById: Map<string, ToolRequest>
|
|
45
|
+
readonly chatsLogPath: string
|
|
46
|
+
readonly turnsLogPath: string
|
|
47
|
+
/** Read the current write-chain promise. */
|
|
48
|
+
getWriteChain: () => Promise<void>
|
|
49
|
+
/** Replace the write-chain promise. */
|
|
50
|
+
setWriteChain: (p: Promise<void>) => void
|
|
51
|
+
/** Core append: writes event to disk + applies to in-memory state. */
|
|
52
|
+
append: <T extends StoreEvent>(filePath: string, event: T) => Promise<void>
|
|
53
|
+
/** Returns transcript entries for a chat (from cache or disk). */
|
|
54
|
+
getMessages: (chatId: string) => TranscriptEntry[]
|
|
55
|
+
/** Loads the transcript into cache (populating seen messageIds) without cloning. */
|
|
56
|
+
ensureTranscriptLoaded: (chatId: string) => void
|
|
57
|
+
/** Returns (or lazily creates) the seen-messageId dedup set for a chat. */
|
|
58
|
+
getSeenMessageIds: (chatId: string) => Set<string>
|
|
59
|
+
/** Returns pending tool requests for a chat. */
|
|
60
|
+
listPendingToolRequests: (chatId: string) => ToolRequest[]
|
|
61
|
+
/** Records a delta op for the `chat.ops` broadcast path. */
|
|
62
|
+
recordChatOp: (chatId: string, op: ChatOp) => void
|
|
63
|
+
/** Drops the chat's op-log (chat deleted/pruned). */
|
|
64
|
+
clearChatOps: (chatId: string) => void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Private helpers ───────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function transcriptPath(deps: ChatTranscriptWriteDeps, chatId: string): string {
|
|
70
|
+
return path.join(deps.transcriptsDir, `${chatId}.jsonl`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireChat(deps: ChatTranscriptWriteDeps, chatId: string): ChatRecord {
|
|
74
|
+
const chat = deps.chatsById.get(chatId)
|
|
75
|
+
if (!chat || chat.deletedAt) throw new Error("Chat not found")
|
|
76
|
+
return chat
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Exported functions ────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/** Removes the subagent-results directory for a deleted chat (best-effort). */
|
|
82
|
+
export async function removeSubagentResultsDir(
|
|
83
|
+
deps: ChatTranscriptWriteDeps,
|
|
84
|
+
projectId: string,
|
|
85
|
+
chatId: string,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const dir = path.join(
|
|
88
|
+
deps.dataDir, "projects", projectId, "chats", chatId, "subagent-results",
|
|
89
|
+
)
|
|
90
|
+
try {
|
|
91
|
+
await deps.storage.remove(dir, { recursive: true })
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.warn(`${LOG_PREFIX} subagent-results cleanup failed`, { chatId, err })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Forks a chat: creates a new chat sharing the source transcript and session. */
|
|
98
|
+
export async function forkChat(
|
|
99
|
+
deps: ChatTranscriptWriteDeps,
|
|
100
|
+
sourceChatId: string,
|
|
101
|
+
): Promise<ChatRecord> {
|
|
102
|
+
const sourceChat = requireChat(deps, sourceChatId)
|
|
103
|
+
const sourceProvider = sourceChat.provider
|
|
104
|
+
if (!sourceProvider) throw new Error("Chat cannot be forked")
|
|
105
|
+
|
|
106
|
+
const sourceSessionToken =
|
|
107
|
+
sourceChat.sessionTokensByProvider[sourceProvider]
|
|
108
|
+
?? (sourceChat.pendingForkSessionToken?.provider === sourceProvider
|
|
109
|
+
? sourceChat.pendingForkSessionToken.token
|
|
110
|
+
: null)
|
|
111
|
+
if (!sourceSessionToken) throw new Error("Chat cannot be forked")
|
|
112
|
+
|
|
113
|
+
const chatId = crypto.randomUUID()
|
|
114
|
+
const createdAt = Date.now()
|
|
115
|
+
const createEvent: ChatEvent = {
|
|
116
|
+
v: STORE_VERSION,
|
|
117
|
+
type: "chat_created",
|
|
118
|
+
timestamp: createdAt,
|
|
119
|
+
chatId,
|
|
120
|
+
projectId: sourceChat.projectId,
|
|
121
|
+
title: getForkedChatTitle(sourceChat.title),
|
|
122
|
+
...(sourceChat.stackId !== undefined ? { stackId: sourceChat.stackId } : {}),
|
|
123
|
+
...(sourceChat.stackBindings !== undefined
|
|
124
|
+
? { stackBindings: sourceChat.stackBindings.map((b) => ({ ...b })) }
|
|
125
|
+
: {}),
|
|
126
|
+
}
|
|
127
|
+
await deps.append(deps.chatsLogPath, createEvent)
|
|
128
|
+
|
|
129
|
+
const providerEv = buildChatProviderEvent(deps.chatsById, chatId, sourceProvider)
|
|
130
|
+
if (providerEv) await deps.append(deps.chatsLogPath, providerEv)
|
|
131
|
+
|
|
132
|
+
const planEv = buildPlanModeEvent(deps.chatsById, chatId, sourceChat.planMode)
|
|
133
|
+
if (planEv) await deps.append(deps.chatsLogPath, planEv)
|
|
134
|
+
|
|
135
|
+
const forkTokenEv = buildPendingForkSessionTokenEvent(
|
|
136
|
+
deps.chatsById, chatId, { provider: sourceProvider, token: sourceSessionToken },
|
|
137
|
+
)
|
|
138
|
+
if (forkTokenEv) await deps.append(deps.turnsLogPath, forkTokenEv)
|
|
139
|
+
|
|
140
|
+
const sourceEntries = deps.getMessages(sourceChatId)
|
|
141
|
+
if (sourceEntries.length > 0) {
|
|
142
|
+
const tPath = transcriptPath(deps, chatId)
|
|
143
|
+
const payload = sourceEntries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
144
|
+
const newChain = deps.getWriteChain().then(async () => {
|
|
145
|
+
await deps.storage.mkdir(deps.transcriptsDir)
|
|
146
|
+
await deps.storage.writeText(tPath, `${payload}\n`)
|
|
147
|
+
const chat = deps.chatsById.get(chatId)
|
|
148
|
+
if (chat) {
|
|
149
|
+
chat.hasMessages = true
|
|
150
|
+
chat.updatedAt = Math.max(chat.updatedAt, createdAt)
|
|
151
|
+
}
|
|
152
|
+
if (deps.transcriptCache.has(chatId)) {
|
|
153
|
+
deps.transcriptCache.set(chatId, cloneTranscriptEntries(sourceEntries))
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
deps.setWriteChain(newChain)
|
|
157
|
+
await newChain
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return deps.chatsById.get(chatId)!
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Deletes a chat: appends a delete event, clears tool requests, and removes the results dir. */
|
|
164
|
+
export async function deleteChat(
|
|
165
|
+
deps: ChatTranscriptWriteDeps,
|
|
166
|
+
chatId: string,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
const chat = requireChat(deps, chatId)
|
|
169
|
+
const projectId = chat.projectId
|
|
170
|
+
const event: ChatEvent = {
|
|
171
|
+
v: STORE_VERSION,
|
|
172
|
+
type: "chat_deleted",
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
chatId,
|
|
175
|
+
}
|
|
176
|
+
await deps.append(deps.chatsLogPath, event)
|
|
177
|
+
deleteToolRequestsForChat(deps.toolRequestsById, chatId)
|
|
178
|
+
deps.clearChatOps(chatId)
|
|
179
|
+
await removeSubagentResultsDir(deps, projectId, chatId)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Archives a chat. */
|
|
183
|
+
export async function archiveChat(
|
|
184
|
+
deps: ChatTranscriptWriteDeps,
|
|
185
|
+
chatId: string,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
await deps.append(deps.chatsLogPath, buildArchiveChatEvent(deps.chatsById, chatId))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Unarchives a chat. */
|
|
191
|
+
export async function unarchiveChat(
|
|
192
|
+
deps: ChatTranscriptWriteDeps,
|
|
193
|
+
chatId: string,
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
await deps.append(deps.chatsLogPath, buildUnarchiveChatEvent(deps.chatsById, chatId))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Prunes empty chats that have been idle past the max age. Returns pruned chat IDs. */
|
|
199
|
+
export async function pruneStaleEmptyChats(
|
|
200
|
+
deps: ChatTranscriptWriteDeps,
|
|
201
|
+
args?: {
|
|
202
|
+
now?: number
|
|
203
|
+
maxAgeMs?: number
|
|
204
|
+
activeChatIds?: Iterable<string>
|
|
205
|
+
protectedChatIds?: Iterable<string>
|
|
206
|
+
},
|
|
207
|
+
): Promise<string[]> {
|
|
208
|
+
const now = args?.now ?? Date.now()
|
|
209
|
+
const maxAgeMs = args?.maxAgeMs ?? STALE_EMPTY_CHAT_MAX_AGE_MS
|
|
210
|
+
const protectedChatIds = new Set([
|
|
211
|
+
...(args?.activeChatIds ?? []),
|
|
212
|
+
...(args?.protectedChatIds ?? []),
|
|
213
|
+
])
|
|
214
|
+
const prunedChatIds: string[] = []
|
|
215
|
+
|
|
216
|
+
for (const chat of deps.chatsById.values()) {
|
|
217
|
+
if (chat.deletedAt || chat.archivedAt || protectedChatIds.has(chat.id)) continue
|
|
218
|
+
if (now - chat.createdAt < maxAgeMs) continue
|
|
219
|
+
if (chat.hasMessages) continue
|
|
220
|
+
if (deps.getMessages(chat.id).length > 0) {
|
|
221
|
+
chat.hasMessages = true
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const event: ChatEvent = {
|
|
226
|
+
v: STORE_VERSION,
|
|
227
|
+
type: "chat_deleted",
|
|
228
|
+
timestamp: now,
|
|
229
|
+
chatId: chat.id,
|
|
230
|
+
}
|
|
231
|
+
await deps.append(deps.chatsLogPath, event)
|
|
232
|
+
|
|
233
|
+
const tPath = transcriptPath(deps, chat.id)
|
|
234
|
+
await deps.storage.remove(tPath)
|
|
235
|
+
deps.transcriptCache.invalidate(chat.id)
|
|
236
|
+
deps.clearChatOps(chat.id)
|
|
237
|
+
await removeSubagentResultsDir(deps, chat.projectId, chat.id)
|
|
238
|
+
|
|
239
|
+
prunedChatIds.push(chat.id)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return prunedChatIds
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Appends a transcript entry for a chat, with deduplication by messageId. */
|
|
246
|
+
export async function appendMessage(
|
|
247
|
+
deps: ChatTranscriptWriteDeps,
|
|
248
|
+
chatId: string,
|
|
249
|
+
entry: TranscriptEntry,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
requireChat(deps, chatId)
|
|
252
|
+
const payload = `${JSON.stringify(entry)}\n`
|
|
253
|
+
const tPath = transcriptPath(deps, chatId)
|
|
254
|
+
const queuedAt = performance.now()
|
|
255
|
+
const newChain = deps.getWriteChain().then(async () => {
|
|
256
|
+
const startedAt = performance.now()
|
|
257
|
+
const queueDelayMs = Number((startedAt - queuedAt).toFixed(1))
|
|
258
|
+
const mid = entry.messageId
|
|
259
|
+
if (typeof mid === "string" && mid.length > 0) {
|
|
260
|
+
deps.ensureTranscriptLoaded(chatId)
|
|
261
|
+
const seen = deps.getSeenMessageIds(chatId)
|
|
262
|
+
if (seen.has(mid)) {
|
|
263
|
+
logSendToStartingProfile("event_store.append_message_dedup", {
|
|
264
|
+
chatId,
|
|
265
|
+
messageId: mid,
|
|
266
|
+
kind: entry.kind,
|
|
267
|
+
})
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
seen.add(mid)
|
|
271
|
+
}
|
|
272
|
+
await deps.storage.mkdir(deps.transcriptsDir)
|
|
273
|
+
const beforeAppendAt = performance.now()
|
|
274
|
+
await deps.storage.appendText(tPath, payload)
|
|
275
|
+
const afterAppendAt = performance.now()
|
|
276
|
+
applyChatMessageMetadata(deps.chatsById, chatId, entry)
|
|
277
|
+
deps.transcriptCache.appendTo(chatId, { ...entry })
|
|
278
|
+
deps.recordChatOp(chatId, { kind: "entries.append", entries: [{ ...entry }] })
|
|
279
|
+
logSendToStartingProfile("event_store.append_message", {
|
|
280
|
+
chatId,
|
|
281
|
+
entryId: entry._id,
|
|
282
|
+
kind: entry.kind,
|
|
283
|
+
payloadBytes: payload.length,
|
|
284
|
+
queueDelayMs,
|
|
285
|
+
appendMs: Number((afterAppendAt - beforeAppendAt).toFixed(1)),
|
|
286
|
+
totalMs: Number((afterAppendAt - queuedAt).toFixed(1)),
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
deps.setWriteChain(newChain)
|
|
290
|
+
return newChain
|
|
291
|
+
}
|