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
package/events/log.js ADDED
@@ -0,0 +1,457 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const {
6
+ CANONICAL_EVENT_SCHEMA_VERSION,
7
+ EVENT_STREAMS,
8
+ resolveTypeStream,
9
+ validateCanonicalEvent,
10
+ } = require('./schema');
11
+ const {
12
+ CANONICAL_REPLAY_ERROR_CODES,
13
+ createCanonicalReplayError,
14
+ } = require('./replay');
15
+
16
+ const EVENT_STREAM_HEAD_SCHEMA_VERSION = 1;
17
+
18
+ function defaultSanitizeBranchName(branchName) {
19
+ if (!branchName || branchName === 'main') return 'main';
20
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(branchName)) {
21
+ throw new Error('Invalid branch name');
22
+ }
23
+ return branchName;
24
+ }
25
+
26
+ function cloneJsonValue(value) {
27
+ return value == null ? value : JSON.parse(JSON.stringify(value));
28
+ }
29
+
30
+ function readJsonlObjects(filePath) {
31
+ if (!filePath || !fs.existsSync(filePath)) return [];
32
+
33
+ const raw = fs.readFileSync(filePath, 'utf8');
34
+ if (!raw.trim()) return [];
35
+
36
+ return raw
37
+ .split(/\r?\n/)
38
+ .filter(Boolean)
39
+ .map((line, index) => {
40
+ try {
41
+ return JSON.parse(line);
42
+ } catch (error) {
43
+ throw createCanonicalReplayError(
44
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_JSONL,
45
+ `Canonical event replay rejected invalid JSONL at ${filePath}:${index + 1} (${error.message})`,
46
+ {
47
+ file_path: filePath,
48
+ line_number: index + 1,
49
+ }
50
+ );
51
+ }
52
+ });
53
+ }
54
+
55
+ function readJsonObject(filePath) {
56
+ if (!filePath || !fs.existsSync(filePath)) return null;
57
+
58
+ try {
59
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
60
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function readLastJsonlObject(filePath) {
67
+ if (!filePath || !fs.existsSync(filePath)) return null;
68
+
69
+ const stats = fs.statSync(filePath);
70
+ if (stats.size === 0) return null;
71
+
72
+ const fd = fs.openSync(filePath, 'r');
73
+ const byte = Buffer.alloc(1);
74
+
75
+ try {
76
+ let position = stats.size - 1;
77
+
78
+ while (position >= 0) {
79
+ fs.readSync(fd, byte, 0, 1, position);
80
+ if (byte[0] !== 0x0a && byte[0] !== 0x0d) break;
81
+ position -= 1;
82
+ }
83
+
84
+ if (position < 0) return null;
85
+
86
+ const lineEnd = position + 1;
87
+ while (position >= 0) {
88
+ fs.readSync(fd, byte, 0, 1, position);
89
+ if (byte[0] === 0x0a || byte[0] === 0x0d) break;
90
+ position -= 1;
91
+ }
92
+
93
+ const lineStart = position + 1;
94
+ const lineLength = lineEnd - lineStart;
95
+ const lineBuffer = Buffer.alloc(lineLength);
96
+ fs.readSync(fd, lineBuffer, 0, lineLength, lineStart);
97
+
98
+ try {
99
+ return JSON.parse(lineBuffer.toString('utf8'));
100
+ } catch (error) {
101
+ throw createCanonicalReplayError(
102
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_JSONL,
103
+ `Canonical event replay rejected invalid JSONL near the tail of ${filePath} (${error.message})`,
104
+ {
105
+ file_path: filePath,
106
+ }
107
+ );
108
+ }
109
+ } finally {
110
+ fs.closeSync(fd);
111
+ }
112
+ }
113
+
114
+ function readFileFingerprint(filePath) {
115
+ if (!filePath || !fs.existsSync(filePath)) {
116
+ return {
117
+ exists: false,
118
+ size: 0,
119
+ mtime_ms: 0,
120
+ };
121
+ }
122
+
123
+ const stats = fs.statSync(filePath);
124
+ return {
125
+ exists: true,
126
+ size: stats.size,
127
+ mtime_ms: stats.mtimeMs,
128
+ };
129
+ }
130
+
131
+ function sameFileFingerprint(left, right) {
132
+ return !!left
133
+ && !!right
134
+ && left.exists === right.exists
135
+ && left.size === right.size
136
+ && left.mtime_ms === right.mtime_ms;
137
+ }
138
+
139
+ function normalizeHeadFingerprint(head) {
140
+ return {
141
+ exists: !!head.file_exists,
142
+ size: Number.isInteger(head.file_size) ? head.file_size : 0,
143
+ mtime_ms: Number.isFinite(head.file_mtime_ms) ? head.file_mtime_ms : 0,
144
+ };
145
+ }
146
+
147
+ function normalizeEventStreamHead(head) {
148
+ if (!head || typeof head !== 'object' || Array.isArray(head)) return null;
149
+ if (head.schema_version !== EVENT_STREAM_HEAD_SCHEMA_VERSION) return null;
150
+ if (!Object.values(EVENT_STREAMS).includes(head.stream)) return null;
151
+ if (!Number.isInteger(head.last_seq) || head.last_seq < 0) return null;
152
+ if (!Number.isInteger(head.event_count) || head.event_count < 0) return null;
153
+
154
+ return {
155
+ schema_version: EVENT_STREAM_HEAD_SCHEMA_VERSION,
156
+ stream: head.stream,
157
+ branch_id: head.stream === EVENT_STREAMS.BRANCH ? (head.branch_id || 'main') : null,
158
+ last_seq: head.last_seq,
159
+ event_count: head.event_count,
160
+ last_event_id: typeof head.last_event_id === 'string' ? head.last_event_id : null,
161
+ last_event_type: typeof head.last_event_type === 'string' ? head.last_event_type : null,
162
+ last_occurred_at: typeof head.last_occurred_at === 'string' ? head.last_occurred_at : null,
163
+ file_exists: !!head.file_exists,
164
+ file_size: Number.isInteger(head.file_size) ? head.file_size : 0,
165
+ file_mtime_ms: Number.isFinite(head.file_mtime_ms) ? head.file_mtime_ms : 0,
166
+ updated_at: typeof head.updated_at === 'string' ? head.updated_at : null,
167
+ };
168
+ }
169
+
170
+ function buildEventStreamHead(params = {}) {
171
+ const {
172
+ stream,
173
+ branchId = null,
174
+ fingerprint,
175
+ lastEvent = null,
176
+ eventCount = 0,
177
+ updatedAt = null,
178
+ } = params;
179
+
180
+ return {
181
+ schema_version: EVENT_STREAM_HEAD_SCHEMA_VERSION,
182
+ stream,
183
+ branch_id: stream === EVENT_STREAMS.BRANCH ? (branchId || 'main') : null,
184
+ last_seq: lastEvent && Number.isInteger(lastEvent.seq) ? lastEvent.seq : 0,
185
+ event_count: Number.isInteger(eventCount) ? eventCount : 0,
186
+ last_event_id: lastEvent && typeof lastEvent.event_id === 'string' ? lastEvent.event_id : null,
187
+ last_event_type: lastEvent && typeof lastEvent.type === 'string' ? lastEvent.type : null,
188
+ last_occurred_at: lastEvent && typeof lastEvent.occurred_at === 'string' ? lastEvent.occurred_at : null,
189
+ file_exists: !!(fingerprint && fingerprint.exists),
190
+ file_size: fingerprint && Number.isInteger(fingerprint.size) ? fingerprint.size : 0,
191
+ file_mtime_ms: fingerprint && Number.isFinite(fingerprint.mtime_ms) ? fingerprint.mtime_ms : 0,
192
+ updated_at: updatedAt,
193
+ };
194
+ }
195
+
196
+ function createCanonicalEventLog(options = {}) {
197
+ const {
198
+ dataDir,
199
+ withLock,
200
+ onCommitted = null,
201
+ sanitizeBranchName = defaultSanitizeBranchName,
202
+ createEventId = () => crypto.randomUUID(),
203
+ now = () => new Date().toISOString(),
204
+ } = options;
205
+ const streamHeadCache = new Map();
206
+
207
+ function runWithLock(filePath, fn) {
208
+ if (typeof withLock === 'function') {
209
+ return withLock(filePath, fn);
210
+ }
211
+ return fn();
212
+ }
213
+
214
+ function getRuntimeEventsFile() {
215
+ return path.join(dataDir, 'runtime', 'events.jsonl');
216
+ }
217
+
218
+ function getBranchEventsFile(branchName = 'main') {
219
+ return path.join(dataDir, 'runtime', 'branches', sanitizeBranchName(branchName), 'events.jsonl');
220
+ }
221
+
222
+ function getRuntimeEventsHeadFile() {
223
+ return path.join(dataDir, 'runtime', 'events.head.json');
224
+ }
225
+
226
+ function getBranchEventsHeadFile(branchName = 'main') {
227
+ return path.join(dataDir, 'runtime', 'branches', sanitizeBranchName(branchName), 'events.head.json');
228
+ }
229
+
230
+ function getEventsFile(stream, branchId) {
231
+ if (stream === EVENT_STREAMS.RUNTIME) {
232
+ return getRuntimeEventsFile();
233
+ }
234
+
235
+ if (stream === EVENT_STREAMS.BRANCH) {
236
+ return getBranchEventsFile(branchId || 'main');
237
+ }
238
+
239
+ throw new Error(`Unsupported canonical event stream: ${String(stream)}`);
240
+ }
241
+
242
+ function getEventsHeadFile(stream, branchId) {
243
+ if (stream === EVENT_STREAMS.RUNTIME) {
244
+ return getRuntimeEventsHeadFile();
245
+ }
246
+
247
+ if (stream === EVENT_STREAMS.BRANCH) {
248
+ return getBranchEventsHeadFile(branchId || 'main');
249
+ }
250
+
251
+ throw new Error(`Unsupported canonical event stream: ${String(stream)}`);
252
+ }
253
+
254
+ function cacheStreamHead(headFile, head) {
255
+ const normalized = normalizeEventStreamHead(head);
256
+ if (!normalized) {
257
+ streamHeadCache.delete(headFile);
258
+ return null;
259
+ }
260
+
261
+ streamHeadCache.set(headFile, {
262
+ head: normalized,
263
+ fingerprint: normalizeHeadFingerprint(normalized),
264
+ });
265
+ return normalized;
266
+ }
267
+
268
+ function writeStreamHead(headFile, head) {
269
+ const normalized = cacheStreamHead(headFile, head);
270
+ if (!normalized) return null;
271
+ fs.mkdirSync(path.dirname(headFile), { recursive: true });
272
+ fs.writeFileSync(headFile, JSON.stringify(normalized, null, 2));
273
+ return normalized;
274
+ }
275
+
276
+ function scanEventsHead(stream, branchId, eventFile, headFile, updatedAt) {
277
+ const events = readJsonlObjects(eventFile);
278
+ const lastEvent = events.length > 0 ? events[events.length - 1] : null;
279
+ const head = buildEventStreamHead({
280
+ stream,
281
+ branchId,
282
+ fingerprint: readFileFingerprint(eventFile),
283
+ lastEvent,
284
+ eventCount: events.length,
285
+ updatedAt,
286
+ });
287
+ return writeStreamHead(headFile, head);
288
+ }
289
+
290
+ function repairEventsHeadFromTail(stream, branchId, eventFile, headFile, updatedAt) {
291
+ try {
292
+ const lastEvent = readLastJsonlObject(eventFile);
293
+ const fingerprint = readFileFingerprint(eventFile);
294
+
295
+ if (!lastEvent) {
296
+ return writeStreamHead(headFile, buildEventStreamHead({
297
+ stream,
298
+ branchId,
299
+ fingerprint,
300
+ eventCount: 0,
301
+ updatedAt,
302
+ }));
303
+ }
304
+
305
+ if (!Number.isInteger(lastEvent.seq)
306
+ || lastEvent.seq < 1
307
+ || lastEvent.stream !== stream
308
+ || (stream === EVENT_STREAMS.BRANCH && lastEvent.branch_id !== branchId)
309
+ || (stream === EVENT_STREAMS.RUNTIME && lastEvent.branch_id !== null)) {
310
+ return null;
311
+ }
312
+
313
+ return writeStreamHead(headFile, buildEventStreamHead({
314
+ stream,
315
+ branchId,
316
+ fingerprint,
317
+ lastEvent,
318
+ eventCount: lastEvent.seq,
319
+ updatedAt,
320
+ }));
321
+ } catch {
322
+ return null;
323
+ }
324
+ }
325
+
326
+ function getEventsHead(params = {}) {
327
+ const stream = params.stream || EVENT_STREAMS.BRANCH;
328
+ const branchId = stream === EVENT_STREAMS.BRANCH
329
+ ? sanitizeBranchName(params.branchId || params.branch_id || 'main')
330
+ : null;
331
+ const eventFile = getEventsFile(stream, branchId);
332
+ const headFile = getEventsHeadFile(stream, branchId);
333
+ const currentFingerprint = readFileFingerprint(eventFile);
334
+ const cached = streamHeadCache.get(headFile);
335
+
336
+ if (cached && sameFileFingerprint(cached.fingerprint, currentFingerprint)) {
337
+ return cloneJsonValue(cached.head);
338
+ }
339
+
340
+ const persisted = normalizeEventStreamHead(readJsonObject(headFile));
341
+ if (persisted
342
+ && persisted.stream === stream
343
+ && persisted.branch_id === (stream === EVENT_STREAMS.BRANCH ? branchId : null)
344
+ && sameFileFingerprint(normalizeHeadFingerprint(persisted), currentFingerprint)) {
345
+ cacheStreamHead(headFile, persisted);
346
+ return cloneJsonValue(persisted);
347
+ }
348
+
349
+ const repairedFromTail = repairEventsHeadFromTail(stream, branchId, eventFile, headFile, params.at || now());
350
+ if (repairedFromTail) {
351
+ return cloneJsonValue(repairedFromTail);
352
+ }
353
+
354
+ return cloneJsonValue(scanEventsHead(stream, branchId, eventFile, headFile, params.at || now()));
355
+ }
356
+
357
+ function appendEvent(params = {}) {
358
+ const stream = params.stream || resolveTypeStream(params.type);
359
+ if (!stream) {
360
+ throw new Error(`Cannot resolve canonical event stream for type: ${String(params.type)}`);
361
+ }
362
+
363
+ const branchId = stream === EVENT_STREAMS.BRANCH
364
+ ? sanitizeBranchName(params.branchId || params.branch_id || 'main')
365
+ : null;
366
+ const eventFile = getEventsFile(stream, branchId);
367
+ const headFile = getEventsHeadFile(stream, branchId);
368
+
369
+ return runWithLock(eventFile, () => {
370
+ fs.mkdirSync(path.dirname(eventFile), { recursive: true });
371
+
372
+ const currentHead = getEventsHead({ stream, branchId, at: params.occurredAt || params.occurred_at || now() });
373
+ const lastSeq = currentHead && Number.isInteger(currentHead.last_seq) ? currentHead.last_seq : 0;
374
+ const event = {
375
+ event_id: params.eventId || params.event_id || createEventId(),
376
+ stream,
377
+ branch_id: branchId,
378
+ seq: lastSeq + 1,
379
+ type: params.type,
380
+ occurred_at: params.occurredAt || params.occurred_at || now(),
381
+ schema_version: params.schemaVersion || params.schema_version || CANONICAL_EVENT_SCHEMA_VERSION,
382
+ actor_agent: params.actorAgent || params.actor_agent || 'system',
383
+ session_id: params.sessionId || params.session_id || null,
384
+ command_id: params.commandId || params.command_id || null,
385
+ causation_id: params.causationId || params.causation_id || null,
386
+ correlation_id: params.correlationId || params.correlation_id || null,
387
+ payload: cloneJsonValue(params.payload === undefined ? {} : params.payload),
388
+ };
389
+
390
+ if (params.extra && typeof params.extra === 'object' && !Array.isArray(params.extra)) {
391
+ Object.assign(event, cloneJsonValue(params.extra));
392
+ }
393
+
394
+ const validation = validateCanonicalEvent(event);
395
+ if (!validation.ok) {
396
+ throw new Error(`Invalid canonical event ${String(params.type)}: ${validation.problems.join('; ')}`);
397
+ }
398
+
399
+ fs.appendFileSync(eventFile, JSON.stringify(event) + '\n');
400
+ writeStreamHead(headFile, buildEventStreamHead({
401
+ stream,
402
+ branchId,
403
+ fingerprint: readFileFingerprint(eventFile),
404
+ lastEvent: event,
405
+ eventCount: (currentHead && Number.isInteger(currentHead.event_count) ? currentHead.event_count : 0) + 1,
406
+ updatedAt: event.occurred_at,
407
+ }));
408
+ if (typeof onCommitted === 'function') {
409
+ try {
410
+ onCommitted(cloneJsonValue(event));
411
+ } catch {}
412
+ }
413
+ return event;
414
+ });
415
+ }
416
+
417
+ function readEvents(params = {}) {
418
+ const stream = params.stream || EVENT_STREAMS.BRANCH;
419
+ const branchId = stream === EVENT_STREAMS.BRANCH
420
+ ? sanitizeBranchName(params.branchId || params.branch_id || 'main')
421
+ : null;
422
+ const eventFile = getEventsFile(stream, branchId);
423
+ const events = readJsonlObjects(eventFile);
424
+
425
+ if (Array.isArray(params.types) && params.types.length > 0) {
426
+ const typeSet = new Set(params.types);
427
+ return events.filter((event) => typeSet.has(event.type));
428
+ }
429
+
430
+ if (params.typePrefix) {
431
+ return events.filter((event) => typeof event.type === 'string' && event.type.startsWith(params.typePrefix));
432
+ }
433
+
434
+ return events;
435
+ }
436
+
437
+ function readBranchEvents(branchName = 'main', options = {}) {
438
+ return readEvents({
439
+ ...options,
440
+ stream: EVENT_STREAMS.BRANCH,
441
+ branchId: branchName,
442
+ });
443
+ }
444
+
445
+ return {
446
+ appendEvent,
447
+ getBranchEventsHeadFile,
448
+ getRuntimeEventsFile,
449
+ getRuntimeEventsHeadFile,
450
+ getBranchEventsFile,
451
+ getEventsHead,
452
+ readEvents,
453
+ readBranchEvents,
454
+ };
455
+ }
456
+
457
+ module.exports = { createCanonicalEventLog };
@@ -0,0 +1,33 @@
1
+ const CANONICAL_REPLAY_ERROR_CODES = Object.freeze({
2
+ INVALID_JSONL: 'canonical_replay.invalid_jsonl',
3
+ INVALID_EVENT: 'canonical_replay.invalid_event',
4
+ INVALID_SEQUENCE: 'canonical_replay.invalid_sequence',
5
+ MISSING_CANONICAL_STREAM: 'canonical_replay.missing_canonical_stream',
6
+ });
7
+
8
+ function createCanonicalReplayError(code, message, details = {}) {
9
+ const error = new Error(message);
10
+ error.name = 'CanonicalReplayError';
11
+ error.code = code;
12
+
13
+ if (details && typeof details === 'object' && !Array.isArray(details)) {
14
+ Object.assign(error, details);
15
+ }
16
+
17
+ return error;
18
+ }
19
+
20
+ function isCanonicalReplayError(error) {
21
+ return Boolean(
22
+ error
23
+ && error.name === 'CanonicalReplayError'
24
+ && typeof error.code === 'string'
25
+ && Object.values(CANONICAL_REPLAY_ERROR_CODES).includes(error.code)
26
+ );
27
+ }
28
+
29
+ module.exports = {
30
+ CANONICAL_REPLAY_ERROR_CODES,
31
+ createCanonicalReplayError,
32
+ isCanonicalReplayError,
33
+ };