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);
}