fe8a5e74d1
Compare memory userId and candidate deltas during manifest drift checks, add unit tests for false-positive cases, and verify /agent/resume plus session history loading against local v1.49. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.5 KiB
JavaScript
201 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Compare two Goose v1.49 memory manifests and report drift.
|
|
*/
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
function usage() {
|
|
return [
|
|
'Usage: node scripts/compare-goose-v149-memory-manifest.mjs [--baseline <path>] [--current <path>] [--json]',
|
|
'',
|
|
'Defaults:',
|
|
' baseline docs/baselines/goose-v149-memory-latest.json',
|
|
' current freshly exported manifest (via child process) unless --current is set',
|
|
].join('\n');
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
baseline: path.join(root, 'docs', 'baselines', 'goose-v149-memory-latest.json'),
|
|
current: '',
|
|
json: false,
|
|
exportCurrent: true,
|
|
};
|
|
for (let i = 2; i < argv.length; i += 1) {
|
|
const arg = argv[i];
|
|
if (arg === '--baseline' && argv[i + 1]) args.baseline = path.resolve(argv[++i]);
|
|
else if (arg === '--current' && argv[i + 1]) {
|
|
args.current = path.resolve(argv[++i]);
|
|
args.exportCurrent = false;
|
|
} else if (arg === '--json') args.json = true;
|
|
else if (arg === '--help' || arg === '-h') {
|
|
console.log(usage());
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`Unknown argument: ${arg}`);
|
|
console.error(usage());
|
|
process.exit(2);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
async function readManifest(filePath) {
|
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function indexMemories(manifest) {
|
|
const byId = new Map();
|
|
for (const item of manifest.memories ?? []) {
|
|
byId.set(item.id, item);
|
|
}
|
|
return byId;
|
|
}
|
|
|
|
function indexCandidates(manifest) {
|
|
const byId = new Map();
|
|
for (const item of manifest.candidates ?? []) {
|
|
byId.set(item.id, item);
|
|
}
|
|
return byId;
|
|
}
|
|
|
|
function compareManifests(baseline, current) {
|
|
const drift = {
|
|
baselineDigest: baseline.summary?.digest ?? null,
|
|
currentDigest: current.summary?.digest ?? null,
|
|
summary: {
|
|
userCountDelta: (current.summary?.userCount ?? 0) - (baseline.summary?.userCount ?? 0),
|
|
memoryCountDelta: (current.summary?.memoryItemCount ?? 0) - (baseline.summary?.memoryItemCount ?? 0),
|
|
candidateCountDelta: (current.summary?.candidateCount ?? 0) - (baseline.summary?.candidateCount ?? 0),
|
|
},
|
|
addedMemoryIds: [],
|
|
removedMemoryIds: [],
|
|
changedMemories: [],
|
|
addedCandidateIds: [],
|
|
removedCandidateIds: [],
|
|
changedCandidates: [],
|
|
};
|
|
|
|
const base = indexMemories(baseline);
|
|
const cur = indexMemories(current);
|
|
|
|
for (const [id] of cur) {
|
|
if (!base.has(id)) drift.addedMemoryIds.push(id);
|
|
}
|
|
for (const [id] of base) {
|
|
if (!cur.has(id)) drift.removedMemoryIds.push(id);
|
|
}
|
|
for (const [id, baseItem] of base) {
|
|
const curItem = cur.get(id);
|
|
if (!curItem) continue;
|
|
const fields = [
|
|
'userId',
|
|
'memoryHash',
|
|
'status',
|
|
'sourceSessionId',
|
|
'evidenceMessageId',
|
|
];
|
|
const changes = {};
|
|
for (const field of fields) {
|
|
if (String(baseItem[field] ?? '') !== String(curItem[field] ?? '')) {
|
|
changes[field] = { baseline: baseItem[field] ?? null, current: curItem[field] ?? null };
|
|
}
|
|
}
|
|
if (Object.keys(changes).length) {
|
|
drift.changedMemories.push({ id, userId: baseItem.userId, changes });
|
|
}
|
|
}
|
|
|
|
const baseCandidates = indexCandidates(baseline);
|
|
const curCandidates = indexCandidates(current);
|
|
for (const [id] of curCandidates) {
|
|
if (!baseCandidates.has(id)) drift.addedCandidateIds.push(id);
|
|
}
|
|
for (const [id] of baseCandidates) {
|
|
if (!curCandidates.has(id)) drift.removedCandidateIds.push(id);
|
|
}
|
|
for (const [id, baseItem] of baseCandidates) {
|
|
const curItem = curCandidates.get(id);
|
|
if (!curItem) continue;
|
|
const fields = ['userId', 'status', 'sourceSessionId'];
|
|
const changes = {};
|
|
for (const field of fields) {
|
|
if (String(baseItem[field] ?? '') !== String(curItem[field] ?? '')) {
|
|
changes[field] = { baseline: baseItem[field] ?? null, current: curItem[field] ?? null };
|
|
}
|
|
}
|
|
if (Object.keys(changes).length) {
|
|
drift.changedCandidates.push({ id, userId: baseItem.userId, changes });
|
|
}
|
|
}
|
|
|
|
drift.ok =
|
|
drift.addedMemoryIds.length === 0
|
|
&& drift.removedMemoryIds.length === 0
|
|
&& drift.changedMemories.length === 0
|
|
&& drift.addedCandidateIds.length === 0
|
|
&& drift.removedCandidateIds.length === 0
|
|
&& drift.changedCandidates.length === 0
|
|
&& drift.summary.candidateCountDelta === 0;
|
|
return drift;
|
|
}
|
|
|
|
async function exportCurrentManifest() {
|
|
const { spawnSync } = await import('node:child_process');
|
|
const tmpPath = path.join(root, 'docs', 'baselines', '.goose-v149-memory-current.json');
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
['scripts/export-goose-v149-memory-manifest.mjs', '--output', tmpPath, '--json'],
|
|
{ cwd: root, encoding: 'utf8', env: process.env },
|
|
);
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr || result.stdout || 'export manifest failed');
|
|
}
|
|
return readManifest(tmpPath);
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv);
|
|
const baseline = await readManifest(args.baseline);
|
|
const current = args.exportCurrent ? await exportCurrentManifest() : await readManifest(args.current);
|
|
const report = compareManifests(baseline, current);
|
|
|
|
if (args.json) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
} else if (report.ok) {
|
|
console.log('GOOSE_V149_MEMORY_DRIFT_OK: no user/memory/candidate drift');
|
|
console.log(` baselineDigest=${report.baselineDigest}`);
|
|
} else {
|
|
console.error('GOOSE_V149_MEMORY_DRIFT_FAIL:');
|
|
console.error(
|
|
` memory added=${report.addedMemoryIds.length} removed=${report.removedMemoryIds.length} `
|
|
+ `changed=${report.changedMemories.length}`,
|
|
);
|
|
console.error(
|
|
` candidate added=${report.addedCandidateIds.length} removed=${report.removedCandidateIds.length} `
|
|
+ `changed=${report.changedCandidates.length} delta=${report.summary.candidateCountDelta}`,
|
|
);
|
|
if (report.addedMemoryIds.length) {
|
|
console.error(` added sample: ${report.addedMemoryIds.slice(0, 5).join(', ')}`);
|
|
}
|
|
if (report.removedMemoryIds.length) {
|
|
console.error(` removed sample: ${report.removedMemoryIds.slice(0, 5).join(', ')}`);
|
|
}
|
|
if (report.changedMemories.length) {
|
|
console.error(` changed sample: ${JSON.stringify(report.changedMemories.slice(0, 3), null, 2)}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`GOOSE_V149_MEMORY_DRIFT_FAIL: ${error.message}`);
|
|
process.exit(1);
|
|
});
|