let-them-talk 5.2.5 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7214
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
@@ -0,0 +1,3068 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const { createDefaultContractMetadata } = require('../agent-contracts');
6
+
7
+ const { createCanonicalEventLog } = require('../events/log');
8
+ const { createCanonicalHookState } = require('../events/hooks');
9
+ const { createStateIo } = require('./io');
10
+ const { createMessagesState } = require('./messages');
11
+ const { createAgentsState } = require('./agents');
12
+ const { createEvidenceState } = require('./evidence');
13
+ const { createDashboardQueries } = require('./dashboard-queries');
14
+ const {
15
+ DEFAULT_MARKDOWN_WORKSPACE_DIR_NAME,
16
+ exportMarkdownWorkspace,
17
+ } = require('./markdown-workspace');
18
+ const { createSessionsState } = require('./sessions');
19
+ const { createTasksWorkflowsState } = require('./tasks-workflows');
20
+ const {
21
+ CANONICAL_REPLAY_ERROR_CODES,
22
+ createCanonicalReplayError,
23
+ } = require('../events/replay');
24
+
25
+ function sanitizeBranchName(branchName) {
26
+ if (!branchName || branchName === 'main') return 'main';
27
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(branchName)) {
28
+ throw new Error('Invalid branch name');
29
+ }
30
+ return branchName;
31
+ }
32
+
33
+ function sanitizeScopedName(name, label = 'name', maxLength = 64) {
34
+ if (typeof name !== 'string' || name.length < 1 || name.length > maxLength || !/^[a-zA-Z0-9_-]+$/.test(name)) {
35
+ throw new Error(`Invalid ${label}`);
36
+ }
37
+ return name;
38
+ }
39
+
40
+ function createBranchPathResolvers(dataDir) {
41
+ const messagesFile = path.join(dataDir, 'messages.jsonl');
42
+ const historyFile = path.join(dataDir, 'history.jsonl');
43
+ const runtimeDir = path.join(dataDir, 'runtime');
44
+ const runtimeProjectionsDir = path.join(runtimeDir, 'projections');
45
+ const agentsFile = path.join(dataDir, 'agents.json');
46
+ const acksFile = path.join(dataDir, 'acks.json');
47
+ const tasksFile = path.join(dataDir, 'tasks.json');
48
+ const evidenceFile = path.join(dataDir, 'evidence.json');
49
+ const profilesFile = path.join(dataDir, 'profiles.json');
50
+ const workflowsFile = path.join(dataDir, 'workflows.json');
51
+ const branchesFile = path.join(dataDir, 'branches.json');
52
+ const readReceiptsFile = path.join(dataDir, 'read_receipts.json');
53
+ const permissionsFile = path.join(dataDir, 'permissions.json');
54
+ const configFile = path.join(dataDir, 'config.json');
55
+ const channelsFile = path.join(dataDir, 'channels.json');
56
+ const compressedFile = path.join(dataDir, 'compressed.json');
57
+ const workspacesDir = path.join(dataDir, 'workspaces');
58
+ const decisionsFile = path.join(dataDir, 'decisions.json');
59
+ const kbFile = path.join(dataDir, 'kb.json');
60
+ const reviewsFile = path.join(dataDir, 'reviews.json');
61
+ const dependenciesFile = path.join(dataDir, 'dependencies.json');
62
+ const votesFile = path.join(dataDir, 'votes.json');
63
+ const rulesFile = path.join(dataDir, 'rules.json');
64
+ const progressFile = path.join(dataDir, 'progress.json');
65
+
66
+ function getScopedBranchFile(branchName, mainFilePath, branchSuffix) {
67
+ const branch = sanitizeBranchName(branchName);
68
+ return branch === 'main'
69
+ ? mainFilePath
70
+ : path.join(dataDir, `branch-${branch}-${branchSuffix}`);
71
+ }
72
+
73
+ function getMessagesFile(branchName = 'main') {
74
+ return getScopedBranchFile(branchName, messagesFile, 'messages.jsonl');
75
+ }
76
+
77
+ function getHistoryFile(branchName = 'main') {
78
+ return getScopedBranchFile(branchName, historyFile, 'history.jsonl');
79
+ }
80
+
81
+ function getAcksFile(branchName = 'main') {
82
+ return getScopedBranchFile(branchName, acksFile, 'acks.json');
83
+ }
84
+
85
+ function getTasksFile(branchName = 'main') {
86
+ return getScopedBranchFile(branchName, tasksFile, 'tasks.json');
87
+ }
88
+
89
+ function getWorkflowsFile(branchName = 'main') {
90
+ return getScopedBranchFile(branchName, workflowsFile, 'workflows.json');
91
+ }
92
+
93
+ function getReadReceiptsFile(branchName = 'main') {
94
+ return getScopedBranchFile(branchName, readReceiptsFile, 'read_receipts.json');
95
+ }
96
+
97
+ function getConfigFile(branchName = 'main') {
98
+ return getScopedBranchFile(branchName, configFile, 'config.json');
99
+ }
100
+
101
+ function getChannelsFile(branchName = 'main') {
102
+ return getScopedBranchFile(branchName, channelsFile, 'channels.json');
103
+ }
104
+
105
+ function getCompressedFile(branchName = 'main') {
106
+ return getScopedBranchFile(branchName, compressedFile, 'compressed.json');
107
+ }
108
+
109
+ function getDecisionsFile(branchName = 'main') {
110
+ return getScopedBranchFile(branchName, decisionsFile, 'decisions.json');
111
+ }
112
+
113
+ function getKnowledgeBaseFile(branchName = 'main') {
114
+ return getScopedBranchFile(branchName, kbFile, 'kb.json');
115
+ }
116
+
117
+ function getReviewsFile(branchName = 'main') {
118
+ return getScopedBranchFile(branchName, reviewsFile, 'reviews.json');
119
+ }
120
+
121
+ function getDependenciesFile(branchName = 'main') {
122
+ return getScopedBranchFile(branchName, dependenciesFile, 'dependencies.json');
123
+ }
124
+
125
+ function getVotesFile(branchName = 'main') {
126
+ return getScopedBranchFile(branchName, votesFile, 'votes.json');
127
+ }
128
+
129
+ function getRulesFile(branchName = 'main') {
130
+ return getScopedBranchFile(branchName, rulesFile, 'rules.json');
131
+ }
132
+
133
+ function getProgressFile(branchName = 'main') {
134
+ return getScopedBranchFile(branchName, progressFile, 'progress.json');
135
+ }
136
+
137
+ function getWorkspacesDir(branchName = 'main') {
138
+ const branch = sanitizeBranchName(branchName);
139
+ return branch === 'main'
140
+ ? workspacesDir
141
+ : path.join(dataDir, `branch-${branch}-workspaces`);
142
+ }
143
+
144
+ function getWorkspaceFile(agentName, branchName = 'main') {
145
+ const agent = sanitizeScopedName(agentName, 'agent name', 20);
146
+ return path.join(getWorkspacesDir(branchName), `${agent}.json`);
147
+ }
148
+
149
+ function getConsumedFile(agentName, branchName = 'main') {
150
+ const agent = sanitizeScopedName(agentName, 'agent name');
151
+ const branch = sanitizeBranchName(branchName);
152
+ return branch === 'main'
153
+ ? path.join(dataDir, `consumed-${agent}.json`)
154
+ : path.join(dataDir, `branch-${branch}-consumed-${agent}.json`);
155
+ }
156
+
157
+ function getEvidenceFile(branchName = 'main') {
158
+ return getScopedBranchFile(branchName, evidenceFile, 'evidence.json');
159
+ }
160
+
161
+ function getChannelMessagesFile(channelName, branchName = 'main') {
162
+ const branch = sanitizeBranchName(branchName);
163
+ if (!channelName || channelName === 'general') return getMessagesFile(branch);
164
+ const channel = sanitizeScopedName(channelName, 'channel name');
165
+ return branch === 'main'
166
+ ? path.join(dataDir, `channel-${channel}-messages.jsonl`)
167
+ : path.join(dataDir, `branch-${branch}-channel-${channel}-messages.jsonl`);
168
+ }
169
+
170
+ function getChannelHistoryFile(channelName, branchName = 'main') {
171
+ const branch = sanitizeBranchName(branchName);
172
+ if (!channelName || channelName === 'general') return getHistoryFile(branch);
173
+ const channel = sanitizeScopedName(channelName, 'channel name');
174
+ return branch === 'main'
175
+ ? path.join(dataDir, `channel-${channel}-history.jsonl`)
176
+ : path.join(dataDir, `branch-${branch}-channel-${channel}-history.jsonl`);
177
+ }
178
+
179
+ function getMessageTargets(branchName = 'main') {
180
+ const branch = sanitizeBranchName(branchName);
181
+ return {
182
+ branch,
183
+ messageFile: getMessagesFile(branch),
184
+ historyFile: getHistoryFile(branch),
185
+ };
186
+ }
187
+
188
+ function getRuntimeBranchDir(branchName = 'main') {
189
+ return path.join(runtimeDir, 'branches', sanitizeBranchName(branchName));
190
+ }
191
+
192
+ function getBranchSessionsDir(branchName = 'main') {
193
+ return path.join(getRuntimeBranchDir(branchName), 'sessions');
194
+ }
195
+
196
+ function getBranchSessionFile(sessionId, branchName = 'main') {
197
+ const branch = sanitizeBranchName(branchName);
198
+ const normalizedSessionId = sanitizeScopedName(sessionId, 'session id', 128);
199
+ return path.join(getBranchSessionsDir(branch), `${normalizedSessionId}.json`);
200
+ }
201
+
202
+ function getSessionsIndexFile() {
203
+ return path.join(runtimeProjectionsDir, 'sessions-index.json');
204
+ }
205
+
206
+ function getBranchDashboardProjectionFile(branchName = 'main') {
207
+ return path.join(getRuntimeBranchDir(branchName), 'dashboard-query-projection.json');
208
+ }
209
+
210
+ return {
211
+ messagesFile,
212
+ historyFile,
213
+ runtimeDir,
214
+ runtimeProjectionsDir,
215
+ agentsFile,
216
+ acksFile,
217
+ tasksFile,
218
+ evidenceFile,
219
+ profilesFile,
220
+ workflowsFile,
221
+ branchesFile,
222
+ readReceiptsFile,
223
+ permissionsFile,
224
+ configFile,
225
+ channelsFile,
226
+ compressedFile,
227
+ workspacesDir,
228
+ decisionsFile,
229
+ kbFile,
230
+ reviewsFile,
231
+ dependenciesFile,
232
+ votesFile,
233
+ rulesFile,
234
+ progressFile,
235
+ getMessagesFile,
236
+ getHistoryFile,
237
+ getAcksFile,
238
+ getTasksFile,
239
+ getWorkflowsFile,
240
+ getReadReceiptsFile,
241
+ getConfigFile,
242
+ getChannelsFile,
243
+ getCompressedFile,
244
+ getDecisionsFile,
245
+ getKnowledgeBaseFile,
246
+ getReviewsFile,
247
+ getDependenciesFile,
248
+ getVotesFile,
249
+ getRulesFile,
250
+ getProgressFile,
251
+ getWorkspacesDir,
252
+ getWorkspaceFile,
253
+ getConsumedFile,
254
+ getEvidenceFile,
255
+ getChannelMessagesFile,
256
+ getChannelHistoryFile,
257
+ getMessageTargets,
258
+ getRuntimeBranchDir,
259
+ getBranchSessionsDir,
260
+ getBranchSessionFile,
261
+ getSessionsIndexFile,
262
+ getBranchDashboardProjectionFile,
263
+ };
264
+ }
265
+
266
+ function createLockingFileHelper(processPid) {
267
+ return function withFileLock(filePath, fn) {
268
+ const lockPath = filePath + '.lock';
269
+ const maxWait = 5000;
270
+ const start = Date.now();
271
+ let backoff = 1;
272
+ let locked = false;
273
+
274
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
275
+
276
+ while (Date.now() - start < maxWait) {
277
+ try {
278
+ fs.writeFileSync(lockPath, String(processPid), { flag: 'wx' });
279
+ locked = true;
280
+ break;
281
+ } catch {}
282
+
283
+ const waitStart = Date.now();
284
+ while (Date.now() - waitStart < backoff) {}
285
+ backoff = Math.min(backoff * 2, 500);
286
+ }
287
+
288
+ if (!locked) {
289
+ let activeLockError = null;
290
+ try {
291
+ const lockPid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
292
+ if (lockPid && lockPid !== processPid) {
293
+ try {
294
+ process.kill(lockPid, 0);
295
+ throw new Error(`File is locked by active process ${lockPid}: ${filePath}`);
296
+ } catch (error) {
297
+ if (error.code !== 'ESRCH') activeLockError = error;
298
+ }
299
+ }
300
+ } catch {}
301
+
302
+ if (activeLockError) throw activeLockError;
303
+
304
+ try { fs.unlinkSync(lockPath); } catch {}
305
+ fs.writeFileSync(lockPath, String(processPid), { flag: 'wx' });
306
+ locked = true;
307
+ }
308
+
309
+ try {
310
+ return fn();
311
+ } finally {
312
+ if (locked) {
313
+ try { fs.unlinkSync(lockPath); } catch {}
314
+ }
315
+ }
316
+ };
317
+ }
318
+
319
+ function cloneJsonValue(value) {
320
+ return value == null ? value : JSON.parse(JSON.stringify(value));
321
+ }
322
+
323
+ function createEvidenceId() {
324
+ return `evidence_${crypto.randomUUID()}`;
325
+ }
326
+
327
+ function createEvidenceReference(record, branch) {
328
+ return {
329
+ evidence_id: record.evidence_id,
330
+ branch_id: branch,
331
+ recorded_at: record.recorded_at,
332
+ recorded_by_session: record.recorded_by_session,
333
+ };
334
+ }
335
+
336
+ function buildVerificationProjection(record, evidenceRef) {
337
+ return {
338
+ evidence_ref: cloneJsonValue(evidenceRef),
339
+ summary: record.summary,
340
+ verification: record.verification,
341
+ files_changed: Array.isArray(record.files_changed) ? [...record.files_changed] : [],
342
+ confidence: record.confidence,
343
+ learnings: record.learnings || null,
344
+ verified_at: record.recorded_at,
345
+ verified_by: record.recorded_by || null,
346
+ recorded_by_session: record.recorded_by_session,
347
+ };
348
+ }
349
+
350
+ function projectEvidenceRecord(record, evidenceRef) {
351
+ return buildVerificationProjection(record, evidenceRef);
352
+ }
353
+
354
+ function findReadyWorkflowSteps(workflow) {
355
+ return workflow.steps.filter((step) => {
356
+ if (step.status !== 'pending') return false;
357
+ if (!Array.isArray(step.depends_on) || step.depends_on.length === 0) return true;
358
+ return step.depends_on.every((dependencyId) => {
359
+ const dependency = workflow.steps.find((entry) => entry.id === dependencyId);
360
+ return dependency && dependency.status === 'done';
361
+ });
362
+ });
363
+ }
364
+
365
+ function createCanonicalState(options = {}) {
366
+ const {
367
+ dataDir,
368
+ processPid = process.pid,
369
+ invalidateCache,
370
+ } = options;
371
+
372
+ const withFileLock = createLockingFileHelper(processPid);
373
+ const io = createStateIo({ dataDir, invalidateCache, withFileLock });
374
+ const branchPaths = createBranchPathResolvers(dataDir);
375
+ const {
376
+ messagesFile,
377
+ historyFile,
378
+ runtimeDir,
379
+ agentsFile,
380
+ acksFile,
381
+ tasksFile,
382
+ evidenceFile,
383
+ profilesFile,
384
+ workflowsFile,
385
+ branchesFile,
386
+ readReceiptsFile,
387
+ permissionsFile,
388
+ configFile,
389
+ channelsFile,
390
+ compressedFile,
391
+ workspacesDir,
392
+ decisionsFile,
393
+ kbFile,
394
+ reviewsFile,
395
+ dependenciesFile,
396
+ votesFile,
397
+ rulesFile,
398
+ progressFile,
399
+ getMessagesFile,
400
+ getHistoryFile,
401
+ getConsumedFile,
402
+ getEvidenceFile,
403
+ getChannelMessagesFile,
404
+ getChannelHistoryFile,
405
+ getMessageTargets,
406
+ getAcksFile,
407
+ getTasksFile,
408
+ getWorkflowsFile,
409
+ getReadReceiptsFile,
410
+ getConfigFile,
411
+ getChannelsFile,
412
+ getCompressedFile,
413
+ getDecisionsFile,
414
+ getKnowledgeBaseFile,
415
+ getReviewsFile,
416
+ getDependenciesFile,
417
+ getVotesFile,
418
+ getRulesFile,
419
+ getProgressFile,
420
+ getWorkspacesDir,
421
+ getWorkspaceFile,
422
+ getBranchSessionsDir,
423
+ getSessionsIndexFile,
424
+ getBranchDashboardProjectionFile,
425
+ } = branchPaths;
426
+
427
+ const messagesState = createMessagesState({ io });
428
+ const canonicalHooks = createCanonicalHookState({
429
+ dataDir,
430
+ withLock: withFileLock,
431
+ sanitizeBranchName,
432
+ });
433
+ const canonicalEventLog = createCanonicalEventLog({
434
+ dataDir,
435
+ withLock: withFileLock,
436
+ onCommitted: (event) => canonicalHooks.projectCommittedEvent(event),
437
+ sanitizeBranchName,
438
+ });
439
+ const agentsState = createAgentsState({
440
+ io,
441
+ agentsFile,
442
+ profilesFile,
443
+ heartbeatFile: (name) => path.join(dataDir, `heartbeat-${name}.json`),
444
+ withAgentsFileLock: (fn) => withFileLock(agentsFile, fn),
445
+ processPid,
446
+ });
447
+ const evidenceState = createEvidenceState({ io });
448
+ const sessionsState = createSessionsState({
449
+ io,
450
+ branchPaths,
451
+ canonicalEventLog,
452
+ });
453
+ const tasksWorkflowsState = createTasksWorkflowsState({
454
+ io,
455
+ tasksFile,
456
+ workflowsFile,
457
+ getTasksFile,
458
+ getWorkflowsFile,
459
+ });
460
+
461
+ function readJson(filePath, fallback) {
462
+ return io.readJsonFile(filePath, fallback);
463
+ }
464
+
465
+ function readArray(filePath) {
466
+ const value = readJson(filePath, []);
467
+ return Array.isArray(value) ? value : [];
468
+ }
469
+
470
+ function readObject(filePath) {
471
+ const value = readJson(filePath, {});
472
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
473
+ }
474
+
475
+ const dashboardQueries = createDashboardQueries({
476
+ dataDir,
477
+ readJson,
478
+ agentsFile,
479
+ profilesFile,
480
+ getTasksFile,
481
+ getWorkflowsFile,
482
+ workspacesDir,
483
+ getAcksFile,
484
+ getHistoryFile,
485
+ getChannelsFile,
486
+ getChannelHistoryFile,
487
+ getBranchDashboardProjectionFile,
488
+ sanitizeBranchName,
489
+ });
490
+
491
+ function listAgents() {
492
+ return dashboardQueries.listAgents();
493
+ }
494
+
495
+ function listProfiles() {
496
+ return dashboardQueries.listProfiles();
497
+ }
498
+
499
+ function resolveBranchViewParam(params) {
500
+ if (typeof params === 'string') {
501
+ return sanitizeBranchName(params || 'main');
502
+ }
503
+
504
+ if (params && typeof params === 'object' && !Array.isArray(params)) {
505
+ return sanitizeBranchName(params.branch || 'main');
506
+ }
507
+
508
+ return 'main';
509
+ }
510
+
511
+ function getBranchCacheKey(surface, branch) {
512
+ return `${surface}:${sanitizeBranchName(branch || 'main')}`;
513
+ }
514
+
515
+ function readBranchArray(getFilePath, params = {}) {
516
+ const branch = resolveBranchViewParam(params);
517
+ return readArray(getFilePath(branch));
518
+ }
519
+
520
+ function readBranchObject(getFilePath, params = {}) {
521
+ const branch = resolveBranchViewParam(params);
522
+ return readObject(getFilePath(branch));
523
+ }
524
+
525
+ function listTasks(params = {}) {
526
+ return dashboardQueries.listTasks(resolveBranchViewParam(params));
527
+ }
528
+
529
+ function listWorkflows(params = {}) {
530
+ return dashboardQueries.listWorkflows(resolveBranchViewParam(params));
531
+ }
532
+
533
+ function getPlanStatusView(params = {}) {
534
+ return dashboardQueries.getPlanStatusView({ branch: resolveBranchViewParam(params) });
535
+ }
536
+
537
+ function getPlanReportView(params = {}) {
538
+ return dashboardQueries.getPlanReportView({ branch: resolveBranchViewParam(params) });
539
+ }
540
+
541
+ function getConversationConfigView(params = {}) {
542
+ const branch = resolveBranchViewParam(params);
543
+ return cloneJsonValue(readObject(getConfigFile(branch)));
544
+ }
545
+
546
+ function getLatestSessionSummaryForAgent(params = {}) {
547
+ const branch = resolveBranchViewParam(params);
548
+ const agentName = sanitizeScopedName(params.agentName || params.agent, 'agent name', 20);
549
+ const summary = sessionsState.getLatestSessionSummaryForAgent(branch, agentName);
550
+ return summary ? cloneJsonValue(summary) : null;
551
+ }
552
+
553
+ function upsertProfile(params = {}) {
554
+ const name = sanitizeScopedName(params.name, 'agent name', 20);
555
+ const updatedAt = params.updatedAt || new Date().toISOString();
556
+
557
+ const profile = agentsState.updateProfile(name, (currentProfile) => {
558
+ if (Object.keys(currentProfile).length === 0) {
559
+ Object.assign(currentProfile, {
560
+ display_name: name,
561
+ bio: '',
562
+ role: '',
563
+ created_at: updatedAt,
564
+ }, createDefaultContractMetadata());
565
+ }
566
+
567
+ if (params.displayName !== undefined) {
568
+ currentProfile.display_name = String(params.displayName || name).substring(0, 30);
569
+ }
570
+
571
+ if (params.avatar !== undefined && params.avatar) {
572
+ currentProfile.avatar = params.avatar;
573
+ }
574
+
575
+ if (params.bio !== undefined) {
576
+ currentProfile.bio = String(params.bio || '').substring(0, 200);
577
+ }
578
+
579
+ if (params.role !== undefined) {
580
+ currentProfile.role = String(params.role || '').substring(0, 30);
581
+ }
582
+
583
+ if (Object.prototype.hasOwnProperty.call(params, 'archetype')) {
584
+ currentProfile.archetype = params.archetype || '';
585
+ }
586
+
587
+ if (Object.prototype.hasOwnProperty.call(params, 'skills')) {
588
+ currentProfile.skills = Array.isArray(params.skills) ? cloneJsonValue(params.skills) : [];
589
+ }
590
+
591
+ if (Object.prototype.hasOwnProperty.call(params, 'contractMode')) {
592
+ currentProfile.contract_mode = params.contractMode || '';
593
+ }
594
+
595
+ if (params.appearance && typeof params.appearance === 'object' && !Array.isArray(params.appearance)) {
596
+ currentProfile.appearance = Object.assign(currentProfile.appearance || {}, cloneJsonValue(params.appearance));
597
+ }
598
+
599
+ currentProfile.updated_at = updatedAt;
600
+ });
601
+
602
+ if (profile) {
603
+ appendCanonicalEvent({
604
+ type: 'profile.updated',
605
+ actorAgent: params.actorAgent || name,
606
+ sessionId: params.sessionId || null,
607
+ commandId: params.commandId || null,
608
+ causationId: params.causationId || null,
609
+ correlationId: params.correlationId || name,
610
+ payload: {
611
+ agent_name: name,
612
+ profile: cloneJsonValue(profile),
613
+ updated_at: updatedAt,
614
+ },
615
+ });
616
+ }
617
+
618
+ return profile ? cloneJsonValue(profile) : null;
619
+ }
620
+
621
+ function listRules(params = {}) {
622
+ return readBranchArray(getRulesFile, params);
623
+ }
624
+
625
+ function listDecisions(params = {}) {
626
+ return readBranchArray(getDecisionsFile, params);
627
+ }
628
+
629
+ function readKnowledgeBase(params = {}) {
630
+ return cloneJsonValue(readBranchObject(getKnowledgeBaseFile, params));
631
+ }
632
+
633
+ function listReviews(params = {}) {
634
+ return readBranchArray(getReviewsFile, params);
635
+ }
636
+
637
+ function listDependencies(params = {}) {
638
+ return readBranchArray(getDependenciesFile, params);
639
+ }
640
+
641
+ function listVotes(params = {}) {
642
+ return readBranchArray(getVotesFile, params);
643
+ }
644
+
645
+ function readProgress(params = {}) {
646
+ return cloneJsonValue(readBranchObject(getProgressFile, params));
647
+ }
648
+
649
+ function getProjectNotesView(params = {}) {
650
+ const branch = resolveBranchViewParam(params);
651
+ return {
652
+ knowledge_base: readKnowledgeBase({ branch }),
653
+ rules: listRules({ branch }),
654
+ progress: readProgress({ branch }),
655
+ };
656
+ }
657
+
658
+ function getTeamNotesView(params = {}) {
659
+ const branch = resolveBranchViewParam(params);
660
+ return {
661
+ decisions: listDecisions({ branch }),
662
+ reviews: listReviews({ branch }),
663
+ dependencies: listDependencies({ branch }),
664
+ votes: listVotes({ branch }),
665
+ };
666
+ }
667
+
668
+ function mutateBranchArraySurface(getFilePath, surface, mutator, params = {}) {
669
+ const branch = sanitizeBranchName(params.branch || 'main');
670
+ const filePath = getFilePath(branch);
671
+ return io.withLock(filePath, () => {
672
+ const entries = readArray(filePath);
673
+ const result = mutator(entries, branch);
674
+ io.writeJson(filePath, entries, {
675
+ cacheKey: getBranchCacheKey(surface, branch),
676
+ space: params.space === undefined ? 2 : params.space,
677
+ });
678
+ return result;
679
+ });
680
+ }
681
+
682
+ function mutateReviews(mutator, params = {}) {
683
+ return mutateBranchArraySurface(getReviewsFile, 'reviews', mutator, params);
684
+ }
685
+
686
+ function mutateDependencies(mutator, params = {}) {
687
+ return mutateBranchArraySurface(getDependenciesFile, 'dependencies', mutator, params);
688
+ }
689
+
690
+ function readWorkspace(agentName, params = {}) {
691
+ const agent = sanitizeScopedName(agentName, 'agent name', 20);
692
+ const branch = resolveBranchViewParam(params);
693
+ const filePath = getWorkspaceFile(agent, branch);
694
+ const data = readJson(filePath, {});
695
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
696
+ }
697
+
698
+ function listWorkspaces(params = {}) {
699
+ const branch = resolveBranchViewParam(params);
700
+ const directory = getWorkspacesDir(branch);
701
+ if (!fs.existsSync(directory)) return [];
702
+
703
+ return fs.readdirSync(directory)
704
+ .filter((fileName) => fileName.endsWith('.json'))
705
+ .map((fileName) => {
706
+ const agent = fileName.replace(/\.json$/i, '');
707
+ const data = readWorkspace(agent, { branch });
708
+ return {
709
+ agent,
710
+ data,
711
+ key_count: Object.keys(data).length,
712
+ };
713
+ })
714
+ .sort((left, right) => left.agent.localeCompare(right.agent));
715
+ }
716
+
717
+ function addRule(params = {}) {
718
+ const branch = sanitizeBranchName(params.branch || 'main');
719
+ const rulesFilePath = getRulesFile(branch);
720
+ const rule = cloneJsonValue(params.rule || null);
721
+ if (!rule || typeof rule !== 'object' || !rule.id) {
722
+ return { error: 'Rule payload is required.' };
723
+ }
724
+
725
+ let result = { error: 'Rule payload is required.' };
726
+ io.withLock(rulesFilePath, () => {
727
+ const rules = readArray(rulesFilePath);
728
+ rules.push(rule);
729
+ io.writeJson(rulesFilePath, rules, { cacheKey: getBranchCacheKey('rules', branch), space: 2 });
730
+
731
+ const event = appendCanonicalEvent({
732
+ type: 'rule.added',
733
+ branchId: branch,
734
+ actorAgent: params.actor || rule.created_by || 'system',
735
+ sessionId: params.sessionId || null,
736
+ commandId: params.commandId || null,
737
+ causationId: params.causationId || null,
738
+ correlationId: params.correlationId || rule.id,
739
+ payload: {
740
+ rule_id: rule.id,
741
+ text: rule.text || '',
742
+ category: rule.category || 'custom',
743
+ priority: rule.priority || 'normal',
744
+ created_by: rule.created_by || null,
745
+ created_at: rule.created_at || null,
746
+ active: rule.active !== false,
747
+ },
748
+ });
749
+
750
+ result = {
751
+ success: true,
752
+ rule: cloneJsonValue(rule),
753
+ rule_event_id: event.event_id,
754
+ };
755
+ });
756
+
757
+ return result;
758
+ }
759
+
760
+ function updateRule(params = {}) {
761
+ const branch = sanitizeBranchName(params.branch || 'main');
762
+ const rulesFilePath = getRulesFile(branch);
763
+ let result = { error: 'Rule not found' };
764
+
765
+ io.withLock(rulesFilePath, () => {
766
+ const rules = readArray(rulesFilePath);
767
+ const rule = rules.find((entry) => entry.id === params.ruleId);
768
+ if (!rule) return;
769
+
770
+ const previousActive = rule.active !== false;
771
+ let changed = false;
772
+ let activeChanged = false;
773
+
774
+ if (Object.prototype.hasOwnProperty.call(params, 'text')) {
775
+ rule.text = typeof params.text === 'string' ? params.text : rule.text;
776
+ changed = true;
777
+ }
778
+ if (Object.prototype.hasOwnProperty.call(params, 'category')) {
779
+ rule.category = params.category;
780
+ changed = true;
781
+ }
782
+ if (Object.prototype.hasOwnProperty.call(params, 'priority')) {
783
+ rule.priority = params.priority;
784
+ changed = true;
785
+ }
786
+ if (Object.prototype.hasOwnProperty.call(params, 'active')) {
787
+ const nextActive = params.active !== false;
788
+ activeChanged = previousActive !== nextActive;
789
+ rule.active = nextActive;
790
+ changed = true;
791
+ }
792
+
793
+ if (!changed) {
794
+ result = { success: true, rule: cloneJsonValue(rule), rule_event_id: null };
795
+ return;
796
+ }
797
+
798
+ const updatedAt = params.updatedAt || new Date().toISOString();
799
+ rule.updated_at = updatedAt;
800
+ io.writeJson(rulesFilePath, rules, { cacheKey: getBranchCacheKey('rules', branch), space: 2 });
801
+
802
+ let event = null;
803
+ if (activeChanged) {
804
+ event = appendCanonicalEvent({
805
+ type: 'rule.toggled',
806
+ branchId: branch,
807
+ actorAgent: params.actor || 'system',
808
+ sessionId: params.sessionId || null,
809
+ commandId: params.commandId || null,
810
+ causationId: params.causationId || null,
811
+ correlationId: params.correlationId || rule.id,
812
+ payload: {
813
+ rule_id: rule.id,
814
+ active: rule.active !== false,
815
+ previous_active: previousActive,
816
+ updated_at: updatedAt,
817
+ },
818
+ });
819
+ }
820
+
821
+ result = {
822
+ success: true,
823
+ rule: cloneJsonValue(rule),
824
+ rule_event_id: event ? event.event_id : null,
825
+ };
826
+ });
827
+
828
+ return result;
829
+ }
830
+
831
+ function toggleRule(params = {}) {
832
+ const branch = sanitizeBranchName(params.branch || 'main');
833
+ const rulesFilePath = getRulesFile(branch);
834
+ let result = { error: 'Rule not found' };
835
+
836
+ io.withLock(rulesFilePath, () => {
837
+ const rules = readArray(rulesFilePath);
838
+ const rule = rules.find((entry) => entry.id === params.ruleId);
839
+ if (!rule) return;
840
+
841
+ const previousActive = rule.active !== false;
842
+ rule.active = !previousActive;
843
+ rule.updated_at = params.updatedAt || new Date().toISOString();
844
+ io.writeJson(rulesFilePath, rules, { cacheKey: getBranchCacheKey('rules', branch), space: 2 });
845
+
846
+ const event = appendCanonicalEvent({
847
+ type: 'rule.toggled',
848
+ branchId: branch,
849
+ actorAgent: params.actor || 'system',
850
+ sessionId: params.sessionId || null,
851
+ commandId: params.commandId || null,
852
+ causationId: params.causationId || null,
853
+ correlationId: params.correlationId || rule.id,
854
+ payload: {
855
+ rule_id: rule.id,
856
+ active: rule.active !== false,
857
+ previous_active: previousActive,
858
+ updated_at: rule.updated_at,
859
+ },
860
+ });
861
+
862
+ result = {
863
+ success: true,
864
+ rule: cloneJsonValue(rule),
865
+ rule_event_id: event.event_id,
866
+ };
867
+ });
868
+
869
+ return result;
870
+ }
871
+
872
+ function removeRule(params = {}) {
873
+ const branch = sanitizeBranchName(params.branch || 'main');
874
+ const rulesFilePath = getRulesFile(branch);
875
+ let result = { error: 'Rule not found' };
876
+
877
+ io.withLock(rulesFilePath, () => {
878
+ const rules = readArray(rulesFilePath);
879
+ const index = rules.findIndex((entry) => entry.id === params.ruleId);
880
+ if (index === -1) return;
881
+
882
+ const removed = rules.splice(index, 1)[0];
883
+ io.writeJson(rulesFilePath, rules, { cacheKey: getBranchCacheKey('rules', branch), space: 2 });
884
+
885
+ const event = appendCanonicalEvent({
886
+ type: 'rule.removed',
887
+ branchId: branch,
888
+ actorAgent: params.actor || 'system',
889
+ sessionId: params.sessionId || null,
890
+ commandId: params.commandId || null,
891
+ causationId: params.causationId || null,
892
+ correlationId: params.correlationId || removed.id,
893
+ payload: {
894
+ rule_id: removed.id,
895
+ text: removed.text || '',
896
+ category: removed.category || 'custom',
897
+ priority: removed.priority || 'normal',
898
+ removed_by: params.actor || 'system',
899
+ removed_at: params.removedAt || new Date().toISOString(),
900
+ },
901
+ });
902
+
903
+ result = {
904
+ success: true,
905
+ rule: cloneJsonValue(removed),
906
+ rule_event_id: event.event_id,
907
+ };
908
+ });
909
+
910
+ return result;
911
+ }
912
+
913
+ function saveWorkspace(agentName, workspace, params = {}) {
914
+ const agent = sanitizeScopedName(agentName, 'agent name', 20);
915
+ const branch = sanitizeBranchName(params.branch || 'main');
916
+ const nextWorkspace = workspace && typeof workspace === 'object' && !Array.isArray(workspace)
917
+ ? cloneJsonValue(workspace)
918
+ : {};
919
+ const filePath = getWorkspaceFile(agent, branch);
920
+ const updatedAt = params.updatedAt || new Date().toISOString();
921
+
922
+ io.withLock(filePath, () => {
923
+ fs.mkdirSync(getWorkspacesDir(branch), { recursive: true, mode: 0o700 });
924
+ io.writeJson(filePath, nextWorkspace, { space: 2 });
925
+ });
926
+
927
+ const event = appendCanonicalEvent({
928
+ type: 'workspace.written',
929
+ branchId: branch,
930
+ actorAgent: params.actor || agent,
931
+ sessionId: params.sessionId || null,
932
+ commandId: params.commandId || null,
933
+ causationId: params.causationId || null,
934
+ correlationId: params.correlationId || agent,
935
+ payload: {
936
+ agent_name: agent,
937
+ key: typeof params.key === 'string' ? params.key : null,
938
+ keys: Array.isArray(params.keys) ? [...params.keys] : null,
939
+ key_count: Object.keys(nextWorkspace).length,
940
+ updated_at: updatedAt,
941
+ },
942
+ });
943
+
944
+ return {
945
+ workspace: cloneJsonValue(nextWorkspace),
946
+ event,
947
+ };
948
+ }
949
+
950
+ function logDecision(params = {}) {
951
+ const branch = sanitizeBranchName(params.branch || 'main');
952
+ const decisionsFilePath = getDecisionsFile(branch);
953
+ const entry = cloneJsonValue(params.entry || null);
954
+ if (!entry || typeof entry !== 'object' || !entry.id) {
955
+ return { error: 'Decision payload is required.' };
956
+ }
957
+
958
+ let result = { error: 'Decision payload is required.' };
959
+ io.withLock(decisionsFilePath, () => {
960
+ const decisions = readArray(decisionsFilePath);
961
+ decisions.push(entry);
962
+ if (Number.isInteger(params.maxEntries) && params.maxEntries > 0 && decisions.length > params.maxEntries) {
963
+ decisions.splice(0, decisions.length - params.maxEntries);
964
+ }
965
+ io.writeJson(decisionsFilePath, decisions, { cacheKey: getBranchCacheKey('decisions', branch), space: 2 });
966
+
967
+ const event = appendCanonicalEvent({
968
+ type: 'decision.logged',
969
+ branchId: branch,
970
+ actorAgent: params.actor || entry.decided_by || 'system',
971
+ sessionId: params.sessionId || null,
972
+ commandId: params.commandId || null,
973
+ causationId: params.causationId || null,
974
+ correlationId: params.correlationId || entry.id,
975
+ payload: {
976
+ decision_id: entry.id,
977
+ decision: entry.decision || '',
978
+ topic: entry.topic || 'general',
979
+ decided_by: entry.decided_by || null,
980
+ decided_at: entry.decided_at || null,
981
+ },
982
+ });
983
+
984
+ result = {
985
+ success: true,
986
+ entry: cloneJsonValue(entry),
987
+ decision_event_id: event.event_id,
988
+ };
989
+ });
990
+
991
+ return result;
992
+ }
993
+
994
+ function writeKnowledgeBaseEntry(params = {}) {
995
+ const branch = sanitizeBranchName(params.branch || 'main');
996
+ const kbFilePath = getKnowledgeBaseFile(branch);
997
+ const key = typeof params.key === 'string' ? params.key : '';
998
+ const value = cloneJsonValue(params.value || null);
999
+ if (!key || !value || typeof value !== 'object' || Array.isArray(value)) {
1000
+ return { error: 'Knowledge base entry is required.' };
1001
+ }
1002
+
1003
+ let result = { error: 'Knowledge base entry is required.' };
1004
+ io.withLock(kbFilePath, () => {
1005
+ const kb = readObject(kbFilePath);
1006
+ kb[key] = value;
1007
+ if (Number.isInteger(params.maxEntries) && params.maxEntries > 0 && Object.keys(kb).length > params.maxEntries) {
1008
+ result = { error: `Knowledge base full (max ${params.maxEntries} keys)` };
1009
+ return;
1010
+ }
1011
+
1012
+ io.writeJson(kbFilePath, kb, { cacheKey: getBranchCacheKey('kb', branch), space: 2 });
1013
+
1014
+ const event = appendCanonicalEvent({
1015
+ type: 'kb.written',
1016
+ branchId: branch,
1017
+ actorAgent: params.actor || value.updated_by || 'system',
1018
+ sessionId: params.sessionId || null,
1019
+ commandId: params.commandId || null,
1020
+ causationId: params.causationId || null,
1021
+ correlationId: params.correlationId || key,
1022
+ payload: {
1023
+ key,
1024
+ updated_by: value.updated_by || null,
1025
+ updated_at: value.updated_at || null,
1026
+ content_size: typeof value.content === 'string' ? Buffer.byteLength(value.content, 'utf8') : null,
1027
+ },
1028
+ });
1029
+
1030
+ result = {
1031
+ success: true,
1032
+ key,
1033
+ entry: cloneJsonValue(value),
1034
+ total_keys: Object.keys(kb).length,
1035
+ kb_event_id: event.event_id,
1036
+ };
1037
+ });
1038
+
1039
+ return result;
1040
+ }
1041
+
1042
+ function updateProgressRecord(params = {}) {
1043
+ const branch = sanitizeBranchName(params.branch || 'main');
1044
+ const progressFilePath = getProgressFile(branch);
1045
+ const feature = typeof params.feature === 'string' ? params.feature : '';
1046
+ const value = cloneJsonValue(params.value || null);
1047
+ if (!feature || !value || typeof value !== 'object' || Array.isArray(value)) {
1048
+ return { error: 'Progress payload is required.' };
1049
+ }
1050
+
1051
+ let result = { error: 'Progress payload is required.' };
1052
+ io.withLock(progressFilePath, () => {
1053
+ const progress = readObject(progressFilePath);
1054
+ progress[feature] = value;
1055
+ io.writeJson(progressFilePath, progress, { cacheKey: getBranchCacheKey('progress', branch), space: 2 });
1056
+
1057
+ const event = appendCanonicalEvent({
1058
+ type: 'progress.updated',
1059
+ branchId: branch,
1060
+ actorAgent: params.actor || value.updated_by || 'system',
1061
+ sessionId: params.sessionId || null,
1062
+ commandId: params.commandId || null,
1063
+ causationId: params.causationId || null,
1064
+ correlationId: params.correlationId || feature,
1065
+ payload: {
1066
+ feature,
1067
+ percent: value.percent,
1068
+ notes: value.notes || '',
1069
+ updated_by: value.updated_by || null,
1070
+ updated_at: value.updated_at || null,
1071
+ },
1072
+ });
1073
+
1074
+ result = {
1075
+ success: true,
1076
+ feature,
1077
+ entry: cloneJsonValue(value),
1078
+ progress_event_id: event.event_id,
1079
+ };
1080
+ });
1081
+
1082
+ return result;
1083
+ }
1084
+
1085
+ function createVote(params = {}) {
1086
+ const branch = sanitizeBranchName(params.branch || 'main');
1087
+ const votesFilePath = getVotesFile(branch);
1088
+ const vote = cloneJsonValue(params.vote || null);
1089
+ if (!vote || typeof vote !== 'object' || !vote.id) {
1090
+ return { error: 'Vote payload is required.' };
1091
+ }
1092
+
1093
+ let result = { error: 'Vote payload is required.' };
1094
+ io.withLock(votesFilePath, () => {
1095
+ const votes = readArray(votesFilePath);
1096
+ if (Number.isInteger(params.maxEntries) && params.maxEntries > 0 && votes.length >= params.maxEntries) {
1097
+ result = { error: `Vote limit reached (max ${params.maxEntries}).` };
1098
+ return;
1099
+ }
1100
+
1101
+ votes.push(vote);
1102
+ io.writeJson(votesFilePath, votes, { cacheKey: getBranchCacheKey('votes', branch), space: 2 });
1103
+
1104
+ const event = appendCanonicalEvent({
1105
+ type: 'vote.called',
1106
+ branchId: branch,
1107
+ actorAgent: params.actor || vote.created_by || 'system',
1108
+ sessionId: params.sessionId || null,
1109
+ commandId: params.commandId || null,
1110
+ causationId: params.causationId || null,
1111
+ correlationId: params.correlationId || vote.id,
1112
+ payload: {
1113
+ vote_id: vote.id,
1114
+ question: vote.question || '',
1115
+ options: Array.isArray(vote.options) ? [...vote.options] : [],
1116
+ created_by: vote.created_by || null,
1117
+ created_at: vote.created_at || null,
1118
+ status: vote.status || null,
1119
+ },
1120
+ });
1121
+
1122
+ result = {
1123
+ success: true,
1124
+ vote: cloneJsonValue(vote),
1125
+ vote_event_id: event.event_id,
1126
+ };
1127
+ });
1128
+
1129
+ return result;
1130
+ }
1131
+
1132
+ function castVote(params = {}) {
1133
+ const branch = sanitizeBranchName(params.branch || 'main');
1134
+ const votesFilePath = getVotesFile(branch);
1135
+ let result = { error: 'Vote not found' };
1136
+
1137
+ io.withLock(votesFilePath, () => {
1138
+ const votes = readArray(votesFilePath);
1139
+ const vote = votes.find((entry) => entry.id === params.voteId);
1140
+ if (!vote) return;
1141
+ if (vote.status !== 'open') {
1142
+ result = { error: 'Vote is already closed.' };
1143
+ return;
1144
+ }
1145
+ if (!Array.isArray(vote.options) || !vote.options.includes(params.choice)) {
1146
+ result = { error: `Invalid choice. Options: ${(vote.options || []).join(', ')}` };
1147
+ return;
1148
+ }
1149
+
1150
+ const votedAt = params.votedAt || new Date().toISOString();
1151
+ vote.votes = vote.votes && typeof vote.votes === 'object' && !Array.isArray(vote.votes) ? vote.votes : {};
1152
+ vote.votes[params.voter] = { choice: params.choice, voted_at: votedAt };
1153
+
1154
+ const castEvent = appendCanonicalEvent({
1155
+ type: 'vote.cast',
1156
+ branchId: branch,
1157
+ actorAgent: params.actor || params.voter || 'system',
1158
+ sessionId: params.sessionId || null,
1159
+ commandId: params.commandId || null,
1160
+ causationId: params.causationId || null,
1161
+ correlationId: params.correlationId || vote.id,
1162
+ payload: {
1163
+ vote_id: vote.id,
1164
+ question: vote.question || '',
1165
+ voter: params.voter || null,
1166
+ choice: params.choice,
1167
+ voted_at: votedAt,
1168
+ votes_cast: Object.keys(vote.votes).length,
1169
+ status: vote.status,
1170
+ },
1171
+ });
1172
+
1173
+ let resolvedEvent = null;
1174
+ const onlineAgents = Array.isArray(params.onlineAgents) ? params.onlineAgents : [];
1175
+ const allVoted = onlineAgents.length > 0 && onlineAgents.every((agentName) => vote.votes[agentName]);
1176
+ if (allVoted) {
1177
+ vote.status = 'closed';
1178
+ vote.closed_at = params.closedAt || new Date().toISOString();
1179
+ const results = {};
1180
+ for (const option of vote.options) results[option] = 0;
1181
+ for (const value of Object.values(vote.votes)) {
1182
+ if (value && Object.prototype.hasOwnProperty.call(results, value.choice)) results[value.choice] += 1;
1183
+ }
1184
+ vote.results = results;
1185
+ const winner = Object.entries(results).sort((left, right) => right[1] - left[1])[0] || null;
1186
+ resolvedEvent = appendCanonicalEvent({
1187
+ type: 'vote.resolved',
1188
+ branchId: branch,
1189
+ actorAgent: params.actor || params.voter || 'system',
1190
+ sessionId: params.sessionId || null,
1191
+ commandId: params.commandId || null,
1192
+ causationId: castEvent.event_id,
1193
+ correlationId: params.correlationId || vote.id,
1194
+ payload: {
1195
+ vote_id: vote.id,
1196
+ question: vote.question || '',
1197
+ results,
1198
+ winner: winner ? { choice: winner[0], votes: winner[1] } : null,
1199
+ closed_at: vote.closed_at,
1200
+ },
1201
+ });
1202
+ }
1203
+
1204
+ io.writeJson(votesFilePath, votes, { cacheKey: getBranchCacheKey('votes', branch), space: 2 });
1205
+
1206
+ result = {
1207
+ success: true,
1208
+ vote: cloneJsonValue(vote),
1209
+ vote_cast_event_id: castEvent.event_id,
1210
+ vote_resolved_event_id: resolvedEvent ? resolvedEvent.event_id : null,
1211
+ };
1212
+ });
1213
+
1214
+ return result;
1215
+ }
1216
+
1217
+ function deleteFile(filePath) {
1218
+ if (fs.existsSync(filePath)) {
1219
+ fs.unlinkSync(filePath);
1220
+ }
1221
+ }
1222
+
1223
+ function deleteDirectory(dirPath) {
1224
+ if (fs.existsSync(dirPath)) {
1225
+ fs.rmSync(dirPath, { recursive: true, force: true });
1226
+ }
1227
+ }
1228
+
1229
+ function clearConsumedFiles() {
1230
+ if (!fs.existsSync(dataDir)) return;
1231
+ for (const fileName of fs.readdirSync(dataDir)) {
1232
+ if ((fileName.startsWith('consumed-') || fileName.includes('-consumed-')) && fileName.endsWith('.json')) {
1233
+ deleteFile(path.join(dataDir, fileName));
1234
+ }
1235
+ }
1236
+ }
1237
+
1238
+ function deleteConsumedFilesForAgent(agentName) {
1239
+ const suffix = `consumed-${sanitizeScopedName(agentName, 'agent name')}.json`;
1240
+ if (!fs.existsSync(dataDir)) return;
1241
+ for (const fileName of fs.readdirSync(dataDir)) {
1242
+ if (fileName === suffix || fileName.endsWith(`-${suffix}`)) {
1243
+ deleteFile(path.join(dataDir, fileName));
1244
+ }
1245
+ }
1246
+ }
1247
+
1248
+ function readConsumedMessageIds(agentName, options = {}) {
1249
+ const branch = sanitizeBranchName(options.branch || 'main');
1250
+ const raw = readJson(getConsumedFile(agentName, branch), []);
1251
+ return new Set(
1252
+ (Array.isArray(raw) ? raw : [])
1253
+ .filter((value) => typeof value === 'string' && value.length > 0)
1254
+ );
1255
+ }
1256
+
1257
+ function writeConsumedMessageIds(agentName, ids, options = {}) {
1258
+ const branch = sanitizeBranchName(options.branch || 'main');
1259
+ const filePath = getConsumedFile(agentName, branch);
1260
+ const normalized = [...new Set((ids instanceof Set ? [...ids] : (Array.isArray(ids) ? ids : []))
1261
+ .filter((value) => typeof value === 'string' && value.length > 0))];
1262
+
1263
+ withFileLock(filePath, () => {
1264
+ if (normalized.length === 0) {
1265
+ deleteFile(filePath);
1266
+ return;
1267
+ }
1268
+
1269
+ io.writeJson(filePath, normalized, { space: 2 });
1270
+ });
1271
+
1272
+ return new Set(normalized);
1273
+ }
1274
+
1275
+ function listMarkdownBranches() {
1276
+ const branchRegistry = readObject(branchesFile);
1277
+ const branchNames = new Set(['main']);
1278
+ const runtimeBranchesDir = path.join(runtimeDir, 'branches');
1279
+
1280
+ for (const branchName of Object.keys(branchRegistry)) {
1281
+ try {
1282
+ branchNames.add(sanitizeBranchName(branchName));
1283
+ } catch {}
1284
+ }
1285
+
1286
+ if (fs.existsSync(runtimeBranchesDir)) {
1287
+ for (const entry of fs.readdirSync(runtimeBranchesDir, { withFileTypes: true })) {
1288
+ if (!entry.isDirectory()) continue;
1289
+ try {
1290
+ branchNames.add(sanitizeBranchName(entry.name));
1291
+ } catch {}
1292
+ }
1293
+ }
1294
+
1295
+ return [...branchNames]
1296
+ .sort((left, right) => {
1297
+ if (left === right) return 0;
1298
+ if (left === 'main') return -1;
1299
+ if (right === 'main') return 1;
1300
+ return left.localeCompare(right);
1301
+ })
1302
+ .map((branchName) => {
1303
+ const registryEntry = branchRegistry[branchName] && typeof branchRegistry[branchName] === 'object'
1304
+ ? branchRegistry[branchName]
1305
+ : {};
1306
+ const runtimeBranchDir = path.join(runtimeDir, 'branches', branchName);
1307
+ return {
1308
+ branch: branchName,
1309
+ created_at: registryEntry.created_at || null,
1310
+ created_by: registryEntry.created_by || null,
1311
+ forked_from: registryEntry.forked_from || null,
1312
+ fork_point: registryEntry.fork_point || null,
1313
+ message_count: Number.isInteger(registryEntry.message_count) ? registryEntry.message_count : null,
1314
+ runtime_present: fs.existsSync(runtimeBranchDir),
1315
+ listed_in_registry: Object.prototype.hasOwnProperty.call(branchRegistry, branchName),
1316
+ };
1317
+ });
1318
+ }
1319
+
1320
+ function getBranchEventSequence(branchName = 'main') {
1321
+ const branch = sanitizeBranchName(branchName);
1322
+ const head = canonicalEventLog.getEventsHead({
1323
+ stream: 'branch',
1324
+ branchId: branch,
1325
+ });
1326
+ return head && Number.isInteger(head.last_seq) && head.last_seq > 0 ? head.last_seq : null;
1327
+ }
1328
+
1329
+ function listBranchSessions(branchName = 'main') {
1330
+ return sessionsState.listBranchSessions(branchName);
1331
+ }
1332
+
1333
+ function getBranchSessionManifest(sessionId, branchName = 'main') {
1334
+ return sessionsState.readSessionManifest(sessionId, branchName);
1335
+ }
1336
+
1337
+ function listCompatibilityDecisions() {
1338
+ return listDecisions({ branch: 'main' });
1339
+ }
1340
+
1341
+ function listCompatibilityWorkspaces(params = {}) {
1342
+ return listWorkspaces(params);
1343
+ }
1344
+
1345
+ function getCompatibilityProjectNotesView() {
1346
+ return getProjectNotesView({ branch: 'main' });
1347
+ }
1348
+
1349
+ function getCompatibilityTeamNotesView() {
1350
+ return getTeamNotesView({ branch: 'main' });
1351
+ }
1352
+
1353
+ function exportMarkdownWorkspaceFiles(options = {}) {
1354
+ const projectRoot = options.projectRoot
1355
+ ? path.resolve(options.projectRoot)
1356
+ : path.resolve(dataDir, '..');
1357
+ const outputRoot = options.outputRoot
1358
+ ? path.resolve(options.outputRoot)
1359
+ : path.join(projectRoot, DEFAULT_MARKDOWN_WORKSPACE_DIR_NAME);
1360
+ const selectedBranches = Array.isArray(options.branches)
1361
+ ? listMarkdownBranches().filter((entry) => options.branches.includes(entry.branch))
1362
+ : null;
1363
+
1364
+ return exportMarkdownWorkspace({
1365
+ projectRoot,
1366
+ outputRoot,
1367
+ generatedAt: options.generatedAt,
1368
+ branches: selectedBranches,
1369
+ runtimeDataDir: dataDir,
1370
+ readModel: {
1371
+ listBranches: listMarkdownBranches,
1372
+ getBranchEventSequence,
1373
+ getConversationMessages: dashboardQueries.getConversationMessages,
1374
+ getChannelsView: dashboardQueries.getChannelsView,
1375
+ listBranchSessions,
1376
+ getBranchSessionManifest,
1377
+ readEvidence,
1378
+ listDecisions,
1379
+ listWorkspaces,
1380
+ getPlanStatusView: dashboardQueries.getPlanStatusView,
1381
+ getPlanReportView: dashboardQueries.getPlanReportView,
1382
+ getProjectNotesView,
1383
+ getTeamNotesView,
1384
+ },
1385
+ });
1386
+ }
1387
+
1388
+ function appendCanonicalEvent(params = {}) {
1389
+ return canonicalEventLog.appendEvent(params);
1390
+ }
1391
+
1392
+ function readHooks(params = {}) {
1393
+ return canonicalHooks.readHooks(params);
1394
+ }
1395
+
1396
+ function readBranchHooks(branchName = 'main', options = {}) {
1397
+ return canonicalHooks.readBranchHooks(branchName, options);
1398
+ }
1399
+
1400
+ function readRuntimeHooks(options = {}) {
1401
+ return canonicalHooks.readRuntimeHooks(options);
1402
+ }
1403
+
1404
+ function projectAgentRecord(name, agent) {
1405
+ if (!agent) return null;
1406
+
1407
+ const projected = {
1408
+ name,
1409
+ provider: agent.provider || null,
1410
+ branch: agent.branch || 'main',
1411
+ status: agent.status || null,
1412
+ pid: agent.pid || null,
1413
+ registered_at: agent.timestamp || agent.started_at || null,
1414
+ started_at: agent.started_at || agent.timestamp || null,
1415
+ last_activity: agent.last_activity || agent.timestamp || null,
1416
+ listening_since: agent.listening_since || null,
1417
+ last_listened_at: agent.last_listened_at || null,
1418
+ };
1419
+
1420
+ if (agent.runtime_descriptor && typeof agent.runtime_descriptor === 'object') {
1421
+ projected.runtime_descriptor = cloneJsonValue(agent.runtime_descriptor);
1422
+ }
1423
+
1424
+ if (Array.isArray(agent.capabilities)) {
1425
+ projected.capabilities = [...agent.capabilities];
1426
+ }
1427
+
1428
+ if (agent.bot_capability) {
1429
+ projected.bot_capability = agent.bot_capability;
1430
+ }
1431
+
1432
+ return projected;
1433
+ }
1434
+
1435
+ function appendCanonicalMessageSentEvent(message, options = {}) {
1436
+ return appendCanonicalEvent({
1437
+ type: 'message.sent',
1438
+ branchId: options.branch,
1439
+ actorAgent: options.actorAgent || message.from || 'system',
1440
+ sessionId: options.sessionId || null,
1441
+ commandId: options.commandId || null,
1442
+ causationId: options.causationId || null,
1443
+ correlationId: options.correlationId || null,
1444
+ payload: { message },
1445
+ });
1446
+ }
1447
+
1448
+ function appendCanonicalMessageCorrectedEvent(params = {}) {
1449
+ return appendCanonicalEvent({
1450
+ type: 'message.corrected',
1451
+ branchId: params.branch,
1452
+ actorAgent: params.actorAgent || 'system',
1453
+ sessionId: params.sessionId || null,
1454
+ commandId: params.commandId || null,
1455
+ causationId: params.causationId || null,
1456
+ correlationId: params.correlationId || params.messageId,
1457
+ payload: {
1458
+ message_id: params.messageId,
1459
+ content: params.content,
1460
+ edited_at: params.editedAt,
1461
+ max_edit_history: params.maxEditHistory,
1462
+ },
1463
+ });
1464
+ }
1465
+
1466
+ function appendCanonicalMessageRedactedEvent(params = {}) {
1467
+ return appendCanonicalEvent({
1468
+ type: 'message.redacted',
1469
+ branchId: params.branch,
1470
+ actorAgent: params.actorAgent || 'system',
1471
+ sessionId: params.sessionId || null,
1472
+ commandId: params.commandId || null,
1473
+ causationId: params.causationId || null,
1474
+ correlationId: params.correlationId || params.messageId,
1475
+ payload: {
1476
+ message_id: params.messageId,
1477
+ redacted_at: params.redactedAt,
1478
+ },
1479
+ });
1480
+ }
1481
+
1482
+ function listBranchChannelProjectionNames(branchName = 'main') {
1483
+ const branch = sanitizeBranchName(branchName || 'main');
1484
+ if (!fs.existsSync(dataDir)) return [];
1485
+
1486
+ const prefix = branch === 'main'
1487
+ ? 'channel-'
1488
+ : `branch-${branch}-channel-`;
1489
+ const names = new Set();
1490
+
1491
+ for (const fileName of fs.readdirSync(dataDir)) {
1492
+ const suffix = fileName.endsWith('-messages.jsonl')
1493
+ ? '-messages.jsonl'
1494
+ : (fileName.endsWith('-history.jsonl') ? '-history.jsonl' : null);
1495
+ if (!suffix || !fileName.startsWith(prefix)) continue;
1496
+
1497
+ const channelName = fileName.slice(prefix.length, -suffix.length);
1498
+ if (channelName) names.add(channelName);
1499
+ }
1500
+
1501
+ return [...names].sort();
1502
+ }
1503
+
1504
+ function getScopedMessageProjectionTargets(branchName = 'main') {
1505
+ const branch = sanitizeBranchName(branchName || 'main');
1506
+ const configuredChannels = Object.keys(readObject(getChannelsFile(branch)))
1507
+ .filter((channelName) => channelName && channelName !== 'general');
1508
+ const existingChannels = listBranchChannelProjectionNames(branch);
1509
+ const channelNames = [...new Set([...configuredChannels, ...existingChannels])].sort();
1510
+
1511
+ return {
1512
+ branch: getMessageTargets(branch),
1513
+ channels: Object.fromEntries(
1514
+ channelNames.map((channelName) => [channelName, {
1515
+ messageFile: getChannelMessagesFile(channelName, branch),
1516
+ historyFile: getChannelHistoryFile(channelName, branch),
1517
+ }])
1518
+ ),
1519
+ getChannelTargets(channelName) {
1520
+ return {
1521
+ messageFile: getChannelMessagesFile(channelName, branch),
1522
+ historyFile: getChannelHistoryFile(channelName, branch),
1523
+ };
1524
+ },
1525
+ };
1526
+ }
1527
+
1528
+ function readCanonicalMessageEvents(branchName = 'main') {
1529
+ const branch = sanitizeBranchName(branchName || 'main');
1530
+ return canonicalEventLog.readBranchEvents(branch, { typePrefix: 'message.' });
1531
+ }
1532
+
1533
+ function appendMessage(message, options = {}) {
1534
+ const targets = getMessageTargets(options.branch);
1535
+ appendCanonicalMessageSentEvent(message, { ...options, branch: targets.branch });
1536
+ return messagesState.appendConversationMessage(message, {
1537
+ messageFile: targets.messageFile,
1538
+ historyFile: targets.historyFile,
1539
+ });
1540
+ }
1541
+
1542
+ function appendScopedMessage(message, options = {}) {
1543
+ const branch = sanitizeBranchName(options.branch || 'main');
1544
+ const rawChannel = typeof options.channel === 'string' ? options.channel : (message && message.channel);
1545
+ const channel = rawChannel && rawChannel !== 'general'
1546
+ ? sanitizeScopedName(rawChannel, 'channel name')
1547
+ : null;
1548
+ const targets = channel
1549
+ ? {
1550
+ messageFile: getChannelMessagesFile(channel, branch),
1551
+ historyFile: getChannelHistoryFile(channel, branch),
1552
+ }
1553
+ : getMessageTargets(branch);
1554
+
1555
+ appendCanonicalMessageSentEvent(message, { ...options, branch });
1556
+ return messagesState.appendConversationMessage(message, {
1557
+ messageFile: targets.messageFile,
1558
+ historyFile: targets.historyFile,
1559
+ });
1560
+ }
1561
+
1562
+ function appendMessages(messages, options = {}) {
1563
+ return messages.map((message) => appendMessage(message, options));
1564
+ }
1565
+
1566
+ function assertMessageProjectionRebuildAuthority(branch, targets) {
1567
+ const eventFile = canonicalEventLog.getBranchEventsFile(branch);
1568
+ const branchTargets = targets && targets.branch ? targets.branch : targets;
1569
+ const channelTargets = targets && targets.channels && typeof targets.channels === 'object' && !Array.isArray(targets.channels)
1570
+ ? Object.values(targets.channels)
1571
+ : [];
1572
+ const existingProjectionFiles = [
1573
+ branchTargets.messageFile,
1574
+ branchTargets.historyFile,
1575
+ ...channelTargets.flatMap((channelTarget) => [channelTarget.messageFile, channelTarget.historyFile]),
1576
+ ].filter((filePath) => filePath && fs.existsSync(filePath));
1577
+
1578
+ if (existingProjectionFiles.length > 0 && !fs.existsSync(eventFile)) {
1579
+ throw createCanonicalReplayError(
1580
+ CANONICAL_REPLAY_ERROR_CODES.MISSING_CANONICAL_STREAM,
1581
+ `Canonical message projection rebuild refused legacy-only recovery for branch ${branch}: missing canonical stream ${eventFile} while compatibility projections still exist.`,
1582
+ {
1583
+ branch_id: branch,
1584
+ event_file: eventFile,
1585
+ projection_files: existingProjectionFiles,
1586
+ }
1587
+ );
1588
+ }
1589
+ }
1590
+
1591
+ function rebuildMessageProjections(options = {}) {
1592
+ const targets = getScopedMessageProjectionTargets(options.branch);
1593
+ assertMessageProjectionRebuildAuthority(targets.branch.branch, targets);
1594
+ const events = readCanonicalMessageEvents(targets.branch.branch);
1595
+ return messagesState.rebuildConversationProjectionsFromEvents(events, targets);
1596
+ }
1597
+
1598
+ function editMessage(params) {
1599
+ const branch = sanitizeBranchName(params.branch || 'main');
1600
+ const current = messagesState.getConversationMessageFromEvents(readCanonicalMessageEvents(branch), params.id);
1601
+ if (!current || !current.message) return null;
1602
+
1603
+ const editedAt = params.editedAt || new Date().toISOString();
1604
+ const event = appendCanonicalMessageCorrectedEvent({
1605
+ branch,
1606
+ actorAgent: params.actorAgent || params.actor || 'system',
1607
+ sessionId: params.sessionId || null,
1608
+ commandId: params.commandId || null,
1609
+ causationId: params.causationId || null,
1610
+ correlationId: params.correlationId || params.id,
1611
+ messageId: params.id,
1612
+ content: params.content,
1613
+ editedAt,
1614
+ maxEditHistory: params.maxEditHistory,
1615
+ });
1616
+
1617
+ rebuildMessageProjections({ branch });
1618
+ return { id: params.id, edited_at: editedAt, event_id: event.event_id };
1619
+ }
1620
+
1621
+ function deleteMessage(params) {
1622
+ const branch = sanitizeBranchName(params.branch || 'main');
1623
+ const current = messagesState.getConversationMessageFromEvents(readCanonicalMessageEvents(branch), params.id);
1624
+ if (!current || !current.message) {
1625
+ return { found: false, denied: false, from: null };
1626
+ }
1627
+
1628
+ const allowedFrom = Array.isArray(params.allowedFrom) ? params.allowedFrom : null;
1629
+ const messageFrom = current.message.from || null;
1630
+ if (allowedFrom && !allowedFrom.includes(messageFrom)) {
1631
+ return { found: true, denied: true, from: messageFrom };
1632
+ }
1633
+
1634
+ const redactedAt = params.redactedAt || new Date().toISOString();
1635
+ const event = appendCanonicalMessageRedactedEvent({
1636
+ branch,
1637
+ actorAgent: params.actorAgent || params.actor || 'system',
1638
+ sessionId: params.sessionId || null,
1639
+ commandId: params.commandId || null,
1640
+ causationId: params.causationId || null,
1641
+ correlationId: params.correlationId || params.id,
1642
+ messageId: params.id,
1643
+ redactedAt,
1644
+ });
1645
+
1646
+ rebuildMessageProjections({ branch });
1647
+ return { found: true, deleted: true, from: messageFrom, redacted_at: redactedAt, event_id: event.event_id };
1648
+ }
1649
+
1650
+ function registerApiAgent(params) {
1651
+ const { name, agent, profile, createProfileIfMissing = false } = params;
1652
+ const savedAgent = agentsState.setAgent(name, agent);
1653
+ let savedProfile = null;
1654
+
1655
+ if (profile) {
1656
+ savedProfile = agentsState.updateProfile(name, (currentProfile) => {
1657
+ if (createProfileIfMissing && Object.keys(currentProfile).length > 0) {
1658
+ return;
1659
+ }
1660
+ Object.assign(currentProfile, profile);
1661
+ });
1662
+ }
1663
+
1664
+ const event = appendCanonicalEvent({
1665
+ type: 'agent.registered',
1666
+ actorAgent: params.actorAgent || name,
1667
+ sessionId: params.sessionId || null,
1668
+ commandId: params.commandId || null,
1669
+ causationId: params.causationId || null,
1670
+ correlationId: params.correlationId || name,
1671
+ payload: {
1672
+ agent_name: name,
1673
+ agent: projectAgentRecord(name, savedAgent),
1674
+ reason: params.reason || 'api_register',
1675
+ },
1676
+ });
1677
+
1678
+ return { agent: savedAgent, profile: savedProfile, event };
1679
+ }
1680
+
1681
+ function unregisterApiAgent(name, options = {}) {
1682
+ const { removeConsumed = true, removeProfile = true } = options;
1683
+ const removed = agentsState.removeAgent(name);
1684
+ if (removeProfile) agentsState.deleteProfile(name);
1685
+ if (removeConsumed) deleteConsumedFilesForAgent(name);
1686
+
1687
+ const event = removed
1688
+ ? appendCanonicalEvent({
1689
+ type: 'agent.unregistered',
1690
+ actorAgent: options.actorAgent || name,
1691
+ sessionId: options.sessionId || null,
1692
+ commandId: options.commandId || null,
1693
+ causationId: options.causationId || null,
1694
+ correlationId: options.correlationId || name,
1695
+ payload: {
1696
+ agent_name: name,
1697
+ agent: projectAgentRecord(name, removed),
1698
+ reason: options.reason || 'api_unregister',
1699
+ },
1700
+ })
1701
+ : null;
1702
+
1703
+ return { removed, event };
1704
+ }
1705
+
1706
+ function recordAgentHeartbeat(name, options = {}) {
1707
+ const updated = agentsState.readAgent(name);
1708
+ if (!updated) return null;
1709
+
1710
+ const heartbeat = agentsState.touchHeartbeat(name, options.at);
1711
+ const event = appendCanonicalEvent({
1712
+ type: 'agent.heartbeat_recorded',
1713
+ actorAgent: options.actorAgent || name,
1714
+ sessionId: options.sessionId || null,
1715
+ commandId: options.commandId || null,
1716
+ causationId: options.causationId || null,
1717
+ correlationId: options.correlationId || name,
1718
+ payload: {
1719
+ agent_name: name,
1720
+ agent: projectAgentRecord(name, {
1721
+ ...updated,
1722
+ last_activity: heartbeat && heartbeat.last_activity ? heartbeat.last_activity : (updated.last_activity || null),
1723
+ pid: heartbeat && heartbeat.pid ? heartbeat.pid : (updated.pid || null),
1724
+ }),
1725
+ heartbeat_at: heartbeat && heartbeat.last_activity ? heartbeat.last_activity : null,
1726
+ reason: options.reason || 'heartbeat',
1727
+ },
1728
+ });
1729
+
1730
+ return { agent: updated, heartbeat, event };
1731
+ }
1732
+
1733
+ function updateAgentHeartbeat(name) {
1734
+ const options = arguments[1] || {};
1735
+ const updated = agentsState.updateAgent(name, (agent) => {
1736
+ agent.last_activity = new Date().toISOString();
1737
+ agent.pid = processPid;
1738
+ });
1739
+
1740
+ if (updated) agentsState.touchHeartbeat(name);
1741
+ const event = updated
1742
+ ? appendCanonicalEvent({
1743
+ type: 'agent.heartbeat_recorded',
1744
+ actorAgent: options.actorAgent || name,
1745
+ sessionId: options.sessionId || null,
1746
+ commandId: options.commandId || null,
1747
+ causationId: options.causationId || null,
1748
+ correlationId: options.correlationId || name,
1749
+ payload: {
1750
+ agent_name: name,
1751
+ agent: projectAgentRecord(name, updated),
1752
+ heartbeat_at: updated.last_activity || null,
1753
+ reason: options.reason || 'heartbeat',
1754
+ },
1755
+ })
1756
+ : null;
1757
+
1758
+ return { agent: updated, event };
1759
+ }
1760
+
1761
+ function updateAgentStatus(name, status) {
1762
+ const options = arguments[2] || {};
1763
+ const previous = agentsState.readAgent(name);
1764
+ const updated = agentsState.updateAgent(name, (agent) => {
1765
+ agent.status = status;
1766
+ agent.last_activity = new Date().toISOString();
1767
+ agent.pid = processPid;
1768
+ });
1769
+
1770
+ if (updated) agentsState.touchHeartbeat(name);
1771
+ const event = updated && (!previous || previous.status !== updated.status)
1772
+ ? appendCanonicalEvent({
1773
+ type: 'agent.status_updated',
1774
+ actorAgent: options.actorAgent || name,
1775
+ sessionId: options.sessionId || null,
1776
+ commandId: options.commandId || null,
1777
+ causationId: options.causationId || null,
1778
+ correlationId: options.correlationId || name,
1779
+ payload: {
1780
+ agent_name: name,
1781
+ agent: projectAgentRecord(name, updated),
1782
+ previous_status: previous ? previous.status || null : null,
1783
+ status: updated.status || null,
1784
+ reason: options.reason || 'status_update',
1785
+ },
1786
+ })
1787
+ : null;
1788
+
1789
+ return { agent: updated, event };
1790
+ }
1791
+
1792
+ function updateAgentBranch(name, branchName = 'main', options = {}) {
1793
+ const nextBranch = sanitizeBranchName(branchName);
1794
+ const previous = agentsState.readAgent(name);
1795
+ const updated = agentsState.setBranch(name, nextBranch, options.at);
1796
+ const event = updated && (!previous || previous.branch !== nextBranch || options.forceEvent)
1797
+ ? appendCanonicalEvent({
1798
+ type: 'agent.branch_assigned',
1799
+ actorAgent: options.actorAgent || name,
1800
+ sessionId: options.sessionId || null,
1801
+ commandId: options.commandId || null,
1802
+ causationId: options.causationId || null,
1803
+ correlationId: options.correlationId || name,
1804
+ payload: {
1805
+ agent_name: name,
1806
+ agent: projectAgentRecord(name, updated),
1807
+ previous_branch: previous ? previous.branch || null : null,
1808
+ branch: nextBranch,
1809
+ reason: options.reason || 'branch_assign',
1810
+ },
1811
+ })
1812
+ : null;
1813
+
1814
+ return { agent: updated, event };
1815
+ }
1816
+
1817
+ function setAgentListeningState(name, isListening, options = {}) {
1818
+ const previous = agentsState.readAgent(name);
1819
+ const updated = agentsState.setListeningState(name, isListening, options.at);
1820
+ const previousListening = !!(previous && previous.listening_since);
1821
+ const currentListening = !!(updated && updated.listening_since);
1822
+ const event = updated && (previousListening !== currentListening || options.forceEvent)
1823
+ ? appendCanonicalEvent({
1824
+ type: 'agent.listening_updated',
1825
+ actorAgent: options.actorAgent || name,
1826
+ sessionId: options.sessionId || null,
1827
+ commandId: options.commandId || null,
1828
+ causationId: options.causationId || null,
1829
+ correlationId: options.correlationId || name,
1830
+ payload: {
1831
+ agent_name: name,
1832
+ agent: projectAgentRecord(name, updated),
1833
+ is_listening: currentListening,
1834
+ previous_is_listening: previousListening,
1835
+ reason: options.reason || 'listening_update',
1836
+ },
1837
+ })
1838
+ : null;
1839
+
1840
+ return { agent: updated, event };
1841
+ }
1842
+
1843
+ function ensureAgentSession(params = {}) {
1844
+ const branchName = sanitizeBranchName(params.branchName || params.branch || 'main');
1845
+ const agentName = params.agentName;
1846
+ if (!agentName) throw new Error('ensureAgentSession requires agentName');
1847
+
1848
+ const existingSummary = params.sessionId
1849
+ ? sessionsState.getSessionSummary(params.sessionId, branchName)
1850
+ : sessionsState.getLatestSessionSummaryForAgent(branchName, agentName);
1851
+
1852
+ if (existingSummary && existingSummary.session_id && existingSummary.state === 'active') {
1853
+ const touched = sessionsState.touchSession({
1854
+ sessionId: existingSummary.session_id,
1855
+ branchName,
1856
+ at: params.at,
1857
+ heartbeat: !!params.heartbeat,
1858
+ });
1859
+ const session = touched && touched.session
1860
+ ? touched.session
1861
+ : sessionsState.readSessionManifest(existingSummary.session_id, branchName);
1862
+ return {
1863
+ session,
1864
+ created: false,
1865
+ resumed: false,
1866
+ touched: !!(touched && touched.updated),
1867
+ previous_state: null,
1868
+ };
1869
+ }
1870
+
1871
+ return sessionsState.activateSession({
1872
+ agentName,
1873
+ branchName,
1874
+ at: params.at,
1875
+ reason: params.reason,
1876
+ provider: params.provider,
1877
+ sessionId: params.sessionId,
1878
+ orphanedReason: params.orphanedReason,
1879
+ });
1880
+ }
1881
+
1882
+ function touchSession(params = {}) {
1883
+ return sessionsState.touchSession({
1884
+ sessionId: params.sessionId,
1885
+ branchName: sanitizeBranchName(params.branchName || params.branch || 'main'),
1886
+ at: params.at,
1887
+ heartbeat: !!params.heartbeat,
1888
+ });
1889
+ }
1890
+
1891
+ function transitionSession(params = {}) {
1892
+ return sessionsState.transitionSession({
1893
+ sessionId: params.sessionId,
1894
+ branchName: sanitizeBranchName(params.branchName || params.branch || 'main'),
1895
+ state: params.state,
1896
+ reason: params.reason,
1897
+ at: params.at,
1898
+ recoverySnapshotFile: params.recoverySnapshotFile,
1899
+ });
1900
+ }
1901
+
1902
+ function transitionLatestSessionForAgent(params = {}) {
1903
+ return sessionsState.transitionLatestSessionForAgent({
1904
+ agentName: params.agentName,
1905
+ branchName: sanitizeBranchName(params.branchName || params.branch || 'main'),
1906
+ state: params.state,
1907
+ reason: params.reason,
1908
+ at: params.at,
1909
+ recoverySnapshotFile: params.recoverySnapshotFile,
1910
+ });
1911
+ }
1912
+
1913
+ function readEvidence(branchName = 'main') {
1914
+ return evidenceState.readEvidenceStore(getEvidenceFile(branchName));
1915
+ }
1916
+
1917
+ function findEvidence(branchName = 'main', evidenceId) {
1918
+ return evidenceState.findEvidenceRecord(getEvidenceFile(branchName), evidenceId);
1919
+ }
1920
+
1921
+ function projectEvidence(branchName = 'main', evidenceRef) {
1922
+ if (!evidenceRef || !evidenceRef.evidence_id) return null;
1923
+ const branch = sanitizeBranchName(evidenceRef.branch_id || branchName || 'main');
1924
+ const record = findEvidence(branch, evidenceRef.evidence_id);
1925
+ if (!record) return null;
1926
+ return projectEvidenceRecord(record, createEvidenceReference(record, branch));
1927
+ }
1928
+
1929
+ function recordEvidence(params = {}) {
1930
+ const branch = sanitizeBranchName(params.branch || 'main');
1931
+ const summary = typeof params.summary === 'string' ? params.summary.trim() : '';
1932
+ const verification = typeof params.verification === 'string' ? params.verification.trim() : '';
1933
+ const filesChanged = Array.isArray(params.files_changed)
1934
+ ? params.files_changed.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim())
1935
+ : [];
1936
+ const confidence = params.confidence;
1937
+
1938
+ if (!summary) return { error: 'Evidence summary is required.' };
1939
+ if (!verification) return { error: 'Evidence verification is required.' };
1940
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 100) {
1941
+ return { error: 'Evidence confidence must be a number between 0 and 100.' };
1942
+ }
1943
+
1944
+ const recordedAt = params.recordedAt || new Date().toISOString();
1945
+ const record = {
1946
+ evidence_id: createEvidenceId(),
1947
+ subject_kind: params.subjectKind || 'completion',
1948
+ branch_id: branch,
1949
+ task_id: params.taskId || null,
1950
+ task_title: params.taskTitle || null,
1951
+ workflow_id: params.workflowId || null,
1952
+ workflow_name: params.workflowName || null,
1953
+ step_id: params.stepId || null,
1954
+ step_description: params.stepDescription || null,
1955
+ notes: params.notes || null,
1956
+ summary,
1957
+ verification,
1958
+ files_changed: filesChanged,
1959
+ confidence,
1960
+ learnings: params.learnings || null,
1961
+ flagged: !!params.flagged,
1962
+ flag_reason: params.flagReason || null,
1963
+ recorded_at: recordedAt,
1964
+ recorded_by: params.actor || 'system',
1965
+ recorded_by_session: params.sessionId || null,
1966
+ source_tool: params.sourceTool || null,
1967
+ };
1968
+
1969
+ evidenceState.mutateEvidence(getEvidenceFile(branch), (store) => {
1970
+ store.updated_at = recordedAt;
1971
+ store.records.push(record);
1972
+ }, { space: 2 });
1973
+
1974
+ const event = canonicalEventLog.appendEvent({
1975
+ type: 'evidence.recorded',
1976
+ branchId: branch,
1977
+ actorAgent: params.actor || 'system',
1978
+ sessionId: params.sessionId || null,
1979
+ commandId: params.commandId || null,
1980
+ causationId: params.causationId || null,
1981
+ correlationId: params.correlationId || null,
1982
+ payload: {
1983
+ evidence: record,
1984
+ },
1985
+ });
1986
+
1987
+ return {
1988
+ event,
1989
+ record,
1990
+ reference: createEvidenceReference(record, branch),
1991
+ };
1992
+ }
1993
+
1994
+ function createTask(params = {}) {
1995
+ const branch = sanitizeBranchName(params.branch || 'main');
1996
+ const task = cloneJsonValue(params.task || null);
1997
+ let result = { error: 'Task payload is required.' };
1998
+
1999
+ if (!task || typeof task !== 'object' || !task.id) {
2000
+ return result;
2001
+ }
2002
+
2003
+ tasksWorkflowsState.mutateTasks((tasks) => {
2004
+ if (tasks.length >= 1000) {
2005
+ result = { error: 'Task limit reached (max 1000). Complete or remove existing tasks first.' };
2006
+ return;
2007
+ }
2008
+
2009
+ tasks.push(task);
2010
+
2011
+ const event = appendCanonicalEvent({
2012
+ type: 'task.created',
2013
+ branchId: branch,
2014
+ actorAgent: params.actor || task.created_by || 'system',
2015
+ sessionId: params.sessionId || null,
2016
+ commandId: params.commandId || null,
2017
+ causationId: params.causationId || null,
2018
+ correlationId: params.correlationId || task.id,
2019
+ payload: {
2020
+ task_id: task.id,
2021
+ title: task.title,
2022
+ status: task.status,
2023
+ assignee: task.assignee || null,
2024
+ created_at: task.created_at || null,
2025
+ },
2026
+ });
2027
+
2028
+ result = {
2029
+ success: true,
2030
+ task: cloneJsonValue(task),
2031
+ task_event_id: event.event_id,
2032
+ };
2033
+ }, { branch, space: 2 });
2034
+
2035
+ return result;
2036
+ }
2037
+
2038
+ function updateTaskStatus(params) {
2039
+ const now = params.at || new Date().toISOString();
2040
+ const branch = sanitizeBranchName(params.branch || 'main');
2041
+ let result = { error: 'Task not found' };
2042
+
2043
+ tasksWorkflowsState.mutateTasks((tasks) => {
2044
+ const task = tasks.find((entry) => entry.id === params.taskId);
2045
+ if (!task) return;
2046
+
2047
+ if (params.requireUnassigned && task.assignee) {
2048
+ result = { error: 'Task already claimed.' };
2049
+ return;
2050
+ }
2051
+
2052
+ if (Object.prototype.hasOwnProperty.call(params, 'expectedAssignee')) {
2053
+ const expectedAssignee = params.expectedAssignee || null;
2054
+ const actualAssignee = task.assignee || null;
2055
+ if (expectedAssignee !== actualAssignee) {
2056
+ result = { error: 'Task assignee changed.' };
2057
+ return;
2058
+ }
2059
+ }
2060
+
2061
+ if (Array.isArray(params.expectedStatuses) && params.expectedStatuses.length > 0 && !params.expectedStatuses.includes(task.status)) {
2062
+ result = { error: 'Task status changed.' };
2063
+ return;
2064
+ }
2065
+
2066
+ if (params.status === 'done') {
2067
+ const evidence = recordEvidence({
2068
+ ...(params.evidence || {}),
2069
+ actor: params.actor || 'system',
2070
+ branch,
2071
+ commandId: params.commandId || null,
2072
+ correlationId: params.correlationId || params.taskId || null,
2073
+ recordedAt: now,
2074
+ sessionId: params.sessionId || null,
2075
+ subjectKind: 'task',
2076
+ sourceTool: params.sourceTool || 'update_task',
2077
+ taskId: task.id,
2078
+ taskTitle: task.title,
2079
+ });
2080
+ if (evidence.error) {
2081
+ result = evidence;
2082
+ return;
2083
+ }
2084
+
2085
+ task.status = params.status;
2086
+ task.updated_at = now;
2087
+ task.completed_at = now;
2088
+ task.completed_by = params.actor || null;
2089
+ task.completed_by_session = params.sessionId || null;
2090
+ task.evidence_ref = cloneJsonValue(evidence.reference);
2091
+ task.completion = buildVerificationProjection(evidence.record, evidence.reference);
2092
+ if (params.notes) {
2093
+ if (!Array.isArray(task.notes)) task.notes = [];
2094
+ task.notes.push({ by: params.actor || 'system', text: params.notes, at: now });
2095
+ }
2096
+
2097
+ const completionEvent = canonicalEventLog.appendEvent({
2098
+ type: 'task.completed',
2099
+ branchId: branch,
2100
+ actorAgent: params.actor || 'system',
2101
+ sessionId: params.sessionId || null,
2102
+ commandId: params.commandId || null,
2103
+ causationId: evidence.event.event_id,
2104
+ correlationId: params.correlationId || params.taskId || null,
2105
+ payload: {
2106
+ task_id: task.id,
2107
+ status: task.status,
2108
+ title: task.title,
2109
+ notes: params.notes || null,
2110
+ evidence_ref: cloneJsonValue(evidence.reference),
2111
+ completed_at: now,
2112
+ },
2113
+ });
2114
+
2115
+ result = {
2116
+ success: true,
2117
+ task_id: task.id,
2118
+ status: task.status,
2119
+ title: task.title,
2120
+ evidence_event_id: evidence.event.event_id,
2121
+ evidence_ref: cloneJsonValue(evidence.reference),
2122
+ task_event_id: completionEvent.event_id,
2123
+ };
2124
+ return;
2125
+ }
2126
+
2127
+ const previousStatus = task.status;
2128
+ const previousAssignee = task.assignee || null;
2129
+ task.status = params.status;
2130
+ task.updated_at = now;
2131
+ if (Object.prototype.hasOwnProperty.call(params, 'assignee')) {
2132
+ task.assignee = params.assignee || null;
2133
+ }
2134
+ if (params.trackAttemptAgent && params.actor) {
2135
+ if (!Array.isArray(task.attempt_agents)) task.attempt_agents = [];
2136
+ if (!task.attempt_agents.includes(params.actor)) task.attempt_agents.push(params.actor);
2137
+ }
2138
+ if (params.clearPolicySignal && task.policy_signal) {
2139
+ delete task.policy_signal;
2140
+ }
2141
+ if (Object.prototype.hasOwnProperty.call(params, 'policySignal')) {
2142
+ if (params.policySignal) {
2143
+ task.policy_signal = cloneJsonValue(params.policySignal);
2144
+ } else {
2145
+ delete task.policy_signal;
2146
+ }
2147
+ }
2148
+ if (params.clearEscalatedAt && task.escalated_at) {
2149
+ delete task.escalated_at;
2150
+ }
2151
+ if (Object.prototype.hasOwnProperty.call(params, 'escalatedAt')) {
2152
+ if (params.escalatedAt) {
2153
+ task.escalated_at = params.escalatedAt;
2154
+ } else {
2155
+ delete task.escalated_at;
2156
+ }
2157
+ }
2158
+ if (Object.prototype.hasOwnProperty.call(params, 'blockReason')) {
2159
+ if (params.blockReason) {
2160
+ task.block_reason = params.blockReason;
2161
+ } else {
2162
+ delete task.block_reason;
2163
+ }
2164
+ }
2165
+ if (params.notes) {
2166
+ if (!Array.isArray(task.notes)) task.notes = [];
2167
+ task.notes.push({ by: params.actor || 'Dashboard', text: params.notes, at: now });
2168
+ }
2169
+
2170
+ const eventType = task.status === 'in_progress'
2171
+ && task.assignee
2172
+ && task.assignee !== previousAssignee
2173
+ ? 'task.claimed'
2174
+ : 'task.updated';
2175
+ const event = appendCanonicalEvent({
2176
+ type: eventType,
2177
+ branchId: branch,
2178
+ actorAgent: params.actor || 'system',
2179
+ sessionId: params.sessionId || null,
2180
+ commandId: params.commandId || null,
2181
+ causationId: params.causationId || null,
2182
+ correlationId: params.correlationId || params.taskId || null,
2183
+ payload: {
2184
+ task_id: task.id,
2185
+ title: task.title,
2186
+ status: task.status,
2187
+ previous_status: previousStatus,
2188
+ assignee: task.assignee || null,
2189
+ previous_assignee: previousAssignee,
2190
+ notes: params.notes || null,
2191
+ block_reason: task.block_reason || null,
2192
+ escalated_at: task.escalated_at || null,
2193
+ policy_signal: task.policy_signal || null,
2194
+ updated_at: now,
2195
+ },
2196
+ });
2197
+
2198
+ result = {
2199
+ success: true,
2200
+ task_id: task.id,
2201
+ status: task.status,
2202
+ title: task.title,
2203
+ task: cloneJsonValue(task),
2204
+ task_event_id: event.event_id,
2205
+ };
2206
+ }, { branch, space: 2 });
2207
+
2208
+ return result;
2209
+ }
2210
+
2211
+ function createWorkflow(params = {}) {
2212
+ const branch = sanitizeBranchName(params.branch || 'main');
2213
+ const workflow = cloneJsonValue(params.workflow || null);
2214
+ let result = { error: 'Workflow payload is required.' };
2215
+
2216
+ if (!workflow || typeof workflow !== 'object' || !workflow.id) {
2217
+ return result;
2218
+ }
2219
+
2220
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2221
+ if (workflows.length >= 500) {
2222
+ result = { error: 'Workflow limit reached (max 500).' };
2223
+ return;
2224
+ }
2225
+
2226
+ if (!workflow.branch_id) {
2227
+ workflow.branch_id = branch;
2228
+ }
2229
+
2230
+ workflows.push(workflow);
2231
+
2232
+ const createdEvent = appendCanonicalEvent({
2233
+ type: 'workflow.created',
2234
+ branchId: branch,
2235
+ actorAgent: params.actor || workflow.created_by || 'system',
2236
+ sessionId: params.sessionId || null,
2237
+ commandId: params.commandId || null,
2238
+ causationId: params.causationId || null,
2239
+ correlationId: params.correlationId || workflow.id,
2240
+ payload: {
2241
+ workflow_id: workflow.id,
2242
+ workflow_name: workflow.name,
2243
+ status: workflow.status,
2244
+ autonomous: !!workflow.autonomous,
2245
+ parallel: !!workflow.parallel,
2246
+ created_at: workflow.created_at || null,
2247
+ started_step_ids: workflow.steps.filter((step) => step.status === 'in_progress').map((step) => step.id),
2248
+ },
2249
+ });
2250
+
2251
+ const startedStepEvents = workflow.steps
2252
+ .filter((step) => step.status === 'in_progress')
2253
+ .map((step) => appendCanonicalEvent({
2254
+ type: 'workflow.step_started',
2255
+ branchId: branch,
2256
+ actorAgent: params.actor || workflow.created_by || 'system',
2257
+ sessionId: params.sessionId || null,
2258
+ commandId: params.commandId || null,
2259
+ causationId: createdEvent.event_id,
2260
+ correlationId: params.correlationId || workflow.id,
2261
+ payload: {
2262
+ workflow_id: workflow.id,
2263
+ workflow_name: workflow.name,
2264
+ step_id: step.id,
2265
+ assignee: step.assignee || null,
2266
+ started_at: step.started_at || null,
2267
+ },
2268
+ }));
2269
+
2270
+ result = {
2271
+ success: true,
2272
+ workflow: cloneJsonValue(workflow),
2273
+ workflow_event_id: createdEvent.event_id,
2274
+ started_step_event_ids: startedStepEvents.map((event) => event.event_id),
2275
+ };
2276
+ }, { branch, space: 2 });
2277
+
2278
+ return result;
2279
+ }
2280
+
2281
+ function advanceWorkflow(params) {
2282
+ const now = params.at || new Date().toISOString();
2283
+ const branch = sanitizeBranchName(params.branch || 'main');
2284
+ let result = { error: 'Workflow not found' };
2285
+
2286
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2287
+ const workflow = workflows.find((entry) => entry.id === params.workflowId);
2288
+ if (!workflow) return;
2289
+
2290
+ const currentStep = workflow.steps.find((step) => step.status === 'in_progress');
2291
+ if (!currentStep) {
2292
+ result = { error: 'No step currently in progress' };
2293
+ return;
2294
+ }
2295
+
2296
+ if (params.expectedAssignee && currentStep.assignee !== params.expectedAssignee) {
2297
+ result = { error: 'No active step assigned to you in this workflow.' };
2298
+ return;
2299
+ }
2300
+
2301
+ const evidence = recordEvidence({
2302
+ ...(params.evidence || {}),
2303
+ actor: params.actor || 'system',
2304
+ branch,
2305
+ commandId: params.commandId || null,
2306
+ correlationId: params.correlationId || params.workflowId || null,
2307
+ flagged: !!params.flagged,
2308
+ flagReason: params.flagReason || null,
2309
+ notes: params.notes || null,
2310
+ recordedAt: now,
2311
+ sessionId: params.sessionId || null,
2312
+ sourceTool: params.sourceTool || 'advance_workflow',
2313
+ stepDescription: currentStep.description || null,
2314
+ stepId: currentStep.id,
2315
+ subjectKind: 'workflow_step',
2316
+ workflowId: workflow.id,
2317
+ workflowName: workflow.name,
2318
+ });
2319
+ if (evidence.error) {
2320
+ result = evidence;
2321
+ return;
2322
+ }
2323
+
2324
+ currentStep.status = 'done';
2325
+ currentStep.completed_at = now;
2326
+ currentStep.completed_by = params.actor || null;
2327
+ currentStep.completed_by_session = params.sessionId || null;
2328
+ currentStep.evidence_ref = cloneJsonValue(evidence.reference);
2329
+ currentStep.verification = buildVerificationProjection(evidence.record, evidence.reference);
2330
+ if (params.notes) currentStep.notes = params.notes;
2331
+ if (params.flagged) {
2332
+ currentStep.flagged = true;
2333
+ currentStep.flag_reason = params.flagReason || null;
2334
+ } else {
2335
+ delete currentStep.flagged;
2336
+ delete currentStep.flag_reason;
2337
+ }
2338
+
2339
+ const nextSteps = findReadyWorkflowSteps(workflow);
2340
+ if (nextSteps.length > 0) {
2341
+ for (const nextStep of nextSteps) {
2342
+ nextStep.status = 'in_progress';
2343
+ nextStep.started_at = now;
2344
+ }
2345
+ } else if (!workflow.steps.find((step) => step.status === 'pending' || step.status === 'in_progress')) {
2346
+ workflow.status = 'completed';
2347
+ workflow.completed_at = now;
2348
+ }
2349
+
2350
+ workflow.updated_at = now;
2351
+
2352
+ const stepCompletedEvent = canonicalEventLog.appendEvent({
2353
+ type: 'workflow.step_completed',
2354
+ branchId: branch,
2355
+ actorAgent: params.actor || 'system',
2356
+ sessionId: params.sessionId || null,
2357
+ commandId: params.commandId || null,
2358
+ causationId: evidence.event.event_id,
2359
+ correlationId: params.correlationId || params.workflowId || null,
2360
+ payload: {
2361
+ workflow_id: workflow.id,
2362
+ workflow_name: workflow.name,
2363
+ step_id: currentStep.id,
2364
+ notes: params.notes || null,
2365
+ flagged: !!params.flagged,
2366
+ flag_reason: params.flagReason || null,
2367
+ evidence_ref: cloneJsonValue(evidence.reference),
2368
+ next_step_ids: nextSteps.map((step) => step.id),
2369
+ workflow_status: workflow.status,
2370
+ completed_at: now,
2371
+ },
2372
+ });
2373
+
2374
+ let workflowCompletedEvent = null;
2375
+ if (workflow.status === 'completed') {
2376
+ workflowCompletedEvent = canonicalEventLog.appendEvent({
2377
+ type: 'workflow.completed',
2378
+ branchId: branch,
2379
+ actorAgent: params.actor || 'system',
2380
+ sessionId: params.sessionId || null,
2381
+ commandId: params.commandId || null,
2382
+ causationId: stepCompletedEvent.event_id,
2383
+ correlationId: params.correlationId || params.workflowId || null,
2384
+ payload: {
2385
+ workflow_id: workflow.id,
2386
+ workflow_name: workflow.name,
2387
+ evidence_ref: cloneJsonValue(evidence.reference),
2388
+ completed_at: now,
2389
+ },
2390
+ });
2391
+ }
2392
+
2393
+ const stepStartedEvents = nextSteps.map((step) => appendCanonicalEvent({
2394
+ type: 'workflow.step_started',
2395
+ branchId: branch,
2396
+ actorAgent: params.actor || 'system',
2397
+ sessionId: params.sessionId || null,
2398
+ commandId: params.commandId || null,
2399
+ causationId: stepCompletedEvent.event_id,
2400
+ correlationId: params.correlationId || params.workflowId || null,
2401
+ payload: {
2402
+ workflow_id: workflow.id,
2403
+ workflow_name: workflow.name,
2404
+ step_id: step.id,
2405
+ assignee: step.assignee || null,
2406
+ started_at: step.started_at || now,
2407
+ },
2408
+ }));
2409
+
2410
+ const doneCount = workflow.steps.filter((step) => step.status === 'done').length;
2411
+ const pct = workflow.steps.length > 0 ? Math.round((doneCount / workflow.steps.length) * 100) : 100;
2412
+
2413
+ result = {
2414
+ success: true,
2415
+ workflow_id: workflow.id,
2416
+ workflow_name: workflow.name,
2417
+ completed_step: currentStep.id,
2418
+ completed_step_description: currentStep.description || null,
2419
+ evidence_event_id: evidence.event.event_id,
2420
+ evidence_ref: cloneJsonValue(evidence.reference),
2421
+ flagged: !!params.flagged,
2422
+ flag_reason: params.flagReason || null,
2423
+ next_steps: nextSteps.map((step) => ({
2424
+ id: step.id,
2425
+ description: step.description,
2426
+ assignee: step.assignee || null,
2427
+ })),
2428
+ progress: `${doneCount}/${workflow.steps.length} (${pct}%)`,
2429
+ step_event_id: stepCompletedEvent.event_id,
2430
+ started_step_event_ids: stepStartedEvents.map((event) => event.event_id),
2431
+ workflow_event_id: workflowCompletedEvent ? workflowCompletedEvent.event_id : null,
2432
+ workflow_status: workflow.status,
2433
+ };
2434
+ }, { branch, space: 2 });
2435
+
2436
+ return result;
2437
+ }
2438
+
2439
+ function skipWorkflowStep(params) {
2440
+ const now = new Date().toISOString();
2441
+ const branch = sanitizeBranchName(params.branch || 'main');
2442
+ let result = { error: 'Workflow not found' };
2443
+
2444
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2445
+ const workflow = workflows.find((entry) => entry.id === params.workflowId);
2446
+ if (!workflow) return;
2447
+
2448
+ const step = workflow.steps.find((entry) => entry.id === params.stepId);
2449
+ if (!step) {
2450
+ result = { error: 'Step not found' };
2451
+ return;
2452
+ }
2453
+
2454
+ step.status = 'done';
2455
+ if (params.appendNote && params.note) {
2456
+ step.notes = (step.notes || '') + params.note;
2457
+ } else {
2458
+ step.notes = params.note || step.notes || 'Skipped from dashboard';
2459
+ }
2460
+ step.completed_at = now;
2461
+ if (params.markSkipped) step.skipped = true;
2462
+
2463
+ let readySteps = [];
2464
+ if (params.dependencyAware) {
2465
+ readySteps = workflow.steps.filter((candidate) => {
2466
+ if (candidate.status !== 'pending') return false;
2467
+ if (!candidate.depends_on || candidate.depends_on.length === 0) return true;
2468
+ return candidate.depends_on.every((dependencyId) => {
2469
+ const dependency = workflow.steps.find((entry) => entry.id === dependencyId);
2470
+ return dependency && dependency.status === 'done';
2471
+ });
2472
+ });
2473
+ for (const readyStep of readySteps) {
2474
+ readyStep.status = 'in_progress';
2475
+ readyStep.started_at = now;
2476
+ }
2477
+ } else {
2478
+ const nextStep = workflow.steps.find((candidate) => candidate.status === 'pending');
2479
+ if (nextStep && !workflow.steps.find((candidate) => candidate.status === 'in_progress')) {
2480
+ nextStep.status = 'in_progress';
2481
+ nextStep.started_at = now;
2482
+ readySteps = [nextStep];
2483
+ }
2484
+ }
2485
+
2486
+ if (!workflow.steps.find((candidate) => candidate.status === 'pending' || candidate.status === 'in_progress')) {
2487
+ workflow.status = 'completed';
2488
+ }
2489
+ workflow.updated_at = now;
2490
+
2491
+ result = {
2492
+ success: true,
2493
+ workflow_id: workflow.id,
2494
+ step_id: step.id,
2495
+ ready_steps: readySteps.map((readyStep) => readyStep.id),
2496
+ };
2497
+ }, { branch, space: 2 });
2498
+
2499
+ return result;
2500
+ }
2501
+
2502
+ function reassignWorkflowStep(params) {
2503
+ const now = params.at || new Date().toISOString();
2504
+ const branch = sanitizeBranchName(params.branch || 'main');
2505
+ let result = { error: 'Workflow not found' };
2506
+
2507
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2508
+ const workflow = workflows.find((entry) => entry.id === params.workflowId);
2509
+ if (!workflow) return;
2510
+
2511
+ const step = workflow.steps.find((entry) => entry.id === params.stepId);
2512
+ if (!step) {
2513
+ result = { error: 'Step not found' };
2514
+ return;
2515
+ }
2516
+
2517
+ if (Object.prototype.hasOwnProperty.call(params, 'expectedAssignee')) {
2518
+ const expectedAssignee = params.expectedAssignee || null;
2519
+ const actualAssignee = step.assignee || null;
2520
+ if (expectedAssignee !== actualAssignee) {
2521
+ result = { error: 'Step assignee changed.' };
2522
+ return;
2523
+ }
2524
+ }
2525
+
2526
+ const oldAssignee = step.assignee || null;
2527
+ step.assignee = params.newAssignee;
2528
+ if (params.clearPolicySignal && step.policy_signal) {
2529
+ delete step.policy_signal;
2530
+ }
2531
+ if (Array.isArray(params.clearSignalFields)) {
2532
+ for (const fieldName of params.clearSignalFields) {
2533
+ if (typeof fieldName !== 'string' || !fieldName) continue;
2534
+ if (Object.prototype.hasOwnProperty.call(step, fieldName)) {
2535
+ delete step[fieldName];
2536
+ }
2537
+ }
2538
+ }
2539
+ if (params.restartStartedAt && step.status === 'in_progress') {
2540
+ step.started_at = params.restartStartedAt;
2541
+ }
2542
+ workflow.updated_at = now;
2543
+
2544
+ const event = appendCanonicalEvent({
2545
+ type: 'workflow.step_reassigned',
2546
+ branchId: branch,
2547
+ actorAgent: params.actor || 'system',
2548
+ sessionId: params.sessionId || null,
2549
+ commandId: params.commandId || null,
2550
+ causationId: params.causationId || null,
2551
+ correlationId: params.correlationId || params.workflowId || null,
2552
+ payload: {
2553
+ workflow_id: workflow.id,
2554
+ workflow_name: workflow.name,
2555
+ step_id: step.id,
2556
+ old_assignee: oldAssignee,
2557
+ new_assignee: params.newAssignee,
2558
+ reassigned_at: now,
2559
+ },
2560
+ });
2561
+
2562
+ result = {
2563
+ success: true,
2564
+ workflow_id: workflow.id,
2565
+ step_id: step.id,
2566
+ old_assignee: oldAssignee,
2567
+ new_assignee: params.newAssignee,
2568
+ step: cloneJsonValue(step),
2569
+ step_event_id: event.event_id,
2570
+ };
2571
+ }, { branch, space: 2 });
2572
+
2573
+ return result;
2574
+ }
2575
+
2576
+ function setWorkflowStepPolicySignal(params = {}) {
2577
+ const now = params.at || new Date().toISOString();
2578
+ const branch = sanitizeBranchName(params.branch || 'main');
2579
+ let result = { error: 'Workflow not found' };
2580
+
2581
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2582
+ const workflow = workflows.find((entry) => entry.id === params.workflowId);
2583
+ if (!workflow) return;
2584
+
2585
+ const step = workflow.steps.find((entry) => entry.id === params.stepId);
2586
+ if (!step) {
2587
+ result = { error: 'Step not found' };
2588
+ return;
2589
+ }
2590
+
2591
+ if (Object.prototype.hasOwnProperty.call(params, 'expectedAssignee')) {
2592
+ const expectedAssignee = params.expectedAssignee || null;
2593
+ const actualAssignee = step.assignee || null;
2594
+ if (expectedAssignee !== actualAssignee) {
2595
+ result = { error: 'Step assignee changed.' };
2596
+ return;
2597
+ }
2598
+ }
2599
+
2600
+ if (params.clearPolicySignal && step.policy_signal) {
2601
+ delete step.policy_signal;
2602
+ }
2603
+ if (Object.prototype.hasOwnProperty.call(params, 'policySignal')) {
2604
+ if (params.policySignal) {
2605
+ step.policy_signal = cloneJsonValue(params.policySignal);
2606
+ } else {
2607
+ delete step.policy_signal;
2608
+ }
2609
+ }
2610
+
2611
+ if (params.signalAtField) {
2612
+ step[params.signalAtField] = now;
2613
+ }
2614
+
2615
+ workflow.updated_at = now;
2616
+ result = {
2617
+ success: true,
2618
+ workflow_id: workflow.id,
2619
+ step_id: step.id,
2620
+ step: cloneJsonValue(step),
2621
+ };
2622
+ }, { branch, space: 2 });
2623
+
2624
+ return result;
2625
+ }
2626
+
2627
+ function pausePlan() {
2628
+ const params = arguments[0] || {};
2629
+ const now = new Date().toISOString();
2630
+ const branch = sanitizeBranchName(params.branch || 'main');
2631
+ let result = { error: 'No active autonomous plan' };
2632
+
2633
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2634
+ const workflow = workflows.find((entry) => entry.status === 'active' && entry.autonomous);
2635
+ if (!workflow) return;
2636
+
2637
+ workflow.paused = true;
2638
+ workflow.paused_at = now;
2639
+ workflow.updated_at = now;
2640
+ const event = appendCanonicalEvent({
2641
+ type: 'workflow.paused',
2642
+ branchId: branch,
2643
+ actorAgent: params.actor || 'system',
2644
+ sessionId: params.sessionId || null,
2645
+ commandId: params.commandId || null,
2646
+ correlationId: params.correlationId || workflow.id,
2647
+ payload: {
2648
+ workflow_id: workflow.id,
2649
+ workflow_name: workflow.name,
2650
+ paused_at: now,
2651
+ },
2652
+ });
2653
+ result = { success: true, workflow_id: workflow.id, name: workflow.name, workflow_event_id: event.event_id };
2654
+ }, { branch, space: 2 });
2655
+
2656
+ return result;
2657
+ }
2658
+
2659
+ function resumePlan() {
2660
+ const params = arguments[0] || {};
2661
+ const now = new Date().toISOString();
2662
+ const branch = sanitizeBranchName(params.branch || 'main');
2663
+ let result = { error: 'No paused plan' };
2664
+
2665
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2666
+ const workflow = workflows.find((entry) => entry.status === 'active' && entry.paused);
2667
+ if (!workflow) return;
2668
+
2669
+ workflow.paused = false;
2670
+ delete workflow.paused_at;
2671
+ workflow.updated_at = now;
2672
+ const event = appendCanonicalEvent({
2673
+ type: 'workflow.resumed',
2674
+ branchId: branch,
2675
+ actorAgent: params.actor || 'system',
2676
+ sessionId: params.sessionId || null,
2677
+ commandId: params.commandId || null,
2678
+ correlationId: params.correlationId || workflow.id,
2679
+ payload: {
2680
+ workflow_id: workflow.id,
2681
+ workflow_name: workflow.name,
2682
+ resumed_at: now,
2683
+ },
2684
+ });
2685
+ result = { success: true, workflow_id: workflow.id, name: workflow.name, workflow_event_id: event.event_id };
2686
+ }, { branch, space: 2 });
2687
+
2688
+ return result;
2689
+ }
2690
+
2691
+ function stopPlan() {
2692
+ const params = arguments[0] || {};
2693
+ const now = new Date().toISOString();
2694
+ const branch = sanitizeBranchName(params.branch || 'main');
2695
+ let result = { error: 'No active plan' };
2696
+
2697
+ tasksWorkflowsState.mutateWorkflows((workflows) => {
2698
+ const workflow = workflows.find((entry) => entry.status === 'active');
2699
+ if (!workflow) return;
2700
+
2701
+ workflow.status = 'stopped';
2702
+ workflow.stopped_at = now;
2703
+ workflow.updated_at = now;
2704
+ const event = appendCanonicalEvent({
2705
+ type: 'workflow.stopped',
2706
+ branchId: branch,
2707
+ actorAgent: params.actor || 'system',
2708
+ sessionId: params.sessionId || null,
2709
+ commandId: params.commandId || null,
2710
+ correlationId: params.correlationId || workflow.id,
2711
+ payload: {
2712
+ workflow_id: workflow.id,
2713
+ workflow_name: workflow.name,
2714
+ stopped_at: now,
2715
+ },
2716
+ });
2717
+ result = { success: true, workflow_id: workflow.id, name: workflow.name, workflow_event_id: event.event_id };
2718
+ }, { branch, space: 2 });
2719
+
2720
+ return result;
2721
+ }
2722
+
2723
+ function archiveFiles(params = {}) {
2724
+ const { fileNames = [], destinationDir } = params;
2725
+ if (!destinationDir) throw new Error('archiveFiles requires destinationDir');
2726
+
2727
+ io.ensureDataDir();
2728
+ fs.mkdirSync(destinationDir, { recursive: true });
2729
+
2730
+ let archived = 0;
2731
+ for (const fileName of fileNames) {
2732
+ const sourcePath = path.join(dataDir, fileName);
2733
+ if (!fs.existsSync(sourcePath)) continue;
2734
+ fs.copyFileSync(sourcePath, path.join(destinationDir, fileName));
2735
+ archived++;
2736
+ }
2737
+
2738
+ return { archived, destination: destinationDir };
2739
+ }
2740
+
2741
+ function failClosedConversationMutation(operation, reason) {
2742
+ return {
2743
+ error: `${operation} is unavailable while canonical events are authoritative: ${reason}`,
2744
+ fail_closed: true,
2745
+ };
2746
+ }
2747
+
2748
+ function clearBranchMessageAuxiliaryState(branch) {
2749
+ const auxiliaryFiles = [
2750
+ getAcksFile(branch),
2751
+ getReadReceiptsFile(branch),
2752
+ getCompressedFile(branch),
2753
+ getBranchDashboardProjectionFile(branch),
2754
+ ];
2755
+
2756
+ for (const filePath of auxiliaryFiles) {
2757
+ withFileLock(filePath, () => deleteFile(filePath));
2758
+ }
2759
+ }
2760
+
2761
+ function clearMessages(params = {}) {
2762
+ const branch = sanitizeBranchName(params.branch || 'main');
2763
+ const targets = getScopedMessageProjectionTargets(branch);
2764
+ const actorAgent = params.actorAgent || params.actor || 'system';
2765
+
2766
+ try {
2767
+ assertMessageProjectionRebuildAuthority(branch, targets);
2768
+
2769
+ const currentMessages = dashboardQueries.getConversationMessages({ branch });
2770
+ const redactedAt = params.redactedAt || new Date().toISOString();
2771
+ const clearedMessageIds = [];
2772
+
2773
+ for (const message of Array.isArray(currentMessages) ? currentMessages : []) {
2774
+ if (!message || typeof message.id !== 'string' || !message.id) continue;
2775
+ appendCanonicalMessageRedactedEvent({
2776
+ branch,
2777
+ actorAgent,
2778
+ sessionId: params.sessionId || null,
2779
+ commandId: params.commandId || null,
2780
+ causationId: params.causationId || null,
2781
+ correlationId: params.correlationId || message.id,
2782
+ messageId: message.id,
2783
+ redactedAt,
2784
+ });
2785
+ clearedMessageIds.push(message.id);
2786
+ }
2787
+
2788
+ if (clearedMessageIds.length > 0 || fs.existsSync(canonicalEventLog.getBranchEventsFile(branch))) {
2789
+ rebuildMessageProjections({ branch });
2790
+ }
2791
+ clearBranchMessageAuxiliaryState(branch);
2792
+
2793
+ return {
2794
+ success: true,
2795
+ branch,
2796
+ cleared_messages: clearedMessageIds.length,
2797
+ redacted_at: redactedAt,
2798
+ };
2799
+ } catch (error) {
2800
+ return {
2801
+ error: error && error.message ? error.message : String(error),
2802
+ code: error && error.code ? error.code : null,
2803
+ };
2804
+ }
2805
+ }
2806
+
2807
+ function clearTasks(params = {}) {
2808
+ const branch = sanitizeBranchName(params.branch || 'main');
2809
+ const actorAgent = params.actorAgent || params.actor || 'system';
2810
+ const clearedAt = params.clearedAt || new Date().toISOString();
2811
+ const clearedTaskIds = [];
2812
+
2813
+ try {
2814
+ tasksWorkflowsState.mutateTasks((tasks) => {
2815
+ for (const task of tasks) {
2816
+ if (task && typeof task.id === 'string' && task.id) {
2817
+ clearedTaskIds.push(task.id);
2818
+ }
2819
+ }
2820
+ if (clearedTaskIds.length === 0) return;
2821
+
2822
+ appendCanonicalEvent({
2823
+ type: 'tasks.cleared',
2824
+ branchId: branch,
2825
+ actorAgent,
2826
+ sessionId: params.sessionId || null,
2827
+ commandId: params.commandId || null,
2828
+ causationId: params.causationId || null,
2829
+ correlationId: params.correlationId || null,
2830
+ payload: {
2831
+ cleared_task_ids: [...clearedTaskIds],
2832
+ cleared_count: clearedTaskIds.length,
2833
+ cleared_at: clearedAt,
2834
+ },
2835
+ });
2836
+
2837
+ tasks.length = 0;
2838
+ }, { branch, space: 2 });
2839
+
2840
+ return {
2841
+ success: true,
2842
+ branch,
2843
+ cleared_tasks: clearedTaskIds.length,
2844
+ cleared_at: clearedAt,
2845
+ };
2846
+ } catch (error) {
2847
+ return {
2848
+ error: error && error.message ? error.message : String(error),
2849
+ code: error && error.code ? error.code : null,
2850
+ };
2851
+ }
2852
+ }
2853
+
2854
+ function archiveCurrentConversation() {
2855
+ return failClosedConversationMutation(
2856
+ 'archiveCurrentConversation',
2857
+ 'archiving or rotating projection-only conversation snapshots is not yet modeled in canonical events.'
2858
+ );
2859
+ }
2860
+
2861
+ function loadConversation(name) {
2862
+ const conversationsDir = path.join(dataDir, 'conversations');
2863
+ const sourceMessages = path.join(conversationsDir, name + '.jsonl');
2864
+ if (!fs.existsSync(sourceMessages)) {
2865
+ return { error: 'Conversation not found' };
2866
+ }
2867
+
2868
+ return failClosedConversationMutation(
2869
+ 'loadConversation',
2870
+ 'loading archived projection snapshots back into canonical event history is not supported safely.'
2871
+ );
2872
+ }
2873
+
2874
+ function resetRuntime(params = {}) {
2875
+ const baseFixedFileNames = [
2876
+ 'messages.jsonl',
2877
+ 'history.jsonl',
2878
+ 'agents.json',
2879
+ 'acks.json',
2880
+ 'tasks.json',
2881
+ 'evidence.json',
2882
+ 'profiles.json',
2883
+ 'workflows.json',
2884
+ 'branches.json',
2885
+ 'read_receipts.json',
2886
+ 'permissions.json',
2887
+ 'config.json',
2888
+ 'channels.json',
2889
+ 'compressed.json',
2890
+ 'decisions.json',
2891
+ 'kb.json',
2892
+ 'progress.json',
2893
+ 'votes.json',
2894
+ 'rules.json',
2895
+ 'reviews.json',
2896
+ 'dependencies.json',
2897
+ 'locks.json',
2898
+ 'reputation.json',
2899
+ 'assistant-replies.jsonl',
2900
+ ];
2901
+ const fixedFileNames = Array.from(new Set([...(params.fixedFileNames || []), ...baseFixedFileNames]));
2902
+
2903
+ for (const fileName of fixedFileNames) {
2904
+ const filePath = path.join(dataDir, fileName);
2905
+ withFileLock(filePath, () => deleteFile(filePath));
2906
+ }
2907
+
2908
+ clearConsumedFiles();
2909
+
2910
+ if (fs.existsSync(dataDir)) {
2911
+ for (const fileName of fs.readdirSync(dataDir)) {
2912
+ const isBranchScopedP0File = fileName.startsWith('branch-') && (
2913
+ fileName.endsWith('-messages.jsonl') ||
2914
+ fileName.endsWith('-history.jsonl') ||
2915
+ fileName.endsWith('-acks.json') ||
2916
+ fileName.endsWith('-tasks.json') ||
2917
+ fileName.endsWith('-evidence.json') ||
2918
+ fileName.endsWith('-workflows.json') ||
2919
+ fileName.endsWith('-read_receipts.json') ||
2920
+ fileName.endsWith('-config.json') ||
2921
+ fileName.endsWith('-channels.json') ||
2922
+ fileName.endsWith('-compressed.json') ||
2923
+ fileName.endsWith('-decisions.json') ||
2924
+ fileName.endsWith('-kb.json') ||
2925
+ fileName.endsWith('-reviews.json') ||
2926
+ fileName.endsWith('-dependencies.json') ||
2927
+ fileName.endsWith('-votes.json') ||
2928
+ fileName.endsWith('-rules.json') ||
2929
+ fileName.endsWith('-progress.json') ||
2930
+ fileName.includes('-consumed-') ||
2931
+ (fileName.includes('-channel-') && (fileName.endsWith('-messages.jsonl') || fileName.endsWith('-history.jsonl')))
2932
+ );
2933
+ const isGlobalChannelProjection = fileName.startsWith('channel-') && (fileName.endsWith('-messages.jsonl') || fileName.endsWith('-history.jsonl'));
2934
+ if (isBranchScopedP0File || isGlobalChannelProjection) {
2935
+ const filePath = path.join(dataDir, fileName);
2936
+ withFileLock(filePath, () => deleteFile(filePath));
2937
+ }
2938
+ }
2939
+ }
2940
+
2941
+ withFileLock(getSessionsIndexFile(), () => deleteFile(getSessionsIndexFile()));
2942
+ deleteDirectory(runtimeDir);
2943
+
2944
+ const runtimeBranchesDir = path.join(runtimeDir, 'branches');
2945
+ if (fs.existsSync(runtimeBranchesDir)) {
2946
+ for (const entry of fs.readdirSync(runtimeBranchesDir, { withFileTypes: true })) {
2947
+ if (!entry.isDirectory()) continue;
2948
+ deleteDirectory(getBranchSessionsDir(entry.name));
2949
+ }
2950
+ }
2951
+
2952
+ if (fs.existsSync(workspacesDir)) {
2953
+ for (const fileName of fs.readdirSync(workspacesDir)) {
2954
+ deleteFile(path.join(workspacesDir, fileName));
2955
+ }
2956
+ try { fs.rmdirSync(workspacesDir); } catch {}
2957
+ }
2958
+
2959
+ if (fs.existsSync(dataDir)) {
2960
+ for (const entry of fs.readdirSync(dataDir, { withFileTypes: true })) {
2961
+ if (!entry.isDirectory()) continue;
2962
+ if (!/^branch-[a-zA-Z0-9_-]+-workspaces$/.test(entry.name)) continue;
2963
+ deleteDirectory(path.join(dataDir, entry.name));
2964
+ }
2965
+ }
2966
+
2967
+ return { success: true };
2968
+ }
2969
+
2970
+ return {
2971
+ readJson,
2972
+ appendCanonicalEvent,
2973
+ readHooks,
2974
+ readBranchHooks,
2975
+ readRuntimeHooks,
2976
+ appendMessage,
2977
+ appendScopedMessage,
2978
+ appendMessages,
2979
+ rebuildMessageProjections,
2980
+ editMessage,
2981
+ deleteMessage,
2982
+ registerApiAgent,
2983
+ unregisterApiAgent,
2984
+ recordAgentHeartbeat,
2985
+ updateAgentHeartbeat,
2986
+ updateAgentStatus,
2987
+ updateAgentBranch,
2988
+ setAgentListeningState,
2989
+ listAgents,
2990
+ listProfiles,
2991
+ upsertProfile,
2992
+ listDecisions,
2993
+ readKnowledgeBase,
2994
+ listReviews,
2995
+ listDependencies,
2996
+ listVotes,
2997
+ listRules,
2998
+ readProgress,
2999
+ getProjectNotesView,
3000
+ getTeamNotesView,
3001
+ readWorkspace,
3002
+ listWorkspaces,
3003
+ mutateReviews,
3004
+ mutateDependencies,
3005
+ addRule,
3006
+ updateRule,
3007
+ toggleRule,
3008
+ removeRule,
3009
+ saveWorkspace,
3010
+ logDecision,
3011
+ writeKnowledgeBaseEntry,
3012
+ updateProgressRecord,
3013
+ createVote,
3014
+ castVote,
3015
+ listTasks,
3016
+ listWorkflows,
3017
+ getConversationConfigView,
3018
+ getHistoryView: dashboardQueries.getHistoryView,
3019
+ getChannelsView: dashboardQueries.getChannelsView,
3020
+ getConversationMessages: dashboardQueries.getConversationMessages,
3021
+ getSearchResultsView: dashboardQueries.getSearchResultsView,
3022
+ getPlanStatusView,
3023
+ getPlanReportView,
3024
+ listMarkdownBranches,
3025
+ getBranchEventSequence,
3026
+ listBranchSessions,
3027
+ getBranchSessionManifest,
3028
+ getLatestSessionSummaryForAgent,
3029
+ ensureAgentSession,
3030
+ touchSession,
3031
+ transitionSession,
3032
+ transitionLatestSessionForAgent,
3033
+ findEvidence,
3034
+ projectEvidence,
3035
+ readEvidence,
3036
+ listCompatibilityDecisions,
3037
+ listCompatibilityWorkspaces,
3038
+ getCompatibilityProjectNotesView,
3039
+ getCompatibilityTeamNotesView,
3040
+ readConsumedMessageIds,
3041
+ writeConsumedMessageIds,
3042
+ exportMarkdownWorkspace: exportMarkdownWorkspaceFiles,
3043
+ createTask,
3044
+ updateTaskStatus,
3045
+ createWorkflow,
3046
+ advanceWorkflow,
3047
+ skipWorkflowStep,
3048
+ reassignWorkflowStep,
3049
+ setWorkflowStepPolicySignal,
3050
+ pausePlan,
3051
+ resumePlan,
3052
+ stopPlan,
3053
+ archiveFiles,
3054
+ clearMessages,
3055
+ clearTasks,
3056
+ archiveCurrentConversation,
3057
+ loadConversation,
3058
+ resetRuntime,
3059
+ };
3060
+ }
3061
+
3062
+ module.exports = {
3063
+ buildVerificationProjection,
3064
+ createCanonicalState,
3065
+ createBranchPathResolvers,
3066
+ projectEvidenceRecord,
3067
+ sanitizeBranchName,
3068
+ };