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,669 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const {
5
+ CANONICAL_REPLAY_ERROR_CODES,
6
+ createCanonicalReplayError,
7
+ } = require('../events/replay');
8
+ const { EVENT_STREAMS, validateCanonicalEvent } = require('../events/schema');
9
+
10
+ function createMessagesState(options = {}) {
11
+ const { io } = options;
12
+
13
+ function readJsonlLines(filePath) {
14
+ if (!filePath || !fs.existsSync(filePath)) return [];
15
+ const raw = fs.readFileSync(filePath, 'utf8');
16
+ if (!raw.trim()) return [];
17
+ return raw.split(/\r?\n/).filter(Boolean);
18
+ }
19
+
20
+ function writeJsonlLines(filePath, lines) {
21
+ io.ensureDataDir();
22
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
+ fs.writeFileSync(filePath, lines.join('\n') + (lines.length ? '\n' : ''));
24
+ return lines;
25
+ }
26
+
27
+ function cloneJsonValue(value) {
28
+ return value == null ? value : JSON.parse(JSON.stringify(value));
29
+ }
30
+
31
+ function createConversationProjection() {
32
+ return {
33
+ messages: [],
34
+ history: [],
35
+ };
36
+ }
37
+
38
+ function createScopedConversationProjection() {
39
+ return {
40
+ branch: createConversationProjection(),
41
+ channels: {},
42
+ };
43
+ }
44
+
45
+ function resolveMessageChannelName(message, fallbackChannel = null) {
46
+ const rawChannel = typeof fallbackChannel === 'string'
47
+ ? fallbackChannel
48
+ : (message && typeof message.channel === 'string' ? message.channel : '');
49
+ const channel = typeof rawChannel === 'string' ? rawChannel.trim() : '';
50
+ return channel && channel !== 'general' ? channel : null;
51
+ }
52
+
53
+ function getConversationProjectionForChannel(projection, channelName) {
54
+ if (!channelName) return projection.branch;
55
+ if (!projection.channels[channelName]) {
56
+ projection.channels[channelName] = createConversationProjection();
57
+ }
58
+ return projection.channels[channelName];
59
+ }
60
+
61
+ function listProjectionChannelNames(projection) {
62
+ return Object.keys(projection.channels || {}).sort();
63
+ }
64
+
65
+ function findProjectedMessageRecord(projection, messageId) {
66
+ if (!messageId) return null;
67
+
68
+ const scopes = [
69
+ { channel: null, conversation: projection.branch },
70
+ ...listProjectionChannelNames(projection).map((channelName) => ({
71
+ channel: channelName,
72
+ conversation: projection.channels[channelName],
73
+ })),
74
+ ];
75
+
76
+ for (const scope of scopes) {
77
+ const conversation = scope.conversation;
78
+ const messageIndex = conversation.messages.findIndex((message) => message && message.id === messageId);
79
+ const historyIndex = conversation.history.findIndex((message) => message && message.id === messageId);
80
+ if (messageIndex === -1 && historyIndex === -1) continue;
81
+
82
+ return {
83
+ channel: scope.channel,
84
+ conversation,
85
+ messageIndex,
86
+ historyIndex,
87
+ message: messageIndex >= 0 ? conversation.messages[messageIndex] : null,
88
+ historyMessage: historyIndex >= 0 ? conversation.history[historyIndex] : null,
89
+ };
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ function applyMessageCorrection(message, content, editedAt, options = {}) {
96
+ if (!message || typeof message !== 'object' || Array.isArray(message)) return;
97
+
98
+ if (options.includeHistory) {
99
+ if (!Array.isArray(message.edit_history)) message.edit_history = [];
100
+ message.edit_history.push({
101
+ content: message.content,
102
+ edited_at: editedAt,
103
+ });
104
+ if (message.edit_history.length > options.maxEditHistory) {
105
+ message.edit_history = message.edit_history.slice(-options.maxEditHistory);
106
+ }
107
+ }
108
+
109
+ message.content = content;
110
+ message.edited = true;
111
+ message.edited_at = editedAt;
112
+ }
113
+
114
+ function validateCanonicalMessageCorrectionPayload(event) {
115
+ const payload = event && event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload)
116
+ ? event.payload
117
+ : null;
118
+ if (!payload || typeof payload.message_id !== 'string' || payload.message_id.length === 0) {
119
+ throw createCanonicalReplayError(
120
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
121
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.corrected canonical events require payload.message_id to be a non-empty string.`,
122
+ {
123
+ event_type: event && event.type,
124
+ seq: event && event.seq,
125
+ branch_id: event && event.branch_id,
126
+ }
127
+ );
128
+ }
129
+
130
+ if (typeof payload.content !== 'string') {
131
+ throw createCanonicalReplayError(
132
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
133
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.corrected canonical events require payload.content to be a string.`,
134
+ {
135
+ event_type: event && event.type,
136
+ seq: event && event.seq,
137
+ branch_id: event && event.branch_id,
138
+ message_id: payload.message_id,
139
+ }
140
+ );
141
+ }
142
+
143
+ if ('edited_at' in payload && (typeof payload.edited_at !== 'string' || payload.edited_at.length === 0)) {
144
+ throw createCanonicalReplayError(
145
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
146
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.corrected canonical events require payload.edited_at to be a non-empty string when provided.`,
147
+ {
148
+ event_type: event && event.type,
149
+ seq: event && event.seq,
150
+ branch_id: event && event.branch_id,
151
+ message_id: payload.message_id,
152
+ }
153
+ );
154
+ }
155
+
156
+ if ('max_edit_history' in payload && (!Number.isInteger(payload.max_edit_history) || payload.max_edit_history <= 0)) {
157
+ throw createCanonicalReplayError(
158
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
159
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.corrected canonical events require payload.max_edit_history to be a positive integer when provided.`,
160
+ {
161
+ event_type: event && event.type,
162
+ seq: event && event.seq,
163
+ branch_id: event && event.branch_id,
164
+ message_id: payload.message_id,
165
+ }
166
+ );
167
+ }
168
+
169
+ return {
170
+ messageId: payload.message_id,
171
+ content: payload.content,
172
+ editedAt: typeof payload.edited_at === 'string' && payload.edited_at ? payload.edited_at : event.occurred_at,
173
+ maxEditHistory: Number.isInteger(payload.max_edit_history) && payload.max_edit_history > 0
174
+ ? payload.max_edit_history
175
+ : 10,
176
+ };
177
+ }
178
+
179
+ function validateCanonicalMessageRedactionPayload(event) {
180
+ const payload = event && event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload)
181
+ ? event.payload
182
+ : null;
183
+ if (!payload || typeof payload.message_id !== 'string' || payload.message_id.length === 0) {
184
+ throw createCanonicalReplayError(
185
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
186
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.redacted canonical events require payload.message_id to be a non-empty string.`,
187
+ {
188
+ event_type: event && event.type,
189
+ seq: event && event.seq,
190
+ branch_id: event && event.branch_id,
191
+ }
192
+ );
193
+ }
194
+
195
+ if ('redacted_at' in payload && (typeof payload.redacted_at !== 'string' || payload.redacted_at.length === 0)) {
196
+ throw createCanonicalReplayError(
197
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
198
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.redacted canonical events require payload.redacted_at to be a non-empty string when provided.`,
199
+ {
200
+ event_type: event && event.type,
201
+ seq: event && event.seq,
202
+ branch_id: event && event.branch_id,
203
+ message_id: payload.message_id,
204
+ }
205
+ );
206
+ }
207
+
208
+ return {
209
+ messageId: payload.message_id,
210
+ };
211
+ }
212
+
213
+ function buildConversationProjectionFromEvents(events) {
214
+ const projection = createScopedConversationProjection();
215
+ let eventsApplied = 0;
216
+
217
+ const replayEvents = Array.isArray(events)
218
+ ? events.filter((event) => event && typeof event.type === 'string' && event.type.startsWith('message.'))
219
+ : [];
220
+
221
+ validateCanonicalMessageReplayEvents(replayEvents);
222
+
223
+ for (const event of replayEvents) {
224
+ applyCanonicalMessageEvent(projection, event);
225
+ eventsApplied += 1;
226
+ }
227
+
228
+ return {
229
+ projection,
230
+ eventsApplied,
231
+ };
232
+ }
233
+
234
+ function describeReplayEvent(event) {
235
+ const type = event && typeof event.type === 'string' ? event.type : 'unknown';
236
+ const seq = event && Number.isInteger(event.seq) ? event.seq : 'unknown';
237
+ return `seq ${seq} (${type})`;
238
+ }
239
+
240
+ function validateCanonicalMessageReplayEvents(events) {
241
+ let previousEvent = null;
242
+ let replayBranchId = null;
243
+
244
+ for (const event of Array.isArray(events) ? events : []) {
245
+ const validation = validateCanonicalEvent(event);
246
+ if (!validation.ok) {
247
+ throw createCanonicalReplayError(
248
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
249
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: ${validation.problems.join('; ')}`,
250
+ {
251
+ event_type: event && event.type,
252
+ seq: event && event.seq,
253
+ branch_id: event && event.branch_id,
254
+ problems: validation.problems,
255
+ }
256
+ );
257
+ }
258
+
259
+ if (event.stream !== EVENT_STREAMS.BRANCH) {
260
+ throw createCanonicalReplayError(
261
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
262
+ `Canonical message replay requires branch-scoped events, received ${describeReplayEvent(event)} on ${String(event.stream)}.`,
263
+ {
264
+ event_type: event.type,
265
+ seq: event.seq,
266
+ branch_id: event.branch_id,
267
+ stream: event.stream,
268
+ }
269
+ );
270
+ }
271
+
272
+ if (!event.type.startsWith('message.')) {
273
+ throw createCanonicalReplayError(
274
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
275
+ `Canonical message replay only accepts message.* events, received ${describeReplayEvent(event)}.`,
276
+ {
277
+ event_type: event.type,
278
+ seq: event.seq,
279
+ branch_id: event.branch_id,
280
+ }
281
+ );
282
+ }
283
+
284
+ if (replayBranchId === null) {
285
+ replayBranchId = event.branch_id;
286
+ } else if (event.branch_id !== replayBranchId) {
287
+ throw createCanonicalReplayError(
288
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_SEQUENCE,
289
+ `Canonical message replay cannot mix branch streams: saw branch ${String(event.branch_id)} after branch ${String(replayBranchId)}.`,
290
+ {
291
+ previous_branch_id: replayBranchId,
292
+ branch_id: event.branch_id,
293
+ event_type: event.type,
294
+ seq: event.seq,
295
+ }
296
+ );
297
+ }
298
+
299
+ if (previousEvent && event.seq <= previousEvent.seq) {
300
+ throw createCanonicalReplayError(
301
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_SEQUENCE,
302
+ `Canonical message replay requires strictly increasing seq values for branch ${event.branch_id}: saw seq ${event.seq} after seq ${previousEvent.seq}.`,
303
+ {
304
+ previous_seq: previousEvent.seq,
305
+ seq: event.seq,
306
+ event_type: event.type,
307
+ branch_id: event.branch_id,
308
+ }
309
+ );
310
+ }
311
+
312
+ previousEvent = event;
313
+ }
314
+ }
315
+
316
+ function applyCanonicalMessageEvent(projection, event) {
317
+ const validation = validateCanonicalEvent(event);
318
+ if (!validation.ok) {
319
+ throw createCanonicalReplayError(
320
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
321
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: ${validation.problems.join('; ')}`,
322
+ {
323
+ event_type: event && event.type,
324
+ seq: event && event.seq,
325
+ branch_id: event && event.branch_id,
326
+ problems: validation.problems,
327
+ }
328
+ );
329
+ }
330
+
331
+ if (event.stream !== EVENT_STREAMS.BRANCH) {
332
+ throw createCanonicalReplayError(
333
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
334
+ `Canonical message replay requires branch-scoped events, received ${describeReplayEvent(event)} on ${String(event.stream)}.`,
335
+ {
336
+ event_type: event.type,
337
+ seq: event.seq,
338
+ branch_id: event.branch_id,
339
+ stream: event.stream,
340
+ }
341
+ );
342
+ }
343
+
344
+ switch (event.type) {
345
+ case 'message.sent': {
346
+ const message = event.payload && event.payload.message;
347
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
348
+ throw createCanonicalReplayError(
349
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
350
+ `Canonical message replay rejected invalid event ${describeReplayEvent(event)}: message.sent canonical events require payload.message to be an object.`,
351
+ {
352
+ event_type: event.type,
353
+ seq: event.seq,
354
+ branch_id: event.branch_id,
355
+ }
356
+ );
357
+ }
358
+
359
+ const channelName = resolveMessageChannelName(message);
360
+ const conversation = getConversationProjectionForChannel(projection, channelName);
361
+ conversation.messages.push(cloneJsonValue(message));
362
+ conversation.history.push(cloneJsonValue(message));
363
+ return projection;
364
+ }
365
+
366
+ case 'message.corrected': {
367
+ const correction = validateCanonicalMessageCorrectionPayload(event);
368
+ const record = findProjectedMessageRecord(projection, correction.messageId);
369
+ if (!record) {
370
+ throw createCanonicalReplayError(
371
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_SEQUENCE,
372
+ `Canonical message replay cannot apply ${describeReplayEvent(event)} because message ${correction.messageId} does not exist in the current branch projection.`,
373
+ {
374
+ event_type: event.type,
375
+ seq: event.seq,
376
+ branch_id: event.branch_id,
377
+ message_id: correction.messageId,
378
+ }
379
+ );
380
+ }
381
+
382
+ if (record.messageIndex >= 0) {
383
+ applyMessageCorrection(record.message, correction.content, correction.editedAt, {
384
+ includeHistory: false,
385
+ maxEditHistory: correction.maxEditHistory,
386
+ });
387
+ }
388
+ if (record.historyIndex >= 0) {
389
+ applyMessageCorrection(record.historyMessage, correction.content, correction.editedAt, {
390
+ includeHistory: true,
391
+ maxEditHistory: correction.maxEditHistory,
392
+ });
393
+ }
394
+ return projection;
395
+ }
396
+
397
+ case 'message.redacted': {
398
+ const redaction = validateCanonicalMessageRedactionPayload(event);
399
+ const record = findProjectedMessageRecord(projection, redaction.messageId);
400
+ if (!record) {
401
+ throw createCanonicalReplayError(
402
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_SEQUENCE,
403
+ `Canonical message replay cannot apply ${describeReplayEvent(event)} because message ${redaction.messageId} does not exist in the current branch projection.`,
404
+ {
405
+ event_type: event.type,
406
+ seq: event.seq,
407
+ branch_id: event.branch_id,
408
+ message_id: redaction.messageId,
409
+ }
410
+ );
411
+ }
412
+
413
+ if (record.messageIndex >= 0) {
414
+ record.conversation.messages.splice(record.messageIndex, 1);
415
+ }
416
+ if (record.historyIndex >= 0) {
417
+ record.conversation.history.splice(record.historyIndex, 1);
418
+ }
419
+ if (
420
+ record.channel
421
+ && record.conversation.messages.length === 0
422
+ && record.conversation.history.length === 0
423
+ ) {
424
+ delete projection.channels[record.channel];
425
+ }
426
+ return projection;
427
+ }
428
+
429
+ default:
430
+ throw createCanonicalReplayError(
431
+ CANONICAL_REPLAY_ERROR_CODES.INVALID_EVENT,
432
+ `Unknown canonical message event type: ${String(event.type)}`,
433
+ {
434
+ event_type: event.type,
435
+ seq: event.seq,
436
+ branch_id: event.branch_id,
437
+ }
438
+ );
439
+ }
440
+ }
441
+
442
+ function materializeConversationProjection(projection, targets) {
443
+ const branchTargets = targets && targets.branch
444
+ ? targets.branch
445
+ : targets;
446
+ if (!branchTargets || !branchTargets.messageFile || !branchTargets.historyFile) {
447
+ throw new Error('materializeConversationProjection requires branch messageFile and historyFile');
448
+ }
449
+
450
+ const configuredChannels = targets && targets.channels && typeof targets.channels === 'object' && !Array.isArray(targets.channels)
451
+ ? targets.channels
452
+ : {};
453
+ const resolveChannelTargets = targets && typeof targets.getChannelTargets === 'function'
454
+ ? targets.getChannelTargets
455
+ : null;
456
+ const allChannelNames = new Set([
457
+ ...Object.keys(configuredChannels),
458
+ ...listProjectionChannelNames(projection),
459
+ ]);
460
+
461
+ const writeConversation = (conversationProjection, conversationTargets) => {
462
+ const messageLines = conversationProjection.messages.map((message) => JSON.stringify(message));
463
+ const historyLines = conversationProjection.history.map((message) => JSON.stringify(message));
464
+
465
+ io.withLock(conversationTargets.historyFile, () => {
466
+ io.withLock(conversationTargets.messageFile, () => {
467
+ writeJsonlLines(conversationTargets.historyFile, historyLines);
468
+ writeJsonlLines(conversationTargets.messageFile, messageLines);
469
+ });
470
+ });
471
+ };
472
+
473
+ writeConversation(projection.branch, branchTargets);
474
+
475
+ for (const channelName of [...allChannelNames].sort()) {
476
+ const channelTargets = configuredChannels[channelName]
477
+ || (resolveChannelTargets ? resolveChannelTargets(channelName) : null);
478
+ if (!channelTargets || !channelTargets.messageFile || !channelTargets.historyFile) continue;
479
+ writeConversation(
480
+ projection.channels[channelName] || createConversationProjection(),
481
+ channelTargets
482
+ );
483
+ }
484
+
485
+ return {
486
+ message_count: projection.branch.messages.length,
487
+ history_count: projection.branch.history.length,
488
+ };
489
+ }
490
+
491
+ function rebuildConversationProjectionsFromEvents(events, targets) {
492
+ const { projection, eventsApplied } = buildConversationProjectionFromEvents(events);
493
+ const counts = materializeConversationProjection(projection, targets);
494
+ return {
495
+ events_applied: eventsApplied,
496
+ message_count: counts.message_count,
497
+ history_count: counts.history_count,
498
+ };
499
+ }
500
+
501
+ function getConversationMessageFromEvents(events, messageId) {
502
+ const { projection } = buildConversationProjectionFromEvents(events);
503
+ const record = findProjectedMessageRecord(projection, messageId);
504
+ if (!record) return null;
505
+ const message = record.message || record.historyMessage;
506
+ if (!message) return null;
507
+
508
+ return {
509
+ channel: record.channel,
510
+ message: cloneJsonValue(message),
511
+ };
512
+ }
513
+
514
+ function appendConversationMessage(message, targets) {
515
+ if (!targets || !targets.messageFile || !targets.historyFile) {
516
+ throw new Error('appendConversationMessage requires messageFile and historyFile');
517
+ }
518
+
519
+ io.withLock(targets.historyFile, () => {
520
+ io.withLock(targets.messageFile, () => {
521
+ io.appendJsonl(targets.messageFile, message);
522
+ io.appendJsonl(targets.historyFile, message);
523
+ });
524
+ });
525
+
526
+ return message;
527
+ }
528
+
529
+ function appendAuxiliaryMessage(message, filePath) {
530
+ if (!filePath) {
531
+ throw new Error('appendAuxiliaryMessage requires a file path');
532
+ }
533
+
534
+ io.appendJsonl(filePath, message);
535
+ return message;
536
+ }
537
+
538
+ function editConversationMessage(messageId, content, targets, options = {}) {
539
+ if (!messageId) {
540
+ throw new Error('editConversationMessage requires a message id');
541
+ }
542
+ if (!targets || !targets.messageFile || !targets.historyFile) {
543
+ throw new Error('editConversationMessage requires messageFile and historyFile');
544
+ }
545
+
546
+ const maxEditHistory = options.maxEditHistory || 10;
547
+ const editedAt = options.editedAt || new Date().toISOString();
548
+ let found = false;
549
+
550
+ io.withLock(targets.historyFile, () => {
551
+ const lines = readJsonlLines(targets.historyFile);
552
+ if (lines.length === 0) return;
553
+
554
+ const updated = lines.map((line) => {
555
+ try {
556
+ const message = JSON.parse(line);
557
+ if (message.id !== messageId) return line;
558
+
559
+ found = true;
560
+ if (!Array.isArray(message.edit_history)) message.edit_history = [];
561
+ message.edit_history.push({ content: message.content, edited_at: editedAt });
562
+ if (message.edit_history.length > maxEditHistory) {
563
+ message.edit_history = message.edit_history.slice(-maxEditHistory);
564
+ }
565
+ message.content = content;
566
+ message.edited = true;
567
+ message.edited_at = editedAt;
568
+ return JSON.stringify(message);
569
+ } catch {
570
+ return line;
571
+ }
572
+ });
573
+
574
+ if (found) writeJsonlLines(targets.historyFile, updated);
575
+ });
576
+
577
+ if (!found) return null;
578
+
579
+ io.withLock(targets.messageFile, () => {
580
+ const lines = readJsonlLines(targets.messageFile);
581
+ if (lines.length === 0) return;
582
+
583
+ const updated = lines.map((line) => {
584
+ try {
585
+ const message = JSON.parse(line);
586
+ if (message.id !== messageId) return line;
587
+ message.content = content;
588
+ message.edited = true;
589
+ message.edited_at = editedAt;
590
+ return JSON.stringify(message);
591
+ } catch {
592
+ return line;
593
+ }
594
+ });
595
+
596
+ writeJsonlLines(targets.messageFile, updated);
597
+ });
598
+
599
+ return { id: messageId, edited_at: editedAt };
600
+ }
601
+
602
+ function deleteConversationMessage(messageId, targets, options = {}) {
603
+ if (!messageId) {
604
+ throw new Error('deleteConversationMessage requires a message id');
605
+ }
606
+ if (!targets || !targets.messageFile || !targets.historyFile) {
607
+ throw new Error('deleteConversationMessage requires messageFile and historyFile');
608
+ }
609
+
610
+ const allowedFrom = Array.isArray(options.allowedFrom) ? options.allowedFrom : null;
611
+ let found = false;
612
+ let denied = false;
613
+ let messageFrom = null;
614
+
615
+ io.withLock(targets.historyFile, () => {
616
+ const lines = readJsonlLines(targets.historyFile);
617
+ if (lines.length === 0) return;
618
+
619
+ for (const line of lines) {
620
+ try {
621
+ const message = JSON.parse(line);
622
+ if (message.id === messageId) {
623
+ found = true;
624
+ messageFrom = message.from || null;
625
+ break;
626
+ }
627
+ } catch {}
628
+ }
629
+
630
+ if (!found) return;
631
+ if (allowedFrom && !allowedFrom.includes(messageFrom)) {
632
+ denied = true;
633
+ return;
634
+ }
635
+
636
+ const filtered = lines.filter((line) => {
637
+ try { return JSON.parse(line).id !== messageId; } catch { return true; }
638
+ });
639
+ writeJsonlLines(targets.historyFile, filtered);
640
+ });
641
+
642
+ if (!found || denied) {
643
+ return { found, denied, from: messageFrom };
644
+ }
645
+
646
+ io.withLock(targets.messageFile, () => {
647
+ const lines = readJsonlLines(targets.messageFile);
648
+ const filtered = lines.filter((line) => {
649
+ try { return JSON.parse(line).id !== messageId; } catch { return true; }
650
+ });
651
+ writeJsonlLines(targets.messageFile, filtered);
652
+ });
653
+
654
+ return { found: true, deleted: true, from: messageFrom };
655
+ }
656
+
657
+ return {
658
+ appendConversationMessage,
659
+ appendAuxiliaryMessage,
660
+ applyCanonicalMessageEvent,
661
+ materializeConversationProjection,
662
+ rebuildConversationProjectionsFromEvents,
663
+ getConversationMessageFromEvents,
664
+ editConversationMessage,
665
+ deleteConversationMessage,
666
+ };
667
+ }
668
+
669
+ module.exports = { createMessagesState };