@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
|
@@ -1,260 +1,57 @@
|
|
|
1
1
|
import { homedir } from "node:os"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import { getDataDir
|
|
4
|
-
import type { AnyValue } from "../shared/errors"
|
|
3
|
+
import { getDataDir } from "../shared/branding"
|
|
5
4
|
import { log } from "../shared/log"
|
|
6
5
|
import type { StorageBackend } from "./storage/backend"
|
|
7
6
|
import { FsStorageBackend } from "./storage/fs-storage.adapter"
|
|
8
|
-
import type { AgentProvider, ChatHistoryPage,
|
|
9
|
-
import { STORE_VERSION } from "../shared/types"
|
|
7
|
+
import type { AgentProvider, ChatHistoryPage, QueuedChatMessage, SlashCommand, StackBinding, SubagentRunSnapshot, TranscriptEntry } from "../shared/types"
|
|
10
8
|
import type { AutoContinueEvent } from "./auto-continue/events"
|
|
11
9
|
import {
|
|
12
|
-
type ChatEvent,
|
|
13
|
-
type ChatRecord,
|
|
14
|
-
type ChatTimingState,
|
|
15
10
|
type OrchestrationEvent,
|
|
16
|
-
type OrchRunRecord,
|
|
17
|
-
type OrchTaskRecord,
|
|
18
|
-
type ProjectEvent,
|
|
19
|
-
type QueuedMessageEvent,
|
|
20
|
-
type SnapshotFile,
|
|
21
|
-
type StackEvent,
|
|
22
11
|
type StackRecord,
|
|
23
12
|
type StoreEvent,
|
|
24
13
|
type StoreState,
|
|
25
14
|
type SubagentRunEvent,
|
|
26
|
-
type ToolRequestEvent,
|
|
27
|
-
type TurnEvent,
|
|
28
15
|
type TurnRunConfig,
|
|
29
|
-
cloneTranscriptEntries,
|
|
30
16
|
createEmptyState,
|
|
31
17
|
} from "./events"
|
|
32
|
-
import type { OrchRunSnapshot
|
|
33
|
-
import { isTaskEventType, nextRunStatus, nextTaskState } from "./orchestration-state-machine"
|
|
18
|
+
import type { OrchRunSnapshot } from "../shared/orchestration-types"
|
|
34
19
|
import type { ChatPermissionPolicyOverride, ToolRequest, ToolRequestDecision, ToolRequestStatus } from "../shared/permission-policy"
|
|
35
|
-
import { resolveLocalPath } from "./paths"
|
|
36
20
|
import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
|
|
37
21
|
import type { PushEvent, PushEventStore } from "./push/events"
|
|
38
22
|
import type { ShareEvent } from "./session-share/share-projection"
|
|
39
|
-
import {
|
|
40
|
-
|
|
23
|
+
import {
|
|
24
|
+
getSubagentRuns as getSubagentRunsFromMap,
|
|
25
|
+
runningSubagentRuns as runningSubagentRunsFromMap,
|
|
26
|
+
appendSubagentEvent as appendSubagentEventFn,
|
|
27
|
+
type AppendSubagentDeps,
|
|
28
|
+
} from "./event-store-subagent"
|
|
29
|
+
import {
|
|
30
|
+
getToolRequest as getToolRequestFromMap,
|
|
31
|
+
listPendingToolRequests as listPendingToolRequestsFromMap,
|
|
32
|
+
scanAllToolRequests as scanAllToolRequestsFromMap,
|
|
33
|
+
putToolRequest as putToolRequestFn,
|
|
34
|
+
resolveToolRequest as resolveToolRequestFn,
|
|
35
|
+
type ToolRequestWriteDeps,
|
|
36
|
+
} from "./event-store-tool-requests"
|
|
37
|
+
import * as OrchSubscription from "./event-store-orch-subscription"
|
|
38
|
+
import { applyStoreEvent } from "./event-store-apply"
|
|
39
|
+
import * as PeripheralEvents from "./event-store-peripheral-events.adapter"
|
|
40
|
+
import * as MessageRead from "./event-store-messages.adapter"
|
|
41
|
+
import * as EntityWrite from "./event-store-entity-write"
|
|
42
|
+
import * as TranscriptWrite from "./event-store-transcript-write.adapter"
|
|
43
|
+
import {
|
|
44
|
+
initializeEventStore,
|
|
45
|
+
getLegacyTranscriptStats as getLegacyTranscriptStatsFn,
|
|
46
|
+
hasLegacyTranscriptData as hasLegacyTranscriptDataFn,
|
|
47
|
+
snapshotAndTruncateLogs as snapshotAndTruncateLogsFn,
|
|
48
|
+
migrateLegacyTranscripts as migrateLegacyTranscriptsFn,
|
|
49
|
+
type EventStoreInitDeps,
|
|
50
|
+
type LegacyTranscriptStats,
|
|
51
|
+
} from "./event-store-init"
|
|
41
52
|
|
|
42
|
-
const SNAPSHOT_THRESHOLD_BYTES = 2 * 1024 * 1024
|
|
43
|
-
const STALE_EMPTY_CHAT_MAX_AGE_MS = 30 * 60 * 1000
|
|
44
53
|
const SIDEBAR_PROJECT_ORDER_FILE = "sidebar-order.json"
|
|
45
54
|
|
|
46
|
-
function normalizeSidebarProjectOrder<T>(value: T) {
|
|
47
|
-
if (!Array.isArray(value)) {
|
|
48
|
-
return []
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const seen = new Set<string>()
|
|
52
|
-
const projectIds: string[] = []
|
|
53
|
-
for (const entry of value) {
|
|
54
|
-
if (typeof entry !== "string") continue
|
|
55
|
-
const projectId = entry.trim()
|
|
56
|
-
if (!projectId || seen.has(projectId)) continue
|
|
57
|
-
seen.add(projectId)
|
|
58
|
-
projectIds.push(projectId)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return projectIds
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isSendToStartingProfilingEnabled() {
|
|
65
|
-
return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function logSendToStartingProfile(stage: string, details?: Record<string, unknown>) {
|
|
69
|
-
if (!isSendToStartingProfilingEnabled()) {
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
74
|
-
stage,
|
|
75
|
-
...details,
|
|
76
|
-
}))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface LegacyTranscriptStats {
|
|
80
|
-
hasLegacyData: boolean
|
|
81
|
-
sources: Array<"snapshot" | "messages_log">
|
|
82
|
-
chatCount: number
|
|
83
|
-
entryCount: number
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface TranscriptPageResult {
|
|
87
|
-
entries: TranscriptEntry[]
|
|
88
|
-
hasOlder: boolean
|
|
89
|
-
olderCursor: string | null
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface ParsedReplayEvent {
|
|
93
|
-
event: StoreEvent
|
|
94
|
-
sourceIndex: number
|
|
95
|
-
lineIndex: number
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getReplayEventPriority(event: StoreEvent): number {
|
|
99
|
-
const discriminator = "type" in event ? event.type : event.kind
|
|
100
|
-
switch (discriminator) {
|
|
101
|
-
case "project_opened":
|
|
102
|
-
case "project_removed":
|
|
103
|
-
case "sidebar_project_order_set":
|
|
104
|
-
case "project_star_set":
|
|
105
|
-
return 0
|
|
106
|
-
case "chat_created":
|
|
107
|
-
return 1
|
|
108
|
-
case "chat_renamed":
|
|
109
|
-
case "chat_provider_set":
|
|
110
|
-
case "chat_plan_mode_set":
|
|
111
|
-
return 2
|
|
112
|
-
case "message_appended":
|
|
113
|
-
return 3
|
|
114
|
-
case "queued_message_enqueued":
|
|
115
|
-
case "queued_message_removed":
|
|
116
|
-
return 4
|
|
117
|
-
case "turn_started":
|
|
118
|
-
return 5
|
|
119
|
-
case "session_token_set":
|
|
120
|
-
case "session_commands_loaded":
|
|
121
|
-
return 6
|
|
122
|
-
case "pending_fork_session_token_set":
|
|
123
|
-
return 6
|
|
124
|
-
case "turn_cancelled":
|
|
125
|
-
return 7
|
|
126
|
-
case "turn_finished":
|
|
127
|
-
case "turn_failed":
|
|
128
|
-
return 8
|
|
129
|
-
case "chat_read_state_set":
|
|
130
|
-
case "chat_source_hash_set":
|
|
131
|
-
case "chat_policy_override_set":
|
|
132
|
-
case "chat_compact_failures_set":
|
|
133
|
-
return 9
|
|
134
|
-
case "chat_deleted":
|
|
135
|
-
case "chat_archived":
|
|
136
|
-
case "chat_unarchived":
|
|
137
|
-
return 10
|
|
138
|
-
case "auto_continue_proposed":
|
|
139
|
-
case "auto_continue_accepted":
|
|
140
|
-
case "auto_continue_rescheduled":
|
|
141
|
-
case "auto_continue_cancelled":
|
|
142
|
-
case "auto_continue_fired":
|
|
143
|
-
case "loop_armed":
|
|
144
|
-
case "loop_disarmed":
|
|
145
|
-
return 11
|
|
146
|
-
case "stack_added":
|
|
147
|
-
case "stack_removed":
|
|
148
|
-
case "stack_renamed":
|
|
149
|
-
case "stack_project_added":
|
|
150
|
-
case "stack_project_removed":
|
|
151
|
-
return 0
|
|
152
|
-
case "subagent_run_started":
|
|
153
|
-
case "subagent_message_delta":
|
|
154
|
-
case "subagent_entry_appended":
|
|
155
|
-
case "subagent_run_completed":
|
|
156
|
-
case "subagent_run_failed":
|
|
157
|
-
case "subagent_run_cancelled":
|
|
158
|
-
case "subagent_tool_pending":
|
|
159
|
-
case "subagent_tool_resolved":
|
|
160
|
-
return 5
|
|
161
|
-
// tool_request_put shares priority 5 with subagent_* events; sourceIndex
|
|
162
|
-
// tie-break orders them (tool-requests has sourceIndex 7, turns has 5).
|
|
163
|
-
case "tool_request_put":
|
|
164
|
-
return 5
|
|
165
|
-
case "tool_request_resolved":
|
|
166
|
-
return 6
|
|
167
|
-
case "orch_run_created":
|
|
168
|
-
case "orch_worktree_provisioned":
|
|
169
|
-
case "orch_worktree_init_started":
|
|
170
|
-
case "orch_worktree_init_completed":
|
|
171
|
-
case "orch_task_claimed":
|
|
172
|
-
case "orch_phase_started":
|
|
173
|
-
case "orch_phase_completed":
|
|
174
|
-
case "orch_gate_opened":
|
|
175
|
-
case "orch_gate_resolved":
|
|
176
|
-
case "orch_scope_overlap_flagged":
|
|
177
|
-
case "orch_config_warning":
|
|
178
|
-
case "orch_verify_started":
|
|
179
|
-
case "orch_verify_completed":
|
|
180
|
-
case "orch_task_committed":
|
|
181
|
-
case "orch_task_failed":
|
|
182
|
-
case "orch_task_requeued":
|
|
183
|
-
case "orch_run_completed":
|
|
184
|
-
case "orch_run_cancelled":
|
|
185
|
-
return 5
|
|
186
|
-
default: {
|
|
187
|
-
const _exhaustive: never = discriminator
|
|
188
|
-
throw new Error(`Unhandled replay event type: ${String(_exhaustive)}`)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function encodeHistoryCursor(index: number) {
|
|
194
|
-
return `idx:${index}`
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function decodeCursor(cursor: string) {
|
|
198
|
-
if (cursor.startsWith("idx:")) {
|
|
199
|
-
const value = Number.parseInt(cursor.slice("idx:".length), 10)
|
|
200
|
-
if (!Number.isInteger(value) || value < 0) {
|
|
201
|
-
throw new Error("Invalid history cursor")
|
|
202
|
-
}
|
|
203
|
-
return value
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
throw new Error("Invalid history cursor")
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function slashCommandsEqual(a: SlashCommand[], b: SlashCommand[]) {
|
|
210
|
-
if (a.length !== b.length) return false
|
|
211
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
212
|
-
const ai = a[i]
|
|
213
|
-
const bi = b[i]
|
|
214
|
-
if (ai.name !== bi.name || ai.description !== bi.description || ai.argumentHint !== bi.argumentHint) {
|
|
215
|
-
return false
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return true
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// `context_window_updated` entries are token-readout noise emitted once per
|
|
222
|
-
// stream tick — only the latest value matters. A single turn can emit hundreds
|
|
223
|
-
// of them, and the bounded live window (getMessagesPageFromEntries) counts each
|
|
224
|
-
// 1:1 against the limit, so a flood evicts real turns (e.g. the user_prompt)
|
|
225
|
-
// out of the snapshot the client renders. Collapsing each maximal run of
|
|
226
|
-
// consecutive cwu entries to its last entry keeps the latest readout while
|
|
227
|
-
// freeing window budget for real turns. Applied only on the live-window read
|
|
228
|
-
// path (not getMessages), so getLatestContextWindowUsage / full-transcript
|
|
229
|
-
// export / importer still observe every persisted cwu.
|
|
230
|
-
function coalesceContextWindowUpdates(entries: TranscriptEntry[]): TranscriptEntry[] {
|
|
231
|
-
const result: TranscriptEntry[] = []
|
|
232
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
233
|
-
const entry = entries[index]!
|
|
234
|
-
const next = entries[index + 1]
|
|
235
|
-
if (entry.kind === "context_window_updated" && next?.kind === "context_window_updated") {
|
|
236
|
-
// Drop this cwu; the last of the run survives.
|
|
237
|
-
continue
|
|
238
|
-
}
|
|
239
|
-
result.push(entry)
|
|
240
|
-
}
|
|
241
|
-
return result
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function getHistorySnapshot(page: TranscriptPageResult, recentLimit: number): ChatHistorySnapshot {
|
|
245
|
-
return {
|
|
246
|
-
hasOlder: page.hasOlder,
|
|
247
|
-
olderCursor: page.olderCursor,
|
|
248
|
-
recentLimit,
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getForkedChatTitle(title: string) {
|
|
253
|
-
const trimmed = title.trim()
|
|
254
|
-
if (!trimmed) return "Fork: New Chat"
|
|
255
|
-
return trimmed.startsWith("Fork: ") ? trimmed : `Fork: ${trimmed}`
|
|
256
|
-
}
|
|
257
|
-
|
|
258
55
|
export class EventStore implements PushEventStore {
|
|
259
56
|
readonly dataDir: string
|
|
260
57
|
readonly state: StoreState = createEmptyState()
|
|
@@ -283,9 +80,9 @@ export class EventStore implements PushEventStore {
|
|
|
283
80
|
// --resume; on cold-wake the reader starts at byte 0 and would re-emit).
|
|
284
81
|
private seenMessageIdsByChatId = new Map<string, Set<string>>()
|
|
285
82
|
private legacySidebarProjectOrder: string[] = []
|
|
286
|
-
private
|
|
83
|
+
private readonly sidebarProjectOrderRef: { value: string[] } = { value: [] }
|
|
287
84
|
private snapshotHasLegacyMessages = false
|
|
288
|
-
private
|
|
85
|
+
private readonly cachedTranscriptRef: MessageRead.CachedTranscriptRef = { value: null }
|
|
289
86
|
private readonly tunnelEventsByChatId = new Map<string, CloudflareTunnelEvent[]>()
|
|
290
87
|
private shareEventsAll: ShareEvent[] = []
|
|
291
88
|
private replayChatProvider = new Map<string, AgentProvider | null>()
|
|
@@ -313,787 +110,16 @@ export class EventStore implements PushEventStore {
|
|
|
313
110
|
}
|
|
314
111
|
|
|
315
112
|
async initialize() {
|
|
316
|
-
await this.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await this.ensureFile(this.queuedMessagesLogPath)
|
|
322
|
-
await this.ensureFile(this.turnsLogPath)
|
|
323
|
-
await this.ensureFile(this.schedulesLogPath)
|
|
324
|
-
await this.ensureFile(this.tunnelLogPath)
|
|
325
|
-
await this.ensureFile(this.sharesLogPath)
|
|
326
|
-
await this.ensureFile(this.pushLogPath)
|
|
327
|
-
await this.ensureFile(this.stacksLogPath)
|
|
328
|
-
await this.ensureFile(this.toolRequestsLogPath)
|
|
329
|
-
await this.ensureFile(this.orchLogPath)
|
|
330
|
-
await this.loadSnapshot()
|
|
331
|
-
await this.replayLogs()
|
|
332
|
-
await this.loadTunnelEvents()
|
|
333
|
-
await this.loadShareEvents()
|
|
334
|
-
await this.loadSidebarProjectOrder()
|
|
335
|
-
if (!(await this.hasLegacyTranscriptData()) && await this.shouldSnapshotLogs()) {
|
|
336
|
-
await this.snapshotAndTruncateLogs()
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private async ensureFile(filePath: string) {
|
|
341
|
-
if (!(await this.storage.exists(filePath))) {
|
|
342
|
-
await this.storage.writeText(filePath, "")
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private async clearStorage() {
|
|
347
|
-
if (this.storageReset) return
|
|
348
|
-
this.storageReset = true
|
|
349
|
-
this.resetState()
|
|
350
|
-
this.clearLegacyTranscriptState()
|
|
351
|
-
await Promise.all([
|
|
352
|
-
this.storage.writeText(this.snapshotPath, ""),
|
|
353
|
-
this.storage.writeText(this.projectsLogPath, ""),
|
|
354
|
-
this.storage.writeText(this.chatsLogPath, ""),
|
|
355
|
-
this.storage.writeText(this.messagesLogPath, ""),
|
|
356
|
-
this.storage.writeText(this.queuedMessagesLogPath, ""),
|
|
357
|
-
this.storage.writeText(this.turnsLogPath, ""),
|
|
358
|
-
this.storage.writeText(this.schedulesLogPath, ""),
|
|
359
|
-
this.storage.writeText(this.tunnelLogPath, ""),
|
|
360
|
-
this.storage.writeText(this.sharesLogPath, ""),
|
|
361
|
-
this.storage.writeText(this.stacksLogPath, ""),
|
|
362
|
-
this.storage.writeText(this.toolRequestsLogPath, ""),
|
|
363
|
-
this.storage.writeText(this.orchLogPath, ""),
|
|
364
|
-
])
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private async loadSnapshot() {
|
|
368
|
-
if (!(await this.storage.exists(this.snapshotPath))) return
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
const text = await this.storage.readText(this.snapshotPath)
|
|
372
|
-
if (!text.trim()) return
|
|
373
|
-
const parsed: SnapshotFile = JSON.parse(text)
|
|
374
|
-
if (parsed.v !== STORE_VERSION) {
|
|
375
|
-
log.warn(`${LOG_PREFIX} Resetting local chat history for store version ${STORE_VERSION}`)
|
|
376
|
-
await this.clearStorage()
|
|
377
|
-
return
|
|
378
|
-
}
|
|
379
|
-
for (const project of parsed.projects) {
|
|
380
|
-
this.state.projectsById.set(project.id, { ...project })
|
|
381
|
-
this.state.projectIdsByPath.set(project.localPath, project.id)
|
|
382
|
-
}
|
|
383
|
-
for (const chat of parsed.chats) {
|
|
384
|
-
// Access legacy fields from old snapshot data via Reflect.get (avoids `as` casts).
|
|
385
|
-
const legacySessionToken: string | null | undefined = Reflect.get(chat, "sessionToken")
|
|
386
|
-
const legacyPendingFork: string | null | { provider: AgentProvider; token: string } | undefined = Reflect.get(chat, "pendingForkSessionToken")
|
|
387
|
-
const legacyTokensByProvider: Partial<Record<AgentProvider, string | null>> | undefined = Reflect.get(chat, "sessionTokensByProvider")
|
|
388
|
-
|
|
389
|
-
const sessionTokensByProvider: Partial<Record<AgentProvider, string | null>> =
|
|
390
|
-
legacyTokensByProvider ? { ...legacyTokensByProvider } : {}
|
|
391
|
-
if (
|
|
392
|
-
typeof legacySessionToken === "string"
|
|
393
|
-
&& chat.provider
|
|
394
|
-
&& sessionTokensByProvider[chat.provider] == null
|
|
395
|
-
) {
|
|
396
|
-
sessionTokensByProvider[chat.provider] = legacySessionToken
|
|
397
|
-
}
|
|
398
|
-
let pendingForkSessionToken: ChatRecord["pendingForkSessionToken"] = null
|
|
399
|
-
if (legacyPendingFork && typeof legacyPendingFork === "object" && "token" in legacyPendingFork) {
|
|
400
|
-
pendingForkSessionToken = legacyPendingFork
|
|
401
|
-
} else if (typeof legacyPendingFork === "string" && chat.provider) {
|
|
402
|
-
pendingForkSessionToken = { provider: chat.provider, token: legacyPendingFork }
|
|
403
|
-
}
|
|
404
|
-
this.state.chatsById.set(chat.id, {
|
|
405
|
-
...chat,
|
|
406
|
-
unread: chat.unread ?? false,
|
|
407
|
-
sessionTokensByProvider,
|
|
408
|
-
pendingForkSessionToken,
|
|
409
|
-
})
|
|
410
|
-
}
|
|
411
|
-
this.legacySidebarProjectOrder = normalizeSidebarProjectOrder(parsed.sidebarProjectOrder)
|
|
412
|
-
if (parsed.queuedMessages?.length) {
|
|
413
|
-
for (const queuedSet of parsed.queuedMessages) {
|
|
414
|
-
this.state.queuedMessagesByChatId.set(queuedSet.chatId, queuedSet.entries.map((entry) => ({
|
|
415
|
-
...entry,
|
|
416
|
-
attachments: [...entry.attachments],
|
|
417
|
-
})))
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
if (parsed.messages?.length) {
|
|
421
|
-
this.snapshotHasLegacyMessages = true
|
|
422
|
-
for (const messageSet of parsed.messages) {
|
|
423
|
-
this.legacyMessagesByChatId.set(messageSet.chatId, cloneTranscriptEntries(messageSet.entries))
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (parsed.autoContinueEvents?.length) {
|
|
427
|
-
for (const entry of parsed.autoContinueEvents) {
|
|
428
|
-
this.state.autoContinueEventsByChatId.set(entry.chatId, [...entry.events])
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (parsed.stacks?.length) {
|
|
432
|
-
for (const stack of parsed.stacks) {
|
|
433
|
-
this.state.stacksById.set(stack.id, { ...stack, projectIds: [...stack.projectIds] })
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
} catch (error) {
|
|
437
|
-
log.warn(`${LOG_PREFIX} Failed to load snapshot, resetting local history:`, String(error))
|
|
438
|
-
await this.clearStorage()
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
private resetState() {
|
|
443
|
-
this.state.projectsById.clear()
|
|
444
|
-
this.state.projectIdsByPath.clear()
|
|
445
|
-
this.state.chatsById.clear()
|
|
446
|
-
this.state.queuedMessagesByChatId.clear()
|
|
447
|
-
this.state.sidebarProjectOrder = []
|
|
448
|
-
this.state.autoContinueEventsByChatId.clear()
|
|
449
|
-
this.state.stacksById.clear()
|
|
450
|
-
this.tunnelEventsByChatId.clear()
|
|
451
|
-
this.sidebarProjectOrder = []
|
|
452
|
-
this.legacySidebarProjectOrder = []
|
|
453
|
-
this.cachedTranscript = null
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private clearLegacyTranscriptState() {
|
|
457
|
-
this.legacyMessagesByChatId.clear()
|
|
458
|
-
this.snapshotHasLegacyMessages = false
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private async loadSidebarProjectOrder() {
|
|
462
|
-
if (await this.storage.exists(this.sidebarProjectOrderPath)) {
|
|
463
|
-
try {
|
|
464
|
-
const text = await this.storage.readText(this.sidebarProjectOrderPath)
|
|
465
|
-
if (!text.trim()) {
|
|
466
|
-
this.sidebarProjectOrder = []
|
|
467
|
-
return
|
|
468
|
-
}
|
|
469
|
-
this.sidebarProjectOrder = normalizeSidebarProjectOrder(JSON.parse(text))
|
|
470
|
-
} catch (error) {
|
|
471
|
-
log.warn(`${LOG_PREFIX} Failed to load ${SIDEBAR_PROJECT_ORDER_FILE}, ignoring saved order:`, String(error))
|
|
472
|
-
this.sidebarProjectOrder = []
|
|
473
|
-
}
|
|
474
|
-
return
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const legacySidebarProjectOrder = await this.loadLegacySidebarProjectOrder()
|
|
478
|
-
this.sidebarProjectOrder = legacySidebarProjectOrder
|
|
479
|
-
if (legacySidebarProjectOrder.length > 0) {
|
|
480
|
-
await this.writeSidebarProjectOrderFile(legacySidebarProjectOrder)
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private async loadLegacySidebarProjectOrder() {
|
|
485
|
-
const fromProjectsLog = await this.readLegacySidebarProjectOrderFromProjectsLog()
|
|
486
|
-
if (fromProjectsLog.length > 0) {
|
|
487
|
-
return fromProjectsLog
|
|
488
|
-
}
|
|
489
|
-
return [...this.legacySidebarProjectOrder]
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private async readLegacySidebarProjectOrderFromProjectsLog() {
|
|
493
|
-
if (!(await this.storage.exists(this.projectsLogPath))) return []
|
|
494
|
-
|
|
495
|
-
const text = await this.storage.readText(this.projectsLogPath)
|
|
496
|
-
if (!text.trim()) return []
|
|
497
|
-
|
|
498
|
-
const lines = text.split("\n")
|
|
499
|
-
let lastNonEmpty = -1
|
|
500
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
501
|
-
if (lines[index].trim()) {
|
|
502
|
-
lastNonEmpty = index
|
|
503
|
-
break
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
let projectIds: string[] = []
|
|
508
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
509
|
-
const line = lines[index].trim()
|
|
510
|
-
if (!line) continue
|
|
511
|
-
try {
|
|
512
|
-
const event: { v?: number; type?: string; projectIds?: AnyValue } = JSON.parse(line)
|
|
513
|
-
if (event.v !== STORE_VERSION || event.type !== "sidebar_project_order_set") {
|
|
514
|
-
continue
|
|
515
|
-
}
|
|
516
|
-
projectIds = normalizeSidebarProjectOrder(event.projectIds)
|
|
517
|
-
} catch (error) {
|
|
518
|
-
if (index === lastNonEmpty) {
|
|
519
|
-
log.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(this.projectsLogPath)} while migrating sidebar order`)
|
|
520
|
-
return projectIds
|
|
521
|
-
}
|
|
522
|
-
log.warn(`${LOG_PREFIX} Failed to migrate sidebar order from ${path.basename(this.projectsLogPath)}:`, String(error))
|
|
523
|
-
return []
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return projectIds
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
private async writeSidebarProjectOrderFile(projectIds: string[]) {
|
|
531
|
-
await this.storage.mkdir(this.dataDir)
|
|
532
|
-
await this.storage.writeText(this.sidebarProjectOrderPath, `${JSON.stringify(projectIds, null, 2)}\n`)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
private async replayLogs() {
|
|
536
|
-
if (this.storageReset) return
|
|
537
|
-
const replayEvents = [
|
|
538
|
-
...await this.loadReplayEvents(this.projectsLogPath, 0),
|
|
539
|
-
...await this.loadReplayEvents(this.stacksLogPath, 1),
|
|
540
|
-
...await this.loadReplayEvents(this.chatsLogPath, 2),
|
|
541
|
-
...await this.loadReplayEvents(this.messagesLogPath, 3),
|
|
542
|
-
...await this.loadReplayEvents(this.queuedMessagesLogPath, 4),
|
|
543
|
-
...await this.loadReplayEvents(this.turnsLogPath, 5),
|
|
544
|
-
...await this.loadReplayEvents(this.schedulesLogPath, 6),
|
|
545
|
-
...await this.loadReplayEvents(this.toolRequestsLogPath, 7),
|
|
546
|
-
...await this.loadReplayEvents(this.orchLogPath, 8),
|
|
547
|
-
]
|
|
548
|
-
if (this.storageReset) return
|
|
549
|
-
|
|
550
|
-
replayEvents
|
|
551
|
-
.sort((left, right) => (
|
|
552
|
-
left.event.timestamp - right.event.timestamp
|
|
553
|
-
|| getReplayEventPriority(left.event) - getReplayEventPriority(right.event)
|
|
554
|
-
|| left.sourceIndex - right.sourceIndex
|
|
555
|
-
|| left.lineIndex - right.lineIndex
|
|
556
|
-
))
|
|
557
|
-
.forEach(({ event }) => {
|
|
558
|
-
this.applyEvent(event)
|
|
559
|
-
})
|
|
560
|
-
this.replayChatProvider.clear()
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
private async loadReplayEvents(filePath: string, sourceIndex: number): Promise<ParsedReplayEvent[]> {
|
|
564
|
-
if (!(await this.storage.exists(filePath))) return []
|
|
565
|
-
const text = await this.storage.readText(filePath)
|
|
566
|
-
if (!text.trim()) return []
|
|
567
|
-
|
|
568
|
-
const parsedEvents: ParsedReplayEvent[] = []
|
|
569
|
-
const lines = text.split("\n")
|
|
570
|
-
let lastNonEmpty = -1
|
|
571
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
572
|
-
if (lines[index].trim()) {
|
|
573
|
-
lastNonEmpty = index
|
|
574
|
-
break
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
579
|
-
const line = lines[index].trim()
|
|
580
|
-
if (!line) continue
|
|
581
|
-
try {
|
|
582
|
-
const event: StoreEvent & { v?: number; type?: string } = JSON.parse(line)
|
|
583
|
-
if (event.v !== STORE_VERSION) {
|
|
584
|
-
log.warn(`${LOG_PREFIX} Resetting local history from incompatible event log`)
|
|
585
|
-
await this.clearStorage()
|
|
586
|
-
return []
|
|
587
|
-
}
|
|
588
|
-
if (event.type === "sidebar_project_order_set") {
|
|
589
|
-
continue
|
|
590
|
-
}
|
|
591
|
-
parsedEvents.push({
|
|
592
|
-
event,
|
|
593
|
-
sourceIndex,
|
|
594
|
-
lineIndex: index,
|
|
595
|
-
})
|
|
596
|
-
} catch (error) {
|
|
597
|
-
if (index === lastNonEmpty) {
|
|
598
|
-
log.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(filePath)}`)
|
|
599
|
-
return parsedEvents
|
|
600
|
-
}
|
|
601
|
-
log.warn(`${LOG_PREFIX} Failed to replay ${path.basename(filePath)}, resetting local history:`, String(error))
|
|
602
|
-
await this.clearStorage()
|
|
603
|
-
return []
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return parsedEvents
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private applyEvent(event: StoreEvent) {
|
|
611
|
-
if ("kind" in event) {
|
|
612
|
-
this.applyAutoContinueEvent(event)
|
|
613
|
-
return
|
|
614
|
-
}
|
|
615
|
-
const e: Exclude<StoreEvent, AutoContinueEvent> = event
|
|
616
|
-
switch (e.type) {
|
|
617
|
-
case "project_opened": {
|
|
618
|
-
const localPath = resolveLocalPath(e.localPath)
|
|
619
|
-
const project = {
|
|
620
|
-
id: e.projectId,
|
|
621
|
-
localPath,
|
|
622
|
-
title: e.title,
|
|
623
|
-
createdAt: e.timestamp,
|
|
624
|
-
updatedAt: e.timestamp,
|
|
625
|
-
}
|
|
626
|
-
this.state.projectsById.set(project.id, project)
|
|
627
|
-
this.state.projectIdsByPath.set(localPath, project.id)
|
|
628
|
-
break
|
|
629
|
-
}
|
|
630
|
-
case "project_removed": {
|
|
631
|
-
const project = this.state.projectsById.get(e.projectId)
|
|
632
|
-
if (!project) break
|
|
633
|
-
project.deletedAt = e.timestamp
|
|
634
|
-
project.updatedAt = e.timestamp
|
|
635
|
-
this.state.projectIdsByPath.delete(project.localPath)
|
|
636
|
-
break
|
|
637
|
-
}
|
|
638
|
-
case "sidebar_project_order_set": {
|
|
639
|
-
this.state.sidebarProjectOrder = [...e.projectIds]
|
|
640
|
-
break
|
|
641
|
-
}
|
|
642
|
-
case "project_star_set": {
|
|
643
|
-
const project = this.state.projectsById.get(e.projectId)
|
|
644
|
-
if (!project) break
|
|
645
|
-
if (e.starredAt == null) {
|
|
646
|
-
delete project.starredAt
|
|
647
|
-
} else {
|
|
648
|
-
project.starredAt = e.starredAt
|
|
649
|
-
}
|
|
650
|
-
project.updatedAt = e.timestamp
|
|
651
|
-
break
|
|
652
|
-
}
|
|
653
|
-
case "chat_created": {
|
|
654
|
-
const chat: ChatRecord = {
|
|
655
|
-
id: e.chatId,
|
|
656
|
-
projectId: e.projectId,
|
|
657
|
-
title: e.title,
|
|
658
|
-
createdAt: e.timestamp,
|
|
659
|
-
updatedAt: e.timestamp,
|
|
660
|
-
unread: false,
|
|
661
|
-
provider: null,
|
|
662
|
-
planMode: false,
|
|
663
|
-
sessionTokensByProvider: {},
|
|
664
|
-
sourceHash: null,
|
|
665
|
-
pendingForkSessionToken: null,
|
|
666
|
-
hasMessages: false,
|
|
667
|
-
lastTurnOutcome: null,
|
|
668
|
-
}
|
|
669
|
-
if (e.stackId !== undefined) chat.stackId = e.stackId
|
|
670
|
-
if (e.stackBindings !== undefined) chat.stackBindings = e.stackBindings.map((b) => ({ ...b }))
|
|
671
|
-
this.state.chatsById.set(chat.id, chat)
|
|
672
|
-
this.replayChatProvider.set(e.chatId, null)
|
|
673
|
-
this.state.subagentRunsByChatId.set(e.chatId, new Map())
|
|
674
|
-
this.updateTiming(e.chatId, e.timestamp, "idle")
|
|
675
|
-
break
|
|
676
|
-
}
|
|
677
|
-
case "chat_renamed": {
|
|
678
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
679
|
-
if (!chat) break
|
|
680
|
-
chat.title = e.title
|
|
681
|
-
chat.updatedAt = e.timestamp
|
|
682
|
-
break
|
|
683
|
-
}
|
|
684
|
-
case "chat_deleted": {
|
|
685
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
686
|
-
if (!chat) break
|
|
687
|
-
chat.deletedAt = e.timestamp
|
|
688
|
-
chat.updatedAt = e.timestamp
|
|
689
|
-
this.state.queuedMessagesByChatId.delete(e.chatId)
|
|
690
|
-
this.state.autoContinueEventsByChatId.delete(e.chatId)
|
|
691
|
-
this.state.chatTimingsByChatId.delete(e.chatId)
|
|
692
|
-
this.state.subagentRunsByChatId.delete(e.chatId)
|
|
693
|
-
break
|
|
694
|
-
}
|
|
695
|
-
case "chat_archived": {
|
|
696
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
697
|
-
if (!chat) break
|
|
698
|
-
chat.archivedAt = e.timestamp
|
|
699
|
-
chat.updatedAt = e.timestamp
|
|
700
|
-
break
|
|
701
|
-
}
|
|
702
|
-
case "chat_unarchived": {
|
|
703
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
704
|
-
if (!chat) break
|
|
705
|
-
delete chat.archivedAt
|
|
706
|
-
chat.updatedAt = e.timestamp
|
|
707
|
-
break
|
|
708
|
-
}
|
|
709
|
-
case "chat_provider_set": {
|
|
710
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
711
|
-
if (!chat) break
|
|
712
|
-
chat.provider = e.provider
|
|
713
|
-
chat.updatedAt = e.timestamp
|
|
714
|
-
this.replayChatProvider.set(e.chatId, e.provider)
|
|
715
|
-
break
|
|
716
|
-
}
|
|
717
|
-
case "chat_plan_mode_set": {
|
|
718
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
719
|
-
if (!chat) break
|
|
720
|
-
chat.planMode = e.planMode
|
|
721
|
-
chat.updatedAt = e.timestamp
|
|
722
|
-
break
|
|
723
|
-
}
|
|
724
|
-
case "chat_read_state_set": {
|
|
725
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
726
|
-
if (!chat) break
|
|
727
|
-
chat.unread = e.unread
|
|
728
|
-
chat.updatedAt = e.timestamp
|
|
729
|
-
break
|
|
730
|
-
}
|
|
731
|
-
case "chat_source_hash_set": {
|
|
732
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
733
|
-
if (!chat) break
|
|
734
|
-
chat.sourceHash = e.sourceHash
|
|
735
|
-
chat.updatedAt = e.timestamp
|
|
736
|
-
break
|
|
737
|
-
}
|
|
738
|
-
case "chat_policy_override_set": {
|
|
739
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
740
|
-
if (!chat) break
|
|
741
|
-
chat.policyOverride = e.policyOverride
|
|
742
|
-
chat.updatedAt = e.timestamp
|
|
743
|
-
break
|
|
744
|
-
}
|
|
745
|
-
case "chat_compact_failures_set": {
|
|
746
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
747
|
-
if (!chat) break
|
|
748
|
-
chat.compactFailureCount = e.compactFailureCount
|
|
749
|
-
chat.updatedAt = e.timestamp
|
|
750
|
-
break
|
|
751
|
-
}
|
|
752
|
-
case "message_appended": {
|
|
753
|
-
this.applyMessageMetadata(e.chatId, e.entry)
|
|
754
|
-
const existing = this.legacyMessagesByChatId.get(e.chatId) ?? []
|
|
755
|
-
existing.push({ ...e.entry })
|
|
756
|
-
this.legacyMessagesByChatId.set(e.chatId, existing)
|
|
757
|
-
break
|
|
758
|
-
}
|
|
759
|
-
case "queued_message_enqueued": {
|
|
760
|
-
const existing = this.state.queuedMessagesByChatId.get(e.chatId) ?? []
|
|
761
|
-
existing.push({
|
|
762
|
-
...e.message,
|
|
763
|
-
attachments: [...e.message.attachments],
|
|
764
|
-
})
|
|
765
|
-
this.state.queuedMessagesByChatId.set(e.chatId, existing)
|
|
766
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
767
|
-
if (chat) {
|
|
768
|
-
chat.updatedAt = e.timestamp
|
|
769
|
-
}
|
|
770
|
-
break
|
|
771
|
-
}
|
|
772
|
-
case "queued_message_removed": {
|
|
773
|
-
const existing = this.state.queuedMessagesByChatId.get(e.chatId) ?? []
|
|
774
|
-
const next = existing.filter((entry) => entry.id !== e.queuedMessageId)
|
|
775
|
-
if (next.length > 0) {
|
|
776
|
-
this.state.queuedMessagesByChatId.set(e.chatId, next)
|
|
777
|
-
} else {
|
|
778
|
-
this.state.queuedMessagesByChatId.delete(e.chatId)
|
|
779
|
-
}
|
|
780
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
781
|
-
if (chat) {
|
|
782
|
-
chat.updatedAt = e.timestamp
|
|
783
|
-
}
|
|
784
|
-
break
|
|
785
|
-
}
|
|
786
|
-
case "turn_started": {
|
|
787
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
788
|
-
if (!chat) break
|
|
789
|
-
chat.updatedAt = e.timestamp
|
|
790
|
-
this.updateTiming(e.chatId, e.timestamp, "running", true, false)
|
|
791
|
-
break
|
|
792
|
-
}
|
|
793
|
-
case "turn_finished": {
|
|
794
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
795
|
-
if (!chat) break
|
|
796
|
-
chat.updatedAt = e.timestamp
|
|
797
|
-
chat.unread = true
|
|
798
|
-
chat.lastTurnOutcome = "success"
|
|
799
|
-
this.updateTiming(e.chatId, e.timestamp, "idle", false, true)
|
|
800
|
-
break
|
|
801
|
-
}
|
|
802
|
-
case "turn_failed": {
|
|
803
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
804
|
-
if (!chat) break
|
|
805
|
-
chat.updatedAt = e.timestamp
|
|
806
|
-
chat.unread = true
|
|
807
|
-
chat.lastTurnOutcome = "failed"
|
|
808
|
-
this.updateTiming(e.chatId, e.timestamp, "failed", false, true)
|
|
809
|
-
break
|
|
810
|
-
}
|
|
811
|
-
case "turn_cancelled": {
|
|
812
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
813
|
-
if (!chat) break
|
|
814
|
-
chat.updatedAt = e.timestamp
|
|
815
|
-
chat.lastTurnOutcome = "cancelled"
|
|
816
|
-
this.updateTiming(e.chatId, e.timestamp, "idle", false, true)
|
|
817
|
-
break
|
|
818
|
-
}
|
|
819
|
-
case "session_token_set": {
|
|
820
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
821
|
-
if (!chat) break
|
|
822
|
-
const provider = e.provider ?? this.replayChatProvider.get(e.chatId) ?? chat.provider
|
|
823
|
-
if (!provider) break
|
|
824
|
-
chat.sessionTokensByProvider = {
|
|
825
|
-
...chat.sessionTokensByProvider,
|
|
826
|
-
[provider]: e.sessionToken,
|
|
827
|
-
}
|
|
828
|
-
chat.updatedAt = e.timestamp
|
|
829
|
-
break
|
|
830
|
-
}
|
|
831
|
-
case "session_commands_loaded": {
|
|
832
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
833
|
-
if (!chat) break
|
|
834
|
-
chat.slashCommands = e.commands.map((c) => ({ ...c }))
|
|
835
|
-
chat.updatedAt = e.timestamp
|
|
836
|
-
break
|
|
837
|
-
}
|
|
838
|
-
case "pending_fork_session_token_set": {
|
|
839
|
-
const chat = this.state.chatsById.get(e.chatId)
|
|
840
|
-
if (!chat) break
|
|
841
|
-
if (e.pendingForkSessionToken == null) {
|
|
842
|
-
chat.pendingForkSessionToken = null
|
|
843
|
-
} else {
|
|
844
|
-
const provider = e.provider ?? this.replayChatProvider.get(e.chatId) ?? chat.provider
|
|
845
|
-
if (!provider) break
|
|
846
|
-
chat.pendingForkSessionToken = { provider, token: e.pendingForkSessionToken }
|
|
847
|
-
}
|
|
848
|
-
chat.updatedAt = e.timestamp
|
|
849
|
-
break
|
|
850
|
-
}
|
|
851
|
-
case "stack_added": {
|
|
852
|
-
const record: StackRecord = {
|
|
853
|
-
id: e.stackId,
|
|
854
|
-
title: e.title,
|
|
855
|
-
projectIds: [...e.projectIds],
|
|
856
|
-
createdAt: e.timestamp,
|
|
857
|
-
updatedAt: e.timestamp,
|
|
858
|
-
}
|
|
859
|
-
this.state.stacksById.set(record.id, record)
|
|
860
|
-
break
|
|
861
|
-
}
|
|
862
|
-
case "stack_removed": {
|
|
863
|
-
const stack = this.state.stacksById.get(e.stackId)
|
|
864
|
-
if (!stack || stack.deletedAt) break
|
|
865
|
-
stack.deletedAt = e.timestamp
|
|
866
|
-
stack.updatedAt = e.timestamp
|
|
867
|
-
break
|
|
868
|
-
}
|
|
869
|
-
case "stack_renamed": {
|
|
870
|
-
const stack = this.state.stacksById.get(e.stackId)
|
|
871
|
-
if (!stack || stack.deletedAt) break
|
|
872
|
-
stack.title = e.title
|
|
873
|
-
stack.updatedAt = e.timestamp
|
|
874
|
-
break
|
|
875
|
-
}
|
|
876
|
-
case "stack_project_added": {
|
|
877
|
-
const stack = this.state.stacksById.get(e.stackId)
|
|
878
|
-
if (!stack || stack.deletedAt) break
|
|
879
|
-
if (stack.projectIds.includes(e.projectId)) break
|
|
880
|
-
stack.projectIds = [...stack.projectIds, e.projectId]
|
|
881
|
-
stack.updatedAt = e.timestamp
|
|
882
|
-
break
|
|
883
|
-
}
|
|
884
|
-
case "stack_project_removed": {
|
|
885
|
-
const stack = this.state.stacksById.get(e.stackId)
|
|
886
|
-
if (!stack || stack.deletedAt) break
|
|
887
|
-
const next = stack.projectIds.filter((id) => id !== e.projectId)
|
|
888
|
-
stack.projectIds = next
|
|
889
|
-
stack.updatedAt = e.timestamp
|
|
890
|
-
break
|
|
891
|
-
}
|
|
892
|
-
case "subagent_run_started": {
|
|
893
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
894
|
-
if (!map) break
|
|
895
|
-
map.set(e.runId, {
|
|
896
|
-
runId: e.runId,
|
|
897
|
-
chatId: e.chatId,
|
|
898
|
-
subagentId: e.subagentId,
|
|
899
|
-
subagentName: e.subagentName,
|
|
900
|
-
label: e.label ?? null,
|
|
901
|
-
provider: e.provider,
|
|
902
|
-
model: e.model,
|
|
903
|
-
status: "running",
|
|
904
|
-
parentUserMessageId: e.parentUserMessageId,
|
|
905
|
-
parentRunId: e.parentRunId,
|
|
906
|
-
depth: e.depth,
|
|
907
|
-
startedAt: e.timestamp,
|
|
908
|
-
finishedAt: null,
|
|
909
|
-
finalText: null,
|
|
910
|
-
error: null,
|
|
911
|
-
usage: null,
|
|
912
|
-
entries: [],
|
|
913
|
-
pendingTool: null,
|
|
914
|
-
})
|
|
915
|
-
break
|
|
916
|
-
}
|
|
917
|
-
case "subagent_message_delta": {
|
|
918
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
919
|
-
const run = map?.get(e.runId)
|
|
920
|
-
if (!run) break
|
|
921
|
-
run.finalText = (run.finalText ?? "") + e.content
|
|
922
|
-
break
|
|
923
|
-
}
|
|
924
|
-
case "subagent_entry_appended": {
|
|
925
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
926
|
-
const run = map?.get(e.runId)
|
|
927
|
-
if (!run) break
|
|
928
|
-
run.entries.push(e.entry)
|
|
929
|
-
// If the entry carries usage (the SDK's terminal "result" message), mirror
|
|
930
|
-
// it onto run.usage so callers can read it without scanning entries.
|
|
931
|
-
if (e.entry.kind === "result") {
|
|
932
|
-
const usage = e.entry.usage
|
|
933
|
-
const cost = e.entry.costUsd
|
|
934
|
-
run.usage = {
|
|
935
|
-
inputTokens: usage?.inputTokens,
|
|
936
|
-
outputTokens: usage?.outputTokens,
|
|
937
|
-
cachedInputTokens: usage?.cachedInputTokens,
|
|
938
|
-
costUsd: cost,
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
break
|
|
942
|
-
}
|
|
943
|
-
case "subagent_run_completed": {
|
|
944
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
945
|
-
const run = map?.get(e.runId)
|
|
946
|
-
if (!run) break
|
|
947
|
-
run.status = "completed"
|
|
948
|
-
run.finishedAt = e.timestamp
|
|
949
|
-
run.finalText = e.finalContent
|
|
950
|
-
// Merge: prefer e.usage if present, otherwise keep what subagent_entry_appended
|
|
951
|
-
// already mirrored. Otherwise null. Without this guard a streaming run
|
|
952
|
-
// whose completion event omits usage would silently erase it.
|
|
953
|
-
run.usage = e.usage ?? run.usage ?? null
|
|
954
|
-
break
|
|
955
|
-
}
|
|
956
|
-
case "subagent_run_failed": {
|
|
957
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
958
|
-
const run = map?.get(e.runId)
|
|
959
|
-
if (!run) break
|
|
960
|
-
run.status = "failed"
|
|
961
|
-
run.finishedAt = e.timestamp
|
|
962
|
-
run.error = e.error
|
|
963
|
-
run.pendingTool = null
|
|
964
|
-
break
|
|
965
|
-
}
|
|
966
|
-
case "subagent_run_cancelled": {
|
|
967
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
968
|
-
const run = map?.get(e.runId)
|
|
969
|
-
if (!run) break
|
|
970
|
-
run.status = "cancelled"
|
|
971
|
-
run.finishedAt = e.timestamp
|
|
972
|
-
run.pendingTool = null
|
|
973
|
-
break
|
|
974
|
-
}
|
|
975
|
-
case "subagent_tool_pending": {
|
|
976
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
977
|
-
const run = map?.get(e.runId)
|
|
978
|
-
if (!run) break
|
|
979
|
-
run.pendingTool = {
|
|
980
|
-
toolUseId: e.toolUseId,
|
|
981
|
-
toolKind: e.toolKind,
|
|
982
|
-
input: e.input,
|
|
983
|
-
requestedAt: e.timestamp,
|
|
984
|
-
}
|
|
985
|
-
break
|
|
986
|
-
}
|
|
987
|
-
case "subagent_tool_resolved": {
|
|
988
|
-
const map = this.state.subagentRunsByChatId.get(e.chatId)
|
|
989
|
-
const run = map?.get(e.runId)
|
|
990
|
-
if (!run) break
|
|
991
|
-
run.pendingTool = null
|
|
992
|
-
const syntheticEntry: TranscriptEntry = {
|
|
993
|
-
kind: "tool_result",
|
|
994
|
-
_id: `${e.runId}:${e.toolUseId}:resolved`,
|
|
995
|
-
createdAt: e.timestamp,
|
|
996
|
-
toolId: e.toolUseId,
|
|
997
|
-
content: e.result,
|
|
998
|
-
}
|
|
999
|
-
run.entries.push(syntheticEntry)
|
|
1000
|
-
break
|
|
1001
|
-
}
|
|
1002
|
-
case "tool_request_put": {
|
|
1003
|
-
this.state.toolRequestsById.set(e.request.id, { ...e.request })
|
|
1004
|
-
break
|
|
1005
|
-
}
|
|
1006
|
-
case "tool_request_resolved": {
|
|
1007
|
-
const existing = this.state.toolRequestsById.get(e.id)
|
|
1008
|
-
if (!existing) break
|
|
1009
|
-
this.state.toolRequestsById.set(e.id, {
|
|
1010
|
-
...existing,
|
|
1011
|
-
status: e.status,
|
|
1012
|
-
decision: e.decision ?? existing.decision,
|
|
1013
|
-
resolvedAt: e.resolvedAt,
|
|
1014
|
-
mismatchReason: e.mismatchReason,
|
|
1015
|
-
})
|
|
1016
|
-
break
|
|
1017
|
-
}
|
|
1018
|
-
case "orch_run_created":
|
|
1019
|
-
case "orch_worktree_provisioned":
|
|
1020
|
-
case "orch_worktree_init_started":
|
|
1021
|
-
case "orch_worktree_init_completed":
|
|
1022
|
-
case "orch_task_claimed":
|
|
1023
|
-
case "orch_phase_started":
|
|
1024
|
-
case "orch_phase_completed":
|
|
1025
|
-
case "orch_gate_opened":
|
|
1026
|
-
case "orch_gate_resolved":
|
|
1027
|
-
case "orch_scope_overlap_flagged":
|
|
1028
|
-
case "orch_config_warning":
|
|
1029
|
-
case "orch_verify_started":
|
|
1030
|
-
case "orch_verify_completed":
|
|
1031
|
-
case "orch_task_committed":
|
|
1032
|
-
case "orch_task_failed":
|
|
1033
|
-
case "orch_task_requeued":
|
|
1034
|
-
case "orch_run_completed":
|
|
1035
|
-
case "orch_run_cancelled":
|
|
1036
|
-
this.applyOrchestrationEvent(e)
|
|
1037
|
-
break
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
private updateTiming(chatId: string, eventTs: number, nextStatus: ChatTimingState["status"], onTurnStart?: boolean, onTurnFinish?: boolean) {
|
|
1042
|
-
const prev = this.state.chatTimingsByChatId.get(chatId)
|
|
1043
|
-
if (!prev) {
|
|
1044
|
-
// chat_created path: seed
|
|
1045
|
-
this.state.chatTimingsByChatId.set(chatId, {
|
|
1046
|
-
status: nextStatus,
|
|
1047
|
-
stateEnteredAt: eventTs,
|
|
1048
|
-
activeSessionStartedAt: eventTs,
|
|
1049
|
-
lastTurnStartedAt: null,
|
|
1050
|
-
lastTurnDurationMs: null,
|
|
1051
|
-
cumulativeMs: { idle: 0, starting: 0, running: 0, failed: 0 },
|
|
1052
|
-
})
|
|
1053
|
-
return
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
const segmentMs = Math.max(0, eventTs - prev.stateEnteredAt)
|
|
1057
|
-
let activeSessionStartedAt = prev.activeSessionStartedAt
|
|
1058
|
-
let cumulativeMs = { ...prev.cumulativeMs }
|
|
1059
|
-
|
|
1060
|
-
// Detect long idle gap when leaving idle -> something
|
|
1061
|
-
if (prev.status === "idle" && nextStatus !== "idle" && segmentMs > ACTIVE_SESSION_IDLE_GAP_MS) {
|
|
1062
|
-
activeSessionStartedAt = eventTs
|
|
1063
|
-
cumulativeMs = { idle: 0, starting: 0, running: 0, failed: 0 }
|
|
1064
|
-
} else {
|
|
1065
|
-
cumulativeMs[prev.status] += segmentMs
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
let lastTurnStartedAt = prev.lastTurnStartedAt
|
|
1069
|
-
let lastTurnDurationMs = prev.lastTurnDurationMs
|
|
1070
|
-
if (onTurnStart) lastTurnStartedAt = eventTs
|
|
1071
|
-
if (onTurnFinish && lastTurnStartedAt != null) lastTurnDurationMs = Math.max(0, eventTs - lastTurnStartedAt)
|
|
1072
|
-
|
|
1073
|
-
this.state.chatTimingsByChatId.set(chatId, {
|
|
1074
|
-
status: nextStatus,
|
|
1075
|
-
stateEnteredAt: eventTs,
|
|
1076
|
-
activeSessionStartedAt,
|
|
1077
|
-
lastTurnStartedAt,
|
|
1078
|
-
lastTurnDurationMs,
|
|
1079
|
-
cumulativeMs,
|
|
113
|
+
await initializeEventStore(this.buildInitDeps(), {
|
|
114
|
+
loadTunnelEvents: () => this.loadTunnelEvents(),
|
|
115
|
+
loadShareEvents: () => this.loadShareEvents(),
|
|
116
|
+
hasLegacyTranscriptData: () => this.hasLegacyTranscriptData(),
|
|
117
|
+
snapshotAndTruncateLogs: () => this.snapshotAndTruncateLogs(),
|
|
1080
118
|
})
|
|
1081
119
|
}
|
|
1082
120
|
|
|
1083
|
-
private
|
|
1084
|
-
|
|
1085
|
-
existing.push(event)
|
|
1086
|
-
this.state.autoContinueEventsByChatId.set(event.chatId, existing)
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
private applyMessageMetadata(chatId: string, entry: TranscriptEntry) {
|
|
1090
|
-
const chat = this.state.chatsById.get(chatId)
|
|
1091
|
-
if (!chat) return
|
|
1092
|
-
chat.hasMessages = true
|
|
1093
|
-
if (entry.kind === "user_prompt") {
|
|
1094
|
-
chat.lastMessageAt = entry.createdAt
|
|
1095
|
-
}
|
|
1096
|
-
chat.updatedAt = Math.max(chat.updatedAt, entry.createdAt)
|
|
121
|
+
private applyEvent(event: StoreEvent) {
|
|
122
|
+
applyStoreEvent(event, this.state, this.legacyMessagesByChatId, this.replayChatProvider)
|
|
1097
123
|
}
|
|
1098
124
|
|
|
1099
125
|
private enqueueDiskAppend(filePath: string, payload: string): void {
|
|
@@ -1113,390 +139,192 @@ export class EventStore implements PushEventStore {
|
|
|
1113
139
|
return this.writeChain
|
|
1114
140
|
}
|
|
1115
141
|
|
|
1116
|
-
|
|
1117
|
-
return path.join(this.transcriptsDir, `${chatId}.jsonl`)
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
private loadTranscriptFromDisk(chatId: string) {
|
|
1121
|
-
const transcriptPath = this.transcriptPath(chatId)
|
|
1122
|
-
if (!this.storage.existsSync(transcriptPath)) {
|
|
1123
|
-
return []
|
|
1124
|
-
}
|
|
142
|
+
// ─── Deps builders ──────────────────────────────────────────────────────────
|
|
1125
143
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
144
|
+
private buildInitDeps(): EventStoreInitDeps {
|
|
145
|
+
return {
|
|
146
|
+
storage: this.storage,
|
|
147
|
+
dataDir: this.dataDir,
|
|
148
|
+
snapshotPath: this.snapshotPath,
|
|
149
|
+
projectsLogPath: this.projectsLogPath,
|
|
150
|
+
chatsLogPath: this.chatsLogPath,
|
|
151
|
+
messagesLogPath: this.messagesLogPath,
|
|
152
|
+
queuedMessagesLogPath: this.queuedMessagesLogPath,
|
|
153
|
+
turnsLogPath: this.turnsLogPath,
|
|
154
|
+
schedulesLogPath: this.schedulesLogPath,
|
|
155
|
+
tunnelLogPath: this.tunnelLogPath,
|
|
156
|
+
sharesLogPath: this.sharesLogPath,
|
|
157
|
+
pushLogPath: this.pushLogPath,
|
|
158
|
+
stacksLogPath: this.stacksLogPath,
|
|
159
|
+
toolRequestsLogPath: this.toolRequestsLogPath,
|
|
160
|
+
orchLogPath: this.orchLogPath,
|
|
161
|
+
transcriptsDir: this.transcriptsDir,
|
|
162
|
+
sidebarProjectOrderPath: this.sidebarProjectOrderPath,
|
|
163
|
+
state: this.state,
|
|
164
|
+
legacyMessagesByChatId: this.legacyMessagesByChatId,
|
|
165
|
+
tunnelEventsByChatId: this.tunnelEventsByChatId,
|
|
166
|
+
cachedTranscriptRef: this.cachedTranscriptRef,
|
|
167
|
+
sidebarProjectOrderRef: this.sidebarProjectOrderRef,
|
|
168
|
+
getLegacySidebarProjectOrder: () => this.legacySidebarProjectOrder,
|
|
169
|
+
setLegacySidebarProjectOrder: (v) => { this.legacySidebarProjectOrder = v },
|
|
170
|
+
getSnapshotHasLegacyMessages: () => this.snapshotHasLegacyMessages,
|
|
171
|
+
setSnapshotHasLegacyMessages: (v) => { this.snapshotHasLegacyMessages = v },
|
|
172
|
+
getStorageReset: () => this.storageReset,
|
|
173
|
+
setStorageReset: (v) => { this.storageReset = v },
|
|
174
|
+
replayChatProvider: this.replayChatProvider,
|
|
175
|
+
applyEvent: (event) => { this.applyEvent(event) },
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private buildMessageReadDeps(): MessageRead.MessageReadDeps {
|
|
180
|
+
return {
|
|
181
|
+
storage: this.storage,
|
|
182
|
+
transcriptsDir: this.transcriptsDir,
|
|
183
|
+
cachedTranscriptRef: this.cachedTranscriptRef,
|
|
184
|
+
legacyMessagesByChatId: this.legacyMessagesByChatId,
|
|
185
|
+
seenMessageIdsByChatId: this.seenMessageIdsByChatId,
|
|
186
|
+
queuedMessagesByChatId: this.state.queuedMessagesByChatId,
|
|
187
|
+
chatsById: this.state.chatsById,
|
|
188
|
+
listPendingToolRequests: (chatId) => this.listPendingToolRequests(chatId),
|
|
1140
189
|
}
|
|
1141
|
-
return entries
|
|
1142
190
|
}
|
|
1143
191
|
|
|
1144
|
-
private
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
this.
|
|
192
|
+
private buildPeripheralEventsDeps(): PeripheralEvents.PeripheralEventsDeps {
|
|
193
|
+
return {
|
|
194
|
+
storage: this.storage,
|
|
195
|
+
tunnelLogPath: this.tunnelLogPath,
|
|
196
|
+
sharesLogPath: this.sharesLogPath,
|
|
197
|
+
pushLogPath: this.pushLogPath,
|
|
198
|
+
tunnelEventsByChatId: this.tunnelEventsByChatId,
|
|
199
|
+
shareEventsAll: this.shareEventsAll,
|
|
200
|
+
getWriteChain: () => this.writeChain,
|
|
201
|
+
setWriteChain: (p) => { this.writeChain = p },
|
|
1149
202
|
}
|
|
1150
|
-
return set
|
|
1151
203
|
}
|
|
1152
204
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
205
|
+
private buildEntityWriteDeps(): EntityWrite.EntityWriteDeps {
|
|
206
|
+
return {
|
|
207
|
+
storage: this.storage,
|
|
208
|
+
dataDir: this.dataDir,
|
|
209
|
+
sidebarProjectOrderPath: this.sidebarProjectOrderPath,
|
|
210
|
+
projectsLogPath: this.projectsLogPath,
|
|
211
|
+
chatsLogPath: this.chatsLogPath,
|
|
212
|
+
queuedMessagesLogPath: this.queuedMessagesLogPath,
|
|
213
|
+
stacksLogPath: this.stacksLogPath,
|
|
214
|
+
projectsById: this.state.projectsById,
|
|
215
|
+
projectIdsByPath: this.state.projectIdsByPath,
|
|
216
|
+
chatsById: this.state.chatsById,
|
|
217
|
+
queuedMessagesByChatId: this.state.queuedMessagesByChatId,
|
|
218
|
+
stacksById: this.state.stacksById,
|
|
219
|
+
sidebarProjectOrderRef: this.sidebarProjectOrderRef,
|
|
220
|
+
getWriteChain: () => this.writeChain,
|
|
221
|
+
setWriteChain: (p) => { this.writeChain = p },
|
|
222
|
+
append: (filePath, event) => this.append(filePath, event),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private buildSessionWriteDeps(): EntityWrite.SessionWriteDeps {
|
|
227
|
+
return {
|
|
228
|
+
chatsById: this.state.chatsById,
|
|
229
|
+
turnsLogPath: this.turnsLogPath,
|
|
230
|
+
schedulesLogPath: this.schedulesLogPath,
|
|
231
|
+
append: (filePath, event) => this.append(filePath, event),
|
|
1173
232
|
}
|
|
1174
|
-
await this.append(this.projectsLogPath, event)
|
|
1175
|
-
return this.state.projectsById.get(projectId)!
|
|
1176
233
|
}
|
|
1177
234
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
235
|
+
private buildChatTranscriptWriteDeps(): TranscriptWrite.ChatTranscriptWriteDeps {
|
|
236
|
+
return {
|
|
237
|
+
storage: this.storage,
|
|
238
|
+
transcriptsDir: this.transcriptsDir,
|
|
239
|
+
dataDir: this.dataDir,
|
|
240
|
+
cachedTranscriptRef: this.cachedTranscriptRef,
|
|
241
|
+
seenMessageIdsByChatId: this.seenMessageIdsByChatId,
|
|
242
|
+
chatsById: this.state.chatsById,
|
|
243
|
+
toolRequestsById: this.state.toolRequestsById,
|
|
244
|
+
chatsLogPath: this.chatsLogPath,
|
|
245
|
+
turnsLogPath: this.turnsLogPath,
|
|
246
|
+
getWriteChain: () => this.writeChain,
|
|
247
|
+
setWriteChain: (p) => { this.writeChain = p },
|
|
248
|
+
append: (filePath, event) => this.append(filePath, event),
|
|
249
|
+
getMessages: (chatId) => this.getMessages(chatId),
|
|
250
|
+
getSeenMessageIds: (chatId) => this.getSeenMessageIds(chatId),
|
|
251
|
+
listPendingToolRequests: (chatId) => this.listPendingToolRequests(chatId),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private buildOrchSubscriptionDeps(): OrchSubscription.OrchSubscriptionDeps {
|
|
256
|
+
return {
|
|
257
|
+
orchRunsById: this.state.orchRunsById,
|
|
258
|
+
orchLogPath: this.orchLogPath,
|
|
259
|
+
orchRunsSubscribers: this.orchSubscriptionState.orchRunsSubscribers,
|
|
260
|
+
applyEvent: (e) => { this.applyEvent(e) },
|
|
261
|
+
enqueueDiskAppend: (fp, p) => { this.enqueueDiskAppend(fp, p) },
|
|
1189
262
|
}
|
|
1190
|
-
await this.append(this.projectsLogPath, event)
|
|
1191
263
|
}
|
|
1192
264
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
const now = Date.now()
|
|
1199
|
-
const event: ProjectEvent = {
|
|
1200
|
-
v: STORE_VERSION,
|
|
1201
|
-
type: "project_star_set",
|
|
1202
|
-
timestamp: now,
|
|
1203
|
-
projectId,
|
|
1204
|
-
starredAt: starred ? now : null,
|
|
265
|
+
private buildToolRequestWriteDeps(): ToolRequestWriteDeps {
|
|
266
|
+
return {
|
|
267
|
+
toolRequestsById: this.state.toolRequestsById,
|
|
268
|
+
toolRequestsLogPath: this.toolRequestsLogPath,
|
|
269
|
+
append: (fp, e) => this.append(fp, e),
|
|
1205
270
|
}
|
|
1206
|
-
await this.append(this.projectsLogPath, event)
|
|
1207
271
|
}
|
|
1208
272
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
if (!project || project.deletedAt) throw new Error(`Project not found: ${projectId}`)
|
|
1217
|
-
}
|
|
1218
|
-
const stackId = crypto.randomUUID()
|
|
1219
|
-
const event: StackEvent = {
|
|
1220
|
-
v: STORE_VERSION,
|
|
1221
|
-
type: "stack_added",
|
|
1222
|
-
timestamp: Date.now(),
|
|
1223
|
-
stackId,
|
|
1224
|
-
title: trimmed,
|
|
1225
|
-
projectIds: [...projectIds],
|
|
273
|
+
private buildAppendSubagentDeps(): AppendSubagentDeps {
|
|
274
|
+
return {
|
|
275
|
+
chatsById: this.state.chatsById,
|
|
276
|
+
turnsLogPath: this.turnsLogPath,
|
|
277
|
+
dataDir: this.dataDir,
|
|
278
|
+
applyEvent: (e) => { this.applyEvent(e) },
|
|
279
|
+
enqueueDiskAppend: (fp, p) => { this.enqueueDiskAppend(fp, p) },
|
|
1226
280
|
}
|
|
1227
|
-
await this.append(this.stacksLogPath, event)
|
|
1228
|
-
return this.state.stacksById.get(stackId)!
|
|
1229
281
|
}
|
|
1230
282
|
|
|
1231
|
-
|
|
1232
|
-
const stack = this.state.stacksById.get(stackId)
|
|
1233
|
-
return stack && !stack.deletedAt ? stack : null
|
|
1234
|
-
}
|
|
283
|
+
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
1235
284
|
|
|
1236
|
-
|
|
1237
|
-
return [...this.state.stacksById.values()].filter((s) => !s.deletedAt)
|
|
1238
|
-
}
|
|
285
|
+
private getSeenMessageIds(chatId: string): Set<string> { return MessageRead.getSeenMessageIds(this.buildMessageReadDeps(), chatId) }
|
|
1239
286
|
|
|
1240
|
-
async
|
|
1241
|
-
const stack = this.state.stacksById.get(stackId)
|
|
1242
|
-
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
1243
|
-
const trimmed = title.trim()
|
|
1244
|
-
if (trimmed === "") throw new Error("Stack title cannot be empty")
|
|
1245
|
-
if (trimmed === stack.title) return
|
|
1246
|
-
const event: StackEvent = {
|
|
1247
|
-
v: STORE_VERSION,
|
|
1248
|
-
type: "stack_renamed",
|
|
1249
|
-
timestamp: Date.now(),
|
|
1250
|
-
stackId,
|
|
1251
|
-
title: trimmed,
|
|
1252
|
-
}
|
|
1253
|
-
await this.append(this.stacksLogPath, event)
|
|
1254
|
-
}
|
|
287
|
+
async openProject(localPath: string, title?: string) { return EntityWrite.openProject(this.buildEntityWriteDeps(), localPath, title) }
|
|
1255
288
|
|
|
1256
|
-
async
|
|
1257
|
-
const stack = this.state.stacksById.get(stackId)
|
|
1258
|
-
if (!stack) throw new Error("Stack not found")
|
|
1259
|
-
if (stack.deletedAt) return
|
|
1260
|
-
const event: StackEvent = {
|
|
1261
|
-
v: STORE_VERSION,
|
|
1262
|
-
type: "stack_removed",
|
|
1263
|
-
timestamp: Date.now(),
|
|
1264
|
-
stackId,
|
|
1265
|
-
}
|
|
1266
|
-
await this.append(this.stacksLogPath, event)
|
|
1267
|
-
}
|
|
289
|
+
async removeProject(projectId: string) { return EntityWrite.removeProject(this.buildEntityWriteDeps(), projectId) }
|
|
1268
290
|
|
|
1269
|
-
async
|
|
1270
|
-
const stack = this.state.stacksById.get(stackId)
|
|
1271
|
-
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
1272
|
-
const project = this.state.projectsById.get(projectId)
|
|
1273
|
-
if (!project || project.deletedAt) throw new Error("Project not found")
|
|
1274
|
-
if (stack.projectIds.includes(projectId)) return
|
|
1275
|
-
const event: StackEvent = {
|
|
1276
|
-
v: STORE_VERSION,
|
|
1277
|
-
type: "stack_project_added",
|
|
1278
|
-
timestamp: Date.now(),
|
|
1279
|
-
stackId,
|
|
1280
|
-
projectId,
|
|
1281
|
-
}
|
|
1282
|
-
await this.append(this.stacksLogPath, event)
|
|
1283
|
-
}
|
|
291
|
+
async setProjectStar(projectId: string, starred: boolean) { return EntityWrite.setProjectStar(this.buildEntityWriteDeps(), projectId, starred) }
|
|
1284
292
|
|
|
1285
|
-
async
|
|
293
|
+
async createStack(title: string, projectIds: string[]): Promise<StackRecord> { return EntityWrite.createStack(this.buildEntityWriteDeps(), title, projectIds) }
|
|
294
|
+
|
|
295
|
+
getStack(stackId: string): StackRecord | null {
|
|
1286
296
|
const stack = this.state.stacksById.get(stackId)
|
|
1287
|
-
|
|
1288
|
-
if (!stack.projectIds.includes(projectId)) return
|
|
1289
|
-
if (stack.projectIds.length <= 2) {
|
|
1290
|
-
throw new Error("Stack must keep at least 2 projects. Delete the stack instead.")
|
|
1291
|
-
}
|
|
1292
|
-
const event: StackEvent = {
|
|
1293
|
-
v: STORE_VERSION,
|
|
1294
|
-
type: "stack_project_removed",
|
|
1295
|
-
timestamp: Date.now(),
|
|
1296
|
-
stackId,
|
|
1297
|
-
projectId,
|
|
1298
|
-
}
|
|
1299
|
-
await this.append(this.stacksLogPath, event)
|
|
297
|
+
return stack && !stack.deletedAt ? stack : null
|
|
1300
298
|
}
|
|
1301
299
|
|
|
1302
|
-
|
|
1303
|
-
const validProjectIds = projectIds.filter((projectId) => {
|
|
1304
|
-
const project = this.state.projectsById.get(projectId)
|
|
1305
|
-
return Boolean(project && !project.deletedAt)
|
|
1306
|
-
})
|
|
300
|
+
listStacks(): StackRecord[] { return [...this.state.stacksById.values()].filter((s) => !s.deletedAt) }
|
|
1307
301
|
|
|
1308
|
-
|
|
1309
|
-
const current = this.sidebarProjectOrder
|
|
1310
|
-
if (
|
|
1311
|
-
uniqueProjectIds.length === current.length
|
|
1312
|
-
&& uniqueProjectIds.every((projectId, index) => current[index] === projectId)
|
|
1313
|
-
) {
|
|
1314
|
-
return
|
|
1315
|
-
}
|
|
302
|
+
async renameStack(stackId: string, title: string): Promise<void> { return EntityWrite.renameStack(this.buildEntityWriteDeps(), stackId, title) }
|
|
1316
303
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
304
|
+
async removeStack(stackId: string): Promise<void> { return EntityWrite.removeStack(this.buildEntityWriteDeps(), stackId) }
|
|
305
|
+
|
|
306
|
+
async addProjectToStack(stackId: string, projectId: string): Promise<void> { return EntityWrite.addProjectToStack(this.buildEntityWriteDeps(), stackId, projectId) }
|
|
307
|
+
|
|
308
|
+
async removeProjectFromStack(stackId: string, projectId: string): Promise<void> { return EntityWrite.removeProjectFromStack(this.buildEntityWriteDeps(), stackId, projectId) }
|
|
309
|
+
|
|
310
|
+
async setSidebarProjectOrder(projectIds: string[]) { return EntityWrite.setSidebarProjectOrder(this.buildEntityWriteDeps(), projectIds) }
|
|
1323
311
|
|
|
1324
312
|
async createChat(
|
|
1325
313
|
projectId: string,
|
|
1326
314
|
options?: { stackId?: string; stackBindings?: StackBinding[] },
|
|
1327
315
|
): Promise<import("./events").ChatRecord> {
|
|
1328
|
-
|
|
1329
|
-
if (!project || project.deletedAt) {
|
|
1330
|
-
throw new Error("Project not found")
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
if (options?.stackId !== undefined || options?.stackBindings !== undefined) {
|
|
1334
|
-
if (options.stackId === undefined || options.stackBindings === undefined) {
|
|
1335
|
-
throw new Error("stackId and stackBindings must be provided together")
|
|
1336
|
-
}
|
|
1337
|
-
const stack = this.state.stacksById.get(options.stackId)
|
|
1338
|
-
if (!stack || stack.deletedAt) throw new Error("Stack not found")
|
|
1339
|
-
if (options.stackBindings.length === 0) throw new Error("stackBindings cannot be empty")
|
|
1340
|
-
const primaries = options.stackBindings.filter((b) => b.role === "primary")
|
|
1341
|
-
if (primaries.length !== 1) throw new Error("Exactly one primary binding required")
|
|
1342
|
-
const seenProjects = new Set<string>()
|
|
1343
|
-
for (const binding of options.stackBindings) {
|
|
1344
|
-
if (seenProjects.has(binding.projectId)) {
|
|
1345
|
-
throw new Error("Duplicate projectId in stackBindings")
|
|
1346
|
-
}
|
|
1347
|
-
seenProjects.add(binding.projectId)
|
|
1348
|
-
if (!stack.projectIds.includes(binding.projectId)) {
|
|
1349
|
-
throw new Error(`Binding projectId not a member of stack: ${binding.projectId}`)
|
|
1350
|
-
}
|
|
1351
|
-
const peerProject = this.state.projectsById.get(binding.projectId)
|
|
1352
|
-
if (!peerProject || peerProject.deletedAt) {
|
|
1353
|
-
throw new Error(`Project not found: ${binding.projectId}`)
|
|
1354
|
-
}
|
|
1355
|
-
if (typeof binding.worktreePath !== "string" || binding.worktreePath.trim() === "") {
|
|
1356
|
-
throw new Error("worktreePath must be a non-empty string")
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
if (primaries[0].projectId !== projectId) {
|
|
1360
|
-
throw new Error("Primary binding projectId must match createChat projectId")
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
const chatId = crypto.randomUUID()
|
|
1365
|
-
const event: ChatEvent = {
|
|
1366
|
-
v: STORE_VERSION,
|
|
1367
|
-
type: "chat_created",
|
|
1368
|
-
timestamp: Date.now(),
|
|
1369
|
-
chatId,
|
|
1370
|
-
projectId,
|
|
1371
|
-
title: "New Chat",
|
|
1372
|
-
...(options?.stackId !== undefined ? { stackId: options.stackId } : {}),
|
|
1373
|
-
...(options?.stackBindings !== undefined ? { stackBindings: options.stackBindings.map((b) => ({ ...b })) } : {}),
|
|
1374
|
-
}
|
|
1375
|
-
await this.append(this.chatsLogPath, event)
|
|
1376
|
-
return this.state.chatsById.get(chatId)!
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
async forkChat(sourceChatId: string) {
|
|
1380
|
-
const sourceChat = this.requireChat(sourceChatId)
|
|
1381
|
-
const sourceProvider = sourceChat.provider
|
|
1382
|
-
if (!sourceProvider) {
|
|
1383
|
-
throw new Error("Chat cannot be forked")
|
|
1384
|
-
}
|
|
1385
|
-
const sourceSessionToken =
|
|
1386
|
-
sourceChat.sessionTokensByProvider[sourceProvider]
|
|
1387
|
-
?? (sourceChat.pendingForkSessionToken?.provider === sourceProvider
|
|
1388
|
-
? sourceChat.pendingForkSessionToken.token
|
|
1389
|
-
: null)
|
|
1390
|
-
if (!sourceSessionToken) {
|
|
1391
|
-
throw new Error("Chat cannot be forked")
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
const chatId = crypto.randomUUID()
|
|
1395
|
-
const createdAt = Date.now()
|
|
1396
|
-
const createEvent: ChatEvent = {
|
|
1397
|
-
v: STORE_VERSION,
|
|
1398
|
-
type: "chat_created",
|
|
1399
|
-
timestamp: createdAt,
|
|
1400
|
-
chatId,
|
|
1401
|
-
projectId: sourceChat.projectId,
|
|
1402
|
-
title: getForkedChatTitle(sourceChat.title),
|
|
1403
|
-
...(sourceChat.stackId !== undefined ? { stackId: sourceChat.stackId } : {}),
|
|
1404
|
-
...(sourceChat.stackBindings !== undefined
|
|
1405
|
-
? { stackBindings: sourceChat.stackBindings.map((b) => ({ ...b })) }
|
|
1406
|
-
: {}),
|
|
1407
|
-
}
|
|
1408
|
-
await this.append(this.chatsLogPath, createEvent)
|
|
1409
|
-
await this.setChatProvider(chatId, sourceProvider)
|
|
1410
|
-
await this.setPlanMode(chatId, sourceChat.planMode)
|
|
1411
|
-
await this.setPendingForkSessionToken(chatId, { provider: sourceProvider, token: sourceSessionToken })
|
|
1412
|
-
|
|
1413
|
-
const sourceEntries = this.getMessages(sourceChatId)
|
|
1414
|
-
if (sourceEntries.length > 0) {
|
|
1415
|
-
const transcriptPath = this.transcriptPath(chatId)
|
|
1416
|
-
const payload = sourceEntries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
1417
|
-
this.writeChain = this.writeChain.then(async () => {
|
|
1418
|
-
await this.storage.mkdir(this.transcriptsDir)
|
|
1419
|
-
await this.storage.writeText(transcriptPath, `${payload}\n`)
|
|
1420
|
-
const chat = this.state.chatsById.get(chatId)
|
|
1421
|
-
if (chat) {
|
|
1422
|
-
chat.hasMessages = true
|
|
1423
|
-
chat.updatedAt = Math.max(chat.updatedAt, createdAt)
|
|
1424
|
-
}
|
|
1425
|
-
if (this.cachedTranscript?.chatId === chatId) {
|
|
1426
|
-
this.cachedTranscript = { chatId, entries: cloneTranscriptEntries(sourceEntries) }
|
|
1427
|
-
}
|
|
1428
|
-
})
|
|
1429
|
-
await this.writeChain
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
return this.state.chatsById.get(chatId)!
|
|
316
|
+
return EntityWrite.createChat(this.buildEntityWriteDeps(), projectId, options)
|
|
1433
317
|
}
|
|
1434
318
|
|
|
1435
|
-
async
|
|
1436
|
-
const trimmed = title.trim()
|
|
1437
|
-
if (!trimmed) return
|
|
1438
|
-
const chat = this.requireChat(chatId)
|
|
1439
|
-
if (chat.title === trimmed) return
|
|
1440
|
-
const event: ChatEvent = {
|
|
1441
|
-
v: STORE_VERSION,
|
|
1442
|
-
type: "chat_renamed",
|
|
1443
|
-
timestamp: Date.now(),
|
|
1444
|
-
chatId,
|
|
1445
|
-
title: trimmed,
|
|
1446
|
-
}
|
|
1447
|
-
await this.append(this.chatsLogPath, event)
|
|
1448
|
-
}
|
|
319
|
+
async forkChat(sourceChatId: string) { return TranscriptWrite.forkChat(this.buildChatTranscriptWriteDeps(), sourceChatId) }
|
|
1449
320
|
|
|
1450
|
-
async
|
|
1451
|
-
const chat = this.requireChat(chatId)
|
|
1452
|
-
const projectId = chat.projectId
|
|
1453
|
-
const event: ChatEvent = {
|
|
1454
|
-
v: STORE_VERSION,
|
|
1455
|
-
type: "chat_deleted",
|
|
1456
|
-
timestamp: Date.now(),
|
|
1457
|
-
chatId,
|
|
1458
|
-
}
|
|
1459
|
-
await this.append(this.chatsLogPath, event)
|
|
1460
|
-
for (const [id, req] of this.state.toolRequestsById) {
|
|
1461
|
-
if (req.chatId === chatId) {
|
|
1462
|
-
this.state.toolRequestsById.delete(id)
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
await this.removeSubagentResultsDir(projectId, chatId)
|
|
1466
|
-
}
|
|
321
|
+
async renameChat(chatId: string, title: string) { return EntityWrite.renameChat(this.buildEntityWriteDeps(), chatId, title) }
|
|
1467
322
|
|
|
1468
|
-
|
|
1469
|
-
const dir = path.join(
|
|
1470
|
-
this.dataDir, "projects", projectId, "chats", chatId, "subagent-results",
|
|
1471
|
-
)
|
|
1472
|
-
try {
|
|
1473
|
-
await this.storage.remove(dir, { recursive: true })
|
|
1474
|
-
} catch (err) {
|
|
1475
|
-
log.warn(`${LOG_PREFIX} subagent-results cleanup failed`, { chatId, err })
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
323
|
+
async deleteChat(chatId: string) { return TranscriptWrite.deleteChat(this.buildChatTranscriptWriteDeps(), chatId) }
|
|
1478
324
|
|
|
1479
|
-
async archiveChat(chatId: string) {
|
|
1480
|
-
this.requireChat(chatId)
|
|
1481
|
-
const event: ChatEvent = {
|
|
1482
|
-
v: STORE_VERSION,
|
|
1483
|
-
type: "chat_archived",
|
|
1484
|
-
timestamp: Date.now(),
|
|
1485
|
-
chatId,
|
|
1486
|
-
}
|
|
1487
|
-
await this.append(this.chatsLogPath, event)
|
|
1488
|
-
}
|
|
325
|
+
async archiveChat(chatId: string) { return TranscriptWrite.archiveChat(this.buildChatTranscriptWriteDeps(), chatId) }
|
|
1489
326
|
|
|
1490
|
-
async unarchiveChat(chatId: string) {
|
|
1491
|
-
this.requireChat(chatId)
|
|
1492
|
-
const event: ChatEvent = {
|
|
1493
|
-
v: STORE_VERSION,
|
|
1494
|
-
type: "chat_unarchived",
|
|
1495
|
-
timestamp: Date.now(),
|
|
1496
|
-
chatId,
|
|
1497
|
-
}
|
|
1498
|
-
await this.append(this.chatsLogPath, event)
|
|
1499
|
-
}
|
|
327
|
+
async unarchiveChat(chatId: string) { return TranscriptWrite.unarchiveChat(this.buildChatTranscriptWriteDeps(), chatId) }
|
|
1500
328
|
|
|
1501
329
|
async pruneStaleEmptyChats(args?: {
|
|
1502
330
|
now?: number
|
|
@@ -1504,352 +332,48 @@ export class EventStore implements PushEventStore {
|
|
|
1504
332
|
activeChatIds?: Iterable<string>
|
|
1505
333
|
protectedChatIds?: Iterable<string>
|
|
1506
334
|
}) {
|
|
1507
|
-
|
|
1508
|
-
const maxAgeMs = args?.maxAgeMs ?? STALE_EMPTY_CHAT_MAX_AGE_MS
|
|
1509
|
-
const protectedChatIds = new Set([
|
|
1510
|
-
...(args?.activeChatIds ?? []),
|
|
1511
|
-
...(args?.protectedChatIds ?? []),
|
|
1512
|
-
])
|
|
1513
|
-
const prunedChatIds: string[] = []
|
|
1514
|
-
|
|
1515
|
-
for (const chat of this.state.chatsById.values()) {
|
|
1516
|
-
if (chat.deletedAt || chat.archivedAt || protectedChatIds.has(chat.id)) continue
|
|
1517
|
-
if (now - chat.createdAt < maxAgeMs) continue
|
|
1518
|
-
if (chat.hasMessages) continue
|
|
1519
|
-
if (this.getMessages(chat.id).length > 0) {
|
|
1520
|
-
chat.hasMessages = true
|
|
1521
|
-
continue
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
const event: ChatEvent = {
|
|
1525
|
-
v: STORE_VERSION,
|
|
1526
|
-
type: "chat_deleted",
|
|
1527
|
-
timestamp: now,
|
|
1528
|
-
chatId: chat.id,
|
|
1529
|
-
}
|
|
1530
|
-
await this.append(this.chatsLogPath, event)
|
|
1531
|
-
|
|
1532
|
-
const transcriptPath = this.transcriptPath(chat.id)
|
|
1533
|
-
await this.storage.remove(transcriptPath)
|
|
1534
|
-
if (this.cachedTranscript?.chatId === chat.id) {
|
|
1535
|
-
this.cachedTranscript = null
|
|
1536
|
-
}
|
|
1537
|
-
await this.removeSubagentResultsDir(chat.projectId, chat.id)
|
|
1538
|
-
|
|
1539
|
-
prunedChatIds.push(chat.id)
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
return prunedChatIds
|
|
335
|
+
return TranscriptWrite.pruneStaleEmptyChats(this.buildChatTranscriptWriteDeps(), args)
|
|
1543
336
|
}
|
|
1544
337
|
|
|
1545
|
-
async setChatProvider(chatId: string, provider: AgentProvider) {
|
|
1546
|
-
const chat = this.requireChat(chatId)
|
|
1547
|
-
if (chat.provider === provider) return
|
|
1548
|
-
const event: ChatEvent = {
|
|
1549
|
-
v: STORE_VERSION,
|
|
1550
|
-
type: "chat_provider_set",
|
|
1551
|
-
timestamp: Date.now(),
|
|
1552
|
-
chatId,
|
|
1553
|
-
provider,
|
|
1554
|
-
}
|
|
1555
|
-
await this.append(this.chatsLogPath, event)
|
|
1556
|
-
}
|
|
338
|
+
async setChatProvider(chatId: string, provider: AgentProvider) { return EntityWrite.setChatProvider(this.buildEntityWriteDeps(), chatId, provider) }
|
|
1557
339
|
|
|
1558
|
-
async setPlanMode(chatId: string, planMode: boolean) {
|
|
1559
|
-
const chat = this.requireChat(chatId)
|
|
1560
|
-
if (chat.planMode === planMode) return
|
|
1561
|
-
const event: ChatEvent = {
|
|
1562
|
-
v: STORE_VERSION,
|
|
1563
|
-
type: "chat_plan_mode_set",
|
|
1564
|
-
timestamp: Date.now(),
|
|
1565
|
-
chatId,
|
|
1566
|
-
planMode,
|
|
1567
|
-
}
|
|
1568
|
-
await this.append(this.chatsLogPath, event)
|
|
1569
|
-
}
|
|
340
|
+
async setPlanMode(chatId: string, planMode: boolean) { return EntityWrite.setPlanMode(this.buildEntityWriteDeps(), chatId, planMode) }
|
|
1570
341
|
|
|
1571
|
-
async setCompactFailureCount(chatId: string, compactFailureCount: number) {
|
|
1572
|
-
const chat = this.requireChat(chatId)
|
|
1573
|
-
if ((chat.compactFailureCount ?? 0) === compactFailureCount) return
|
|
1574
|
-
const event: ChatEvent = {
|
|
1575
|
-
v: STORE_VERSION,
|
|
1576
|
-
type: "chat_compact_failures_set",
|
|
1577
|
-
timestamp: Date.now(),
|
|
1578
|
-
chatId,
|
|
1579
|
-
compactFailureCount,
|
|
1580
|
-
}
|
|
1581
|
-
await this.append(this.chatsLogPath, event)
|
|
1582
|
-
}
|
|
342
|
+
async setCompactFailureCount(chatId: string, compactFailureCount: number) { return EntityWrite.setCompactFailureCount(this.buildEntityWriteDeps(), chatId, compactFailureCount) }
|
|
1583
343
|
|
|
1584
|
-
async setChatReadState(chatId: string, unread: boolean) {
|
|
1585
|
-
const chat = this.requireChat(chatId)
|
|
1586
|
-
if (chat.unread === unread) return
|
|
1587
|
-
const event: ChatEvent = {
|
|
1588
|
-
v: STORE_VERSION,
|
|
1589
|
-
type: "chat_read_state_set",
|
|
1590
|
-
timestamp: Date.now(),
|
|
1591
|
-
chatId,
|
|
1592
|
-
unread,
|
|
1593
|
-
}
|
|
1594
|
-
await this.append(this.chatsLogPath, event)
|
|
1595
|
-
}
|
|
344
|
+
async setChatReadState(chatId: string, unread: boolean) { return EntityWrite.setChatReadState(this.buildEntityWriteDeps(), chatId, unread) }
|
|
1596
345
|
|
|
1597
|
-
async setChatPolicyOverride(chatId: string, policyOverride: ChatPermissionPolicyOverride | null) {
|
|
1598
|
-
this.requireChat(chatId)
|
|
1599
|
-
const event: ChatEvent = {
|
|
1600
|
-
v: STORE_VERSION,
|
|
1601
|
-
type: "chat_policy_override_set",
|
|
1602
|
-
timestamp: Date.now(),
|
|
1603
|
-
chatId,
|
|
1604
|
-
policyOverride,
|
|
1605
|
-
}
|
|
1606
|
-
await this.append(this.chatsLogPath, event)
|
|
1607
|
-
}
|
|
346
|
+
async setChatPolicyOverride(chatId: string, policyOverride: ChatPermissionPolicyOverride | null) { return EntityWrite.setChatPolicyOverride(this.buildEntityWriteDeps(), chatId, policyOverride) }
|
|
1608
347
|
|
|
1609
|
-
async appendMessage(chatId: string, entry: TranscriptEntry) {
|
|
1610
|
-
this.requireChat(chatId)
|
|
1611
|
-
const payload = `${JSON.stringify(entry)}\n`
|
|
1612
|
-
const transcriptPath = this.transcriptPath(chatId)
|
|
1613
|
-
const queuedAt = performance.now()
|
|
1614
|
-
this.writeChain = this.writeChain.then(async () => {
|
|
1615
|
-
const startedAt = performance.now()
|
|
1616
|
-
const queueDelayMs = Number((startedAt - queuedAt).toFixed(1))
|
|
1617
|
-
// Dedupe by messageId: if a transcript entry from the same JSONL source
|
|
1618
|
-
// message has already been appended, skip. Server-generated entries
|
|
1619
|
-
// without messageId (e.g. interrupted, context_cleared) always append.
|
|
1620
|
-
const mid = entry.messageId
|
|
1621
|
-
if (typeof mid === "string" && mid.length > 0) {
|
|
1622
|
-
// Ensure the transcript is loaded so the seen set is populated.
|
|
1623
|
-
this.getMessages(chatId)
|
|
1624
|
-
const seen = this.getSeenMessageIds(chatId)
|
|
1625
|
-
if (seen.has(mid)) {
|
|
1626
|
-
logSendToStartingProfile("event_store.append_message_dedup", {
|
|
1627
|
-
chatId,
|
|
1628
|
-
messageId: mid,
|
|
1629
|
-
kind: entry.kind,
|
|
1630
|
-
})
|
|
1631
|
-
return
|
|
1632
|
-
}
|
|
1633
|
-
seen.add(mid)
|
|
1634
|
-
}
|
|
1635
|
-
await this.storage.mkdir(this.transcriptsDir)
|
|
1636
|
-
const beforeAppendAt = performance.now()
|
|
1637
|
-
await this.storage.appendText(transcriptPath, payload)
|
|
1638
|
-
const afterAppendAt = performance.now()
|
|
1639
|
-
this.applyMessageMetadata(chatId, entry)
|
|
1640
|
-
if (this.cachedTranscript?.chatId === chatId) {
|
|
1641
|
-
this.cachedTranscript.entries.push({ ...entry })
|
|
1642
|
-
}
|
|
1643
|
-
logSendToStartingProfile("event_store.append_message", {
|
|
1644
|
-
chatId,
|
|
1645
|
-
entryId: entry._id,
|
|
1646
|
-
kind: entry.kind,
|
|
1647
|
-
payloadBytes: payload.length,
|
|
1648
|
-
queueDelayMs,
|
|
1649
|
-
appendMs: Number((afterAppendAt - beforeAppendAt).toFixed(1)),
|
|
1650
|
-
totalMs: Number((afterAppendAt - queuedAt).toFixed(1)),
|
|
1651
|
-
})
|
|
1652
|
-
})
|
|
1653
|
-
return this.writeChain
|
|
1654
|
-
}
|
|
348
|
+
async appendMessage(chatId: string, entry: TranscriptEntry) { return TranscriptWrite.appendMessage(this.buildChatTranscriptWriteDeps(), chatId, entry) }
|
|
1655
349
|
|
|
1656
|
-
async enqueueMessage(chatId: string, message: Omit<QueuedChatMessage, "id" | "createdAt"> & Partial<Pick<QueuedChatMessage, "id" | "createdAt">>) {
|
|
1657
|
-
this.requireChat(chatId)
|
|
1658
|
-
const queuedMessage: QueuedChatMessage = {
|
|
1659
|
-
id: message.id ?? crypto.randomUUID(),
|
|
1660
|
-
content: message.content,
|
|
1661
|
-
attachments: [...(message.attachments ?? [])],
|
|
1662
|
-
createdAt: message.createdAt ?? Date.now(),
|
|
1663
|
-
provider: message.provider,
|
|
1664
|
-
model: message.model,
|
|
1665
|
-
modelOptions: message.modelOptions,
|
|
1666
|
-
planMode: message.planMode,
|
|
1667
|
-
autoContinue: message.autoContinue,
|
|
1668
|
-
}
|
|
1669
|
-
const event: QueuedMessageEvent = {
|
|
1670
|
-
v: STORE_VERSION,
|
|
1671
|
-
type: "queued_message_enqueued",
|
|
1672
|
-
timestamp: queuedMessage.createdAt,
|
|
1673
|
-
chatId,
|
|
1674
|
-
message: queuedMessage,
|
|
1675
|
-
}
|
|
1676
|
-
await this.append(this.queuedMessagesLogPath, event)
|
|
1677
|
-
return queuedMessage
|
|
1678
|
-
}
|
|
350
|
+
async enqueueMessage(chatId: string, message: Omit<QueuedChatMessage, "id" | "createdAt"> & Partial<Pick<QueuedChatMessage, "id" | "createdAt">>) { return EntityWrite.enqueueMessage(this.buildEntityWriteDeps(), chatId, message) }
|
|
1679
351
|
|
|
1680
|
-
async removeQueuedMessage(chatId: string, queuedMessageId: string) {
|
|
1681
|
-
this.requireChat(chatId)
|
|
1682
|
-
const existing = this.getQueuedMessages(chatId)
|
|
1683
|
-
if (!existing.some((entry) => entry.id === queuedMessageId)) {
|
|
1684
|
-
throw new Error("Queued message not found")
|
|
1685
|
-
}
|
|
1686
|
-
const event: QueuedMessageEvent = {
|
|
1687
|
-
v: STORE_VERSION,
|
|
1688
|
-
type: "queued_message_removed",
|
|
1689
|
-
timestamp: Date.now(),
|
|
1690
|
-
chatId,
|
|
1691
|
-
queuedMessageId,
|
|
1692
|
-
}
|
|
1693
|
-
await this.append(this.queuedMessagesLogPath, event)
|
|
1694
|
-
}
|
|
352
|
+
async removeQueuedMessage(chatId: string, queuedMessageId: string) { return EntityWrite.removeQueuedMessage(this.buildEntityWriteDeps(), chatId, queuedMessageId) }
|
|
1695
353
|
|
|
1696
|
-
async recordTurnStarted(chatId: string, runConfig?: TurnRunConfig) {
|
|
1697
|
-
this.requireChat(chatId)
|
|
1698
|
-
const event: TurnEvent = {
|
|
1699
|
-
v: STORE_VERSION,
|
|
1700
|
-
type: "turn_started",
|
|
1701
|
-
timestamp: Date.now(),
|
|
1702
|
-
chatId,
|
|
1703
|
-
...(runConfig ? { runConfig } : {}),
|
|
1704
|
-
}
|
|
1705
|
-
await this.append(this.turnsLogPath, event)
|
|
1706
|
-
}
|
|
354
|
+
async recordTurnStarted(chatId: string, runConfig?: TurnRunConfig) { return EntityWrite.recordTurnStarted(this.buildSessionWriteDeps(), chatId, runConfig) }
|
|
1707
355
|
|
|
1708
|
-
async recordTurnFinished(chatId: string) {
|
|
1709
|
-
this.requireChat(chatId)
|
|
1710
|
-
const event: TurnEvent = {
|
|
1711
|
-
v: STORE_VERSION,
|
|
1712
|
-
type: "turn_finished",
|
|
1713
|
-
timestamp: Date.now(),
|
|
1714
|
-
chatId,
|
|
1715
|
-
}
|
|
1716
|
-
await this.append(this.turnsLogPath, event)
|
|
1717
|
-
}
|
|
356
|
+
async recordTurnFinished(chatId: string) { return EntityWrite.recordTurnFinished(this.buildSessionWriteDeps(), chatId) }
|
|
1718
357
|
|
|
1719
|
-
async recordTurnFailed(chatId: string, error: string) {
|
|
1720
|
-
this.requireChat(chatId)
|
|
1721
|
-
const event: TurnEvent = {
|
|
1722
|
-
v: STORE_VERSION,
|
|
1723
|
-
type: "turn_failed",
|
|
1724
|
-
timestamp: Date.now(),
|
|
1725
|
-
chatId,
|
|
1726
|
-
error,
|
|
1727
|
-
}
|
|
1728
|
-
await this.append(this.turnsLogPath, event)
|
|
1729
|
-
}
|
|
358
|
+
async recordTurnFailed(chatId: string, error: string) { return EntityWrite.recordTurnFailed(this.buildSessionWriteDeps(), chatId, error) }
|
|
1730
359
|
|
|
1731
|
-
async recordTurnCancelled(chatId: string) {
|
|
1732
|
-
this.requireChat(chatId)
|
|
1733
|
-
const event: TurnEvent = {
|
|
1734
|
-
v: STORE_VERSION,
|
|
1735
|
-
type: "turn_cancelled",
|
|
1736
|
-
timestamp: Date.now(),
|
|
1737
|
-
chatId,
|
|
1738
|
-
}
|
|
1739
|
-
await this.append(this.turnsLogPath, event)
|
|
1740
|
-
}
|
|
360
|
+
async recordTurnCancelled(chatId: string) { return EntityWrite.recordTurnCancelled(this.buildSessionWriteDeps(), chatId) }
|
|
1741
361
|
|
|
1742
|
-
async appendSubagentEvent(event: SubagentRunEvent) {
|
|
1743
|
-
let effectiveEvent = event
|
|
1744
|
-
if (event.type === "subagent_entry_appended" && event.entry.kind === "tool_result") {
|
|
1745
|
-
const chat = this.state.chatsById.get(event.chatId)
|
|
1746
|
-
if (chat) {
|
|
1747
|
-
effectiveEvent = {
|
|
1748
|
-
...event,
|
|
1749
|
-
entry: await capTranscriptEntry({
|
|
1750
|
-
entry: event.entry,
|
|
1751
|
-
chatId: event.chatId,
|
|
1752
|
-
runId: event.runId,
|
|
1753
|
-
projectId: chat.projectId,
|
|
1754
|
-
kannaRoot: this.dataDir,
|
|
1755
|
-
}),
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
// Apply in-memory synchronously so the UI sees the update immediately,
|
|
1760
|
-
// decoupled from disk I/O backlog on writeChain (scoped to ephemeral
|
|
1761
|
-
// subagent_* events only — structural events keep strict append→apply ordering).
|
|
1762
|
-
this.applyEvent(effectiveEvent)
|
|
1763
|
-
this.enqueueDiskAppend(this.turnsLogPath, `${JSON.stringify(effectiveEvent)}\n`)
|
|
1764
|
-
}
|
|
362
|
+
async appendSubagentEvent(event: SubagentRunEvent) { return appendSubagentEventFn(this.buildAppendSubagentDeps(), event) }
|
|
1765
363
|
|
|
1766
|
-
getSubagentRuns(chatId: string): Record<string, SubagentRunSnapshot> {
|
|
1767
|
-
const map = this.state.subagentRunsByChatId.get(chatId)
|
|
1768
|
-
if (!map) return {}
|
|
1769
|
-
return Object.fromEntries(map.entries())
|
|
1770
|
-
}
|
|
364
|
+
getSubagentRuns(chatId: string): Record<string, SubagentRunSnapshot> { return getSubagentRunsFromMap(this.state.subagentRunsByChatId, chatId) }
|
|
1771
365
|
|
|
1772
|
-
*runningSubagentRuns(): Iterable<SubagentRunSnapshot> {
|
|
1773
|
-
for (const map of this.state.subagentRunsByChatId.values()) {
|
|
1774
|
-
for (const run of map.values()) {
|
|
1775
|
-
if (run.status === "running") yield run
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
366
|
+
*runningSubagentRuns(): Iterable<SubagentRunSnapshot> { yield* runningSubagentRunsFromMap(this.state.subagentRunsByChatId) }
|
|
1779
367
|
|
|
1780
|
-
async setSessionTokenForProvider(
|
|
1781
|
-
chatId: string,
|
|
1782
|
-
provider: AgentProvider,
|
|
1783
|
-
sessionToken: string | null,
|
|
1784
|
-
) {
|
|
1785
|
-
const chat = this.requireChat(chatId)
|
|
1786
|
-
if ((chat.sessionTokensByProvider[provider] ?? null) === sessionToken) return
|
|
1787
|
-
const event: TurnEvent = {
|
|
1788
|
-
v: STORE_VERSION,
|
|
1789
|
-
type: "session_token_set",
|
|
1790
|
-
timestamp: Date.now(),
|
|
1791
|
-
chatId,
|
|
1792
|
-
sessionToken,
|
|
1793
|
-
provider,
|
|
1794
|
-
}
|
|
1795
|
-
await this.append(this.turnsLogPath, event)
|
|
1796
|
-
}
|
|
368
|
+
async setSessionTokenForProvider(chatId: string, provider: AgentProvider, sessionToken: string | null) { return EntityWrite.setSessionTokenForProvider(this.buildSessionWriteDeps(), chatId, provider, sessionToken) }
|
|
1797
369
|
|
|
1798
|
-
async recordSessionCommandsLoaded(chatId: string, commands: SlashCommand[]) {
|
|
1799
|
-
const chat = this.requireChat(chatId)
|
|
1800
|
-
const normalized = commands.map((c) => ({
|
|
1801
|
-
name: c.name,
|
|
1802
|
-
description: c.description,
|
|
1803
|
-
argumentHint: c.argumentHint,
|
|
1804
|
-
}))
|
|
1805
|
-
if (chat.slashCommands && slashCommandsEqual(chat.slashCommands, normalized)) {
|
|
1806
|
-
return
|
|
1807
|
-
}
|
|
1808
|
-
const event: TurnEvent = {
|
|
1809
|
-
v: STORE_VERSION,
|
|
1810
|
-
type: "session_commands_loaded",
|
|
1811
|
-
timestamp: Date.now(),
|
|
1812
|
-
chatId,
|
|
1813
|
-
commands: normalized,
|
|
1814
|
-
}
|
|
1815
|
-
await this.append(this.turnsLogPath, event)
|
|
1816
|
-
}
|
|
370
|
+
async recordSessionCommandsLoaded(chatId: string, commands: SlashCommand[]) { return EntityWrite.recordSessionCommandsLoaded(this.buildSessionWriteDeps(), chatId, commands) }
|
|
1817
371
|
|
|
1818
|
-
async setPendingForkSessionToken(
|
|
1819
|
-
chatId
|
|
1820
|
-
value: { provider: AgentProvider; token: string } | null,
|
|
1821
|
-
) {
|
|
1822
|
-
const chat = this.requireChat(chatId)
|
|
1823
|
-
const current = chat.pendingForkSessionToken ?? null
|
|
1824
|
-
const same =
|
|
1825
|
-
(current == null && value == null)
|
|
1826
|
-
|| (current != null && value != null
|
|
1827
|
-
&& current.provider === value.provider
|
|
1828
|
-
&& current.token === value.token)
|
|
1829
|
-
if (same) return
|
|
1830
|
-
const event: TurnEvent = {
|
|
1831
|
-
v: STORE_VERSION,
|
|
1832
|
-
type: "pending_fork_session_token_set",
|
|
1833
|
-
timestamp: Date.now(),
|
|
1834
|
-
chatId,
|
|
1835
|
-
pendingForkSessionToken: value?.token ?? null,
|
|
1836
|
-
provider: value?.provider,
|
|
1837
|
-
}
|
|
1838
|
-
await this.append(this.turnsLogPath, event)
|
|
372
|
+
async setPendingForkSessionToken(chatId: string, value: { provider: AgentProvider; token: string } | null) {
|
|
373
|
+
return EntityWrite.setPendingForkSessionToken(this.buildSessionWriteDeps(), chatId, value)
|
|
1839
374
|
}
|
|
1840
375
|
|
|
1841
|
-
async setSourceHash(chatId: string, sourceHash: string | null) {
|
|
1842
|
-
const chat = this.requireChat(chatId)
|
|
1843
|
-
if (chat.sourceHash === sourceHash) return
|
|
1844
|
-
const event: ChatEvent = {
|
|
1845
|
-
v: STORE_VERSION,
|
|
1846
|
-
type: "chat_source_hash_set",
|
|
1847
|
-
timestamp: Date.now(),
|
|
1848
|
-
chatId,
|
|
1849
|
-
sourceHash,
|
|
1850
|
-
}
|
|
1851
|
-
await this.append(this.chatsLogPath, event)
|
|
1852
|
-
}
|
|
376
|
+
async setSourceHash(chatId: string, sourceHash: string | null) { return EntityWrite.setSourceHash(this.buildEntityWriteDeps(), chatId, sourceHash) }
|
|
1853
377
|
|
|
1854
378
|
getProject(projectId: string) {
|
|
1855
379
|
const project = this.state.projectsById.get(projectId)
|
|
@@ -1871,110 +395,23 @@ export class EventStore implements PushEventStore {
|
|
|
1871
395
|
return chat
|
|
1872
396
|
}
|
|
1873
397
|
|
|
1874
|
-
getSidebarProjectOrder() {
|
|
1875
|
-
return [...this.sidebarProjectOrder]
|
|
1876
|
-
}
|
|
398
|
+
getSidebarProjectOrder() { return [...this.sidebarProjectOrderRef.value] }
|
|
1877
399
|
|
|
1878
|
-
|
|
1879
|
-
if (entries.length === 0) {
|
|
1880
|
-
return { entries: [], hasOlder: false, olderCursor: null }
|
|
1881
|
-
}
|
|
400
|
+
// ─── Message read methods (thin delegates) ────────────────────────────────
|
|
1882
401
|
|
|
1883
|
-
|
|
1884
|
-
const startIndex = Math.max(0, endIndex - limit)
|
|
1885
|
-
return {
|
|
1886
|
-
entries: cloneTranscriptEntries(entries.slice(startIndex, endIndex)),
|
|
1887
|
-
hasOlder: startIndex > 0,
|
|
1888
|
-
olderCursor: startIndex > 0 ? encodeHistoryCursor(startIndex) : null,
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
402
|
+
getMessages(chatId: string) { return MessageRead.getMessages(this.buildMessageReadDeps(), chatId) }
|
|
1891
403
|
|
|
1892
|
-
|
|
1893
|
-
if (this.cachedTranscript?.chatId === chatId) {
|
|
1894
|
-
return cloneTranscriptEntries(this.cachedTranscript.entries)
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
const legacyEntries = this.legacyMessagesByChatId.get(chatId)
|
|
1898
|
-
if (legacyEntries) {
|
|
1899
|
-
this.cachedTranscript = { chatId, entries: cloneTranscriptEntries(legacyEntries) }
|
|
1900
|
-
return cloneTranscriptEntries(this.cachedTranscript.entries)
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
const entries = this.loadTranscriptFromDisk(chatId)
|
|
1904
|
-
this.cachedTranscript = { chatId, entries }
|
|
1905
|
-
return cloneTranscriptEntries(entries)
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
getQueuedMessages(chatId: string) {
|
|
1909
|
-
const entries = this.state.queuedMessagesByChatId.get(chatId) ?? []
|
|
1910
|
-
return entries.map((entry) => ({
|
|
1911
|
-
...entry,
|
|
1912
|
-
attachments: [...entry.attachments],
|
|
1913
|
-
}))
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
getQueuedMessage(chatId: string, queuedMessageId: string) {
|
|
1917
|
-
return this.getQueuedMessages(chatId).find((entry) => entry.id === queuedMessageId) ?? null
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
getRecentMessagesPage(chatId: string, limit: number): ChatHistoryPage {
|
|
1921
|
-
if (limit <= 0) {
|
|
1922
|
-
return { messages: [], hasOlder: false, olderCursor: null }
|
|
1923
|
-
}
|
|
404
|
+
getQueuedMessages(chatId: string) { return MessageRead.getQueuedMessages(this.buildMessageReadDeps(), chatId) }
|
|
1924
405
|
|
|
1925
|
-
|
|
1926
|
-
const page = this.getMessagesPageFromEntries(entries, limit)
|
|
406
|
+
getQueuedMessage(chatId: string, queuedMessageId: string) { return MessageRead.getQueuedMessage(this.buildMessageReadDeps(), chatId, queuedMessageId) }
|
|
1927
407
|
|
|
1928
|
-
|
|
1929
|
-
messages: page.entries,
|
|
1930
|
-
hasOlder: page.hasOlder,
|
|
1931
|
-
olderCursor: page.olderCursor,
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
408
|
+
getRecentMessagesPage(chatId: string, limit: number): ChatHistoryPage { return MessageRead.getRecentMessagesPage(this.buildMessageReadDeps(), chatId, limit) }
|
|
1934
409
|
|
|
1935
|
-
getMessagesPageBefore(chatId: string, beforeCursor: string, limit: number): ChatHistoryPage {
|
|
1936
|
-
if (limit <= 0) {
|
|
1937
|
-
return { messages: [], hasOlder: false, olderCursor: null }
|
|
1938
|
-
}
|
|
410
|
+
getMessagesPageBefore(chatId: string, beforeCursor: string, limit: number): ChatHistoryPage { return MessageRead.getMessagesPageBefore(this.buildMessageReadDeps(), chatId, beforeCursor, limit) }
|
|
1939
411
|
|
|
1940
|
-
|
|
1941
|
-
// Coalesce identically to getRecentMessagesPage so cursors (which index the
|
|
1942
|
-
// coalesced array) stay consistent across recent + load-older paging.
|
|
1943
|
-
const entries = coalesceContextWindowUpdates(this.getMessages(chatId))
|
|
1944
|
-
const page = this.getMessagesPageFromEntries(entries, limit, beforeIndex)
|
|
412
|
+
getRecentChatHistory(chatId: string, recentLimit: number) { return MessageRead.getRecentChatHistory(this.buildMessageReadDeps(), chatId, recentLimit) }
|
|
1945
413
|
|
|
1946
|
-
|
|
1947
|
-
messages: page.entries,
|
|
1948
|
-
hasOlder: page.hasOlder,
|
|
1949
|
-
olderCursor: page.olderCursor,
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
getRecentChatHistory(chatId: string, recentLimit: number) {
|
|
1954
|
-
const page = this.getRecentMessagesPage(chatId, recentLimit)
|
|
1955
|
-
const pending = this.listPendingToolRequests(chatId)
|
|
1956
|
-
const pendingEntries: TranscriptEntry[] = pending.map((req) => ({
|
|
1957
|
-
_id: `pending-tool-request-${req.id}`,
|
|
1958
|
-
createdAt: req.createdAt,
|
|
1959
|
-
kind: "pending_tool_request",
|
|
1960
|
-
toolRequestId: req.id,
|
|
1961
|
-
toolName: req.toolName,
|
|
1962
|
-
arguments: req.arguments,
|
|
1963
|
-
}))
|
|
1964
|
-
const merged = [...page.messages, ...pendingEntries]
|
|
1965
|
-
return {
|
|
1966
|
-
messages: merged,
|
|
1967
|
-
history: getHistorySnapshot({
|
|
1968
|
-
entries: merged,
|
|
1969
|
-
hasOlder: page.hasOlder,
|
|
1970
|
-
olderCursor: page.olderCursor,
|
|
1971
|
-
}, recentLimit),
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
listProjects() {
|
|
1976
|
-
return [...this.state.projectsById.values()].filter((project) => !project.deletedAt)
|
|
1977
|
-
}
|
|
414
|
+
listProjects() { return [...this.state.projectsById.values()].filter((project) => !project.deletedAt) }
|
|
1978
415
|
|
|
1979
416
|
listChatsByProject(projectId: string) {
|
|
1980
417
|
return [...this.state.chatsById.values()]
|
|
@@ -1982,556 +419,85 @@ export class EventStore implements PushEventStore {
|
|
|
1982
419
|
.sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
|
|
1983
420
|
}
|
|
1984
421
|
|
|
1985
|
-
getChatCount(projectId: string) {
|
|
1986
|
-
return this.listChatsByProject(projectId).length
|
|
1987
|
-
}
|
|
422
|
+
getChatCount(projectId: string) { return MessageRead.getChatCount(this.buildMessageReadDeps(), projectId) }
|
|
1988
423
|
|
|
1989
|
-
async getLegacyTranscriptStats(): Promise<LegacyTranscriptStats> {
|
|
1990
|
-
const messagesLogSize = await this.storage.size(this.messagesLogPath)
|
|
1991
|
-
const sources: LegacyTranscriptStats["sources"] = []
|
|
1992
|
-
if (this.snapshotHasLegacyMessages) {
|
|
1993
|
-
sources.push("snapshot")
|
|
1994
|
-
}
|
|
1995
|
-
if (messagesLogSize > 0) {
|
|
1996
|
-
sources.push("messages_log")
|
|
1997
|
-
}
|
|
424
|
+
async getLegacyTranscriptStats(): Promise<LegacyTranscriptStats> { return getLegacyTranscriptStatsFn(this.buildInitDeps()) }
|
|
1998
425
|
|
|
1999
|
-
|
|
2000
|
-
for (const entries of this.legacyMessagesByChatId.values()) {
|
|
2001
|
-
entryCount += entries.length
|
|
2002
|
-
}
|
|
426
|
+
async hasLegacyTranscriptData() { return hasLegacyTranscriptDataFn(this.buildInitDeps()) }
|
|
2003
427
|
|
|
2004
|
-
|
|
2005
|
-
hasLegacyData: sources.length > 0 || this.legacyMessagesByChatId.size > 0,
|
|
2006
|
-
sources,
|
|
2007
|
-
chatCount: this.legacyMessagesByChatId.size,
|
|
2008
|
-
entryCount,
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
428
|
+
async snapshotAndTruncateLogs() { return snapshotAndTruncateLogsFn(this.buildInitDeps()) }
|
|
2011
429
|
|
|
2012
|
-
async
|
|
2013
|
-
return (await this.getLegacyTranscriptStats()).hasLegacyData
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
private createSnapshot(): SnapshotFile {
|
|
2017
|
-
return {
|
|
2018
|
-
v: STORE_VERSION,
|
|
2019
|
-
generatedAt: Date.now(),
|
|
2020
|
-
projects: this.listProjects().map((project) => ({ ...project })),
|
|
2021
|
-
chats: [...this.state.chatsById.values()]
|
|
2022
|
-
.filter((chat) => !chat.deletedAt)
|
|
2023
|
-
.map((chat) => ({ ...chat })),
|
|
2024
|
-
queuedMessages: [...this.state.queuedMessagesByChatId.entries()]
|
|
2025
|
-
.map(([chatId, entries]) => ({
|
|
2026
|
-
chatId,
|
|
2027
|
-
entries: entries.map((entry) => ({
|
|
2028
|
-
...entry,
|
|
2029
|
-
attachments: [...entry.attachments],
|
|
2030
|
-
})),
|
|
2031
|
-
})),
|
|
2032
|
-
autoContinueEvents: [...this.state.autoContinueEventsByChatId.entries()].map(([chatId, events]) => ({
|
|
2033
|
-
chatId,
|
|
2034
|
-
events: [...events],
|
|
2035
|
-
})),
|
|
2036
|
-
stacks: [...this.state.stacksById.values()]
|
|
2037
|
-
.filter((stack) => !stack.deletedAt)
|
|
2038
|
-
.map((stack) => ({ ...stack, projectIds: [...stack.projectIds] })),
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
430
|
+
async migrateLegacyTranscripts(onProgress?: (message: string) => void) { return migrateLegacyTranscriptsFn(this.buildInitDeps(), onProgress) }
|
|
2041
431
|
|
|
2042
|
-
async
|
|
2043
|
-
const snapshot = this.createSnapshot()
|
|
2044
|
-
await this.storage.writeText(this.snapshotPath, JSON.stringify(snapshot, null, 2))
|
|
2045
|
-
await Promise.all([
|
|
2046
|
-
this.storage.writeText(this.projectsLogPath, ""),
|
|
2047
|
-
this.storage.writeText(this.chatsLogPath, ""),
|
|
2048
|
-
this.storage.writeText(this.messagesLogPath, ""),
|
|
2049
|
-
this.storage.writeText(this.queuedMessagesLogPath, ""),
|
|
2050
|
-
this.storage.writeText(this.turnsLogPath, ""),
|
|
2051
|
-
this.storage.writeText(this.schedulesLogPath, ""),
|
|
2052
|
-
this.storage.writeText(this.stacksLogPath, ""),
|
|
2053
|
-
// tunnels.jsonl is NOT compacted into the snapshot — it's left as-is
|
|
2054
|
-
// so that active tunnel state survives server restarts.
|
|
2055
|
-
// tool-requests.jsonl is NOT persisted to the snapshot. After compaction,
|
|
2056
|
-
// in-memory state remains intact for the current process lifetime.
|
|
2057
|
-
// On next server boot, tool-requests will be absent (fail-closed);
|
|
2058
|
-
// Task 7 recoverOnStartup marks them session_closed.
|
|
2059
|
-
this.storage.writeText(this.toolRequestsLogPath, ""),
|
|
2060
|
-
])
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
async migrateLegacyTranscripts(onProgress?: (message: string) => void) {
|
|
2064
|
-
const stats = await this.getLegacyTranscriptStats()
|
|
2065
|
-
if (!stats.hasLegacyData) return false
|
|
2066
|
-
|
|
2067
|
-
const sourceSummary = stats.sources.map((source) => source === "messages_log" ? "messages.jsonl" : "snapshot.json").join(", ")
|
|
2068
|
-
onProgress?.(`${LOG_PREFIX} transcript migration detected: ${stats.chatCount} chats, ${stats.entryCount} entries from ${sourceSummary}`)
|
|
2069
|
-
|
|
2070
|
-
const messageSets = [...this.legacyMessagesByChatId.entries()]
|
|
2071
|
-
onProgress?.(`${LOG_PREFIX} transcript migration: writing ${messageSets.length} per-chat transcript files`)
|
|
2072
|
-
|
|
2073
|
-
await this.storage.mkdir(this.transcriptsDir)
|
|
2074
|
-
const logEveryChat = messageSets.length <= 10
|
|
2075
|
-
for (let index = 0; index < messageSets.length; index += 1) {
|
|
2076
|
-
const [chatId, entries] = messageSets[index]
|
|
2077
|
-
const transcriptPath = this.transcriptPath(chatId)
|
|
2078
|
-
const tempPath = `${transcriptPath}.tmp`
|
|
2079
|
-
const payload = entries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
2080
|
-
await this.storage.writeText(tempPath, payload ? `${payload}\n` : "")
|
|
2081
|
-
await this.storage.rename(tempPath, transcriptPath)
|
|
2082
|
-
if (logEveryChat || (index + 1) % 25 === 0 || index === messageSets.length - 1) {
|
|
2083
|
-
onProgress?.(`${LOG_PREFIX} transcript migration: ${index + 1}/${messageSets.length} chats`)
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
this.clearLegacyTranscriptState()
|
|
2088
|
-
await this.snapshotAndTruncateLogs()
|
|
2089
|
-
this.cachedTranscript = null
|
|
2090
|
-
onProgress?.(`${LOG_PREFIX} transcript migration complete`)
|
|
2091
|
-
return true
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
private async shouldSnapshotLogs() {
|
|
2095
|
-
const sizes = await Promise.all([
|
|
2096
|
-
this.storage.size(this.projectsLogPath),
|
|
2097
|
-
this.storage.size(this.chatsLogPath),
|
|
2098
|
-
this.storage.size(this.messagesLogPath),
|
|
2099
|
-
this.storage.size(this.queuedMessagesLogPath),
|
|
2100
|
-
this.storage.size(this.turnsLogPath),
|
|
2101
|
-
this.storage.size(this.schedulesLogPath),
|
|
2102
|
-
this.storage.size(this.stacksLogPath),
|
|
2103
|
-
this.storage.size(this.toolRequestsLogPath),
|
|
2104
|
-
])
|
|
2105
|
-
return sizes.reduce((total, size) => total + size, 0) >= SNAPSHOT_THRESHOLD_BYTES
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
async appendAutoContinueEvent(event: AutoContinueEvent) {
|
|
2109
|
-
return this.append(this.schedulesLogPath, event)
|
|
2110
|
-
}
|
|
432
|
+
async appendAutoContinueEvent(event: AutoContinueEvent) { return EntityWrite.appendAutoContinueEvent(this.buildSessionWriteDeps(), event) }
|
|
2111
433
|
|
|
2112
434
|
getAutoContinueEvents(chatId: string): AutoContinueEvent[] {
|
|
2113
435
|
const list = this.state.autoContinueEventsByChatId.get(chatId)
|
|
2114
436
|
return list ? [...list] : []
|
|
2115
437
|
}
|
|
2116
438
|
|
|
2117
|
-
listAutoContinueChats(): string[] {
|
|
2118
|
-
return [...this.state.autoContinueEventsByChatId.keys()]
|
|
2119
|
-
}
|
|
439
|
+
listAutoContinueChats(): string[] { return [...this.state.autoContinueEventsByChatId.keys()] }
|
|
2120
440
|
|
|
2121
|
-
|
|
2122
|
-
const payload = `${JSON.stringify(event)}\n`
|
|
2123
|
-
this.writeChain = this.writeChain.then(async () => {
|
|
2124
|
-
await this.storage.appendText(this.tunnelLogPath, payload)
|
|
2125
|
-
this.applyTunnelEvent(event)
|
|
2126
|
-
})
|
|
2127
|
-
await this.writeChain
|
|
2128
|
-
}
|
|
441
|
+
// ─── Peripheral event methods (thin delegates) ───────────────────────────
|
|
2129
442
|
|
|
2130
|
-
|
|
2131
|
-
const list = this.tunnelEventsByChatId.get(chatId)
|
|
2132
|
-
return list ? [...list] : []
|
|
2133
|
-
}
|
|
443
|
+
async appendTunnelEvent(event: CloudflareTunnelEvent): Promise<void> { return PeripheralEvents.appendTunnelEvent(this.buildPeripheralEventsDeps(), event) }
|
|
2134
444
|
|
|
2135
|
-
|
|
2136
|
-
return [...this.tunnelEventsByChatId.keys()]
|
|
2137
|
-
}
|
|
445
|
+
getTunnelEvents(chatId: string): CloudflareTunnelEvent[] { return PeripheralEvents.getTunnelEvents(this.buildPeripheralEventsDeps(), chatId) }
|
|
2138
446
|
|
|
2139
|
-
|
|
2140
|
-
const existing = this.tunnelEventsByChatId.get(event.chatId) ?? []
|
|
2141
|
-
existing.push(event)
|
|
2142
|
-
this.tunnelEventsByChatId.set(event.chatId, existing)
|
|
2143
|
-
}
|
|
447
|
+
listTunnelChats(): string[] { return PeripheralEvents.listTunnelChats(this.buildPeripheralEventsDeps()) }
|
|
2144
448
|
|
|
2145
|
-
private async loadTunnelEvents(): Promise<void> {
|
|
2146
|
-
if (!(await this.storage.exists(this.tunnelLogPath))) return
|
|
2147
|
-
const text = await this.storage.readText(this.tunnelLogPath)
|
|
2148
|
-
if (!text.trim()) return
|
|
2149
|
-
|
|
2150
|
-
for (const rawLine of text.split("\n")) {
|
|
2151
|
-
const line = rawLine.trim()
|
|
2152
|
-
if (!line) continue
|
|
2153
|
-
try {
|
|
2154
|
-
const event: CloudflareTunnelEvent = JSON.parse(line)
|
|
2155
|
-
this.applyTunnelEvent(event)
|
|
2156
|
-
} catch {
|
|
2157
|
-
log.warn(`${LOG_PREFIX} Ignoring malformed line in tunnels.jsonl`)
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
449
|
+
private async loadTunnelEvents(): Promise<void> { await PeripheralEvents.loadTunnelEvents(this.buildPeripheralEventsDeps()) }
|
|
2161
450
|
|
|
2162
|
-
async appendShareEvent(event: ShareEvent): Promise<void> {
|
|
2163
|
-
const payload = `${JSON.stringify(event)}\n`
|
|
2164
|
-
this.writeChain = this.writeChain.then(async () => {
|
|
2165
|
-
await this.storage.appendText(this.sharesLogPath, payload)
|
|
2166
|
-
this.shareEventsAll.push(event)
|
|
2167
|
-
})
|
|
2168
|
-
await this.writeChain
|
|
2169
|
-
}
|
|
451
|
+
async appendShareEvent(event: ShareEvent): Promise<void> { return PeripheralEvents.appendShareEvent(this.buildPeripheralEventsDeps(), event) }
|
|
2170
452
|
|
|
2171
|
-
getShareEvents(): ShareEvent[] {
|
|
2172
|
-
return [...this.shareEventsAll]
|
|
2173
|
-
}
|
|
453
|
+
getShareEvents(): ShareEvent[] { return PeripheralEvents.getShareEvents(this.buildPeripheralEventsDeps()) }
|
|
2174
454
|
|
|
2175
|
-
private async loadShareEvents(): Promise<void> {
|
|
2176
|
-
if (!(await this.storage.exists(this.sharesLogPath))) return
|
|
2177
|
-
const text = await this.storage.readText(this.sharesLogPath)
|
|
2178
|
-
if (!text.trim()) return
|
|
2179
|
-
for (const rawLine of text.split("\n")) {
|
|
2180
|
-
const line = rawLine.trim()
|
|
2181
|
-
if (!line) continue
|
|
2182
|
-
try {
|
|
2183
|
-
const event: ShareEvent = JSON.parse(line)
|
|
2184
|
-
this.shareEventsAll.push(event)
|
|
2185
|
-
} catch {
|
|
2186
|
-
log.warn(`${LOG_PREFIX} Ignoring malformed line in shares.jsonl`)
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
455
|
+
private async loadShareEvents(): Promise<void> { await PeripheralEvents.loadShareEvents(this.buildPeripheralEventsDeps()) }
|
|
2190
456
|
|
|
2191
|
-
async appendPushEvent(event: PushEvent): Promise<void> {
|
|
2192
|
-
const payload = `${JSON.stringify(event)}\n`
|
|
2193
|
-
this.writeChain = this.writeChain.then(async () => {
|
|
2194
|
-
await this.storage.appendText(this.pushLogPath, payload)
|
|
2195
|
-
})
|
|
2196
|
-
await this.writeChain
|
|
2197
|
-
}
|
|
457
|
+
async appendPushEvent(event: PushEvent): Promise<void> { return PeripheralEvents.appendPushEvent(this.buildPeripheralEventsDeps(), event) }
|
|
2198
458
|
|
|
2199
|
-
async loadPushEvents(): Promise<PushEvent[]> {
|
|
2200
|
-
if (!(await this.storage.exists(this.pushLogPath))) return []
|
|
2201
|
-
const text = await this.storage.readText(this.pushLogPath)
|
|
2202
|
-
if (!text.trim()) return []
|
|
2203
|
-
|
|
2204
|
-
const events: PushEvent[] = []
|
|
2205
|
-
for (const rawLine of text.split("\n")) {
|
|
2206
|
-
const line = rawLine.trim()
|
|
2207
|
-
if (!line) continue
|
|
2208
|
-
try {
|
|
2209
|
-
const pushEvent: PushEvent = JSON.parse(line)
|
|
2210
|
-
events.push(pushEvent)
|
|
2211
|
-
} catch {
|
|
2212
|
-
log.warn(`${LOG_PREFIX} Ignoring malformed line in push.jsonl`)
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
return events
|
|
2216
|
-
}
|
|
459
|
+
async loadPushEvents(): Promise<PushEvent[]> { return PeripheralEvents.loadPushEvents(this.buildPeripheralEventsDeps()) }
|
|
2217
460
|
|
|
2218
|
-
async putToolRequest(req: ToolRequest): Promise<void> {
|
|
2219
|
-
this.state.toolRequestsById.set(req.id, { ...req })
|
|
2220
|
-
await this.append(this.toolRequestsLogPath, {
|
|
2221
|
-
v: 3,
|
|
2222
|
-
type: "tool_request_put",
|
|
2223
|
-
timestamp: Date.now(),
|
|
2224
|
-
request: req,
|
|
2225
|
-
} satisfies ToolRequestEvent)
|
|
2226
|
-
}
|
|
461
|
+
async putToolRequest(req: ToolRequest): Promise<void> { return putToolRequestFn(this.buildToolRequestWriteDeps(), req) }
|
|
2227
462
|
|
|
2228
|
-
getToolRequest(id: string): ToolRequest | null {
|
|
2229
|
-
const req = this.state.toolRequestsById.get(id)
|
|
2230
|
-
return req ? { ...req } : null
|
|
2231
|
-
}
|
|
463
|
+
getToolRequest(id: string): ToolRequest | null { return getToolRequestFromMap(this.state.toolRequestsById, id) }
|
|
2232
464
|
|
|
2233
|
-
listPendingToolRequests(chatId: string): ToolRequest[] {
|
|
2234
|
-
const out: ToolRequest[] = []
|
|
2235
|
-
for (const req of this.state.toolRequestsById.values()) {
|
|
2236
|
-
if (req.chatId !== chatId) continue
|
|
2237
|
-
if (req.status !== "pending") continue
|
|
2238
|
-
out.push({ ...req })
|
|
2239
|
-
}
|
|
2240
|
-
return out
|
|
2241
|
-
}
|
|
465
|
+
listPendingToolRequests(chatId: string): ToolRequest[] { return listPendingToolRequestsFromMap(this.state.toolRequestsById, chatId) }
|
|
2242
466
|
|
|
2243
467
|
async resolveToolRequest(
|
|
2244
468
|
id: string,
|
|
2245
|
-
args: {
|
|
2246
|
-
status: ToolRequestStatus
|
|
2247
|
-
decision?: ToolRequestDecision
|
|
2248
|
-
resolvedAt: number
|
|
2249
|
-
mismatchReason?: string
|
|
2250
|
-
},
|
|
469
|
+
args: { status: ToolRequestStatus; decision?: ToolRequestDecision; resolvedAt: number; mismatchReason?: string },
|
|
2251
470
|
): Promise<void> {
|
|
2252
|
-
|
|
2253
|
-
if (!existing) throw new Error(`resolveToolRequest: unknown id ${id}`)
|
|
2254
|
-
const next: ToolRequest = {
|
|
2255
|
-
...existing,
|
|
2256
|
-
status: args.status,
|
|
2257
|
-
decision: args.decision ?? existing.decision,
|
|
2258
|
-
resolvedAt: args.resolvedAt,
|
|
2259
|
-
mismatchReason: args.mismatchReason,
|
|
2260
|
-
}
|
|
2261
|
-
this.state.toolRequestsById.set(id, next)
|
|
2262
|
-
await this.append(this.toolRequestsLogPath, {
|
|
2263
|
-
v: 3,
|
|
2264
|
-
type: "tool_request_resolved",
|
|
2265
|
-
timestamp: Date.now(),
|
|
2266
|
-
id,
|
|
2267
|
-
status: args.status,
|
|
2268
|
-
decision: args.decision,
|
|
2269
|
-
resolvedAt: args.resolvedAt,
|
|
2270
|
-
mismatchReason: args.mismatchReason,
|
|
2271
|
-
} satisfies ToolRequestEvent)
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
scanAllToolRequests(): ToolRequest[] {
|
|
2275
|
-
return [...this.state.toolRequestsById.values()].map((req) => ({ ...req }))
|
|
471
|
+
return resolveToolRequestFn(this.buildToolRequestWriteDeps(), id, args)
|
|
2276
472
|
}
|
|
2277
473
|
|
|
2278
|
-
|
|
2279
|
-
await this.writeChain
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
private readonly orchRunsSubscribers = new Set<() => void>()
|
|
2283
|
-
|
|
2284
|
-
private applyOrchestrationEvent(event: OrchestrationEvent) {
|
|
2285
|
-
if (event.type === "orch_run_created") {
|
|
2286
|
-
const tasksById = new Map<string, OrchTaskRecord>()
|
|
2287
|
-
for (const spec of event.tasks) {
|
|
2288
|
-
tasksById.set(spec.id, {
|
|
2289
|
-
taskId: spec.id,
|
|
2290
|
-
title: spec.title,
|
|
2291
|
-
prompt: spec.prompt,
|
|
2292
|
-
scopePaths: spec.scopePaths ?? [],
|
|
2293
|
-
state: "queued",
|
|
2294
|
-
ownerWorkerId: null,
|
|
2295
|
-
worktreePath: null,
|
|
2296
|
-
branch: null,
|
|
2297
|
-
baseSha: null,
|
|
2298
|
-
phaseIndex: -1,
|
|
2299
|
-
attempts: 0,
|
|
2300
|
-
error: null,
|
|
2301
|
-
commitSha: null,
|
|
2302
|
-
lastPhaseOutput: null,
|
|
2303
|
-
verifying: false,
|
|
2304
|
-
updatedAt: event.timestamp,
|
|
2305
|
-
})
|
|
2306
|
-
}
|
|
2307
|
-
this.state.orchRunsById.set(event.runId, {
|
|
2308
|
-
runId: event.runId,
|
|
2309
|
-
status: "running",
|
|
2310
|
-
config: event.config,
|
|
2311
|
-
tasksById,
|
|
2312
|
-
taskOrder: event.tasks.map((t) => t.id),
|
|
2313
|
-
worktrees: [],
|
|
2314
|
-
eventLog: [event],
|
|
2315
|
-
createdAt: event.timestamp,
|
|
2316
|
-
updatedAt: event.timestamp,
|
|
2317
|
-
})
|
|
2318
|
-
return
|
|
2319
|
-
}
|
|
2320
|
-
const run = this.state.orchRunsById.get(event.runId)
|
|
2321
|
-
if (!run) return
|
|
2322
|
-
run.eventLog.push(event)
|
|
2323
|
-
run.updatedAt = event.timestamp
|
|
2324
|
-
if (event.type === "orch_run_completed" || event.type === "orch_run_cancelled") {
|
|
2325
|
-
const trans = nextRunStatus(run.status, event.type)
|
|
2326
|
-
if (!trans.ok) {
|
|
2327
|
-
log.warn(`${LOG_PREFIX} dropped illegal orch run transition`, { runId: event.runId, from: run.status, event: event.type })
|
|
2328
|
-
return
|
|
2329
|
-
}
|
|
2330
|
-
run.status = trans.next
|
|
2331
|
-
return
|
|
2332
|
-
}
|
|
2333
|
-
if (event.type === "orch_scope_overlap_flagged") return // observability-only, no state fold
|
|
2334
|
-
if (event.type === "orch_config_warning") return // observability-only, no state fold
|
|
2335
|
-
// Worktree pool fold (F13)
|
|
2336
|
-
if (event.type === "orch_worktree_provisioned") {
|
|
2337
|
-
run.worktrees.push({
|
|
2338
|
-
index: event.index, path: event.path, branch: event.branch,
|
|
2339
|
-
heldByTaskId: null, initialized: false,
|
|
2340
|
-
})
|
|
2341
|
-
return
|
|
2342
|
-
}
|
|
2343
|
-
if (event.type === "orch_worktree_init_started") return // timeline-only
|
|
2344
|
-
if (event.type === "orch_worktree_init_completed") {
|
|
2345
|
-
const slot = run.worktrees.find((w) => w.index === event.index)
|
|
2346
|
-
if (slot) slot.initialized = event.ok
|
|
2347
|
-
return
|
|
2348
|
-
}
|
|
2349
|
-
const task = run.tasksById.get(event.taskId)
|
|
2350
|
-
if (!task) return
|
|
2351
|
-
// FSM guard: drop an illegal task transition rather than corrupt the read
|
|
2352
|
-
// model. The engine only emits legal transitions, so this is a defensive
|
|
2353
|
-
// invariant (also protects replay of an older/foreign log).
|
|
2354
|
-
if (isTaskEventType(event.type)) {
|
|
2355
|
-
const trans = nextTaskState(task.state, event.type)
|
|
2356
|
-
if (!trans.ok) {
|
|
2357
|
-
log.warn(`${LOG_PREFIX} dropped illegal orch task transition`, { runId: event.runId, taskId: event.taskId, from: task.state, event: event.type })
|
|
2358
|
-
return
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
task.updatedAt = event.timestamp
|
|
2362
|
-
const slotOf = (t: OrchTaskRecord) => run.worktrees.find((w) => w.path === t.worktreePath)
|
|
2363
|
-
switch (event.type) {
|
|
2364
|
-
case "orch_task_claimed":
|
|
2365
|
-
task.state = "claimed"
|
|
2366
|
-
task.ownerWorkerId = event.workerId
|
|
2367
|
-
task.worktreePath = event.worktreePath
|
|
2368
|
-
task.branch = event.branch
|
|
2369
|
-
task.baseSha = event.baseSha
|
|
2370
|
-
task.attempts += 1
|
|
2371
|
-
{
|
|
2372
|
-
const slot = slotOf(task)
|
|
2373
|
-
if (slot) slot.heldByTaskId = task.taskId
|
|
2374
|
-
}
|
|
2375
|
-
break
|
|
2376
|
-
case "orch_phase_started":
|
|
2377
|
-
task.state = "running"
|
|
2378
|
-
task.phaseIndex = event.phaseIndex
|
|
2379
|
-
task.verifying = false
|
|
2380
|
-
break
|
|
2381
|
-
case "orch_phase_completed":
|
|
2382
|
-
task.lastPhaseOutput = event.output
|
|
2383
|
-
break
|
|
2384
|
-
case "orch_gate_opened":
|
|
2385
|
-
task.state = "gated"
|
|
2386
|
-
break
|
|
2387
|
-
case "orch_gate_resolved":
|
|
2388
|
-
if (event.decision === "approve") task.state = "running"
|
|
2389
|
-
// reject: state stays gated; the engine appends orch_task_failed next
|
|
2390
|
-
break
|
|
2391
|
-
case "orch_verify_started":
|
|
2392
|
-
task.verifying = true // observable stage; task stays "running"
|
|
2393
|
-
break
|
|
2394
|
-
case "orch_verify_completed":
|
|
2395
|
-
task.verifying = false
|
|
2396
|
-
break
|
|
2397
|
-
case "orch_task_committed":
|
|
2398
|
-
task.state = "committed"
|
|
2399
|
-
task.ownerWorkerId = null
|
|
2400
|
-
task.commitSha = event.commitSha
|
|
2401
|
-
task.verifying = false
|
|
2402
|
-
{
|
|
2403
|
-
const slot = slotOf(task)
|
|
2404
|
-
if (slot?.heldByTaskId === task.taskId) slot.heldByTaskId = null
|
|
2405
|
-
}
|
|
2406
|
-
break
|
|
2407
|
-
case "orch_task_failed":
|
|
2408
|
-
task.state = "failed"
|
|
2409
|
-
task.ownerWorkerId = null
|
|
2410
|
-
task.error = event.error
|
|
2411
|
-
task.verifying = false
|
|
2412
|
-
{
|
|
2413
|
-
const slot = slotOf(task)
|
|
2414
|
-
if (slot?.heldByTaskId === task.taskId) slot.heldByTaskId = null
|
|
2415
|
-
}
|
|
2416
|
-
break
|
|
2417
|
-
case "orch_task_requeued":
|
|
2418
|
-
// Slot hold deliberately KEPT (F13/F2): the task's uncommitted progress
|
|
2419
|
-
// lives in its worktree — re-claim resumes the SAME slot.
|
|
2420
|
-
task.state = "queued"
|
|
2421
|
-
task.ownerWorkerId = null
|
|
2422
|
-
task.verifying = false
|
|
2423
|
-
break
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
474
|
+
scanAllToolRequests(): ToolRequest[] { return scanAllToolRequestsFromMap(this.state.toolRequestsById) }
|
|
2426
475
|
|
|
2427
|
-
|
|
2428
|
-
* Apply synchronously, then enqueue the disk append — the sync apply is what
|
|
2429
|
-
* makes an orchestration claim atomic within one event-loop turn (same
|
|
2430
|
-
* pattern as appendSubagentEvent).
|
|
2431
|
-
*/
|
|
2432
|
-
appendOrchestrationEvent(event: OrchestrationEvent): Promise<void> {
|
|
2433
|
-
this.applyEvent(event)
|
|
2434
|
-
this.enqueueDiskAppend(this.orchLogPath, `${JSON.stringify(event)}\n`)
|
|
2435
|
-
this.notifyOrchRunsChanged()
|
|
2436
|
-
return Promise.resolve()
|
|
2437
|
-
}
|
|
476
|
+
async flush(): Promise<void> { await this.writeChain }
|
|
2438
477
|
|
|
2439
|
-
|
|
2440
|
-
* Observe orchestration read-model changes. The callback fires after each
|
|
2441
|
-
* live orch event is applied (not during boot replay — no subscribers yet).
|
|
2442
|
-
* Mirrors the registry `.subscribe()` pattern; used by ws-router to push the
|
|
2443
|
-
* `orch-runs` topic. Returns an unsubscribe fn.
|
|
2444
|
-
*/
|
|
2445
|
-
subscribeOrchRuns(cb: () => void): () => void {
|
|
2446
|
-
this.orchRunsSubscribers.add(cb)
|
|
2447
|
-
return () => { this.orchRunsSubscribers.delete(cb) }
|
|
2448
|
-
}
|
|
478
|
+
private readonly orchSubscriptionState = OrchSubscription.createOrchSubscriptionState()
|
|
2449
479
|
|
|
2450
|
-
|
|
2451
|
-
for (const cb of this.orchRunsSubscribers) {
|
|
2452
|
-
try { cb() } catch (err) { log.warn(`${LOG_PREFIX} orch-runs subscriber threw`, { err }) }
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
480
|
+
appendOrchestrationEvent(event: OrchestrationEvent): Promise<void> { return OrchSubscription.appendOrchestrationEvent(this.buildOrchSubscriptionDeps(), event) }
|
|
2455
481
|
|
|
2456
|
-
|
|
2457
|
-
const run = this.state.orchRunsById.get(runId)
|
|
2458
|
-
if (!run) return null
|
|
2459
|
-
return this.toOrchRunSnapshot(run)
|
|
2460
|
-
}
|
|
482
|
+
subscribeOrchRuns(cb: () => void): () => void { return OrchSubscription.subscribeOrchRuns(this.buildOrchSubscriptionDeps(), cb) }
|
|
2461
483
|
|
|
2462
|
-
|
|
2463
|
-
return [...this.state.orchRunsById.values()].map((r) => this.toOrchRunSnapshot(r))
|
|
2464
|
-
}
|
|
484
|
+
getOrchRun(runId: string): OrchRunSnapshot | null { return OrchSubscription.getOrchRun(this.state.orchRunsById, runId) }
|
|
2465
485
|
|
|
2466
|
-
|
|
2467
|
-
const tasks: OrchTaskSnapshot[] = run.taskOrder.flatMap((taskId) => {
|
|
2468
|
-
const t = run.tasksById.get(taskId)
|
|
2469
|
-
if (!t) return []
|
|
2470
|
-
return [{
|
|
2471
|
-
taskId: t.taskId,
|
|
2472
|
-
title: t.title,
|
|
2473
|
-
state: t.state,
|
|
2474
|
-
ownerWorkerId: t.ownerWorkerId,
|
|
2475
|
-
worktreePath: t.worktreePath,
|
|
2476
|
-
branch: t.branch,
|
|
2477
|
-
baseSha: t.baseSha,
|
|
2478
|
-
phaseIndex: t.phaseIndex,
|
|
2479
|
-
attempts: t.attempts,
|
|
2480
|
-
error: t.error,
|
|
2481
|
-
commitSha: t.commitSha,
|
|
2482
|
-
verifying: t.verifying,
|
|
2483
|
-
updatedAt: t.updatedAt,
|
|
2484
|
-
}]
|
|
2485
|
-
})
|
|
2486
|
-
return {
|
|
2487
|
-
runId: run.runId,
|
|
2488
|
-
status: run.status,
|
|
2489
|
-
config: run.config,
|
|
2490
|
-
tasks,
|
|
2491
|
-
worktrees: run.worktrees.map((w) => ({ ...w })),
|
|
2492
|
-
createdAt: run.createdAt,
|
|
2493
|
-
updatedAt: run.updatedAt,
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
486
|
+
getOrchRuns(): OrchRunSnapshot[] { return OrchSubscription.getOrchRuns(this.state.orchRunsById) }
|
|
2496
487
|
|
|
2497
|
-
/** Tasks a restart must RE-QUEUE. `gated` is deliberately excluded — a gated task is re-armed in place (gate re-notified), never requeued. */
|
|
2498
488
|
*nonTerminalOrchTasks(): Iterable<{ runId: string; taskId: string; state: "claimed" | "running" }> {
|
|
2499
|
-
|
|
2500
|
-
if (run.status !== "running") continue
|
|
2501
|
-
for (const task of run.tasksById.values()) {
|
|
2502
|
-
if (task.state === "claimed" || task.state === "running") {
|
|
2503
|
-
yield { runId: run.runId, taskId: task.taskId, state: task.state }
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
489
|
+
yield* OrchSubscription.nonTerminalOrchTasks(this.state.orchRunsById)
|
|
2507
490
|
}
|
|
2508
491
|
|
|
2509
|
-
/** Tasks paused at a hard gate — re-armed (not requeued) by recoverOnStartup. */
|
|
2510
492
|
*gatedOrchTasks(): Iterable<{ runId: string; taskId: string; phaseIndex: number }> {
|
|
2511
|
-
|
|
2512
|
-
if (run.status !== "running") continue
|
|
2513
|
-
for (const task of run.tasksById.values()) {
|
|
2514
|
-
if (task.state === "gated") {
|
|
2515
|
-
yield { runId: run.runId, taskId: task.taskId, phaseIndex: task.phaseIndex }
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
493
|
+
yield* OrchSubscription.gatedOrchTasks(this.state.orchRunsById)
|
|
2519
494
|
}
|
|
2520
495
|
|
|
2521
|
-
/** Task spec lookup for the engine (records keep prompt/scope; snapshots do not). */
|
|
2522
496
|
getOrchTaskSpec(runId: string, taskId: string): { prompt: string; scopePaths: string[] } | null {
|
|
2523
|
-
|
|
2524
|
-
if (!task) return null
|
|
2525
|
-
return { prompt: task.prompt, scopePaths: task.scopePaths }
|
|
497
|
+
return OrchSubscription.getOrchTaskSpec(this.state.orchRunsById, runId, taskId)
|
|
2526
498
|
}
|
|
2527
499
|
|
|
2528
|
-
|
|
2529
|
-
getOrchLastPhaseOutput(runId: string, taskId: string): string | null {
|
|
2530
|
-
return this.state.orchRunsById.get(runId)?.tasksById.get(taskId)?.lastPhaseOutput ?? null
|
|
2531
|
-
}
|
|
500
|
+
getOrchLastPhaseOutput(runId: string, taskId: string): string | null { return OrchSubscription.getOrchLastPhaseOutput(this.state.orchRunsById, runId, taskId) }
|
|
2532
501
|
|
|
2533
|
-
|
|
2534
|
-
getOrchRunEvents(runId: string): OrchestrationEvent[] {
|
|
2535
|
-
return [...(this.state.orchRunsById.get(runId)?.eventLog ?? [])]
|
|
2536
|
-
}
|
|
502
|
+
getOrchRunEvents(runId: string): OrchestrationEvent[] { return OrchSubscription.getOrchRunEvents(this.state.orchRunsById, runId) }
|
|
2537
503
|
}
|