@cuongtran001/kanna 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{arc-CAysfmX5.js → arc-BHj370W7.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
- package/dist/client/assets/channel-BP9IMEvd.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
- package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/chat-op-log.test.ts +63 -0
- package/src/server/chat-op-log.ts +71 -0
- package/src/server/chat-ops-diff.test.ts +120 -0
- package/src/server/chat-ops-diff.ts +97 -0
- package/src/server/chat-ops-parity.test.ts +140 -0
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +532 -0
- package/src/server/event-store-messages.adapter.ts +455 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +291 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +75 -0
- package/src/server/event-store.ts +257 -2285
- package/src/server/events.ts +1 -1
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/storage/backend.ts +7 -0
- package/src/server/storage/fs-storage.adapter.ts +21 -1
- package/src/server/storage/in-memory-storage.ts +12 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +560 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +360 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +238 -0
- package/src/server/ws-router.test.ts +90 -0
- package/src/server/ws-router.ts +340 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/chat-ops.test.ts +138 -0
- package/src/shared/chat-ops.ts +71 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/protocol.ts +3 -2
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +31 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -1,260 +1,58 @@
|
|
|
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 { ChatOpLog } from "./chat-op-log"
|
|
40
|
+
import * as PeripheralEvents from "./event-store-peripheral-events.adapter"
|
|
41
|
+
import * as MessageRead from "./event-store-messages.adapter"
|
|
42
|
+
import * as EntityWrite from "./event-store-entity-write"
|
|
43
|
+
import * as TranscriptWrite from "./event-store-transcript-write.adapter"
|
|
44
|
+
import {
|
|
45
|
+
initializeEventStore,
|
|
46
|
+
getLegacyTranscriptStats as getLegacyTranscriptStatsFn,
|
|
47
|
+
hasLegacyTranscriptData as hasLegacyTranscriptDataFn,
|
|
48
|
+
snapshotAndTruncateLogs as snapshotAndTruncateLogsFn,
|
|
49
|
+
migrateLegacyTranscripts as migrateLegacyTranscriptsFn,
|
|
50
|
+
type EventStoreInitDeps,
|
|
51
|
+
type LegacyTranscriptStats,
|
|
52
|
+
} from "./event-store-init"
|
|
41
53
|
|
|
42
|
-
const SNAPSHOT_THRESHOLD_BYTES = 2 * 1024 * 1024
|
|
43
|
-
const STALE_EMPTY_CHAT_MAX_AGE_MS = 30 * 60 * 1000
|
|
44
54
|
const SIDEBAR_PROJECT_ORDER_FILE = "sidebar-order.json"
|
|
45
55
|
|
|
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
56
|
export class EventStore implements PushEventStore {
|
|
259
57
|
readonly dataDir: string
|
|
260
58
|
readonly state: StoreState = createEmptyState()
|
|
@@ -283,9 +81,11 @@ export class EventStore implements PushEventStore {
|
|
|
283
81
|
// --resume; on cold-wake the reader starts at byte 0 and would re-emit).
|
|
284
82
|
private seenMessageIdsByChatId = new Map<string, Set<string>>()
|
|
285
83
|
private legacySidebarProjectOrder: string[] = []
|
|
286
|
-
private
|
|
84
|
+
private readonly sidebarProjectOrderRef: { value: string[] } = { value: [] }
|
|
287
85
|
private snapshotHasLegacyMessages = false
|
|
288
|
-
private
|
|
86
|
+
private readonly transcriptCache = new MessageRead.TranscriptCache()
|
|
87
|
+
/** In-memory delta ring backing the `chat.ops` broadcast path. */
|
|
88
|
+
readonly chatOps = new ChatOpLog()
|
|
289
89
|
private readonly tunnelEventsByChatId = new Map<string, CloudflareTunnelEvent[]>()
|
|
290
90
|
private shareEventsAll: ShareEvent[] = []
|
|
291
91
|
private replayChatProvider = new Map<string, AgentProvider | null>()
|
|
@@ -313,787 +113,16 @@ export class EventStore implements PushEventStore {
|
|
|
313
113
|
}
|
|
314
114
|
|
|
315
115
|
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,
|
|
116
|
+
await initializeEventStore(this.buildInitDeps(), {
|
|
117
|
+
loadTunnelEvents: () => this.loadTunnelEvents(),
|
|
118
|
+
loadShareEvents: () => this.loadShareEvents(),
|
|
119
|
+
hasLegacyTranscriptData: () => this.hasLegacyTranscriptData(),
|
|
120
|
+
snapshotAndTruncateLogs: () => this.snapshotAndTruncateLogs(),
|
|
1080
121
|
})
|
|
1081
122
|
}
|
|
1082
123
|
|
|
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)
|
|
124
|
+
private applyEvent(event: StoreEvent) {
|
|
125
|
+
applyStoreEvent(event, this.state, this.legacyMessagesByChatId, this.replayChatProvider)
|
|
1097
126
|
}
|
|
1098
127
|
|
|
1099
128
|
private enqueueDiskAppend(filePath: string, payload: string): void {
|
|
@@ -1113,390 +142,195 @@ export class EventStore implements PushEventStore {
|
|
|
1113
142
|
return this.writeChain
|
|
1114
143
|
}
|
|
1115
144
|
|
|
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
|
-
}
|
|
145
|
+
// ─── Deps builders ──────────────────────────────────────────────────────────
|
|
1125
146
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
147
|
+
private buildInitDeps(): EventStoreInitDeps {
|
|
148
|
+
return {
|
|
149
|
+
storage: this.storage,
|
|
150
|
+
dataDir: this.dataDir,
|
|
151
|
+
snapshotPath: this.snapshotPath,
|
|
152
|
+
projectsLogPath: this.projectsLogPath,
|
|
153
|
+
chatsLogPath: this.chatsLogPath,
|
|
154
|
+
messagesLogPath: this.messagesLogPath,
|
|
155
|
+
queuedMessagesLogPath: this.queuedMessagesLogPath,
|
|
156
|
+
turnsLogPath: this.turnsLogPath,
|
|
157
|
+
schedulesLogPath: this.schedulesLogPath,
|
|
158
|
+
tunnelLogPath: this.tunnelLogPath,
|
|
159
|
+
sharesLogPath: this.sharesLogPath,
|
|
160
|
+
pushLogPath: this.pushLogPath,
|
|
161
|
+
stacksLogPath: this.stacksLogPath,
|
|
162
|
+
toolRequestsLogPath: this.toolRequestsLogPath,
|
|
163
|
+
orchLogPath: this.orchLogPath,
|
|
164
|
+
transcriptsDir: this.transcriptsDir,
|
|
165
|
+
sidebarProjectOrderPath: this.sidebarProjectOrderPath,
|
|
166
|
+
state: this.state,
|
|
167
|
+
legacyMessagesByChatId: this.legacyMessagesByChatId,
|
|
168
|
+
tunnelEventsByChatId: this.tunnelEventsByChatId,
|
|
169
|
+
transcriptCache: this.transcriptCache,
|
|
170
|
+
sidebarProjectOrderRef: this.sidebarProjectOrderRef,
|
|
171
|
+
getLegacySidebarProjectOrder: () => this.legacySidebarProjectOrder,
|
|
172
|
+
setLegacySidebarProjectOrder: (v) => { this.legacySidebarProjectOrder = v },
|
|
173
|
+
getSnapshotHasLegacyMessages: () => this.snapshotHasLegacyMessages,
|
|
174
|
+
setSnapshotHasLegacyMessages: (v) => { this.snapshotHasLegacyMessages = v },
|
|
175
|
+
getStorageReset: () => this.storageReset,
|
|
176
|
+
setStorageReset: (v) => { this.storageReset = v },
|
|
177
|
+
replayChatProvider: this.replayChatProvider,
|
|
178
|
+
applyEvent: (event) => { this.applyEvent(event) },
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private buildMessageReadDeps(): MessageRead.MessageReadDeps {
|
|
183
|
+
return {
|
|
184
|
+
storage: this.storage,
|
|
185
|
+
transcriptsDir: this.transcriptsDir,
|
|
186
|
+
transcriptCache: this.transcriptCache,
|
|
187
|
+
legacyMessagesByChatId: this.legacyMessagesByChatId,
|
|
188
|
+
seenMessageIdsByChatId: this.seenMessageIdsByChatId,
|
|
189
|
+
queuedMessagesByChatId: this.state.queuedMessagesByChatId,
|
|
190
|
+
chatsById: this.state.chatsById,
|
|
191
|
+
listPendingToolRequests: (chatId) => this.listPendingToolRequests(chatId),
|
|
1140
192
|
}
|
|
1141
|
-
return entries
|
|
1142
193
|
}
|
|
1143
194
|
|
|
1144
|
-
private
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
this.
|
|
195
|
+
private buildPeripheralEventsDeps(): PeripheralEvents.PeripheralEventsDeps {
|
|
196
|
+
return {
|
|
197
|
+
storage: this.storage,
|
|
198
|
+
tunnelLogPath: this.tunnelLogPath,
|
|
199
|
+
sharesLogPath: this.sharesLogPath,
|
|
200
|
+
pushLogPath: this.pushLogPath,
|
|
201
|
+
tunnelEventsByChatId: this.tunnelEventsByChatId,
|
|
202
|
+
shareEventsAll: this.shareEventsAll,
|
|
203
|
+
getWriteChain: () => this.writeChain,
|
|
204
|
+
setWriteChain: (p) => { this.writeChain = p },
|
|
1149
205
|
}
|
|
1150
|
-
return set
|
|
1151
206
|
}
|
|
1152
207
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
208
|
+
private buildEntityWriteDeps(): EntityWrite.EntityWriteDeps {
|
|
209
|
+
return {
|
|
210
|
+
storage: this.storage,
|
|
211
|
+
dataDir: this.dataDir,
|
|
212
|
+
sidebarProjectOrderPath: this.sidebarProjectOrderPath,
|
|
213
|
+
projectsLogPath: this.projectsLogPath,
|
|
214
|
+
chatsLogPath: this.chatsLogPath,
|
|
215
|
+
queuedMessagesLogPath: this.queuedMessagesLogPath,
|
|
216
|
+
stacksLogPath: this.stacksLogPath,
|
|
217
|
+
projectsById: this.state.projectsById,
|
|
218
|
+
projectIdsByPath: this.state.projectIdsByPath,
|
|
219
|
+
chatsById: this.state.chatsById,
|
|
220
|
+
queuedMessagesByChatId: this.state.queuedMessagesByChatId,
|
|
221
|
+
stacksById: this.state.stacksById,
|
|
222
|
+
sidebarProjectOrderRef: this.sidebarProjectOrderRef,
|
|
223
|
+
getWriteChain: () => this.writeChain,
|
|
224
|
+
setWriteChain: (p) => { this.writeChain = p },
|
|
225
|
+
append: (filePath, event) => this.append(filePath, event),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private buildSessionWriteDeps(): EntityWrite.SessionWriteDeps {
|
|
230
|
+
return {
|
|
231
|
+
chatsById: this.state.chatsById,
|
|
232
|
+
turnsLogPath: this.turnsLogPath,
|
|
233
|
+
schedulesLogPath: this.schedulesLogPath,
|
|
234
|
+
append: (filePath, event) => this.append(filePath, event),
|
|
1173
235
|
}
|
|
1174
|
-
await this.append(this.projectsLogPath, event)
|
|
1175
|
-
return this.state.projectsById.get(projectId)!
|
|
1176
236
|
}
|
|
1177
237
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
238
|
+
private buildChatTranscriptWriteDeps(): TranscriptWrite.ChatTranscriptWriteDeps {
|
|
239
|
+
return {
|
|
240
|
+
storage: this.storage,
|
|
241
|
+
transcriptsDir: this.transcriptsDir,
|
|
242
|
+
dataDir: this.dataDir,
|
|
243
|
+
transcriptCache: this.transcriptCache,
|
|
244
|
+
seenMessageIdsByChatId: this.seenMessageIdsByChatId,
|
|
245
|
+
chatsById: this.state.chatsById,
|
|
246
|
+
toolRequestsById: this.state.toolRequestsById,
|
|
247
|
+
chatsLogPath: this.chatsLogPath,
|
|
248
|
+
turnsLogPath: this.turnsLogPath,
|
|
249
|
+
getWriteChain: () => this.writeChain,
|
|
250
|
+
setWriteChain: (p) => { this.writeChain = p },
|
|
251
|
+
append: (filePath, event) => this.append(filePath, event),
|
|
252
|
+
getMessages: (chatId) => this.getMessages(chatId),
|
|
253
|
+
ensureTranscriptLoaded: (chatId) => { MessageRead.getMessagesView(this.buildMessageReadDeps(), chatId) },
|
|
254
|
+
getSeenMessageIds: (chatId) => this.getSeenMessageIds(chatId),
|
|
255
|
+
listPendingToolRequests: (chatId) => this.listPendingToolRequests(chatId),
|
|
256
|
+
recordChatOp: (chatId, op) => { this.chatOps.record(chatId, op) },
|
|
257
|
+
clearChatOps: (chatId) => { this.chatOps.clear(chatId) },
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private buildOrchSubscriptionDeps(): OrchSubscription.OrchSubscriptionDeps {
|
|
262
|
+
return {
|
|
263
|
+
orchRunsById: this.state.orchRunsById,
|
|
264
|
+
orchLogPath: this.orchLogPath,
|
|
265
|
+
orchRunsSubscribers: this.orchSubscriptionState.orchRunsSubscribers,
|
|
266
|
+
applyEvent: (e) => { this.applyEvent(e) },
|
|
267
|
+
enqueueDiskAppend: (fp, p) => { this.enqueueDiskAppend(fp, p) },
|
|
1189
268
|
}
|
|
1190
|
-
await this.append(this.projectsLogPath, event)
|
|
1191
269
|
}
|
|
1192
270
|
|
|
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,
|
|
271
|
+
private buildToolRequestWriteDeps(): ToolRequestWriteDeps {
|
|
272
|
+
return {
|
|
273
|
+
toolRequestsById: this.state.toolRequestsById,
|
|
274
|
+
toolRequestsLogPath: this.toolRequestsLogPath,
|
|
275
|
+
append: (fp, e) => this.append(fp, e),
|
|
1205
276
|
}
|
|
1206
|
-
await this.append(this.projectsLogPath, event)
|
|
1207
277
|
}
|
|
1208
278
|
|
|
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],
|
|
279
|
+
private buildAppendSubagentDeps(): AppendSubagentDeps {
|
|
280
|
+
return {
|
|
281
|
+
chatsById: this.state.chatsById,
|
|
282
|
+
turnsLogPath: this.turnsLogPath,
|
|
283
|
+
dataDir: this.dataDir,
|
|
284
|
+
applyEvent: (e) => { this.applyEvent(e) },
|
|
285
|
+
enqueueDiskAppend: (fp, p) => { this.enqueueDiskAppend(fp, p) },
|
|
1226
286
|
}
|
|
1227
|
-
await this.append(this.stacksLogPath, event)
|
|
1228
|
-
return this.state.stacksById.get(stackId)!
|
|
1229
287
|
}
|
|
1230
288
|
|
|
1231
|
-
|
|
1232
|
-
const stack = this.state.stacksById.get(stackId)
|
|
1233
|
-
return stack && !stack.deletedAt ? stack : null
|
|
1234
|
-
}
|
|
289
|
+
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
1235
290
|
|
|
1236
|
-
|
|
1237
|
-
return [...this.state.stacksById.values()].filter((s) => !s.deletedAt)
|
|
1238
|
-
}
|
|
291
|
+
private getSeenMessageIds(chatId: string): Set<string> { return MessageRead.getSeenMessageIds(this.buildMessageReadDeps(), chatId) }
|
|
1239
292
|
|
|
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
|
-
}
|
|
293
|
+
async openProject(localPath: string, title?: string) { return EntityWrite.openProject(this.buildEntityWriteDeps(), localPath, title) }
|
|
1255
294
|
|
|
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
|
-
}
|
|
295
|
+
async removeProject(projectId: string) { return EntityWrite.removeProject(this.buildEntityWriteDeps(), projectId) }
|
|
1268
296
|
|
|
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
|
-
}
|
|
297
|
+
async setProjectStar(projectId: string, starred: boolean) { return EntityWrite.setProjectStar(this.buildEntityWriteDeps(), projectId, starred) }
|
|
1284
298
|
|
|
1285
|
-
async
|
|
299
|
+
async createStack(title: string, projectIds: string[]): Promise<StackRecord> { return EntityWrite.createStack(this.buildEntityWriteDeps(), title, projectIds) }
|
|
300
|
+
|
|
301
|
+
getStack(stackId: string): StackRecord | null {
|
|
1286
302
|
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)
|
|
303
|
+
return stack && !stack.deletedAt ? stack : null
|
|
1300
304
|
}
|
|
1301
305
|
|
|
1302
|
-
|
|
1303
|
-
const validProjectIds = projectIds.filter((projectId) => {
|
|
1304
|
-
const project = this.state.projectsById.get(projectId)
|
|
1305
|
-
return Boolean(project && !project.deletedAt)
|
|
1306
|
-
})
|
|
306
|
+
listStacks(): StackRecord[] { return [...this.state.stacksById.values()].filter((s) => !s.deletedAt) }
|
|
1307
307
|
|
|
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
|
-
}
|
|
308
|
+
async renameStack(stackId: string, title: string): Promise<void> { return EntityWrite.renameStack(this.buildEntityWriteDeps(), stackId, title) }
|
|
1316
309
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
310
|
+
async removeStack(stackId: string): Promise<void> { return EntityWrite.removeStack(this.buildEntityWriteDeps(), stackId) }
|
|
311
|
+
|
|
312
|
+
async addProjectToStack(stackId: string, projectId: string): Promise<void> { return EntityWrite.addProjectToStack(this.buildEntityWriteDeps(), stackId, projectId) }
|
|
313
|
+
|
|
314
|
+
async removeProjectFromStack(stackId: string, projectId: string): Promise<void> { return EntityWrite.removeProjectFromStack(this.buildEntityWriteDeps(), stackId, projectId) }
|
|
315
|
+
|
|
316
|
+
async setSidebarProjectOrder(projectIds: string[]) { return EntityWrite.setSidebarProjectOrder(this.buildEntityWriteDeps(), projectIds) }
|
|
1323
317
|
|
|
1324
318
|
async createChat(
|
|
1325
319
|
projectId: string,
|
|
1326
320
|
options?: { stackId?: string; stackBindings?: StackBinding[] },
|
|
1327
321
|
): 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)!
|
|
322
|
+
return EntityWrite.createChat(this.buildEntityWriteDeps(), projectId, options)
|
|
1433
323
|
}
|
|
1434
324
|
|
|
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
|
-
}
|
|
325
|
+
async forkChat(sourceChatId: string) { return TranscriptWrite.forkChat(this.buildChatTranscriptWriteDeps(), sourceChatId) }
|
|
1449
326
|
|
|
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
|
-
}
|
|
327
|
+
async renameChat(chatId: string, title: string) { return EntityWrite.renameChat(this.buildEntityWriteDeps(), chatId, title) }
|
|
1467
328
|
|
|
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
|
-
}
|
|
329
|
+
async deleteChat(chatId: string) { return TranscriptWrite.deleteChat(this.buildChatTranscriptWriteDeps(), chatId) }
|
|
1478
330
|
|
|
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
|
-
}
|
|
331
|
+
async archiveChat(chatId: string) { return TranscriptWrite.archiveChat(this.buildChatTranscriptWriteDeps(), chatId) }
|
|
1489
332
|
|
|
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
|
-
}
|
|
333
|
+
async unarchiveChat(chatId: string) { return TranscriptWrite.unarchiveChat(this.buildChatTranscriptWriteDeps(), chatId) }
|
|
1500
334
|
|
|
1501
335
|
async pruneStaleEmptyChats(args?: {
|
|
1502
336
|
now?: number
|
|
@@ -1504,352 +338,48 @@ export class EventStore implements PushEventStore {
|
|
|
1504
338
|
activeChatIds?: Iterable<string>
|
|
1505
339
|
protectedChatIds?: Iterable<string>
|
|
1506
340
|
}) {
|
|
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
|
|
341
|
+
return TranscriptWrite.pruneStaleEmptyChats(this.buildChatTranscriptWriteDeps(), args)
|
|
1543
342
|
}
|
|
1544
343
|
|
|
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
|
-
}
|
|
344
|
+
async setChatProvider(chatId: string, provider: AgentProvider) { return EntityWrite.setChatProvider(this.buildEntityWriteDeps(), chatId, provider) }
|
|
1557
345
|
|
|
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
|
-
}
|
|
346
|
+
async setPlanMode(chatId: string, planMode: boolean) { return EntityWrite.setPlanMode(this.buildEntityWriteDeps(), chatId, planMode) }
|
|
1570
347
|
|
|
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
|
-
}
|
|
348
|
+
async setCompactFailureCount(chatId: string, compactFailureCount: number) { return EntityWrite.setCompactFailureCount(this.buildEntityWriteDeps(), chatId, compactFailureCount) }
|
|
1583
349
|
|
|
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
|
-
}
|
|
350
|
+
async setChatReadState(chatId: string, unread: boolean) { return EntityWrite.setChatReadState(this.buildEntityWriteDeps(), chatId, unread) }
|
|
1596
351
|
|
|
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
|
-
}
|
|
352
|
+
async setChatPolicyOverride(chatId: string, policyOverride: ChatPermissionPolicyOverride | null) { return EntityWrite.setChatPolicyOverride(this.buildEntityWriteDeps(), chatId, policyOverride) }
|
|
1608
353
|
|
|
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
|
-
}
|
|
354
|
+
async appendMessage(chatId: string, entry: TranscriptEntry) { return TranscriptWrite.appendMessage(this.buildChatTranscriptWriteDeps(), chatId, entry) }
|
|
1655
355
|
|
|
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
|
-
}
|
|
356
|
+
async enqueueMessage(chatId: string, message: Omit<QueuedChatMessage, "id" | "createdAt"> & Partial<Pick<QueuedChatMessage, "id" | "createdAt">>) { return EntityWrite.enqueueMessage(this.buildEntityWriteDeps(), chatId, message) }
|
|
1679
357
|
|
|
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
|
-
}
|
|
358
|
+
async removeQueuedMessage(chatId: string, queuedMessageId: string) { return EntityWrite.removeQueuedMessage(this.buildEntityWriteDeps(), chatId, queuedMessageId) }
|
|
1695
359
|
|
|
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
|
-
}
|
|
360
|
+
async recordTurnStarted(chatId: string, runConfig?: TurnRunConfig) { return EntityWrite.recordTurnStarted(this.buildSessionWriteDeps(), chatId, runConfig) }
|
|
1707
361
|
|
|
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
|
-
}
|
|
362
|
+
async recordTurnFinished(chatId: string) { return EntityWrite.recordTurnFinished(this.buildSessionWriteDeps(), chatId) }
|
|
1718
363
|
|
|
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
|
-
}
|
|
364
|
+
async recordTurnFailed(chatId: string, error: string) { return EntityWrite.recordTurnFailed(this.buildSessionWriteDeps(), chatId, error) }
|
|
1730
365
|
|
|
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
|
-
}
|
|
366
|
+
async recordTurnCancelled(chatId: string) { return EntityWrite.recordTurnCancelled(this.buildSessionWriteDeps(), chatId) }
|
|
1741
367
|
|
|
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
|
-
}
|
|
368
|
+
async appendSubagentEvent(event: SubagentRunEvent) { return appendSubagentEventFn(this.buildAppendSubagentDeps(), event) }
|
|
1765
369
|
|
|
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
|
-
}
|
|
370
|
+
getSubagentRuns(chatId: string): Record<string, SubagentRunSnapshot> { return getSubagentRunsFromMap(this.state.subagentRunsByChatId, chatId) }
|
|
1771
371
|
|
|
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
|
-
}
|
|
372
|
+
*runningSubagentRuns(): Iterable<SubagentRunSnapshot> { yield* runningSubagentRunsFromMap(this.state.subagentRunsByChatId) }
|
|
1779
373
|
|
|
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
|
-
}
|
|
374
|
+
async setSessionTokenForProvider(chatId: string, provider: AgentProvider, sessionToken: string | null) { return EntityWrite.setSessionTokenForProvider(this.buildSessionWriteDeps(), chatId, provider, sessionToken) }
|
|
1797
375
|
|
|
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
|
-
}
|
|
376
|
+
async recordSessionCommandsLoaded(chatId: string, commands: SlashCommand[]) { return EntityWrite.recordSessionCommandsLoaded(this.buildSessionWriteDeps(), chatId, commands) }
|
|
1817
377
|
|
|
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)
|
|
378
|
+
async setPendingForkSessionToken(chatId: string, value: { provider: AgentProvider; token: string } | null) {
|
|
379
|
+
return EntityWrite.setPendingForkSessionToken(this.buildSessionWriteDeps(), chatId, value)
|
|
1839
380
|
}
|
|
1840
381
|
|
|
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
|
-
}
|
|
382
|
+
async setSourceHash(chatId: string, sourceHash: string | null) { return EntityWrite.setSourceHash(this.buildEntityWriteDeps(), chatId, sourceHash) }
|
|
1853
383
|
|
|
1854
384
|
getProject(projectId: string) {
|
|
1855
385
|
const project = this.state.projectsById.get(projectId)
|
|
@@ -1871,110 +401,23 @@ export class EventStore implements PushEventStore {
|
|
|
1871
401
|
return chat
|
|
1872
402
|
}
|
|
1873
403
|
|
|
1874
|
-
getSidebarProjectOrder() {
|
|
1875
|
-
return [...this.sidebarProjectOrder]
|
|
1876
|
-
}
|
|
404
|
+
getSidebarProjectOrder() { return [...this.sidebarProjectOrderRef.value] }
|
|
1877
405
|
|
|
1878
|
-
|
|
1879
|
-
if (entries.length === 0) {
|
|
1880
|
-
return { entries: [], hasOlder: false, olderCursor: null }
|
|
1881
|
-
}
|
|
406
|
+
// ─── Message read methods (thin delegates) ────────────────────────────────
|
|
1882
407
|
|
|
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
|
-
}
|
|
408
|
+
getMessages(chatId: string) { return MessageRead.getMessages(this.buildMessageReadDeps(), chatId) }
|
|
1891
409
|
|
|
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
|
-
}
|
|
410
|
+
getQueuedMessages(chatId: string) { return MessageRead.getQueuedMessages(this.buildMessageReadDeps(), chatId) }
|
|
1924
411
|
|
|
1925
|
-
|
|
1926
|
-
const page = this.getMessagesPageFromEntries(entries, limit)
|
|
412
|
+
getQueuedMessage(chatId: string, queuedMessageId: string) { return MessageRead.getQueuedMessage(this.buildMessageReadDeps(), chatId, queuedMessageId) }
|
|
1927
413
|
|
|
1928
|
-
|
|
1929
|
-
messages: page.entries,
|
|
1930
|
-
hasOlder: page.hasOlder,
|
|
1931
|
-
olderCursor: page.olderCursor,
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
414
|
+
getRecentMessagesPage(chatId: string, limit: number): ChatHistoryPage { return MessageRead.getRecentMessagesPage(this.buildMessageReadDeps(), chatId, limit) }
|
|
1934
415
|
|
|
1935
|
-
getMessagesPageBefore(chatId: string, beforeCursor: string, limit: number): ChatHistoryPage {
|
|
1936
|
-
if (limit <= 0) {
|
|
1937
|
-
return { messages: [], hasOlder: false, olderCursor: null }
|
|
1938
|
-
}
|
|
416
|
+
getMessagesPageBefore(chatId: string, beforeCursor: string, limit: number): ChatHistoryPage { return MessageRead.getMessagesPageBefore(this.buildMessageReadDeps(), chatId, beforeCursor, limit) }
|
|
1939
417
|
|
|
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)
|
|
418
|
+
getRecentChatHistory(chatId: string, recentLimit: number) { return MessageRead.getRecentChatHistory(this.buildMessageReadDeps(), chatId, recentLimit) }
|
|
1945
419
|
|
|
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
|
-
}
|
|
420
|
+
listProjects() { return [...this.state.projectsById.values()].filter((project) => !project.deletedAt) }
|
|
1978
421
|
|
|
1979
422
|
listChatsByProject(projectId: string) {
|
|
1980
423
|
return [...this.state.chatsById.values()]
|
|
@@ -1982,556 +425,85 @@ export class EventStore implements PushEventStore {
|
|
|
1982
425
|
.sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
|
|
1983
426
|
}
|
|
1984
427
|
|
|
1985
|
-
getChatCount(projectId: string) {
|
|
1986
|
-
return this.listChatsByProject(projectId).length
|
|
1987
|
-
}
|
|
428
|
+
getChatCount(projectId: string) { return MessageRead.getChatCount(this.buildMessageReadDeps(), projectId) }
|
|
1988
429
|
|
|
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
|
-
}
|
|
430
|
+
async getLegacyTranscriptStats(): Promise<LegacyTranscriptStats> { return getLegacyTranscriptStatsFn(this.buildInitDeps()) }
|
|
1998
431
|
|
|
1999
|
-
|
|
2000
|
-
for (const entries of this.legacyMessagesByChatId.values()) {
|
|
2001
|
-
entryCount += entries.length
|
|
2002
|
-
}
|
|
432
|
+
async hasLegacyTranscriptData() { return hasLegacyTranscriptDataFn(this.buildInitDeps()) }
|
|
2003
433
|
|
|
2004
|
-
|
|
2005
|
-
hasLegacyData: sources.length > 0 || this.legacyMessagesByChatId.size > 0,
|
|
2006
|
-
sources,
|
|
2007
|
-
chatCount: this.legacyMessagesByChatId.size,
|
|
2008
|
-
entryCount,
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
434
|
+
async snapshotAndTruncateLogs() { return snapshotAndTruncateLogsFn(this.buildInitDeps()) }
|
|
2011
435
|
|
|
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
|
-
}
|
|
436
|
+
async migrateLegacyTranscripts(onProgress?: (message: string) => void) { return migrateLegacyTranscriptsFn(this.buildInitDeps(), onProgress) }
|
|
2041
437
|
|
|
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
|
-
}
|
|
438
|
+
async appendAutoContinueEvent(event: AutoContinueEvent) { return EntityWrite.appendAutoContinueEvent(this.buildSessionWriteDeps(), event) }
|
|
2111
439
|
|
|
2112
440
|
getAutoContinueEvents(chatId: string): AutoContinueEvent[] {
|
|
2113
441
|
const list = this.state.autoContinueEventsByChatId.get(chatId)
|
|
2114
442
|
return list ? [...list] : []
|
|
2115
443
|
}
|
|
2116
444
|
|
|
2117
|
-
listAutoContinueChats(): string[] {
|
|
2118
|
-
return [...this.state.autoContinueEventsByChatId.keys()]
|
|
2119
|
-
}
|
|
445
|
+
listAutoContinueChats(): string[] { return [...this.state.autoContinueEventsByChatId.keys()] }
|
|
2120
446
|
|
|
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
|
-
}
|
|
447
|
+
// ─── Peripheral event methods (thin delegates) ───────────────────────────
|
|
2129
448
|
|
|
2130
|
-
|
|
2131
|
-
const list = this.tunnelEventsByChatId.get(chatId)
|
|
2132
|
-
return list ? [...list] : []
|
|
2133
|
-
}
|
|
449
|
+
async appendTunnelEvent(event: CloudflareTunnelEvent): Promise<void> { return PeripheralEvents.appendTunnelEvent(this.buildPeripheralEventsDeps(), event) }
|
|
2134
450
|
|
|
2135
|
-
|
|
2136
|
-
return [...this.tunnelEventsByChatId.keys()]
|
|
2137
|
-
}
|
|
451
|
+
getTunnelEvents(chatId: string): CloudflareTunnelEvent[] { return PeripheralEvents.getTunnelEvents(this.buildPeripheralEventsDeps(), chatId) }
|
|
2138
452
|
|
|
2139
|
-
|
|
2140
|
-
const existing = this.tunnelEventsByChatId.get(event.chatId) ?? []
|
|
2141
|
-
existing.push(event)
|
|
2142
|
-
this.tunnelEventsByChatId.set(event.chatId, existing)
|
|
2143
|
-
}
|
|
453
|
+
listTunnelChats(): string[] { return PeripheralEvents.listTunnelChats(this.buildPeripheralEventsDeps()) }
|
|
2144
454
|
|
|
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
|
-
}
|
|
455
|
+
private async loadTunnelEvents(): Promise<void> { await PeripheralEvents.loadTunnelEvents(this.buildPeripheralEventsDeps()) }
|
|
2161
456
|
|
|
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
|
-
}
|
|
457
|
+
async appendShareEvent(event: ShareEvent): Promise<void> { return PeripheralEvents.appendShareEvent(this.buildPeripheralEventsDeps(), event) }
|
|
2170
458
|
|
|
2171
|
-
getShareEvents(): ShareEvent[] {
|
|
2172
|
-
return [...this.shareEventsAll]
|
|
2173
|
-
}
|
|
459
|
+
getShareEvents(): ShareEvent[] { return PeripheralEvents.getShareEvents(this.buildPeripheralEventsDeps()) }
|
|
2174
460
|
|
|
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
|
-
}
|
|
461
|
+
private async loadShareEvents(): Promise<void> { await PeripheralEvents.loadShareEvents(this.buildPeripheralEventsDeps()) }
|
|
2190
462
|
|
|
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
|
-
}
|
|
463
|
+
async appendPushEvent(event: PushEvent): Promise<void> { return PeripheralEvents.appendPushEvent(this.buildPeripheralEventsDeps(), event) }
|
|
2198
464
|
|
|
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
|
-
}
|
|
465
|
+
async loadPushEvents(): Promise<PushEvent[]> { return PeripheralEvents.loadPushEvents(this.buildPeripheralEventsDeps()) }
|
|
2217
466
|
|
|
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
|
-
}
|
|
467
|
+
async putToolRequest(req: ToolRequest): Promise<void> { return putToolRequestFn(this.buildToolRequestWriteDeps(), req) }
|
|
2227
468
|
|
|
2228
|
-
getToolRequest(id: string): ToolRequest | null {
|
|
2229
|
-
const req = this.state.toolRequestsById.get(id)
|
|
2230
|
-
return req ? { ...req } : null
|
|
2231
|
-
}
|
|
469
|
+
getToolRequest(id: string): ToolRequest | null { return getToolRequestFromMap(this.state.toolRequestsById, id) }
|
|
2232
470
|
|
|
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
|
-
}
|
|
471
|
+
listPendingToolRequests(chatId: string): ToolRequest[] { return listPendingToolRequestsFromMap(this.state.toolRequestsById, chatId) }
|
|
2242
472
|
|
|
2243
473
|
async resolveToolRequest(
|
|
2244
474
|
id: string,
|
|
2245
|
-
args: {
|
|
2246
|
-
status: ToolRequestStatus
|
|
2247
|
-
decision?: ToolRequestDecision
|
|
2248
|
-
resolvedAt: number
|
|
2249
|
-
mismatchReason?: string
|
|
2250
|
-
},
|
|
475
|
+
args: { status: ToolRequestStatus; decision?: ToolRequestDecision; resolvedAt: number; mismatchReason?: string },
|
|
2251
476
|
): 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 }))
|
|
477
|
+
return resolveToolRequestFn(this.buildToolRequestWriteDeps(), id, args)
|
|
2276
478
|
}
|
|
2277
479
|
|
|
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
|
-
}
|
|
480
|
+
scanAllToolRequests(): ToolRequest[] { return scanAllToolRequestsFromMap(this.state.toolRequestsById) }
|
|
2426
481
|
|
|
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
|
-
}
|
|
482
|
+
async flush(): Promise<void> { await this.writeChain }
|
|
2438
483
|
|
|
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
|
-
}
|
|
484
|
+
private readonly orchSubscriptionState = OrchSubscription.createOrchSubscriptionState()
|
|
2449
485
|
|
|
2450
|
-
|
|
2451
|
-
for (const cb of this.orchRunsSubscribers) {
|
|
2452
|
-
try { cb() } catch (err) { log.warn(`${LOG_PREFIX} orch-runs subscriber threw`, { err }) }
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
486
|
+
appendOrchestrationEvent(event: OrchestrationEvent): Promise<void> { return OrchSubscription.appendOrchestrationEvent(this.buildOrchSubscriptionDeps(), event) }
|
|
2455
487
|
|
|
2456
|
-
|
|
2457
|
-
const run = this.state.orchRunsById.get(runId)
|
|
2458
|
-
if (!run) return null
|
|
2459
|
-
return this.toOrchRunSnapshot(run)
|
|
2460
|
-
}
|
|
488
|
+
subscribeOrchRuns(cb: () => void): () => void { return OrchSubscription.subscribeOrchRuns(this.buildOrchSubscriptionDeps(), cb) }
|
|
2461
489
|
|
|
2462
|
-
|
|
2463
|
-
return [...this.state.orchRunsById.values()].map((r) => this.toOrchRunSnapshot(r))
|
|
2464
|
-
}
|
|
490
|
+
getOrchRun(runId: string): OrchRunSnapshot | null { return OrchSubscription.getOrchRun(this.state.orchRunsById, runId) }
|
|
2465
491
|
|
|
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
|
-
}
|
|
492
|
+
getOrchRuns(): OrchRunSnapshot[] { return OrchSubscription.getOrchRuns(this.state.orchRunsById) }
|
|
2496
493
|
|
|
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
494
|
*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
|
-
}
|
|
495
|
+
yield* OrchSubscription.nonTerminalOrchTasks(this.state.orchRunsById)
|
|
2507
496
|
}
|
|
2508
497
|
|
|
2509
|
-
/** Tasks paused at a hard gate — re-armed (not requeued) by recoverOnStartup. */
|
|
2510
498
|
*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
|
-
}
|
|
499
|
+
yield* OrchSubscription.gatedOrchTasks(this.state.orchRunsById)
|
|
2519
500
|
}
|
|
2520
501
|
|
|
2521
|
-
/** Task spec lookup for the engine (records keep prompt/scope; snapshots do not). */
|
|
2522
502
|
getOrchTaskSpec(runId: string, taskId: string): { prompt: string; scopePaths: string[] } | null {
|
|
2523
|
-
|
|
2524
|
-
if (!task) return null
|
|
2525
|
-
return { prompt: task.prompt, scopePaths: task.scopePaths }
|
|
503
|
+
return OrchSubscription.getOrchTaskSpec(this.state.orchRunsById, runId, taskId)
|
|
2526
504
|
}
|
|
2527
505
|
|
|
2528
|
-
|
|
2529
|
-
getOrchLastPhaseOutput(runId: string, taskId: string): string | null {
|
|
2530
|
-
return this.state.orchRunsById.get(runId)?.tasksById.get(taskId)?.lastPhaseOutput ?? null
|
|
2531
|
-
}
|
|
506
|
+
getOrchLastPhaseOutput(runId: string, taskId: string): string | null { return OrchSubscription.getOrchLastPhaseOutput(this.state.orchRunsById, runId, taskId) }
|
|
2532
507
|
|
|
2533
|
-
|
|
2534
|
-
getOrchRunEvents(runId: string): OrchestrationEvent[] {
|
|
2535
|
-
return [...(this.state.orchRunsById.get(runId)?.eventLog ?? [])]
|
|
2536
|
-
}
|
|
508
|
+
getOrchRunEvents(runId: string): OrchestrationEvent[] { return OrchSubscription.getOrchRunEvents(this.state.orchRunsById, runId) }
|
|
2537
509
|
}
|