File size: 6,265 Bytes
5710dd0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import { createReadStream } from 'node:fs';
import { basename, dirname } from 'node:path';
import { createInterface } from 'node:readline';
import {
isNewerWireVersion,
migrateV1_4ToV1_5,
migrateWireRecord,
resolveWireMigrations,
type WireMigration,
} from '@moonshot-ai/agent-core-v2/wire/migration/migration';
import type { AgentRecord, WireEntry } from './agent-record-types';
export interface WireReadResult {
metadata: { protocolVersion: string; createdAt: number };
records: ReadonlyArray<WireEntry>;
warnings: string[];
}
/** Best-effort fallback for a wire file whose declared `protocol_version` is
* below the known migration chain (below 1.0, or otherwise unrecognized-low):
* `resolveWireMigrations` threw for it. We retry from the oldest known version
* (1.0) and warn the caller; if even that fails we pass records through
* unchanged. (Versions at/above the current 1.5 never reach here — they
* resolve to an empty chain and are passed through directly.) */
function bestEffortMigrations(): readonly WireMigration[] {
try {
return resolveWireMigrations('1.0');
} catch {
return [];
}
}
/** Read a single agent's `wire.jsonl`.
*
* Each record is returned as a `WireEntry` containing both the on-disk parsed
* form (`raw`) and the migrated current-protocol form (`data`). The reader
* never rejects a file over its `protocol_version`:
* - below-1.0 (or otherwise unrecognized-low) — `resolveWireMigrations`
* throws, so records run through the 1.0-onwards best-effort chain and a
* warning is added to `warnings[]` so the UI can surface the caveat;
* - no metadata header — mirrors core-v2's recovery path by treating the
* journal as v1.4 and applying the v1.4 → v1.5 migration in memory;
* - at/above the current 1.5 (including future versions) — resolves to an
* empty chain, so records are passed through unchanged, with no migration
* and no warning. */
export async function readAgentWire(path: string): Promise<WireReadResult> {
const stream = createReadStream(path, { encoding: 'utf8' });
const rl = createInterface({ input: stream, crlfDelay: Infinity });
let lineNo = 0;
let metadata: WireReadResult['metadata'] | null = null;
let migrations: readonly WireMigration[] = [];
let newerWireVersion = false;
const records: WireEntry[] = [];
const warnings: string[] = [];
const agentId = basename(dirname(path));
for await (const line of rl) {
lineNo += 1;
if (line.length === 0) continue;
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch (error) {
warnings.push(`line ${lineNo}: invalid JSON (${(error as Error).message})`);
continue;
}
if (!isObject(parsed) || typeof parsed['type'] !== 'string') {
warnings.push(`line ${lineNo}: missing 'type' field`);
continue;
}
if (metadata === null) {
if (parsed['type'] === 'metadata') {
const pv = parsed['protocol_version'];
const ca = parsed['created_at'];
if (typeof pv !== 'string' || typeof ca !== 'number') {
throw new TypeError(`Wire metadata malformed at line ${lineNo}`);
}
newerWireVersion = isNewerWireVersion(pv);
try {
migrations = resolveWireMigrations(pv);
} catch (error) {
warnings.push(
`unrecognised protocol_version "${pv}" — parsing as best-effort (${(error as Error).message})`,
);
migrations = bestEffortMigrations();
}
metadata = { protocolVersion: pv, createdAt: ca };
continue;
} else {
warnings.push(
`line ${lineNo}: missing metadata header — assuming protocol_version "${migrateV1_4ToV1_5.sourceVersion}"`,
);
migrations = [migrateV1_4ToV1_5];
metadata = {
protocolVersion: migrateV1_4ToV1_5.sourceVersion,
createdAt: 0,
};
}
}
const raw = parsed;
let migrated: Record<string, unknown>;
try {
migrated =
migrations.length === 0
? structuredClone(raw)
: (migrateWireRecord(
raw as Record<string, unknown> & { type: string },
migrations,
) as Record<string, unknown>);
} catch (error) {
// A single record that won't migrate is not fatal — keep the raw
// payload so the UI can still render whatever fields it understands.
warnings.push(
`line ${lineNo}: migration failed (${(error as Error).message}); using raw record`,
);
migrated = structuredClone(raw);
}
const normalized = newerWireVersion
? migrated
: normalizePlanRevisionRecord(migrated, agentId);
if (normalized === undefined) {
warnings.push(`line ${lineNo}: invalid legacy plan.revision record skipped`);
continue;
}
records.push({ lineNo, data: normalized as AgentRecord, raw });
}
if (metadata === null) {
throw new Error('Wire file is empty (no metadata)');
}
return { metadata, records, warnings };
}
function normalizePlanRevisionRecord(
record: Record<string, unknown>,
agentId: string,
): Record<string, unknown> | undefined {
if (record['type'] !== 'plan.revision' || 'key' in record) return record;
const legacyPath = record['path'];
if (typeof legacyPath !== 'string') return undefined;
const key = extractLegacyPlanRevisionKey(legacyPath, agentId);
if (key === undefined) return undefined;
const { path: _path, ...rest } = record;
return { ...rest, key };
}
function extractLegacyPlanRevisionKey(path: string, agentId: string): string | undefined {
if (path.includes('\\')) return undefined;
const segments = path.split('/');
if (
segments.length < 8 ||
segments[0] !== 'sessions' ||
segments[3] !== 'agents' ||
segments[4] !== agentId ||
segments
.slice(1, 3)
.some((segment) => segment.length === 0 || segment === '.' || segment === '..')
) {
return undefined;
}
const key = segments.slice(5).join('/');
return /^plan\/[^/]+\/v[0-9]+\.md$/.test(key) ? key : undefined;
}
function isObject(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
|