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