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.
- package/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7214
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- 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 };
|
package/events/replay.js
ADDED
|
@@ -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
|
+
};
|