File size: 3,550 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Pure mapping between the agent-core-v2 session shapes and the v1 SDK wire
 * shapes. Two gaps are bridged here:
 * - the v2 `ISessionIndex` summary carries no filesystem facts, so the v1
 *   `SessionSummary`'s `workDir` / `sessionDir` come in as pre-resolved
 *   `SessionSummaryFacts` (the caller derives them from `ISessionContext`,
 *   `IBootstrapService.sessionDir`, and the workspace catalog);
 * - the v2 `SessionMeta` document keeps epoch-ms timestamps and a `cwd` field
 *   where the v1 `SessionMeta` keeps ISO strings and `workDir`.
 * Everything else is a field rename (`custom` ↔ `metadata`).
 */
import type {
  AgentMeta as V2AgentMeta,
  SessionMeta as V2SessionMeta,
  SessionSummary as V2SessionSummary,
} from '@moonshot-ai/agent-core-v2';

import { resolve, win32 } from 'node:path';

import type { AgentMeta, SessionMeta } from '#/replay';
import type { JsonObject, SessionSummary } from '#/types';

/**
 * Windows-shaped paths resolve through `win32` and fold to forward slashes,
 * everything else resolves against the process cwd. Byte-identical with the
 * legacy v1 `normalizeWorkDir` so stored workdir keys stay stable.
 */
export function normalizeWorkDir(workDir: string): string {
  if (/^[A-Za-z]:[\\/]/.test(workDir) || /^[\\/]{2}[^\\/]+[\\/][^\\/]+/.test(workDir)) {
    return win32.resolve(workDir).replaceAll('\\', '/');
  }
  return resolve(workDir);
}

/** v1 summary fields the v2 index summary does not carry, resolved by the caller. */
export interface SessionSummaryFacts {
  readonly workDir: string;
  readonly sessionDir: string;
  readonly additionalDirs?: readonly string[];
}

export function v2SummaryToSessionSummary(
  summary: V2SessionSummary,
  facts: SessionSummaryFacts,
): SessionSummary {
  return {
    id: summary.id,
    title: summary.title,
    lastPrompt: summary.lastPrompt,
    workDir: facts.workDir,
    sessionDir: facts.sessionDir,
    createdAt: summary.createdAt,
    updatedAt: summary.updatedAt,
    archived: summary.archived,
    metadata: summary.custom as JsonObject | undefined,
    additionalDirs: facts.additionalDirs,
    lastTurnReason: summary.lastTurnReason,
  };
}

/**
 * v1's `SessionMeta` types `title` / `isCustomTitle` / `custom` as required;
 * a v2 document that never set them reports the same neutral values v1's
 * defaults produce (`''` / `false` / `{}`). Timestamps convert epoch ms → ISO.
 */
export function v2MetaToSessionMeta(meta: V2SessionMeta): SessionMeta {
  return {
    createdAt: new Date(meta.createdAt).toISOString(),
    updatedAt: new Date(meta.updatedAt).toISOString(),
    title: meta.title ?? '',
    isCustomTitle: meta.titleKind === 'custom',
    lastPrompt: meta.lastPrompt,
    forkedFrom: meta.forkedFrom,
    workDir: meta.cwd,
    agents: v2AgentsToV1(meta.agents ?? {}),
    custom: (meta.custom ?? {}) as Record<string, unknown>,
  };
}

function v2AgentsToV1(agents: Readonly<Record<string, V2AgentMeta>>): Record<string, AgentMeta> {
  const mapped: Record<string, AgentMeta> = {};
  for (const [agentId, agent] of Object.entries(agents)) {
    mapped[agentId] = {
      homedir: agent.homedir,
      // v2 registers every agent with a type; the fallbacks only cover
      // documents written before that registration existed.
      type: agent.type ?? (agentId === 'main' ? 'main' : 'sub'),
      // v1 persists an explicit null for a parentless agent where v2 leaves
      // the field unset.
      parentAgentId: agent.parentAgentId ?? null,
      swarmItem: agent.swarmItem,
    };
  }
  return mapped;
}