@cuongtran001/kanna 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{arc-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +365 -0
- package/src/server/event-store-messages.adapter.ts +213 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +285 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +35 -0
- package/src/server/event-store.ts +251 -2285
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +488 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +329 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +224 -0
- package/src/server/ws-router.ts +339 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +25 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration read-model layer — pure functions that fold OrchestrationEvents
|
|
3
|
+
* into the `orchRunsById` map and project snapshots from it.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from event-store.ts; EventStore delegates every orch* method here.
|
|
6
|
+
* All functions are pure with respect to IO: they mutate only the map they
|
|
7
|
+
* receive, which is the same Map instance held in StoreState.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { LOG_PREFIX } from "../shared/branding"
|
|
11
|
+
import { log } from "../shared/log"
|
|
12
|
+
import type { OrchRunSnapshot, OrchTaskSnapshot } from "../shared/orchestration-types"
|
|
13
|
+
import type { OrchestrationEvent, OrchRunRecord, OrchTaskRecord } from "./events"
|
|
14
|
+
import { isTaskEventType, nextRunStatus, nextTaskState } from "./orchestration-state-machine"
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Event application
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fold one OrchestrationEvent into the orchRunsById map in place.
|
|
22
|
+
* Mirrors the private `applyOrchestrationEvent` that lived in EventStore.
|
|
23
|
+
*/
|
|
24
|
+
export function applyOrchEvent(
|
|
25
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
26
|
+
event: OrchestrationEvent,
|
|
27
|
+
): void {
|
|
28
|
+
if (event.type === "orch_run_created") {
|
|
29
|
+
const tasksById = new Map<string, OrchTaskRecord>()
|
|
30
|
+
for (const spec of event.tasks) {
|
|
31
|
+
tasksById.set(spec.id, {
|
|
32
|
+
taskId: spec.id,
|
|
33
|
+
title: spec.title,
|
|
34
|
+
prompt: spec.prompt,
|
|
35
|
+
scopePaths: spec.scopePaths ?? [],
|
|
36
|
+
state: "queued",
|
|
37
|
+
ownerWorkerId: null,
|
|
38
|
+
worktreePath: null,
|
|
39
|
+
branch: null,
|
|
40
|
+
baseSha: null,
|
|
41
|
+
phaseIndex: -1,
|
|
42
|
+
attempts: 0,
|
|
43
|
+
error: null,
|
|
44
|
+
commitSha: null,
|
|
45
|
+
lastPhaseOutput: null,
|
|
46
|
+
verifying: false,
|
|
47
|
+
updatedAt: event.timestamp,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
orchRunsById.set(event.runId, {
|
|
51
|
+
runId: event.runId,
|
|
52
|
+
status: "running",
|
|
53
|
+
config: event.config,
|
|
54
|
+
tasksById,
|
|
55
|
+
taskOrder: event.tasks.map((t) => t.id),
|
|
56
|
+
worktrees: [],
|
|
57
|
+
eventLog: [event],
|
|
58
|
+
createdAt: event.timestamp,
|
|
59
|
+
updatedAt: event.timestamp,
|
|
60
|
+
})
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const run = orchRunsById.get(event.runId)
|
|
65
|
+
if (!run) return
|
|
66
|
+
|
|
67
|
+
run.eventLog.push(event)
|
|
68
|
+
run.updatedAt = event.timestamp
|
|
69
|
+
|
|
70
|
+
if (event.type === "orch_run_completed" || event.type === "orch_run_cancelled") {
|
|
71
|
+
const trans = nextRunStatus(run.status, event.type)
|
|
72
|
+
if (!trans.ok) {
|
|
73
|
+
log.warn(`${LOG_PREFIX} dropped illegal orch run transition`, {
|
|
74
|
+
runId: event.runId,
|
|
75
|
+
from: run.status,
|
|
76
|
+
event: event.type,
|
|
77
|
+
})
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
run.status = trans.next
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (event.type === "orch_scope_overlap_flagged") return // observability-only, no state fold
|
|
85
|
+
if (event.type === "orch_config_warning") return // observability-only, no state fold
|
|
86
|
+
|
|
87
|
+
// Worktree pool fold (F13)
|
|
88
|
+
if (event.type === "orch_worktree_provisioned") {
|
|
89
|
+
run.worktrees.push({
|
|
90
|
+
index: event.index,
|
|
91
|
+
path: event.path,
|
|
92
|
+
branch: event.branch,
|
|
93
|
+
heldByTaskId: null,
|
|
94
|
+
initialized: false,
|
|
95
|
+
})
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (event.type === "orch_worktree_init_started") return // timeline-only
|
|
99
|
+
if (event.type === "orch_worktree_init_completed") {
|
|
100
|
+
const slot = run.worktrees.find((w) => w.index === event.index)
|
|
101
|
+
if (slot) slot.initialized = event.ok
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const task = run.tasksById.get(event.taskId)
|
|
106
|
+
if (!task) return
|
|
107
|
+
|
|
108
|
+
// FSM guard: drop an illegal task transition rather than corrupt the read
|
|
109
|
+
// model. The engine only emits legal transitions, so this is a defensive
|
|
110
|
+
// invariant (also protects replay of an older/foreign log).
|
|
111
|
+
if (isTaskEventType(event.type)) {
|
|
112
|
+
const trans = nextTaskState(task.state, event.type)
|
|
113
|
+
if (!trans.ok) {
|
|
114
|
+
log.warn(`${LOG_PREFIX} dropped illegal orch task transition`, {
|
|
115
|
+
runId: event.runId,
|
|
116
|
+
taskId: event.taskId,
|
|
117
|
+
from: task.state,
|
|
118
|
+
event: event.type,
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
task.updatedAt = event.timestamp
|
|
125
|
+
const slotOf = (t: OrchTaskRecord) => run.worktrees.find((w) => w.path === t.worktreePath)
|
|
126
|
+
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
case "orch_task_claimed":
|
|
129
|
+
task.state = "claimed"
|
|
130
|
+
task.ownerWorkerId = event.workerId
|
|
131
|
+
task.worktreePath = event.worktreePath
|
|
132
|
+
task.branch = event.branch
|
|
133
|
+
task.baseSha = event.baseSha
|
|
134
|
+
task.attempts += 1
|
|
135
|
+
{
|
|
136
|
+
const slot = slotOf(task)
|
|
137
|
+
if (slot) slot.heldByTaskId = task.taskId
|
|
138
|
+
}
|
|
139
|
+
break
|
|
140
|
+
case "orch_phase_started":
|
|
141
|
+
task.state = "running"
|
|
142
|
+
task.phaseIndex = event.phaseIndex
|
|
143
|
+
task.verifying = false
|
|
144
|
+
break
|
|
145
|
+
case "orch_phase_completed":
|
|
146
|
+
task.lastPhaseOutput = event.output
|
|
147
|
+
break
|
|
148
|
+
case "orch_gate_opened":
|
|
149
|
+
task.state = "gated"
|
|
150
|
+
break
|
|
151
|
+
case "orch_gate_resolved":
|
|
152
|
+
if (event.decision === "approve") task.state = "running"
|
|
153
|
+
// reject: state stays gated; the engine appends orch_task_failed next
|
|
154
|
+
break
|
|
155
|
+
case "orch_verify_started":
|
|
156
|
+
task.verifying = true // observable stage; task stays "running"
|
|
157
|
+
break
|
|
158
|
+
case "orch_verify_completed":
|
|
159
|
+
task.verifying = false
|
|
160
|
+
break
|
|
161
|
+
case "orch_task_committed":
|
|
162
|
+
task.state = "committed"
|
|
163
|
+
task.ownerWorkerId = null
|
|
164
|
+
task.commitSha = event.commitSha
|
|
165
|
+
task.verifying = false
|
|
166
|
+
{
|
|
167
|
+
const slot = slotOf(task)
|
|
168
|
+
if (slot?.heldByTaskId === task.taskId) slot.heldByTaskId = null
|
|
169
|
+
}
|
|
170
|
+
break
|
|
171
|
+
case "orch_task_failed":
|
|
172
|
+
task.state = "failed"
|
|
173
|
+
task.ownerWorkerId = null
|
|
174
|
+
task.error = event.error
|
|
175
|
+
task.verifying = false
|
|
176
|
+
{
|
|
177
|
+
const slot = slotOf(task)
|
|
178
|
+
if (slot?.heldByTaskId === task.taskId) slot.heldByTaskId = null
|
|
179
|
+
}
|
|
180
|
+
break
|
|
181
|
+
case "orch_task_requeued":
|
|
182
|
+
// Slot hold deliberately KEPT (F13/F2): the task's uncommitted progress
|
|
183
|
+
// lives in its worktree — re-claim resumes the SAME slot.
|
|
184
|
+
task.state = "queued"
|
|
185
|
+
task.ownerWorkerId = null
|
|
186
|
+
task.verifying = false
|
|
187
|
+
break
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Snapshot projection
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/** Project a stored OrchRunRecord into the wire-safe OrchRunSnapshot shape. */
|
|
196
|
+
export function toOrchRunSnapshot(run: OrchRunRecord): OrchRunSnapshot {
|
|
197
|
+
const tasks: OrchTaskSnapshot[] = run.taskOrder.flatMap((taskId) => {
|
|
198
|
+
const t = run.tasksById.get(taskId)
|
|
199
|
+
if (!t) return []
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
taskId: t.taskId,
|
|
203
|
+
title: t.title,
|
|
204
|
+
state: t.state,
|
|
205
|
+
ownerWorkerId: t.ownerWorkerId,
|
|
206
|
+
worktreePath: t.worktreePath,
|
|
207
|
+
branch: t.branch,
|
|
208
|
+
baseSha: t.baseSha,
|
|
209
|
+
phaseIndex: t.phaseIndex,
|
|
210
|
+
attempts: t.attempts,
|
|
211
|
+
error: t.error,
|
|
212
|
+
commitSha: t.commitSha,
|
|
213
|
+
verifying: t.verifying,
|
|
214
|
+
updatedAt: t.updatedAt,
|
|
215
|
+
},
|
|
216
|
+
]
|
|
217
|
+
})
|
|
218
|
+
return {
|
|
219
|
+
runId: run.runId,
|
|
220
|
+
status: run.status,
|
|
221
|
+
config: run.config,
|
|
222
|
+
tasks,
|
|
223
|
+
worktrees: run.worktrees.map((w) => ({ ...w })),
|
|
224
|
+
createdAt: run.createdAt,
|
|
225
|
+
updatedAt: run.updatedAt,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Query helpers
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/** Look up one run by id and project it to a snapshot. */
|
|
234
|
+
export function getOrchRunSnapshot(
|
|
235
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
236
|
+
runId: string,
|
|
237
|
+
): OrchRunSnapshot | null {
|
|
238
|
+
const run = orchRunsById.get(runId)
|
|
239
|
+
if (!run) return null
|
|
240
|
+
return toOrchRunSnapshot(run)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Project all runs to snapshots. */
|
|
244
|
+
export function getAllOrchRunSnapshots(
|
|
245
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
246
|
+
): OrchRunSnapshot[] {
|
|
247
|
+
return [...orchRunsById.values()].map(toOrchRunSnapshot)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Task spec lookup for the engine (records keep prompt/scope; snapshots do not). */
|
|
251
|
+
export function getOrchTaskSpec(
|
|
252
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
253
|
+
runId: string,
|
|
254
|
+
taskId: string,
|
|
255
|
+
): { prompt: string; scopePaths: string[] } | null {
|
|
256
|
+
const task = orchRunsById.get(runId)?.tasksById.get(taskId)
|
|
257
|
+
if (!task) return null
|
|
258
|
+
return { prompt: task.prompt, scopePaths: task.scopePaths }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Last completed phase's output — {{PRIOR}} context when resuming a gated/recovered task. */
|
|
262
|
+
export function getOrchLastPhaseOutput(
|
|
263
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
264
|
+
runId: string,
|
|
265
|
+
taskId: string,
|
|
266
|
+
): string | null {
|
|
267
|
+
return orchRunsById.get(runId)?.tasksById.get(taskId)?.lastPhaseOutput ?? null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Full ordered event timeline for one run — the rich drill-in source (F8). */
|
|
271
|
+
export function getOrchRunEvents(
|
|
272
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
273
|
+
runId: string,
|
|
274
|
+
): OrchestrationEvent[] {
|
|
275
|
+
return [...(orchRunsById.get(runId)?.eventLog ?? [])]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Recovery iterators
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Tasks a restart must RE-QUEUE.
|
|
284
|
+
* `gated` is deliberately excluded — a gated task is re-armed in place
|
|
285
|
+
* (gate re-notified), never requeued.
|
|
286
|
+
*/
|
|
287
|
+
export function* nonTerminalOrchTasks(
|
|
288
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
289
|
+
): Generator<{ runId: string; taskId: string; state: "claimed" | "running" }> {
|
|
290
|
+
for (const run of orchRunsById.values()) {
|
|
291
|
+
if (run.status !== "running") continue
|
|
292
|
+
for (const task of run.tasksById.values()) {
|
|
293
|
+
if (task.state === "claimed" || task.state === "running") {
|
|
294
|
+
yield { runId: run.runId, taskId: task.taskId, state: task.state }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Tasks paused at a hard gate — re-armed (not requeued) by recoverOnStartup. */
|
|
301
|
+
export function* gatedOrchTasks(
|
|
302
|
+
orchRunsById: Map<string, OrchRunRecord>,
|
|
303
|
+
): Generator<{ runId: string; taskId: string; phaseIndex: number }> {
|
|
304
|
+
for (const run of orchRunsById.values()) {
|
|
305
|
+
if (run.status !== "running") continue
|
|
306
|
+
for (const task of run.tasksById.values()) {
|
|
307
|
+
if (task.state === "gated") {
|
|
308
|
+
yield { runId: run.runId, taskId: task.taskId, phaseIndex: task.phaseIndex }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
|
|
3
|
+
import type { PushEvent } from "./push/events"
|
|
4
|
+
import type { ShareEvent } from "./session-share/share-projection"
|
|
5
|
+
import type { StorageBackend } from "./storage/backend"
|
|
6
|
+
import {
|
|
7
|
+
appendPushEvent,
|
|
8
|
+
appendShareEvent,
|
|
9
|
+
appendTunnelEvent,
|
|
10
|
+
getShareEvents,
|
|
11
|
+
getTunnelEvents,
|
|
12
|
+
listTunnelChats,
|
|
13
|
+
loadPushEvents,
|
|
14
|
+
loadShareEvents,
|
|
15
|
+
loadTunnelEvents,
|
|
16
|
+
type PeripheralEventsDeps,
|
|
17
|
+
} from "./event-store-peripheral-events.adapter"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Minimal in-memory StorageBackend stub. */
|
|
24
|
+
function makeStorage(files: Map<string, string> = new Map()): StorageBackend {
|
|
25
|
+
return {
|
|
26
|
+
mkdir: async () => {},
|
|
27
|
+
exists: async (p) => files.has(p),
|
|
28
|
+
existsSync: (p) => files.has(p),
|
|
29
|
+
size: async (p) => files.get(p)?.length ?? 0,
|
|
30
|
+
readText: async (p) => files.get(p) ?? "",
|
|
31
|
+
readTextSync: (p) => files.get(p) ?? "",
|
|
32
|
+
writeText: async (p, v) => { files.set(p, v) },
|
|
33
|
+
appendText: async (p, v) => { files.set(p, (files.get(p) ?? "") + v) },
|
|
34
|
+
rename: async () => {},
|
|
35
|
+
remove: async () => {},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeWriteChainRef() {
|
|
40
|
+
let chain = Promise.resolve()
|
|
41
|
+
return {
|
|
42
|
+
getWriteChain: () => chain,
|
|
43
|
+
setWriteChain: (p: Promise<void>) => { chain = p },
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeDeps(overrides: Partial<PeripheralEventsDeps> = {}): PeripheralEventsDeps {
|
|
48
|
+
const wc = makeWriteChainRef()
|
|
49
|
+
return {
|
|
50
|
+
storage: makeStorage(),
|
|
51
|
+
tunnelLogPath: "/data/tunnels.jsonl",
|
|
52
|
+
sharesLogPath: "/data/shares.jsonl",
|
|
53
|
+
pushLogPath: "/data/push.jsonl",
|
|
54
|
+
tunnelEventsByChatId: new Map(),
|
|
55
|
+
shareEventsAll: [],
|
|
56
|
+
getWriteChain: wc.getWriteChain,
|
|
57
|
+
setWriteChain: wc.setWriteChain,
|
|
58
|
+
...overrides,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeTunnelEvent(chatId = "chat-1"): CloudflareTunnelEvent {
|
|
63
|
+
return { type: "tunnel_started", chatId, tunnelId: "t-1", url: "https://ex.trycloudflare.com", timestamp: 1000 } as unknown as CloudflareTunnelEvent
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeShareEvent(chatId = "chat-1"): ShareEvent {
|
|
67
|
+
return { type: "share_minted", chatId, shareId: "s-1", token: "tok", expiresAt: null, timestamp: 1000 } as unknown as ShareEvent
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makePushEvent(): PushEvent {
|
|
71
|
+
return { type: "push_device_registered", deviceId: "d-1", pushToken: "pt-1", platform: "web", timestamp: 1000 } as unknown as PushEvent
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Tunnel tests
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
describe("getTunnelEvents", () => {
|
|
79
|
+
test("returns empty array for unknown chatId", () => {
|
|
80
|
+
const deps = makeDeps()
|
|
81
|
+
expect(getTunnelEvents(deps, "no-chat")).toEqual([])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("returns list for known chatId", () => {
|
|
85
|
+
const ev = makeTunnelEvent("chat-a")
|
|
86
|
+
const tunnelEventsByChatId = new Map([["chat-a", [ev]]])
|
|
87
|
+
const deps = makeDeps({ tunnelEventsByChatId })
|
|
88
|
+
expect(getTunnelEvents(deps, "chat-a")).toEqual([ev])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("returns a copy (not the original array)", () => {
|
|
92
|
+
const ev = makeTunnelEvent("chat-a")
|
|
93
|
+
const inner: CloudflareTunnelEvent[] = [ev]
|
|
94
|
+
const tunnelEventsByChatId = new Map([["chat-a", inner]])
|
|
95
|
+
const deps = makeDeps({ tunnelEventsByChatId })
|
|
96
|
+
const result = getTunnelEvents(deps, "chat-a")
|
|
97
|
+
expect(result).toEqual([ev])
|
|
98
|
+
expect(result).not.toBe(inner)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("listTunnelChats", () => {
|
|
103
|
+
test("returns empty array when no chats", () => {
|
|
104
|
+
const deps = makeDeps()
|
|
105
|
+
expect(listTunnelChats(deps)).toEqual([])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("returns all chatIds with tunnel events", () => {
|
|
109
|
+
const tunnelEventsByChatId = new Map([
|
|
110
|
+
["c1", [makeTunnelEvent("c1")]],
|
|
111
|
+
["c2", [makeTunnelEvent("c2")]],
|
|
112
|
+
])
|
|
113
|
+
const deps = makeDeps({ tunnelEventsByChatId })
|
|
114
|
+
expect(listTunnelChats(deps)).toEqual(expect.arrayContaining(["c1", "c2"]))
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe("appendTunnelEvent", () => {
|
|
119
|
+
test("writes to tunnelLogPath and updates in-memory map", async () => {
|
|
120
|
+
const files = new Map([[ "/data/tunnels.jsonl", "" ]])
|
|
121
|
+
const storage = makeStorage(files)
|
|
122
|
+
const deps = makeDeps({ storage })
|
|
123
|
+
const ev = makeTunnelEvent("chat-1")
|
|
124
|
+
|
|
125
|
+
await appendTunnelEvent(deps, ev)
|
|
126
|
+
|
|
127
|
+
// Disk
|
|
128
|
+
const content = files.get("/data/tunnels.jsonl") ?? ""
|
|
129
|
+
expect(content.trim()).toBe(JSON.stringify(ev))
|
|
130
|
+
|
|
131
|
+
// In-memory map
|
|
132
|
+
expect(getTunnelEvents(deps, "chat-1")).toEqual([ev])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("chains multiple appends in order", async () => {
|
|
136
|
+
const files = new Map([["/data/tunnels.jsonl", ""]])
|
|
137
|
+
const storage = makeStorage(files)
|
|
138
|
+
const deps = makeDeps({ storage })
|
|
139
|
+
const ev1 = { ...makeTunnelEvent("chat-1"), tunnelId: "t-1" }
|
|
140
|
+
const ev2 = { ...makeTunnelEvent("chat-1"), tunnelId: "t-2" }
|
|
141
|
+
|
|
142
|
+
await Promise.all([appendTunnelEvent(deps, ev1), appendTunnelEvent(deps, ev2)])
|
|
143
|
+
|
|
144
|
+
const lines = (files.get("/data/tunnels.jsonl") ?? "").trim().split("\n")
|
|
145
|
+
expect(lines.length).toBe(2)
|
|
146
|
+
expect(JSON.parse(lines[0]!).tunnelId).toBe("t-1")
|
|
147
|
+
expect(JSON.parse(lines[1]!).tunnelId).toBe("t-2")
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe("loadTunnelEvents", () => {
|
|
152
|
+
test("populates in-memory map from disk", async () => {
|
|
153
|
+
const ev = makeTunnelEvent("chat-x")
|
|
154
|
+
const files = new Map([["/data/tunnels.jsonl", `${JSON.stringify(ev)}\n`]])
|
|
155
|
+
const storage = makeStorage(files)
|
|
156
|
+
const tunnelEventsByChatId = new Map<string, CloudflareTunnelEvent[]>()
|
|
157
|
+
const deps = makeDeps({ storage, tunnelEventsByChatId })
|
|
158
|
+
|
|
159
|
+
await loadTunnelEvents(deps)
|
|
160
|
+
|
|
161
|
+
expect(getTunnelEvents(deps, "chat-x")).toEqual([ev])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test("handles empty log file without errors", async () => {
|
|
165
|
+
const files = new Map([["/data/tunnels.jsonl", ""]])
|
|
166
|
+
const deps = makeDeps({ storage: makeStorage(files) })
|
|
167
|
+
await expect(loadTunnelEvents(deps)).resolves.toBeUndefined()
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Share tests
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
describe("getShareEvents", () => {
|
|
176
|
+
test("returns empty array by default", () => {
|
|
177
|
+
const deps = makeDeps()
|
|
178
|
+
expect(getShareEvents(deps)).toEqual([])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test("returns a copy of shareEventsAll", () => {
|
|
182
|
+
const ev = makeShareEvent()
|
|
183
|
+
const shareEventsAll: ShareEvent[] = [ev]
|
|
184
|
+
const deps = makeDeps({ shareEventsAll })
|
|
185
|
+
const result = getShareEvents(deps)
|
|
186
|
+
expect(result).toEqual([ev])
|
|
187
|
+
expect(result).not.toBe(shareEventsAll)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe("appendShareEvent", () => {
|
|
192
|
+
test("writes to sharesLogPath and pushes into shareEventsAll", async () => {
|
|
193
|
+
const files = new Map([["/data/shares.jsonl", ""]])
|
|
194
|
+
const storage = makeStorage(files)
|
|
195
|
+
const shareEventsAll: ShareEvent[] = []
|
|
196
|
+
const deps = makeDeps({ storage, shareEventsAll })
|
|
197
|
+
const ev = makeShareEvent("chat-2")
|
|
198
|
+
|
|
199
|
+
await appendShareEvent(deps, ev)
|
|
200
|
+
|
|
201
|
+
expect(files.get("/data/shares.jsonl")?.trim()).toBe(JSON.stringify(ev))
|
|
202
|
+
expect(shareEventsAll).toEqual([ev])
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe("loadShareEvents", () => {
|
|
207
|
+
test("fills shareEventsAll from disk", async () => {
|
|
208
|
+
const ev = makeShareEvent("chat-y")
|
|
209
|
+
const files = new Map([["/data/shares.jsonl", `${JSON.stringify(ev)}\n`]])
|
|
210
|
+
const storage = makeStorage(files)
|
|
211
|
+
const shareEventsAll: ShareEvent[] = []
|
|
212
|
+
const deps = makeDeps({ storage, shareEventsAll })
|
|
213
|
+
|
|
214
|
+
await loadShareEvents(deps)
|
|
215
|
+
|
|
216
|
+
expect(shareEventsAll).toEqual([ev])
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Push tests
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
describe("appendPushEvent", () => {
|
|
225
|
+
test("writes to pushLogPath (no in-memory state)", async () => {
|
|
226
|
+
const files = new Map([["/data/push.jsonl", ""]])
|
|
227
|
+
const storage = makeStorage(files)
|
|
228
|
+
const deps = makeDeps({ storage })
|
|
229
|
+
const ev = makePushEvent()
|
|
230
|
+
|
|
231
|
+
await appendPushEvent(deps, ev)
|
|
232
|
+
|
|
233
|
+
expect(files.get("/data/push.jsonl")?.trim()).toBe(JSON.stringify(ev))
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe("loadPushEvents", () => {
|
|
238
|
+
test("returns events from push log", async () => {
|
|
239
|
+
const ev = makePushEvent()
|
|
240
|
+
const files = new Map([["/data/push.jsonl", `${JSON.stringify(ev)}\n`]])
|
|
241
|
+
const deps = makeDeps({ storage: makeStorage(files) })
|
|
242
|
+
|
|
243
|
+
const result = await loadPushEvents(deps)
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual([ev])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test("returns empty array for empty log", async () => {
|
|
249
|
+
const files = new Map([["/data/push.jsonl", ""]])
|
|
250
|
+
const deps = makeDeps({ storage: makeStorage(files) })
|
|
251
|
+
const result = await loadPushEvents(deps)
|
|
252
|
+
expect(result).toEqual([])
|
|
253
|
+
})
|
|
254
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunnel, Share, and Push peripheral event operations extracted from
|
|
3
|
+
* event-store.ts. All IO is performed through the injected StorageBackend.
|
|
4
|
+
*
|
|
5
|
+
* This module is an adapter (`.adapter.ts`) because it performs disk IO.
|
|
6
|
+
* It must NOT import from event-store.ts (no circular deps).
|
|
7
|
+
*/
|
|
8
|
+
import type { StorageBackend } from "./storage/backend"
|
|
9
|
+
import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
|
|
10
|
+
import type { PushEvent } from "./push/events"
|
|
11
|
+
import type { ShareEvent } from "./session-share/share-projection"
|
|
12
|
+
import {
|
|
13
|
+
applyTunnelEventToMap,
|
|
14
|
+
loadPushEventsFromLog,
|
|
15
|
+
loadShareEventsFromLog,
|
|
16
|
+
loadTunnelEventsFromLog,
|
|
17
|
+
} from "./event-store-snapshot"
|
|
18
|
+
|
|
19
|
+
// ─── Deps interface ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface PeripheralEventsDeps {
|
|
22
|
+
readonly storage: StorageBackend
|
|
23
|
+
readonly tunnelLogPath: string
|
|
24
|
+
readonly sharesLogPath: string
|
|
25
|
+
readonly pushLogPath: string
|
|
26
|
+
readonly tunnelEventsByChatId: Map<string, CloudflareTunnelEvent[]>
|
|
27
|
+
/** Mutable share events array — methods push into this directly. */
|
|
28
|
+
readonly shareEventsAll: ShareEvent[]
|
|
29
|
+
/** Read the current write-chain promise. */
|
|
30
|
+
getWriteChain: () => Promise<void>
|
|
31
|
+
/** Replace the write-chain promise (called inside async chain links). */
|
|
32
|
+
setWriteChain: (p: Promise<void>) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Tunnel ────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export async function appendTunnelEvent(
|
|
38
|
+
deps: PeripheralEventsDeps,
|
|
39
|
+
event: CloudflareTunnelEvent,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const payload = `${JSON.stringify(event)}\n`
|
|
42
|
+
const chain = deps.getWriteChain().then(async () => {
|
|
43
|
+
await deps.storage.appendText(deps.tunnelLogPath, payload)
|
|
44
|
+
applyTunnelEventToMap(deps.tunnelEventsByChatId, event)
|
|
45
|
+
})
|
|
46
|
+
deps.setWriteChain(chain)
|
|
47
|
+
await chain
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getTunnelEvents(
|
|
51
|
+
deps: PeripheralEventsDeps,
|
|
52
|
+
chatId: string,
|
|
53
|
+
): CloudflareTunnelEvent[] {
|
|
54
|
+
const list = deps.tunnelEventsByChatId.get(chatId)
|
|
55
|
+
return list ? [...list] : []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listTunnelChats(deps: PeripheralEventsDeps): string[] {
|
|
59
|
+
return [...deps.tunnelEventsByChatId.keys()]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function loadTunnelEvents(deps: PeripheralEventsDeps): Promise<void> {
|
|
63
|
+
await loadTunnelEventsFromLog(deps.storage, deps.tunnelLogPath, deps.tunnelEventsByChatId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Share ─────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export async function appendShareEvent(
|
|
69
|
+
deps: PeripheralEventsDeps,
|
|
70
|
+
event: ShareEvent,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const payload = `${JSON.stringify(event)}\n`
|
|
73
|
+
const chain = deps.getWriteChain().then(async () => {
|
|
74
|
+
await deps.storage.appendText(deps.sharesLogPath, payload)
|
|
75
|
+
deps.shareEventsAll.push(event)
|
|
76
|
+
})
|
|
77
|
+
deps.setWriteChain(chain)
|
|
78
|
+
await chain
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getShareEvents(deps: PeripheralEventsDeps): ShareEvent[] {
|
|
82
|
+
return [...deps.shareEventsAll]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function loadShareEvents(deps: PeripheralEventsDeps): Promise<void> {
|
|
86
|
+
await loadShareEventsFromLog(deps.storage, deps.sharesLogPath, deps.shareEventsAll)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Push ──────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export async function appendPushEvent(
|
|
92
|
+
deps: PeripheralEventsDeps,
|
|
93
|
+
event: PushEvent,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const payload = `${JSON.stringify(event)}\n`
|
|
96
|
+
const chain = deps.getWriteChain().then(async () => {
|
|
97
|
+
await deps.storage.appendText(deps.pushLogPath, payload)
|
|
98
|
+
})
|
|
99
|
+
deps.setWriteChain(chain)
|
|
100
|
+
await chain
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function loadPushEvents(deps: PeripheralEventsDeps): Promise<PushEvent[]> {
|
|
104
|
+
return loadPushEventsFromLog(deps.storage, deps.pushLogPath)
|
|
105
|
+
}
|