(path: string)
| 41 | * empty chain, so records are passed through unchanged, with no migration |
| 42 | * and no warning. */ |
| 43 | export async function readAgentWire(path: string): Promise<WireReadResult> { |
| 44 | const stream = createReadStream(path, { encoding: 'utf8' }); |
| 45 | const rl = createInterface({ input: stream, crlfDelay: Infinity }); |
| 46 | let lineNo = 0; |
| 47 | let metadata: WireReadResult['metadata'] | null = null; |
| 48 | let migrations: readonly WireMigration[] = []; |
| 49 | const records: WireEntry[] = []; |
| 50 | const warnings: string[] = []; |
| 51 | |
| 52 | for await (const line of rl) { |
| 53 | lineNo += 1; |
| 54 | if (line.length === 0) continue; |
| 55 | let parsed: unknown; |
| 56 | try { |
| 57 | parsed = JSON.parse(line); |
| 58 | } catch (error) { |
| 59 | warnings.push(`line ${lineNo}: invalid JSON (${(error as Error).message})`); |
| 60 | continue; |
| 61 | } |
| 62 | if (!isObject(parsed) || typeof parsed['type'] !== 'string') { |
| 63 | warnings.push(`line ${lineNo}: missing 'type' field`); |
| 64 | continue; |
| 65 | } |
| 66 | if (metadata === null) { |
| 67 | if (parsed['type'] !== 'metadata') { |
| 68 | throw new Error(`Wire file missing metadata header at line ${lineNo}`); |
| 69 | } |
| 70 | const pv = parsed['protocol_version']; |
| 71 | const ca = parsed['created_at']; |
| 72 | if (typeof pv !== 'string' || typeof ca !== 'number') { |
| 73 | throw new TypeError(`Wire metadata malformed at line ${lineNo}`); |
| 74 | } |
| 75 | try { |
| 76 | migrations = resolveWireMigrations(pv); |
| 77 | } catch (error) { |
| 78 | warnings.push( |
| 79 | `unrecognised protocol_version "${pv}" — parsing as best-effort (${(error as Error).message})`, |
| 80 | ); |
| 81 | migrations = bestEffortMigrations(); |
| 82 | } |
| 83 | metadata = { protocolVersion: pv, createdAt: ca }; |
| 84 | continue; |
| 85 | } |
| 86 | const raw = parsed as Record<string, unknown>; |
| 87 | let migrated: Record<string, unknown>; |
| 88 | try { |
| 89 | migrated = |
| 90 | migrations.length === 0 |
| 91 | ? (structuredClone(raw) as Record<string, unknown>) |
| 92 | : (migrateWireRecord( |
| 93 | raw as Record<string, unknown> & { type: string }, |
| 94 | migrations, |
| 95 | ) as Record<string, unknown>); |
| 96 | } catch (error) { |
| 97 | // A single record that won't migrate is not fatal — keep the raw |
| 98 | // payload so the UI can still render whatever fields it understands. |
| 99 | warnings.push( |
| 100 | `line ${lineNo}: migration failed (${(error as Error).message}); using raw record`, |
no test coverage detected