let-them-talk 5.3.0 → 5.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/README.md +346 -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 +864 -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 +141 -34
- 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 +9577 -7216
- 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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
|
|
8
|
+
const {
|
|
9
|
+
CANONICAL_REPLAY_ERROR_CODES,
|
|
10
|
+
isCanonicalReplayError,
|
|
11
|
+
} = require(path.resolve(__dirname, '..', 'events', 'replay.js'));
|
|
12
|
+
const {
|
|
13
|
+
createBranchPathResolvers,
|
|
14
|
+
createCanonicalState,
|
|
15
|
+
} = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
16
|
+
|
|
17
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
18
|
+
const RUNTIME_CONTRACT_PATH = path.join(REPO_ROOT, 'docs', 'architecture', 'runtime-contract.md');
|
|
19
|
+
const RUNTIME_CONTRACT_DISPLAY = 'docs/architecture/runtime-contract.md';
|
|
20
|
+
const HARDENING_DOC_PATH = path.join(REPO_ROOT, 'docs', 'architecture', 'runtime-migration-hardening.md');
|
|
21
|
+
const HARDENING_DOC_DISPLAY = 'docs/architecture/runtime-migration-hardening.md';
|
|
22
|
+
const USAGE = 'Usage: node agent-bridge/scripts/check-migration-hardening.js [--scenario healthy|legacy-projection-without-canonical-log]';
|
|
23
|
+
|
|
24
|
+
const SCENARIOS = Object.freeze({
|
|
25
|
+
healthy: 'healthy',
|
|
26
|
+
legacyProjectionWithoutCanonicalLog: 'legacy-projection-without-canonical-log',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const REQUIRED_RUNTIME_CONTRACT_SNIPPETS = Object.freeze([
|
|
30
|
+
'- if a compatibility projection exists but its canonical event stream is missing, rebuild and rollback MUST fail explicitly instead of treating the projection as authoritative.',
|
|
31
|
+
'Migration hardening is not complete until deterministic validation proves both canonical-first rebuild and explicit rejection of legacy-only rollback assumptions. Task 13C freezes that validator-facing slice in `docs/architecture/runtime-migration-hardening.md`.',
|
|
32
|
+
'- Missing canonical stream with surviving compatibility projections -> fail explicit rebuild/rollback checks instead of promoting the projection back to authority.',
|
|
33
|
+
'9. compatibility projections are rejected as rollback authority when the corresponding canonical stream is missing.',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const REQUIRED_HARDENING_DOC_HEADINGS = Object.freeze([
|
|
37
|
+
'## Cutover invariants',
|
|
38
|
+
'## Rollback and recovery rules',
|
|
39
|
+
'## Stale transitional assumptions that stay invalid',
|
|
40
|
+
'## Guarded runtime slice in current code',
|
|
41
|
+
'## Validation path',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const REQUIRED_HARDENING_DOC_SNIPPETS = Object.freeze([
|
|
45
|
+
'Legacy filenames such as `messages.jsonl`, `history.jsonl`, `tasks.json`, and `workflows.json` are compatibility projections during migration. They are not rollback authority.',
|
|
46
|
+
'If a compatibility projection exists without its canonical event stream, rebuild and rollback checks MUST fail explicitly instead of silently promoting the projection back to authority.',
|
|
47
|
+
'Legacy `messages.jsonl` or `history.jsonl` can stand in for a missing canonical branch event log during rebuild',
|
|
48
|
+
'The second command exits `1` by design. It proves the runtime does not treat surviving compatibility projections as rollback authority when the canonical branch event stream is missing.',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function fail(lines, exitCode = 1) {
|
|
52
|
+
fs.writeSync(2, lines.join('\n') + '\n');
|
|
53
|
+
process.exit(exitCode);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
if (argv.length === 0) {
|
|
58
|
+
return { scenario: SCENARIOS.healthy };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (argv.length === 2 && argv[0] === '--scenario') {
|
|
62
|
+
const scenario = argv[1];
|
|
63
|
+
const supportedScenarios = Object.values(SCENARIOS);
|
|
64
|
+
if (!supportedScenarios.includes(scenario)) {
|
|
65
|
+
fail([
|
|
66
|
+
`Unknown scenario: ${scenario}`,
|
|
67
|
+
`Supported scenarios: ${supportedScenarios.join(', ')}`,
|
|
68
|
+
USAGE,
|
|
69
|
+
], 2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { scenario };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fail([USAGE], 2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function collectHeadings(markdown) {
|
|
79
|
+
return new Set(
|
|
80
|
+
markdown
|
|
81
|
+
.split(/\r?\n/)
|
|
82
|
+
.map((line) => line.trimEnd())
|
|
83
|
+
.filter((line) => /^(##|###)\s+/.test(line))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readRequiredFile(filePath, displayPath) {
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
fail([
|
|
90
|
+
'Migration hardening validation failed.',
|
|
91
|
+
`Missing file: ${displayPath}`,
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function expectIncludes(problems, markdown, snippet, label) {
|
|
99
|
+
if (!markdown.includes(snippet)) {
|
|
100
|
+
problems.push(`${label} Missing snippet: ${snippet}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function expectHeading(problems, headings, heading, label) {
|
|
105
|
+
if (!headings.has(heading)) {
|
|
106
|
+
problems.push(`${label} Missing heading: ${heading}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toJsonl(messages) {
|
|
111
|
+
return messages.map((message) => JSON.stringify(message)).join('\n') + (messages.length ? '\n' : '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function relativeFromDataDir(dataDir, filePath) {
|
|
115
|
+
return path.relative(dataDir, filePath).split(path.sep).join('/');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function deleteFile(filePath) {
|
|
119
|
+
if (fs.existsSync(filePath)) {
|
|
120
|
+
fs.unlinkSync(filePath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateDocs() {
|
|
125
|
+
const problems = [];
|
|
126
|
+
const runtimeContract = readRequiredFile(RUNTIME_CONTRACT_PATH, RUNTIME_CONTRACT_DISPLAY);
|
|
127
|
+
const hardeningDoc = readRequiredFile(HARDENING_DOC_PATH, HARDENING_DOC_DISPLAY);
|
|
128
|
+
const hardeningHeadings = collectHeadings(hardeningDoc);
|
|
129
|
+
|
|
130
|
+
for (const snippet of REQUIRED_RUNTIME_CONTRACT_SNIPPETS) {
|
|
131
|
+
expectIncludes(problems, runtimeContract, snippet, `${RUNTIME_CONTRACT_DISPLAY}.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const heading of REQUIRED_HARDENING_DOC_HEADINGS) {
|
|
135
|
+
expectHeading(problems, hardeningHeadings, heading, `${HARDENING_DOC_DISPLAY}.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const snippet of REQUIRED_HARDENING_DOC_SNIPPETS) {
|
|
139
|
+
expectIncludes(problems, hardeningDoc, snippet, `${HARDENING_DOC_DISPLAY}.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return problems;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runHealthyScenario() {
|
|
146
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-migration-hardening-'));
|
|
147
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
148
|
+
const branchName = 'feature_migration_guard';
|
|
149
|
+
const branchPaths = createBranchPathResolvers(dataDir);
|
|
150
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: 4545 });
|
|
151
|
+
const canonicalEventLog = createCanonicalEventLog({ dataDir });
|
|
152
|
+
const message = {
|
|
153
|
+
id: 'msg-migration-guard-1',
|
|
154
|
+
from: 'alpha',
|
|
155
|
+
to: 'beta',
|
|
156
|
+
content: 'Migration guard baseline',
|
|
157
|
+
timestamp: '2026-04-16T23:40:00.000Z',
|
|
158
|
+
reply_to: null,
|
|
159
|
+
system: false,
|
|
160
|
+
};
|
|
161
|
+
const expectedJsonl = toJsonl([message]);
|
|
162
|
+
const problems = [];
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
canonicalState.appendMessage(message, { branch: branchName });
|
|
166
|
+
|
|
167
|
+
const targets = branchPaths.getMessageTargets(branchName);
|
|
168
|
+
const eventFile = canonicalEventLog.getBranchEventsFile(branchName);
|
|
169
|
+
const eventFileRelative = relativeFromDataDir(dataDir, eventFile);
|
|
170
|
+
const messageFileRelative = relativeFromDataDir(dataDir, targets.messageFile);
|
|
171
|
+
const historyFileRelative = relativeFromDataDir(dataDir, targets.historyFile);
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(eventFile)) {
|
|
174
|
+
problems.push(`Missing canonical branch event file: ${eventFileRelative}`);
|
|
175
|
+
}
|
|
176
|
+
if (!fs.existsSync(targets.messageFile)) {
|
|
177
|
+
problems.push(`Missing compatibility message projection: ${messageFileRelative}`);
|
|
178
|
+
}
|
|
179
|
+
if (!fs.existsSync(targets.historyFile)) {
|
|
180
|
+
problems.push(`Missing compatibility history projection: ${historyFileRelative}`);
|
|
181
|
+
}
|
|
182
|
+
if (eventFileRelative !== `runtime/branches/${branchName}/events.jsonl`) {
|
|
183
|
+
problems.push(`Canonical message events should live under runtime/branches/<branch>/events.jsonl. Actual: ${eventFileRelative}`);
|
|
184
|
+
}
|
|
185
|
+
if (messageFileRelative !== `branch-${branchName}-messages.jsonl`) {
|
|
186
|
+
problems.push(`Compatibility message projection path drifted. Actual: ${messageFileRelative}`);
|
|
187
|
+
}
|
|
188
|
+
if (historyFileRelative !== `branch-${branchName}-history.jsonl`) {
|
|
189
|
+
problems.push(`Compatibility history projection path drifted. Actual: ${historyFileRelative}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const canonicalBeforeRebuild = fs.readFileSync(eventFile, 'utf8');
|
|
193
|
+
deleteFile(targets.messageFile);
|
|
194
|
+
deleteFile(targets.historyFile);
|
|
195
|
+
canonicalState.rebuildMessageProjections({ branch: branchName });
|
|
196
|
+
const canonicalAfterRebuild = fs.readFileSync(eventFile, 'utf8');
|
|
197
|
+
|
|
198
|
+
if (canonicalBeforeRebuild !== canonicalAfterRebuild) {
|
|
199
|
+
problems.push('Projection rebuild mutated the canonical event log instead of treating it as read-only authority.');
|
|
200
|
+
}
|
|
201
|
+
if (fs.readFileSync(targets.messageFile, 'utf8') !== expectedJsonl) {
|
|
202
|
+
problems.push('Compatibility messages.jsonl projection did not rebuild deterministically from canonical events.');
|
|
203
|
+
}
|
|
204
|
+
if (fs.readFileSync(targets.historyFile, 'utf8') !== expectedJsonl) {
|
|
205
|
+
problems.push('Compatibility history.jsonl projection did not rebuild deterministically from canonical events.');
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return problems;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runLegacyProjectionWithoutCanonicalLogScenario() {
|
|
215
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-migration-legacy-only-'));
|
|
216
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
217
|
+
const branchName = 'feature_migration_guard';
|
|
218
|
+
const branchPaths = createBranchPathResolvers(dataDir);
|
|
219
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: 4646 });
|
|
220
|
+
const targets = branchPaths.getMessageTargets(branchName);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
fs.mkdirSync(path.dirname(targets.messageFile), { recursive: true });
|
|
224
|
+
fs.writeFileSync(targets.messageFile, '{"id":"legacy-only-message"}\n');
|
|
225
|
+
fs.writeFileSync(targets.historyFile, '{"id":"legacy-only-message"}\n');
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
canonicalState.rebuildMessageProjections({ branch: branchName });
|
|
229
|
+
fail([
|
|
230
|
+
'Migration hardening validation failed.',
|
|
231
|
+
`Scenario: ${SCENARIOS.legacyProjectionWithoutCanonicalLog}`,
|
|
232
|
+
'Expected rebuild to reject the legacy-only projection fixture, but it succeeded.',
|
|
233
|
+
]);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (!isCanonicalReplayError(error) || error.code !== CANONICAL_REPLAY_ERROR_CODES.MISSING_CANONICAL_STREAM) {
|
|
236
|
+
fail([
|
|
237
|
+
'Migration hardening validation failed.',
|
|
238
|
+
`Scenario: ${SCENARIOS.legacyProjectionWithoutCanonicalLog}`,
|
|
239
|
+
`Expected replay error code: ${CANONICAL_REPLAY_ERROR_CODES.MISSING_CANONICAL_STREAM}`,
|
|
240
|
+
`Actual error code: ${error && error.code ? error.code : 'unknown'}`,
|
|
241
|
+
`Actual error message: ${error && error.message ? error.message : String(error)}`,
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!String(error.message).includes('legacy-only recovery')
|
|
246
|
+
|| !String(error.message).includes('compatibility projections still exist')) {
|
|
247
|
+
fail([
|
|
248
|
+
'Migration hardening validation failed.',
|
|
249
|
+
`Scenario: ${SCENARIOS.legacyProjectionWithoutCanonicalLog}`,
|
|
250
|
+
'Expected the rejection message to explain the legacy-only rebuild guard clearly.',
|
|
251
|
+
`Actual error message: ${error.message}`,
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fail([
|
|
256
|
+
'Migration hardening rejected legacy-only rebuild fixture.',
|
|
257
|
+
`Scenario: ${SCENARIOS.legacyProjectionWithoutCanonicalLog}`,
|
|
258
|
+
`Replay error code: ${error.code}`,
|
|
259
|
+
`Replay error message: ${error.message}`,
|
|
260
|
+
]);
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function main() {
|
|
268
|
+
const { scenario } = parseArgs(process.argv.slice(2));
|
|
269
|
+
const docProblems = validateDocs();
|
|
270
|
+
|
|
271
|
+
if (docProblems.length > 0) {
|
|
272
|
+
fail([
|
|
273
|
+
'Migration hardening validation failed.',
|
|
274
|
+
...docProblems.map((problem) => `- ${problem}`),
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (scenario === SCENARIOS.legacyProjectionWithoutCanonicalLog) {
|
|
279
|
+
runLegacyProjectionWithoutCanonicalLogScenario();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const healthyProblems = runHealthyScenario();
|
|
284
|
+
if (healthyProblems.length > 0) {
|
|
285
|
+
fail([
|
|
286
|
+
'Migration hardening validation failed.',
|
|
287
|
+
`Scenario: ${SCENARIOS.healthy}`,
|
|
288
|
+
...healthyProblems.map((problem) => `- ${problem}`),
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log([
|
|
293
|
+
'Migration hardening validation passed.',
|
|
294
|
+
`Checked file: ${RUNTIME_CONTRACT_DISPLAY}`,
|
|
295
|
+
`Checked file: ${HARDENING_DOC_DISPLAY}`,
|
|
296
|
+
'Validated canonical-first rebuild from branch-local event streams, read-only canonical rollback behavior, and explicit rejection of legacy-only projection authority.',
|
|
297
|
+
].join('\n'));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
main();
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { createCanonicalEventLog } = require(path.resolve(__dirname, '..', 'events', 'log.js'));
|
|
8
|
+
const { createCanonicalState, createBranchPathResolvers } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
9
|
+
const { createStateIo } = require(path.resolve(__dirname, '..', 'state', 'io.js'));
|
|
10
|
+
const { createSessionsState } = require(path.resolve(__dirname, '..', 'state', 'sessions.js'));
|
|
11
|
+
|
|
12
|
+
function fail(lines, exitCode = 1) {
|
|
13
|
+
fs.writeSync(2, lines.join('\n') + '\n');
|
|
14
|
+
process.exit(exitCode);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assert(condition, message, problems) {
|
|
18
|
+
if (!condition) problems.push(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeJson(filePath, value) {
|
|
22
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeTarget(filePath) {
|
|
27
|
+
return typeof filePath === 'string' ? path.resolve(filePath) : filePath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function withBlockedReadFileSync(blockedFiles, fn) {
|
|
31
|
+
const blocked = new Set(blockedFiles.map(normalizeTarget));
|
|
32
|
+
const originalReadFileSync = fs.readFileSync;
|
|
33
|
+
|
|
34
|
+
fs.readFileSync = function patchedReadFileSync(filePath, ...args) {
|
|
35
|
+
if (blocked.has(normalizeTarget(filePath))) {
|
|
36
|
+
throw new Error(`Blocked readFileSync for hot-path validation: ${filePath}`);
|
|
37
|
+
}
|
|
38
|
+
return originalReadFileSync.call(this, filePath, ...args);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return fn();
|
|
43
|
+
} finally {
|
|
44
|
+
fs.readFileSync = originalReadFileSync;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function withBlockedReaddirSync(blockedDirs, fn) {
|
|
49
|
+
const blocked = new Set(blockedDirs.map(normalizeTarget));
|
|
50
|
+
const originalReaddirSync = fs.readdirSync;
|
|
51
|
+
|
|
52
|
+
fs.readdirSync = function patchedReaddirSync(dirPath, ...args) {
|
|
53
|
+
if (blocked.has(normalizeTarget(dirPath))) {
|
|
54
|
+
throw new Error(`Blocked readdirSync for hot-path validation: ${dirPath}`);
|
|
55
|
+
}
|
|
56
|
+
return originalReaddirSync.call(this, dirPath, ...args);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return fn();
|
|
61
|
+
} finally {
|
|
62
|
+
fs.readdirSync = originalReaddirSync;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function main() {
|
|
67
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-performance-indexing-'));
|
|
68
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
69
|
+
const branchName = 'feature_perf_index';
|
|
70
|
+
const secondaryBranchName = 'feature_perf_index_secondary';
|
|
71
|
+
const problems = [];
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const branchPaths = createBranchPathResolvers(dataDir);
|
|
75
|
+
const io = createStateIo({ dataDir });
|
|
76
|
+
const eventLog = createCanonicalEventLog({ dataDir });
|
|
77
|
+
const sessionsState = createSessionsState({ io, branchPaths, canonicalEventLog: eventLog });
|
|
78
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: process.pid });
|
|
79
|
+
|
|
80
|
+
eventLog.appendEvent({
|
|
81
|
+
type: 'session.started',
|
|
82
|
+
branchId: branchName,
|
|
83
|
+
actorAgent: 'alpha',
|
|
84
|
+
sessionId: 'sess_perf_alpha',
|
|
85
|
+
occurredAt: '2026-04-16T23:20:00.000Z',
|
|
86
|
+
payload: {
|
|
87
|
+
state: 'active',
|
|
88
|
+
reason: 'validator_setup',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const secondEvent = eventLog.appendEvent({
|
|
92
|
+
type: 'session.resumed',
|
|
93
|
+
branchId: branchName,
|
|
94
|
+
actorAgent: 'alpha',
|
|
95
|
+
sessionId: 'sess_perf_alpha',
|
|
96
|
+
occurredAt: '2026-04-16T23:20:05.000Z',
|
|
97
|
+
payload: {
|
|
98
|
+
state: 'active',
|
|
99
|
+
reason: 'validator_setup',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const eventFile = eventLog.getBranchEventsFile(branchName);
|
|
104
|
+
const headFile = eventLog.getBranchEventsHeadFile(branchName);
|
|
105
|
+
fs.unlinkSync(headFile);
|
|
106
|
+
|
|
107
|
+
const coldHeadLog = createCanonicalEventLog({ dataDir });
|
|
108
|
+
let repairedHead = null;
|
|
109
|
+
withBlockedReadFileSync([eventFile], () => {
|
|
110
|
+
repairedHead = coldHeadLog.getEventsHead({
|
|
111
|
+
stream: 'branch',
|
|
112
|
+
branchId: branchName,
|
|
113
|
+
at: '2026-04-16T23:20:06.000Z',
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert(repairedHead && repairedHead.last_seq === secondEvent.seq, 'Event heads should rebuild from the stream tail without a full event-file read when the head is missing.', problems);
|
|
118
|
+
assert(repairedHead && repairedHead.event_count === secondEvent.seq, 'Tail-repaired event heads should preserve the monotonic branch event count.', problems);
|
|
119
|
+
|
|
120
|
+
fs.unlinkSync(headFile);
|
|
121
|
+
const coldAppendLog = createCanonicalEventLog({ dataDir });
|
|
122
|
+
let thirdEvent = null;
|
|
123
|
+
withBlockedReadFileSync([eventFile], () => {
|
|
124
|
+
thirdEvent = coldAppendLog.appendEvent({
|
|
125
|
+
type: 'session.completed',
|
|
126
|
+
branchId: branchName,
|
|
127
|
+
actorAgent: 'alpha',
|
|
128
|
+
sessionId: 'sess_perf_alpha',
|
|
129
|
+
occurredAt: '2026-04-16T23:20:10.000Z',
|
|
130
|
+
payload: {
|
|
131
|
+
state: 'completed',
|
|
132
|
+
reason: 'validator_setup',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
assert(thirdEvent && thirdEvent.seq === secondEvent.seq + 1, 'Event append should derive the next seq from tail/head metadata without rescanning the branch log.', problems);
|
|
137
|
+
|
|
138
|
+
const generalMessage = {
|
|
139
|
+
id: 'msg_perf_general',
|
|
140
|
+
from: 'alpha',
|
|
141
|
+
to: 'beta',
|
|
142
|
+
content: 'General projection baseline',
|
|
143
|
+
timestamp: '2026-04-16T23:21:00.000Z',
|
|
144
|
+
};
|
|
145
|
+
const labMessage = {
|
|
146
|
+
id: 'msg_perf_lab',
|
|
147
|
+
from: 'beta',
|
|
148
|
+
to: 'alpha',
|
|
149
|
+
channel: 'lab',
|
|
150
|
+
content: 'Lab query needle',
|
|
151
|
+
timestamp: '2026-04-16T23:21:10.000Z',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
canonicalState.appendMessage(generalMessage, { branch: branchName });
|
|
155
|
+
canonicalState.appendScopedMessage(labMessage, { branch: branchName, channel: 'lab' });
|
|
156
|
+
writeJson(branchPaths.getChannelsFile(branchName), {
|
|
157
|
+
general: {
|
|
158
|
+
description: 'General channel',
|
|
159
|
+
members: ['*'],
|
|
160
|
+
},
|
|
161
|
+
lab: {
|
|
162
|
+
description: 'Perf validator lab channel',
|
|
163
|
+
members: ['alpha', 'beta'],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
writeJson(branchPaths.getAcksFile(branchName), {
|
|
167
|
+
[generalMessage.id]: true,
|
|
168
|
+
[labMessage.id]: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const initialConversation = canonicalState.getConversationMessages({ branch: branchName });
|
|
172
|
+
const projectionFile = branchPaths.getBranchDashboardProjectionFile(branchName);
|
|
173
|
+
assert(initialConversation.length === 2, 'Dashboard conversation projection should merge general and non-general branch history.', problems);
|
|
174
|
+
assert(fs.existsSync(projectionFile), 'Dashboard branch queries should persist a branch-scoped merged-history projection file.', problems);
|
|
175
|
+
|
|
176
|
+
const historyFile = branchPaths.getHistoryFile(branchName);
|
|
177
|
+
const channelHistoryFile = branchPaths.getChannelHistoryFile('lab', branchName);
|
|
178
|
+
let cachedConversation = null;
|
|
179
|
+
let cachedChannels = null;
|
|
180
|
+
let cachedSearch = null;
|
|
181
|
+
withBlockedReadFileSync([historyFile, channelHistoryFile], () => {
|
|
182
|
+
cachedConversation = canonicalState.getConversationMessages({ branch: branchName });
|
|
183
|
+
cachedChannels = canonicalState.getChannelsView({ branch: branchName });
|
|
184
|
+
cachedSearch = canonicalState.getSearchResultsView({
|
|
185
|
+
branch: branchName,
|
|
186
|
+
query: 'query needle',
|
|
187
|
+
limit: 10,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
assert(Array.isArray(cachedConversation) && cachedConversation.length === 2, 'Cached dashboard conversation reads should succeed from the shared projection without rereading branch history files.', problems);
|
|
192
|
+
assert(cachedChannels && cachedChannels.lab && cachedChannels.lab.message_count === 1, 'Cached dashboard channel queries should preserve branch-local channel counts from the shared projection.', problems);
|
|
193
|
+
assert(Array.isArray(cachedSearch) && cachedSearch.length === 1 && cachedSearch[0].id === labMessage.id, 'Cached dashboard search should reuse the shared merged projection and stay branch-local.', problems);
|
|
194
|
+
|
|
195
|
+
fs.unlinkSync(projectionFile);
|
|
196
|
+
const rebuiltConversation = canonicalState.getConversationMessages({ branch: branchName });
|
|
197
|
+
assert(Array.isArray(rebuiltConversation) && rebuiltConversation.length === 2 && fs.existsSync(projectionFile), 'Dashboard query projection should rebuild cleanly when the branch projection file is missing.', problems);
|
|
198
|
+
|
|
199
|
+
const activation = sessionsState.activateSession({
|
|
200
|
+
agentName: 'alpha',
|
|
201
|
+
branchName,
|
|
202
|
+
provider: 'claude',
|
|
203
|
+
reason: 'register',
|
|
204
|
+
at: '2026-04-16T23:22:00.000Z',
|
|
205
|
+
});
|
|
206
|
+
sessionsState.touchSession({
|
|
207
|
+
sessionId: activation.session.session_id,
|
|
208
|
+
branchName,
|
|
209
|
+
at: '2026-04-16T23:22:10.000Z',
|
|
210
|
+
heartbeat: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const secondaryActivation = sessionsState.activateSession({
|
|
214
|
+
agentName: 'alpha',
|
|
215
|
+
branchName: secondaryBranchName,
|
|
216
|
+
provider: 'claude',
|
|
217
|
+
reason: 'branch_switch',
|
|
218
|
+
at: '2026-04-16T23:22:20.000Z',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const indexFile = branchPaths.getSessionsIndexFile();
|
|
222
|
+
const indexRaw = fs.readFileSync(indexFile, 'utf8');
|
|
223
|
+
const index = JSON.parse(indexRaw);
|
|
224
|
+
assert(!indexRaw.includes('\n'), 'Session index writes should stay compact on hot heartbeat/touch paths to reduce churn.', problems);
|
|
225
|
+
assert(index.by_agent.alpha && index.by_agent.alpha.latest_branch_id === secondaryBranchName, 'Session index should track the latest branch per agent for hot resume lookups.', problems);
|
|
226
|
+
assert(index.by_agent.alpha && index.by_agent.alpha.active_session_id === secondaryActivation.session.session_id && index.by_agent.alpha.active_branch_id === secondaryBranchName, 'Session index should point agent-level active session metadata at the latest active branch session.', problems);
|
|
227
|
+
assert(index.by_branch[branchName] && index.by_branch[branchName].latest_by_agent && index.by_branch[branchName].latest_by_agent.alpha === activation.session.session_id, 'Session index should expose the latest session id per branch+agent.', problems);
|
|
228
|
+
assert(index.by_branch[secondaryBranchName] && index.by_branch[secondaryBranchName].latest_by_agent && index.by_branch[secondaryBranchName].latest_by_agent.alpha === secondaryActivation.session.session_id, 'Session index should expose the latest session id for newly activated secondary branches.', problems);
|
|
229
|
+
|
|
230
|
+
sessionsState.transitionSession({
|
|
231
|
+
sessionId: secondaryActivation.session.session_id,
|
|
232
|
+
branchName: secondaryBranchName,
|
|
233
|
+
state: 'interrupted',
|
|
234
|
+
reason: 'secondary_branch_paused',
|
|
235
|
+
at: '2026-04-16T23:22:30.000Z',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const repointedIndex = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
239
|
+
assert(repointedIndex.by_agent.alpha && repointedIndex.by_agent.alpha.active_session_id === activation.session.session_id && repointedIndex.by_agent.alpha.active_branch_id === branchName, 'Session index should repoint the agent active-session pointer to another still-active branch session instead of clearing it.', problems);
|
|
240
|
+
|
|
241
|
+
const sessionsDir = branchPaths.getBranchSessionsDir(branchName);
|
|
242
|
+
let latestSession = null;
|
|
243
|
+
let latestSummary = null;
|
|
244
|
+
let exactSummary = null;
|
|
245
|
+
withBlockedReaddirSync([sessionsDir], () => {
|
|
246
|
+
latestSession = sessionsState.getLatestSessionForAgent(branchName, 'alpha');
|
|
247
|
+
latestSummary = sessionsState.getLatestSessionSummaryForAgent(branchName, 'alpha', {
|
|
248
|
+
indexedAt: '2026-04-16T23:22:15.000Z',
|
|
249
|
+
});
|
|
250
|
+
exactSummary = sessionsState.getSessionSummary(activation.session.session_id, branchName, {
|
|
251
|
+
indexedAt: '2026-04-16T23:22:15.000Z',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
assert(latestSession && latestSession.session_id === activation.session.session_id, 'Latest session manifest lookup should resolve from the session index without rescanning the branch sessions directory.', problems);
|
|
256
|
+
assert(latestSummary && latestSummary.session_id === activation.session.session_id, 'Latest session summary lookup should resolve from the session index hot path.', problems);
|
|
257
|
+
assert(exactSummary && exactSummary.session_id === activation.session.session_id && exactSummary.last_heartbeat_at === '2026-04-16T23:22:10.000Z', 'Direct session summary lookup should reuse indexed summary state before falling back to the manifest.', problems);
|
|
258
|
+
|
|
259
|
+
if (problems.length > 0) {
|
|
260
|
+
fail(['Performance/indexing validation failed.', ...problems.map((problem) => `- ${problem}`)], 1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log([
|
|
264
|
+
'Performance/indexing validation passed.',
|
|
265
|
+
'Validated tail-based canonical event-head repair, branch-scoped dashboard query projection reuse/rebuild, and session-index-driven hot lookups with compact touch-path index writes.',
|
|
266
|
+
].join('\n'));
|
|
267
|
+
} finally {
|
|
268
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
main();
|