@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
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot persistence, log-replay, sidebar-order, and event-log-load helpers
|
|
3
|
+
* for EventStore.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from event-store.ts to reduce file size. All IO is performed
|
|
6
|
+
* through the injected StorageBackend abstraction; the sealed side-effect
|
|
7
|
+
* lives in FsStorageBackend.adapter.ts, not here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path"
|
|
11
|
+
import { LOG_PREFIX } from "../shared/branding"
|
|
12
|
+
import type { AnyValue } from "../shared/errors"
|
|
13
|
+
import { log } from "../shared/log"
|
|
14
|
+
import type { AgentProvider, TranscriptEntry } from "../shared/types"
|
|
15
|
+
import { STORE_VERSION } from "../shared/types"
|
|
16
|
+
import type { StorageBackend } from "./storage/backend"
|
|
17
|
+
import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
|
|
18
|
+
import type { PushEvent } from "./push/events"
|
|
19
|
+
import type { ShareEvent } from "./session-share/share-projection"
|
|
20
|
+
import {
|
|
21
|
+
type ChatRecord,
|
|
22
|
+
type ProjectRecord,
|
|
23
|
+
type SnapshotFile,
|
|
24
|
+
type StoreEvent,
|
|
25
|
+
type StoreState,
|
|
26
|
+
cloneTranscriptEntries,
|
|
27
|
+
} from "./events"
|
|
28
|
+
import {
|
|
29
|
+
getReplayEventPriority,
|
|
30
|
+
normalizeSidebarProjectOrder,
|
|
31
|
+
} from "./event-store-helpers"
|
|
32
|
+
|
|
33
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface LegacyTranscriptStats {
|
|
36
|
+
hasLegacyData: boolean
|
|
37
|
+
sources: Array<"snapshot" | "messages_log">
|
|
38
|
+
chatCount: number
|
|
39
|
+
entryCount: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Paths for the log files involved in snapshot/replay. */
|
|
43
|
+
export interface SnapshotLogPaths {
|
|
44
|
+
snapshotPath: string
|
|
45
|
+
projectsLogPath: string
|
|
46
|
+
chatsLogPath: string
|
|
47
|
+
messagesLogPath: string
|
|
48
|
+
queuedMessagesLogPath: string
|
|
49
|
+
turnsLogPath: string
|
|
50
|
+
schedulesLogPath: string
|
|
51
|
+
stacksLogPath: string
|
|
52
|
+
toolRequestsLogPath: string
|
|
53
|
+
orchLogPath: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Returned by {@link loadSnapshotIntoState}. */
|
|
57
|
+
export interface LoadSnapshotResult {
|
|
58
|
+
snapshotHasLegacyMessages: boolean
|
|
59
|
+
legacySidebarProjectOrder: string[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Internal only — used by loadAndReplayLogs.
|
|
63
|
+
interface ParsedReplayEvent {
|
|
64
|
+
event: StoreEvent
|
|
65
|
+
sourceIndex: number
|
|
66
|
+
lineIndex: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Snapshot loading ──────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read `snapshotPath` and populate the in-memory state maps from its content.
|
|
73
|
+
* Calls `clearStorage` when the snapshot version does not match STORE_VERSION
|
|
74
|
+
* or when parsing fails (fail-closed reset).
|
|
75
|
+
*
|
|
76
|
+
* @returns scalar fields that cannot be set through the mutated Maps.
|
|
77
|
+
*/
|
|
78
|
+
export async function loadSnapshotIntoState(
|
|
79
|
+
storage: StorageBackend,
|
|
80
|
+
snapshotPath: string,
|
|
81
|
+
state: StoreState,
|
|
82
|
+
legacyMessagesByChatId: Map<string, TranscriptEntry[]>,
|
|
83
|
+
clearStorage: () => Promise<void>,
|
|
84
|
+
): Promise<LoadSnapshotResult> {
|
|
85
|
+
const empty: LoadSnapshotResult = { snapshotHasLegacyMessages: false, legacySidebarProjectOrder: [] }
|
|
86
|
+
|
|
87
|
+
if (!(await storage.exists(snapshotPath))) return empty
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const text = await storage.readText(snapshotPath)
|
|
91
|
+
if (!text.trim()) return empty
|
|
92
|
+
|
|
93
|
+
const parsed: SnapshotFile = JSON.parse(text)
|
|
94
|
+
if (parsed.v !== STORE_VERSION) {
|
|
95
|
+
log.warn(`${LOG_PREFIX} Resetting local chat history for store version ${STORE_VERSION}`)
|
|
96
|
+
await clearStorage()
|
|
97
|
+
return empty
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const project of parsed.projects) {
|
|
101
|
+
state.projectsById.set(project.id, { ...project })
|
|
102
|
+
state.projectIdsByPath.set(project.localPath, project.id)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const chat of parsed.chats) {
|
|
106
|
+
// Access legacy fields from old snapshot data via Reflect.get (avoids `as` casts).
|
|
107
|
+
const legacySessionToken: string | null | undefined = Reflect.get(chat, "sessionToken")
|
|
108
|
+
const legacyPendingFork: string | null | { provider: AgentProvider; token: string } | undefined =
|
|
109
|
+
Reflect.get(chat, "pendingForkSessionToken")
|
|
110
|
+
const legacyTokensByProvider: Partial<Record<AgentProvider, string | null>> | undefined =
|
|
111
|
+
Reflect.get(chat, "sessionTokensByProvider")
|
|
112
|
+
|
|
113
|
+
const sessionTokensByProvider: Partial<Record<AgentProvider, string | null>> =
|
|
114
|
+
legacyTokensByProvider ? { ...legacyTokensByProvider } : {}
|
|
115
|
+
if (
|
|
116
|
+
typeof legacySessionToken === "string"
|
|
117
|
+
&& chat.provider
|
|
118
|
+
&& sessionTokensByProvider[chat.provider] == null
|
|
119
|
+
) {
|
|
120
|
+
sessionTokensByProvider[chat.provider] = legacySessionToken
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let pendingForkSessionToken: ChatRecord["pendingForkSessionToken"] = null
|
|
124
|
+
if (legacyPendingFork && typeof legacyPendingFork === "object" && "token" in legacyPendingFork) {
|
|
125
|
+
pendingForkSessionToken = legacyPendingFork
|
|
126
|
+
} else if (typeof legacyPendingFork === "string" && chat.provider) {
|
|
127
|
+
pendingForkSessionToken = { provider: chat.provider, token: legacyPendingFork }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
state.chatsById.set(chat.id, {
|
|
131
|
+
...chat,
|
|
132
|
+
unread: chat.unread ?? false,
|
|
133
|
+
sessionTokensByProvider,
|
|
134
|
+
pendingForkSessionToken,
|
|
135
|
+
})
|
|
136
|
+
// Mirror the chat_created handler: every live chat needs a subagent-run
|
|
137
|
+
// map, or subagent_run_started events after a reboot are silently
|
|
138
|
+
// dropped from the read model (the runs never reach the UI).
|
|
139
|
+
if (!chat.deletedAt) {
|
|
140
|
+
state.subagentRunsByChatId.set(chat.id, new Map())
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const legacySidebarProjectOrder = normalizeSidebarProjectOrder(
|
|
145
|
+
parsed.sidebarProjectOrder,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if (parsed.queuedMessages?.length) {
|
|
149
|
+
for (const queuedSet of parsed.queuedMessages) {
|
|
150
|
+
state.queuedMessagesByChatId.set(
|
|
151
|
+
queuedSet.chatId,
|
|
152
|
+
queuedSet.entries.map((entry) => ({
|
|
153
|
+
...entry,
|
|
154
|
+
attachments: [...entry.attachments],
|
|
155
|
+
})),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let snapshotHasLegacyMessages = false
|
|
161
|
+
if (parsed.messages?.length) {
|
|
162
|
+
snapshotHasLegacyMessages = true
|
|
163
|
+
for (const messageSet of parsed.messages) {
|
|
164
|
+
legacyMessagesByChatId.set(
|
|
165
|
+
messageSet.chatId,
|
|
166
|
+
cloneTranscriptEntries(messageSet.entries),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (parsed.autoContinueEvents?.length) {
|
|
172
|
+
for (const entry of parsed.autoContinueEvents) {
|
|
173
|
+
state.autoContinueEventsByChatId.set(entry.chatId, [...entry.events])
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parsed.stacks?.length) {
|
|
178
|
+
for (const stack of parsed.stacks) {
|
|
179
|
+
state.stacksById.set(stack.id, { ...stack, projectIds: [...stack.projectIds] })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { snapshotHasLegacyMessages, legacySidebarProjectOrder }
|
|
184
|
+
} catch (error) {
|
|
185
|
+
log.warn(`${LOG_PREFIX} Failed to load snapshot, resetting local history:`, String(error))
|
|
186
|
+
await clearStorage()
|
|
187
|
+
return empty
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Snapshot persistence ──────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build a `SnapshotFile` from the current in-memory state (pure — no IO).
|
|
195
|
+
*/
|
|
196
|
+
export function buildSnapshotFile(
|
|
197
|
+
state: StoreState,
|
|
198
|
+
projects: ProjectRecord[],
|
|
199
|
+
): SnapshotFile {
|
|
200
|
+
return {
|
|
201
|
+
v: STORE_VERSION,
|
|
202
|
+
generatedAt: Date.now(),
|
|
203
|
+
projects: projects.map((project) => ({ ...project })),
|
|
204
|
+
chats: [...state.chatsById.values()]
|
|
205
|
+
.filter((chat) => !chat.deletedAt)
|
|
206
|
+
.map((chat) => ({ ...chat })),
|
|
207
|
+
queuedMessages: [...state.queuedMessagesByChatId.entries()].map(([chatId, entries]) => ({
|
|
208
|
+
chatId,
|
|
209
|
+
entries: entries.map((entry) => ({
|
|
210
|
+
...entry,
|
|
211
|
+
attachments: [...entry.attachments],
|
|
212
|
+
})),
|
|
213
|
+
})),
|
|
214
|
+
autoContinueEvents: [...state.autoContinueEventsByChatId.entries()].map(
|
|
215
|
+
([chatId, events]) => ({ chatId, events: [...events] }),
|
|
216
|
+
),
|
|
217
|
+
stacks: [...state.stacksById.values()]
|
|
218
|
+
.filter((stack) => !stack.deletedAt)
|
|
219
|
+
.map((stack) => ({ ...stack, projectIds: [...stack.projectIds] })),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Write `snapshotJson` to disk and truncate all compactable log files to empty.
|
|
225
|
+
* (tunnels.jsonl, push.jsonl, tool-requests.jsonl are intentionally excluded;
|
|
226
|
+
* see comments in the original EventStore implementation.)
|
|
227
|
+
*/
|
|
228
|
+
export async function truncateLogsAfterSnapshot(
|
|
229
|
+
storage: StorageBackend,
|
|
230
|
+
paths: SnapshotLogPaths,
|
|
231
|
+
snapshotJson: string,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
await storage.writeText(paths.snapshotPath, snapshotJson)
|
|
234
|
+
await Promise.all([
|
|
235
|
+
storage.writeText(paths.projectsLogPath, ""),
|
|
236
|
+
storage.writeText(paths.chatsLogPath, ""),
|
|
237
|
+
storage.writeText(paths.messagesLogPath, ""),
|
|
238
|
+
storage.writeText(paths.queuedMessagesLogPath, ""),
|
|
239
|
+
storage.writeText(paths.turnsLogPath, ""),
|
|
240
|
+
storage.writeText(paths.schedulesLogPath, ""),
|
|
241
|
+
storage.writeText(paths.stacksLogPath, ""),
|
|
242
|
+
storage.writeText(paths.toolRequestsLogPath, ""),
|
|
243
|
+
])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const SNAPSHOT_THRESHOLD_BYTES = 2 * 1024 * 1024
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns true when the combined size of compactable log files exceeds
|
|
250
|
+
* {@link SNAPSHOT_THRESHOLD_BYTES} (2 MiB).
|
|
251
|
+
*/
|
|
252
|
+
export async function calcShouldTruncateLogs(
|
|
253
|
+
storage: StorageBackend,
|
|
254
|
+
paths: SnapshotLogPaths,
|
|
255
|
+
): Promise<boolean> {
|
|
256
|
+
const sizes = await Promise.all([
|
|
257
|
+
storage.size(paths.projectsLogPath),
|
|
258
|
+
storage.size(paths.chatsLogPath),
|
|
259
|
+
storage.size(paths.messagesLogPath),
|
|
260
|
+
storage.size(paths.queuedMessagesLogPath),
|
|
261
|
+
storage.size(paths.turnsLogPath),
|
|
262
|
+
storage.size(paths.schedulesLogPath),
|
|
263
|
+
storage.size(paths.stacksLogPath),
|
|
264
|
+
storage.size(paths.toolRequestsLogPath),
|
|
265
|
+
])
|
|
266
|
+
return sizes.reduce((total, size) => total + size, 0) >= SNAPSHOT_THRESHOLD_BYTES
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Log replay ────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Load a single JSONL log file and return the parsed events, skipping
|
|
273
|
+
* `sidebar_project_order_set` events (handled separately). On a corrupt
|
|
274
|
+
* trailing line, returns whatever was parsed so far. On an incompatible
|
|
275
|
+
* store version or a corrupt mid-file line, calls `clearStorage` and returns
|
|
276
|
+
* an empty array.
|
|
277
|
+
*/
|
|
278
|
+
export async function loadReplayEventsFromFile(
|
|
279
|
+
storage: StorageBackend,
|
|
280
|
+
filePath: string,
|
|
281
|
+
sourceIndex: number,
|
|
282
|
+
clearStorage: () => Promise<void>,
|
|
283
|
+
): Promise<ParsedReplayEvent[]> {
|
|
284
|
+
if (!(await storage.exists(filePath))) return []
|
|
285
|
+
|
|
286
|
+
const text = await storage.readText(filePath)
|
|
287
|
+
if (!text.trim()) return []
|
|
288
|
+
|
|
289
|
+
const parsedEvents: ParsedReplayEvent[] = []
|
|
290
|
+
const lines = text.split("\n")
|
|
291
|
+
let lastNonEmpty = -1
|
|
292
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
293
|
+
if (lines[index].trim()) {
|
|
294
|
+
lastNonEmpty = index
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
300
|
+
const line = lines[index].trim()
|
|
301
|
+
if (!line) continue
|
|
302
|
+
try {
|
|
303
|
+
const event: StoreEvent & { v?: number; type?: string } = JSON.parse(line)
|
|
304
|
+
if (event.v !== STORE_VERSION) {
|
|
305
|
+
log.warn(`${LOG_PREFIX} Resetting local history from incompatible event log`)
|
|
306
|
+
await clearStorage()
|
|
307
|
+
return []
|
|
308
|
+
}
|
|
309
|
+
if (event.type === "sidebar_project_order_set") {
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
parsedEvents.push({ event, sourceIndex, lineIndex: index })
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (index === lastNonEmpty) {
|
|
315
|
+
log.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(filePath)}`)
|
|
316
|
+
return parsedEvents
|
|
317
|
+
}
|
|
318
|
+
log.warn(
|
|
319
|
+
`${LOG_PREFIX} Failed to replay ${path.basename(filePath)}, resetting local history:`,
|
|
320
|
+
String(error),
|
|
321
|
+
)
|
|
322
|
+
await clearStorage()
|
|
323
|
+
return []
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return parsedEvents
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Load all log files, sort events by timestamp, and apply them in order.
|
|
332
|
+
* Short-circuits if `isStorageReset()` returns true before or after loading
|
|
333
|
+
* (a version mismatch in any file triggers `clearStorage` in
|
|
334
|
+
* {@link loadReplayEventsFromFile}).
|
|
335
|
+
*/
|
|
336
|
+
export async function loadAndReplayLogs(
|
|
337
|
+
storage: StorageBackend,
|
|
338
|
+
paths: SnapshotLogPaths,
|
|
339
|
+
isStorageReset: () => boolean,
|
|
340
|
+
applyEvent: (event: StoreEvent) => void,
|
|
341
|
+
clearStorage: () => Promise<void>,
|
|
342
|
+
onReplayChatProviderClear: () => void,
|
|
343
|
+
): Promise<void> {
|
|
344
|
+
if (isStorageReset()) return
|
|
345
|
+
|
|
346
|
+
const replayEvents = [
|
|
347
|
+
...await loadReplayEventsFromFile(storage, paths.projectsLogPath, 0, clearStorage),
|
|
348
|
+
...await loadReplayEventsFromFile(storage, paths.stacksLogPath, 1, clearStorage),
|
|
349
|
+
...await loadReplayEventsFromFile(storage, paths.chatsLogPath, 2, clearStorage),
|
|
350
|
+
...await loadReplayEventsFromFile(storage, paths.messagesLogPath, 3, clearStorage),
|
|
351
|
+
...await loadReplayEventsFromFile(storage, paths.queuedMessagesLogPath, 4, clearStorage),
|
|
352
|
+
...await loadReplayEventsFromFile(storage, paths.turnsLogPath, 5, clearStorage),
|
|
353
|
+
...await loadReplayEventsFromFile(storage, paths.schedulesLogPath, 6, clearStorage),
|
|
354
|
+
...await loadReplayEventsFromFile(storage, paths.toolRequestsLogPath, 7, clearStorage),
|
|
355
|
+
...await loadReplayEventsFromFile(storage, paths.orchLogPath, 8, clearStorage),
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
if (isStorageReset()) return
|
|
359
|
+
|
|
360
|
+
replayEvents
|
|
361
|
+
.sort(
|
|
362
|
+
(left, right) =>
|
|
363
|
+
left.event.timestamp - right.event.timestamp
|
|
364
|
+
|| getReplayEventPriority(left.event) - getReplayEventPriority(right.event)
|
|
365
|
+
|| left.sourceIndex - right.sourceIndex
|
|
366
|
+
|| left.lineIndex - right.lineIndex,
|
|
367
|
+
)
|
|
368
|
+
.forEach(({ event }) => { applyEvent(event) })
|
|
369
|
+
|
|
370
|
+
onReplayChatProviderClear()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Sidebar project order ─────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Write `projectIds` as JSON to the sidebar order file.
|
|
377
|
+
*/
|
|
378
|
+
export async function writeSidebarOrderFile(
|
|
379
|
+
storage: StorageBackend,
|
|
380
|
+
dataDir: string,
|
|
381
|
+
sidebarProjectOrderPath: string,
|
|
382
|
+
projectIds: string[],
|
|
383
|
+
): Promise<void> {
|
|
384
|
+
await storage.mkdir(dataDir)
|
|
385
|
+
await storage.writeText(sidebarProjectOrderPath, `${JSON.stringify(projectIds, null, 2)}\n`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Scan `projectsLogPath` for the latest `sidebar_project_order_set` event and
|
|
390
|
+
* return the project IDs from it. Returns `[]` if the file is absent, empty,
|
|
391
|
+
* or corrupt.
|
|
392
|
+
*/
|
|
393
|
+
export async function readSidebarOrderFromProjectsLog(
|
|
394
|
+
storage: StorageBackend,
|
|
395
|
+
projectsLogPath: string,
|
|
396
|
+
): Promise<string[]> {
|
|
397
|
+
if (!(await storage.exists(projectsLogPath))) return []
|
|
398
|
+
|
|
399
|
+
const text = await storage.readText(projectsLogPath)
|
|
400
|
+
if (!text.trim()) return []
|
|
401
|
+
|
|
402
|
+
const lines = text.split("\n")
|
|
403
|
+
let lastNonEmpty = -1
|
|
404
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
405
|
+
if (lines[index].trim()) {
|
|
406
|
+
lastNonEmpty = index
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let projectIds: string[] = []
|
|
412
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
413
|
+
const line = lines[index].trim()
|
|
414
|
+
if (!line) continue
|
|
415
|
+
try {
|
|
416
|
+
const event: { v?: number; type?: string; projectIds?: AnyValue } = JSON.parse(line)
|
|
417
|
+
if (event.v !== STORE_VERSION || event.type !== "sidebar_project_order_set") {
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
projectIds = normalizeSidebarProjectOrder(event.projectIds)
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (index === lastNonEmpty) {
|
|
423
|
+
log.warn(
|
|
424
|
+
`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(projectsLogPath)} while migrating sidebar order`,
|
|
425
|
+
)
|
|
426
|
+
return projectIds
|
|
427
|
+
}
|
|
428
|
+
log.warn(
|
|
429
|
+
`${LOG_PREFIX} Failed to migrate sidebar order from ${path.basename(projectsLogPath)}:`,
|
|
430
|
+
String(error),
|
|
431
|
+
)
|
|
432
|
+
return []
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return projectIds
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Load the authoritative sidebar project order from disk.
|
|
441
|
+
*
|
|
442
|
+
* Resolution order:
|
|
443
|
+
* 1. Dedicated `sidebar-order.json` file.
|
|
444
|
+
* 2. Latest `sidebar_project_order_set` event from `projects.jsonl`.
|
|
445
|
+
* 3. `legacySidebarProjectOrder` from the last loaded snapshot.
|
|
446
|
+
*
|
|
447
|
+
* If the dedicated file does not exist but a legacy order is found, the
|
|
448
|
+
* legacy order is written to the dedicated file as a one-time migration.
|
|
449
|
+
*
|
|
450
|
+
* Returns the resolved project IDs array.
|
|
451
|
+
*/
|
|
452
|
+
export async function loadSidebarOrder(
|
|
453
|
+
storage: StorageBackend,
|
|
454
|
+
sidebarProjectOrderPath: string,
|
|
455
|
+
projectsLogPath: string,
|
|
456
|
+
dataDir: string,
|
|
457
|
+
legacySidebarProjectOrder: string[],
|
|
458
|
+
): Promise<string[]> {
|
|
459
|
+
if (await storage.exists(sidebarProjectOrderPath)) {
|
|
460
|
+
try {
|
|
461
|
+
const text = await storage.readText(sidebarProjectOrderPath)
|
|
462
|
+
if (!text.trim()) return []
|
|
463
|
+
return normalizeSidebarProjectOrder(JSON.parse(text))
|
|
464
|
+
} catch (error) {
|
|
465
|
+
log.warn(
|
|
466
|
+
`${LOG_PREFIX} Failed to load sidebar-order.json, ignoring saved order:`,
|
|
467
|
+
String(error),
|
|
468
|
+
)
|
|
469
|
+
return []
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// No dedicated file yet — migrate from the legacy source.
|
|
474
|
+
const fromProjectsLog = await readSidebarOrderFromProjectsLog(storage, projectsLogPath)
|
|
475
|
+
const order = fromProjectsLog.length > 0 ? fromProjectsLog : [...legacySidebarProjectOrder]
|
|
476
|
+
|
|
477
|
+
if (order.length > 0) {
|
|
478
|
+
await writeSidebarOrderFile(storage, dataDir, sidebarProjectOrderPath, order)
|
|
479
|
+
}
|
|
480
|
+
return order
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─── Legacy transcript helpers ─────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Compute legacy-transcript statistics from the current in-memory legacy
|
|
487
|
+
* maps and the storage size of `messages.jsonl`.
|
|
488
|
+
*/
|
|
489
|
+
export async function computeLegacyTranscriptStats(
|
|
490
|
+
storage: StorageBackend,
|
|
491
|
+
messagesLogPath: string,
|
|
492
|
+
snapshotHasLegacyMessages: boolean,
|
|
493
|
+
legacyMessagesByChatId: Map<string, TranscriptEntry[]>,
|
|
494
|
+
): Promise<LegacyTranscriptStats> {
|
|
495
|
+
const messagesLogSize = await storage.size(messagesLogPath)
|
|
496
|
+
const sources: LegacyTranscriptStats["sources"] = []
|
|
497
|
+
if (snapshotHasLegacyMessages) {
|
|
498
|
+
sources.push("snapshot")
|
|
499
|
+
}
|
|
500
|
+
if (messagesLogSize > 0) {
|
|
501
|
+
sources.push("messages_log")
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let entryCount = 0
|
|
505
|
+
for (const entries of legacyMessagesByChatId.values()) {
|
|
506
|
+
entryCount += entries.length
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
hasLegacyData: sources.length > 0 || legacyMessagesByChatId.size > 0,
|
|
511
|
+
sources,
|
|
512
|
+
chatCount: legacyMessagesByChatId.size,
|
|
513
|
+
entryCount,
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Write per-chat transcript files from `legacyMessagesByChatId`, clear
|
|
519
|
+
* the legacy state, snapshot + truncate logs, and invalidate the cache.
|
|
520
|
+
*
|
|
521
|
+
* Returns `false` if no legacy data exists; `true` on success.
|
|
522
|
+
*/
|
|
523
|
+
export async function migrateLegacyTranscripts(
|
|
524
|
+
storage: StorageBackend,
|
|
525
|
+
transcriptsDir: string,
|
|
526
|
+
legacyStats: LegacyTranscriptStats,
|
|
527
|
+
legacyMessagesByChatId: Map<string, TranscriptEntry[]>,
|
|
528
|
+
transcriptPath: (chatId: string) => string,
|
|
529
|
+
onClearLegacyState: () => void,
|
|
530
|
+
onSnapshotAndTruncate: () => Promise<void>,
|
|
531
|
+
onCacheInvalidate: () => void,
|
|
532
|
+
onProgress?: (message: string) => void,
|
|
533
|
+
): Promise<boolean> {
|
|
534
|
+
if (!legacyStats.hasLegacyData) return false
|
|
535
|
+
|
|
536
|
+
const sourceSummary = legacyStats.sources
|
|
537
|
+
.map((source) => (source === "messages_log" ? "messages.jsonl" : "snapshot.json"))
|
|
538
|
+
.join(", ")
|
|
539
|
+
onProgress?.(
|
|
540
|
+
`${LOG_PREFIX} transcript migration detected: ${legacyStats.chatCount} chats, ${legacyStats.entryCount} entries from ${sourceSummary}`,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
const messageSets = [...legacyMessagesByChatId.entries()]
|
|
544
|
+
onProgress?.(
|
|
545
|
+
`${LOG_PREFIX} transcript migration: writing ${messageSets.length} per-chat transcript files`,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
await storage.mkdir(transcriptsDir)
|
|
549
|
+
const logEveryChat = messageSets.length <= 10
|
|
550
|
+
for (let index = 0; index < messageSets.length; index += 1) {
|
|
551
|
+
const [chatId, entries] = messageSets[index]
|
|
552
|
+
const chatTranscriptPath = transcriptPath(chatId)
|
|
553
|
+
const tempPath = `${chatTranscriptPath}.tmp`
|
|
554
|
+
const payload = entries.map((entry) => JSON.stringify(entry)).join("\n")
|
|
555
|
+
await storage.writeText(tempPath, payload ? `${payload}\n` : "")
|
|
556
|
+
await storage.rename(tempPath, chatTranscriptPath)
|
|
557
|
+
if (logEveryChat || (index + 1) % 25 === 0 || index === messageSets.length - 1) {
|
|
558
|
+
onProgress?.(`${LOG_PREFIX} transcript migration: ${index + 1}/${messageSets.length} chats`)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
onClearLegacyState()
|
|
563
|
+
await onSnapshotAndTruncate()
|
|
564
|
+
onCacheInvalidate()
|
|
565
|
+
onProgress?.(`${LOG_PREFIX} transcript migration complete`)
|
|
566
|
+
return true
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ─── Tunnel event apply + load ─────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Apply a single CloudflareTunnelEvent to the in-memory map (pure, no IO).
|
|
573
|
+
*/
|
|
574
|
+
export function applyTunnelEventToMap(
|
|
575
|
+
tunnelEventsByChatId: Map<string, CloudflareTunnelEvent[]>,
|
|
576
|
+
event: CloudflareTunnelEvent,
|
|
577
|
+
): void {
|
|
578
|
+
const existing = tunnelEventsByChatId.get(event.chatId) ?? []
|
|
579
|
+
existing.push(event)
|
|
580
|
+
tunnelEventsByChatId.set(event.chatId, existing)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Load `tunnels.jsonl` from disk and populate `tunnelEventsByChatId`.
|
|
585
|
+
* Malformed lines are skipped with a warning.
|
|
586
|
+
*/
|
|
587
|
+
export async function loadTunnelEventsFromLog(
|
|
588
|
+
storage: StorageBackend,
|
|
589
|
+
tunnelLogPath: string,
|
|
590
|
+
tunnelEventsByChatId: Map<string, CloudflareTunnelEvent[]>,
|
|
591
|
+
): Promise<void> {
|
|
592
|
+
if (!(await storage.exists(tunnelLogPath))) return
|
|
593
|
+
const text = await storage.readText(tunnelLogPath)
|
|
594
|
+
if (!text.trim()) return
|
|
595
|
+
for (const rawLine of text.split("\n")) {
|
|
596
|
+
const line = rawLine.trim()
|
|
597
|
+
if (!line) continue
|
|
598
|
+
try {
|
|
599
|
+
const event: CloudflareTunnelEvent = JSON.parse(line)
|
|
600
|
+
applyTunnelEventToMap(tunnelEventsByChatId, event)
|
|
601
|
+
} catch {
|
|
602
|
+
log.warn(`${LOG_PREFIX} Ignoring malformed line in tunnels.jsonl`)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ─── Share event load ──────────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Load `shares.jsonl` from disk and append to `shareEventsAll`.
|
|
611
|
+
* Malformed lines are skipped with a warning.
|
|
612
|
+
*/
|
|
613
|
+
export async function loadShareEventsFromLog(
|
|
614
|
+
storage: StorageBackend,
|
|
615
|
+
sharesLogPath: string,
|
|
616
|
+
shareEventsAll: ShareEvent[],
|
|
617
|
+
): Promise<void> {
|
|
618
|
+
if (!(await storage.exists(sharesLogPath))) return
|
|
619
|
+
const text = await storage.readText(sharesLogPath)
|
|
620
|
+
if (!text.trim()) return
|
|
621
|
+
for (const rawLine of text.split("\n")) {
|
|
622
|
+
const line = rawLine.trim()
|
|
623
|
+
if (!line) continue
|
|
624
|
+
try {
|
|
625
|
+
const event: ShareEvent = JSON.parse(line)
|
|
626
|
+
shareEventsAll.push(event)
|
|
627
|
+
} catch {
|
|
628
|
+
log.warn(`${LOG_PREFIX} Ignoring malformed line in shares.jsonl`)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ─── Push event load ───────────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Load `push.jsonl` from disk and return all parsed PushEvents.
|
|
637
|
+
* Malformed lines are skipped with a warning.
|
|
638
|
+
*/
|
|
639
|
+
export async function loadPushEventsFromLog(
|
|
640
|
+
storage: StorageBackend,
|
|
641
|
+
pushLogPath: string,
|
|
642
|
+
): Promise<PushEvent[]> {
|
|
643
|
+
if (!(await storage.exists(pushLogPath))) return []
|
|
644
|
+
const text = await storage.readText(pushLogPath)
|
|
645
|
+
if (!text.trim()) return []
|
|
646
|
+
const events: PushEvent[] = []
|
|
647
|
+
for (const rawLine of text.split("\n")) {
|
|
648
|
+
const line = rawLine.trim()
|
|
649
|
+
if (!line) continue
|
|
650
|
+
try {
|
|
651
|
+
const pushEvent: PushEvent = JSON.parse(line)
|
|
652
|
+
events.push(pushEvent)
|
|
653
|
+
} catch {
|
|
654
|
+
log.warn(`${LOG_PREFIX} Ignoring malformed line in push.jsonl`)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return events
|
|
658
|
+
}
|