@ycodium-ai/plugin-session-import 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/dist/index.js +1954 -0
  3. package/package.json +54 -0
package/dist/index.js ADDED
@@ -0,0 +1,1954 @@
1
+ // src/index.ts
2
+ import { definePlugin } from "@ycodium-ai/plugin-api";
3
+
4
+ // src/schemas.ts
5
+ var nonEmptyString = { type: "string", minLength: 1 };
6
+ var nullableString = {
7
+ oneOf: [{ type: "string", minLength: 1 }, { type: "null" }]
8
+ };
9
+ var nonNegativeInt = { type: "integer", minimum: 0 };
10
+ var providerKind = { type: "string", enum: ["claude", "codex"] };
11
+ var phase = { type: "string", enum: ["read", "create", "link"] };
12
+ var candidate = {
13
+ type: "object",
14
+ additionalProperties: false,
15
+ required: [
16
+ "provider",
17
+ "instanceId",
18
+ "nativeSessionId",
19
+ "title",
20
+ "preview",
21
+ "model",
22
+ "turnCount",
23
+ "updatedAt",
24
+ "cwd",
25
+ "availability",
26
+ "openElsewhere"
27
+ ],
28
+ properties: {
29
+ provider: providerKind,
30
+ instanceId: nonEmptyString,
31
+ nativeSessionId: nonEmptyString,
32
+ title: nullableString,
33
+ preview: nullableString,
34
+ model: nullableString,
35
+ turnCount: nonNegativeInt,
36
+ updatedAt: { type: "number" },
37
+ cwd: nonEmptyString,
38
+ availability: { type: "string", enum: ["importable", "alreadyImported"] },
39
+ openElsewhere: { type: "boolean" },
40
+ importedThreadId: nonEmptyString
41
+ }
42
+ };
43
+ var source = {
44
+ type: "object",
45
+ additionalProperties: false,
46
+ required: ["instanceId", "provider", "candidates", "unavailableReason"],
47
+ properties: {
48
+ instanceId: nonEmptyString,
49
+ provider: providerKind,
50
+ candidates: { type: "array", items: candidate },
51
+ unavailableReason: nullableString
52
+ }
53
+ };
54
+ var sessionImportListInputSchema = {
55
+ type: "object",
56
+ additionalProperties: false,
57
+ required: ["projectId"],
58
+ properties: {
59
+ projectId: nonEmptyString,
60
+ directory: nonEmptyString
61
+ }
62
+ };
63
+ var sessionImportListResultSchema = {
64
+ type: "object",
65
+ additionalProperties: false,
66
+ required: ["sources", "directory"],
67
+ properties: {
68
+ sources: { type: "array", items: source },
69
+ directory: nonEmptyString
70
+ }
71
+ };
72
+ var sessionImportInputSchema = {
73
+ type: "object",
74
+ additionalProperties: false,
75
+ required: ["projectId", "instanceId", "provider", "nativeSessionId", "cwd", "threadId"],
76
+ properties: {
77
+ projectId: nonEmptyString,
78
+ instanceId: nonEmptyString,
79
+ provider: providerKind,
80
+ nativeSessionId: nonEmptyString,
81
+ cwd: nonEmptyString,
82
+ threadId: nonEmptyString
83
+ }
84
+ };
85
+ var sessionImportProgressEventSchema = {
86
+ oneOf: [
87
+ {
88
+ type: "object",
89
+ additionalProperties: false,
90
+ required: ["kind", "threadId", "phases"],
91
+ properties: {
92
+ kind: { const: "import_started" },
93
+ threadId: nonEmptyString,
94
+ phases: { type: "array", items: phase }
95
+ }
96
+ },
97
+ {
98
+ type: "object",
99
+ additionalProperties: false,
100
+ required: ["kind", "threadId", "phase"],
101
+ properties: {
102
+ kind: { const: "phase_started" },
103
+ threadId: nonEmptyString,
104
+ phase
105
+ }
106
+ },
107
+ {
108
+ type: "object",
109
+ additionalProperties: false,
110
+ required: ["kind", "threadId", "availableMessages", "restoredMessages"],
111
+ properties: {
112
+ kind: { const: "session_read" },
113
+ threadId: nonEmptyString,
114
+ availableMessages: nonNegativeInt,
115
+ restoredMessages: nonNegativeInt,
116
+ availableActivities: nonNegativeInt,
117
+ restoredActivities: nonNegativeInt
118
+ }
119
+ },
120
+ {
121
+ type: "object",
122
+ additionalProperties: false,
123
+ required: ["kind", "threadId", "restoredMessages"],
124
+ properties: {
125
+ kind: { const: "import_finished" },
126
+ threadId: nonEmptyString,
127
+ restoredMessages: nonNegativeInt,
128
+ restoredActivities: nonNegativeInt
129
+ }
130
+ },
131
+ {
132
+ type: "object",
133
+ additionalProperties: false,
134
+ required: ["kind", "threadId", "phase", "message", "threadCreated"],
135
+ properties: {
136
+ kind: { const: "import_failed" },
137
+ threadId: nonEmptyString,
138
+ phase,
139
+ message: nonEmptyString,
140
+ threadCreated: { type: "boolean" }
141
+ }
142
+ }
143
+ ]
144
+ };
145
+ var sessionImportSyncInputSchema = {
146
+ type: "object",
147
+ additionalProperties: false,
148
+ required: ["threadId"],
149
+ properties: {
150
+ threadId: nonEmptyString
151
+ }
152
+ };
153
+ var sessionImportSyncResultSchema = {
154
+ type: "object",
155
+ additionalProperties: false,
156
+ required: ["status", "appended"],
157
+ properties: {
158
+ status: {
159
+ type: "string",
160
+ enum: ["not-imported", "unchanged", "synced", "skipped-active", "failed"]
161
+ },
162
+ appended: nonNegativeInt
163
+ }
164
+ };
165
+
166
+ // src/sessionImportRuntime.ts
167
+ import * as NodeCrypto3 from "node:crypto";
168
+ import * as NodeProcess2 from "node:process";
169
+
170
+ // src/claudeSessionScan.ts
171
+ import * as NodePath from "node:path";
172
+ import * as NodeProcess from "node:process";
173
+
174
+ // src/sessionScope.ts
175
+ var EXTENDED_LENGTH_UNC = "//?/UNC/";
176
+ var EXTENDED_LENGTH = "//?/";
177
+ function normalize(value, caseInsensitive) {
178
+ let unified = value.trim().replace(/\\/gu, "/");
179
+ const upper = unified.toUpperCase();
180
+ if (upper.startsWith(EXTENDED_LENGTH_UNC)) {
181
+ unified = `//${unified.slice(EXTENDED_LENGTH_UNC.length)}`;
182
+ } else if (upper.startsWith(EXTENDED_LENGTH)) {
183
+ unified = unified.slice(EXTENDED_LENGTH.length);
184
+ }
185
+ const isUnc = unified.startsWith("//");
186
+ unified = unified.replace(/\/+/gu, "/").replace(/\/+$/u, "");
187
+ if (isUnc) unified = `/${unified}`;
188
+ return caseInsensitive ? unified.toLowerCase() : unified;
189
+ }
190
+ function isWithinDirectory(candidate2, root, caseInsensitive) {
191
+ const normalizedRoot = normalize(root, caseInsensitive);
192
+ if (normalizedRoot.length === 0 || normalizedRoot === "/") return false;
193
+ const normalizedCandidate = normalize(candidate2, caseInsensitive);
194
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
195
+ }
196
+
197
+ // src/claudeSessionScan.ts
198
+ var PREVIEW_LIMIT = 200;
199
+ var MAX_SCANNED_FILES = 200;
200
+ var MAX_ROWS = 50;
201
+ var MAX_PARSED_LINE_BYTES = 1e6;
202
+ var TRANSCRIPT_SUFFIX = ".jsonl";
203
+ function truncate(value) {
204
+ const trimmed = value?.trim() ?? "";
205
+ if (trimmed.length === 0) return null;
206
+ return trimmed.length <= PREVIEW_LIMIT ? trimmed : `${trimmed.slice(0, PREVIEW_LIMIT)}\u2026`;
207
+ }
208
+ function encodeClaudeProjectDirName(cwd) {
209
+ return cwd.replace(/[^A-Za-z0-9]/gu, "-");
210
+ }
211
+ function textFromContent(content) {
212
+ if (typeof content === "string") return content.trim();
213
+ if (!Array.isArray(content)) return "";
214
+ const parts = [];
215
+ for (const block of content) {
216
+ if (!block || typeof block !== "object") continue;
217
+ const typed = block;
218
+ if (typed.type === "text" && typeof typed.text === "string") parts.push(typed.text);
219
+ }
220
+ return parts.join("\n\n").trim();
221
+ }
222
+ async function readClaudeTranscriptDigest(fs, filePath) {
223
+ let cwd = null;
224
+ let model = null;
225
+ let preview = null;
226
+ let customTitle = null;
227
+ let aiTitle = null;
228
+ let turnCount = 0;
229
+ try {
230
+ for await (const line of fs.readLines(filePath)) {
231
+ const isUser = line.includes('"type":"user"');
232
+ const wantsAssistant = model === null && line.includes('"type":"assistant"');
233
+ const isTitle = line.includes('"custom-title"') || line.includes('"ai-title"');
234
+ if (!isUser && !wantsAssistant && !isTitle) continue;
235
+ if (line.length > MAX_PARSED_LINE_BYTES) continue;
236
+ let record3;
237
+ try {
238
+ record3 = JSON.parse(line);
239
+ } catch {
240
+ continue;
241
+ }
242
+ if (!record3 || typeof record3 !== "object") continue;
243
+ const typed = record3;
244
+ if (cwd === null && typeof typed.cwd === "string" && typed.cwd.length > 0) cwd = typed.cwd;
245
+ if (typed.type === "custom-title" && typeof typed.customTitle === "string") {
246
+ customTitle = typed.customTitle;
247
+ continue;
248
+ }
249
+ if (typed.type === "ai-title" && typeof typed.aiTitle === "string") {
250
+ aiTitle = typed.aiTitle;
251
+ continue;
252
+ }
253
+ if (typed.isSidechain === true) continue;
254
+ if (typed.type === "assistant") {
255
+ if (model === null && typeof typed.message?.model === "string") model = typed.message.model;
256
+ continue;
257
+ }
258
+ if (typed.type !== "user") continue;
259
+ const text = textFromContent(typed.message?.content);
260
+ if (text.length === 0) continue;
261
+ turnCount += 1;
262
+ if (preview === null) preview = text;
263
+ }
264
+ } catch {
265
+ return null;
266
+ }
267
+ return {
268
+ cwd,
269
+ title: truncate(customTitle ?? aiTitle),
270
+ preview: truncate(preview),
271
+ model,
272
+ turnCount
273
+ };
274
+ }
275
+ async function listTranscriptFiles(fs, transcriptDir, directory) {
276
+ let dirs;
277
+ try {
278
+ dirs = (await fs.readDir(transcriptDir)).filter((entry) => entry.type === "directory").map((entry) => entry.name);
279
+ } catch {
280
+ return [];
281
+ }
282
+ const encodedRoot = encodeClaudeProjectDirName(directory);
283
+ const matched = dirs.filter((name) => name.startsWith(encodedRoot));
284
+ const scanned = matched.length > 0 ? matched : dirs;
285
+ const found = [];
286
+ for (const dir of scanned) {
287
+ const projectDir = NodePath.join(transcriptDir, dir);
288
+ let names;
289
+ try {
290
+ names = (await fs.readDir(projectDir)).filter((entry) => entry.type === "file" && entry.name.endsWith(TRANSCRIPT_SUFFIX)).map((entry) => entry.name);
291
+ } catch {
292
+ continue;
293
+ }
294
+ for (const name of names) {
295
+ let stat;
296
+ try {
297
+ stat = await fs.stat(NodePath.join(projectDir, name));
298
+ } catch {
299
+ continue;
300
+ }
301
+ const mtimeMs = stat.modifiedAtIso === null ? 0 : Date.parse(stat.modifiedAtIso);
302
+ found.push({
303
+ path: NodePath.join(projectDir, name),
304
+ sessionId: name.slice(0, -TRANSCRIPT_SUFFIX.length),
305
+ size: stat.size,
306
+ mtimeMs: Number.isFinite(mtimeMs) ? mtimeMs : 0
307
+ });
308
+ }
309
+ }
310
+ return found;
311
+ }
312
+ async function readClaudeLiveSessionIds(fs, configDir) {
313
+ const live = /* @__PURE__ */ new Set();
314
+ const markerDir = NodePath.join(configDir, "sessions");
315
+ let entries;
316
+ try {
317
+ entries = (await fs.readDir(markerDir)).filter((entry) => entry.type === "file" && entry.name.endsWith(".json")).map((entry) => entry.name);
318
+ } catch {
319
+ return live;
320
+ }
321
+ for (const entry of entries) {
322
+ let marker;
323
+ try {
324
+ marker = JSON.parse(await fs.readFile(NodePath.join(markerDir, entry)));
325
+ } catch {
326
+ continue;
327
+ }
328
+ if (!marker || typeof marker !== "object") continue;
329
+ const typed = marker;
330
+ if (typeof typed.pid !== "number" || typeof typed.sessionId !== "string") continue;
331
+ try {
332
+ NodeProcess.kill(typed.pid, 0);
333
+ } catch {
334
+ continue;
335
+ }
336
+ live.add(typed.sessionId);
337
+ }
338
+ return live;
339
+ }
340
+ var digestCacheKey = (file) => `claude-digest:${file.path}:${String(file.size)}:${String(file.mtimeMs)}`;
341
+ async function scanClaudeSessions(input) {
342
+ const root = await input.fs.stat(input.transcriptDir);
343
+ if (root.type !== "directory") {
344
+ throw new Error(`The Claude transcript directory ${input.transcriptDir} is not a directory.`);
345
+ }
346
+ const files = [...await listTranscriptFiles(input.fs, input.transcriptDir, input.directory)].sort((left, right) => right.mtimeMs - left.mtimeMs).slice(0, MAX_SCANNED_FILES);
347
+ const live = await readClaudeLiveSessionIds(input.fs, input.configDir);
348
+ const rows = [];
349
+ for (const file of files) {
350
+ if (rows.length >= MAX_ROWS) break;
351
+ const key = digestCacheKey(file);
352
+ const cached = input.cache === void 0 ? null : await input.cache.getJson(key);
353
+ let digest = cached?.value ?? null;
354
+ if (digest === null) {
355
+ digest = await readClaudeTranscriptDigest(input.fs, file.path);
356
+ if (digest === null) continue;
357
+ await input.cache?.setJson(key, digest);
358
+ }
359
+ if (digest.cwd === null) continue;
360
+ if (!isWithinDirectory(digest.cwd, input.directory, input.caseInsensitive)) continue;
361
+ rows.push({
362
+ sessionId: file.sessionId,
363
+ cwd: digest.cwd,
364
+ title: digest.title,
365
+ preview: digest.preview,
366
+ model: digest.model,
367
+ turnCount: digest.turnCount,
368
+ updatedAt: file.mtimeMs,
369
+ openElsewhere: live.has(file.sessionId)
370
+ });
371
+ }
372
+ return rows;
373
+ }
374
+ async function readClaudeSessionRecords(input) {
375
+ const path = await resolveTranscriptPath(input);
376
+ if (path === null) {
377
+ throw new Error(`No transcript file for Claude session ${input.sessionId}.`);
378
+ }
379
+ const records = [];
380
+ for await (const line of input.fs.readLines(path)) {
381
+ if (line.length > MAX_PARSED_LINE_BYTES) continue;
382
+ if (!line.includes('"type":"user"') && !line.includes('"type":"assistant"')) continue;
383
+ try {
384
+ records.push(JSON.parse(line));
385
+ } catch {
386
+ continue;
387
+ }
388
+ }
389
+ return records;
390
+ }
391
+ async function resolveTranscriptPath(input) {
392
+ const fileName = `${input.sessionId}${TRANSCRIPT_SUFFIX}`;
393
+ if (input.cwd !== void 0 && input.cwd.length > 0) {
394
+ const direct = NodePath.join(
395
+ input.transcriptDir,
396
+ encodeClaudeProjectDirName(input.cwd),
397
+ fileName
398
+ );
399
+ try {
400
+ if ((await input.fs.stat(direct)).type === "file") return direct;
401
+ } catch {
402
+ }
403
+ }
404
+ let dirs;
405
+ try {
406
+ dirs = (await input.fs.readDir(input.transcriptDir)).filter((entry) => entry.type === "directory").map((entry) => entry.name);
407
+ } catch {
408
+ return null;
409
+ }
410
+ for (const dir of dirs) {
411
+ const candidate2 = NodePath.join(input.transcriptDir, dir, fileName);
412
+ try {
413
+ if ((await input.fs.stat(candidate2)).type === "file") return candidate2;
414
+ } catch {
415
+ continue;
416
+ }
417
+ }
418
+ return null;
419
+ }
420
+ async function claudeSessionModifiedAt(input) {
421
+ const path = await resolveTranscriptPath(input);
422
+ if (path === null) return null;
423
+ try {
424
+ const stat = await input.fs.stat(path);
425
+ const mtimeMs = stat.modifiedAtIso === null ? 0 : Date.parse(stat.modifiedAtIso);
426
+ return Number.isFinite(mtimeMs) ? mtimeMs : 0;
427
+ } catch {
428
+ return null;
429
+ }
430
+ }
431
+
432
+ // src/codexAppServer.ts
433
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
434
+ var STDERR_TAIL_LIMIT = 2e3;
435
+ var CODEX_INITIALIZE_PARAMS = {
436
+ clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
437
+ capabilities: { experimentalApi: true }
438
+ };
439
+ async function openCodexAppServer(input) {
440
+ const handle = await input.process.spawn({
441
+ program: input.program,
442
+ args: ["app-server", ...input.args],
443
+ ...input.env === void 0 ? {} : { env: input.env }
444
+ });
445
+ const pending = /* @__PURE__ */ new Map();
446
+ const decoder = new TextDecoder();
447
+ let nextId = 1;
448
+ let stderrTail = "";
449
+ let exitReason = null;
450
+ const failAll = (reason) => {
451
+ exitReason = reason;
452
+ for (const [, entry] of pending) {
453
+ entry.reject(new Error(`codex app-server ${entry.method} failed: ${reason}`));
454
+ }
455
+ pending.clear();
456
+ };
457
+ const dispatch = (line) => {
458
+ if (line.trim().length === 0) return;
459
+ let message;
460
+ try {
461
+ message = JSON.parse(line);
462
+ } catch {
463
+ return;
464
+ }
465
+ if (!message || typeof message !== "object") return;
466
+ const typed = message;
467
+ if (typeof typed.id !== "number") return;
468
+ const entry = pending.get(typed.id);
469
+ if (entry === void 0) return;
470
+ pending.delete(typed.id);
471
+ if (typed.error !== void 0 && typed.error !== null) {
472
+ const detail = typeof typed.error.message === "string" ? typed.error.message : JSON.stringify(typed.error);
473
+ entry.reject(new Error(`codex app-server ${entry.method} failed: ${detail}`));
474
+ return;
475
+ }
476
+ entry.resolve(typed.result);
477
+ };
478
+ const pumpStdout = (async () => {
479
+ let buffer = "";
480
+ for await (const chunk of handle.stdout) {
481
+ buffer += decoder.decode(chunk, { stream: true });
482
+ let index = buffer.indexOf("\n");
483
+ while (index >= 0) {
484
+ dispatch(buffer.slice(0, index));
485
+ buffer = buffer.slice(index + 1);
486
+ index = buffer.indexOf("\n");
487
+ }
488
+ }
489
+ dispatch(buffer);
490
+ })().catch(() => void 0);
491
+ const pumpStderr = (async () => {
492
+ const errorDecoder = new TextDecoder();
493
+ for await (const chunk of handle.stderr) {
494
+ stderrTail = `${stderrTail}${errorDecoder.decode(chunk, { stream: true })}`.slice(
495
+ -STDERR_TAIL_LIMIT
496
+ );
497
+ }
498
+ })().catch(() => void 0);
499
+ void handle.exited.then((code) => {
500
+ failAll(
501
+ `the process exited with code ${String(code)}${stderrTail.length > 0 ? `: ${stderrTail.trim()}` : ""}`
502
+ );
503
+ }).catch(() => failAll("the process ended unexpectedly"));
504
+ const timeoutMs = input.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
505
+ const request = async (method, params) => {
506
+ if (exitReason !== null) throw new Error(`codex app-server ${method} failed: ${exitReason}`);
507
+ const id = nextId;
508
+ nextId += 1;
509
+ const settled = new Promise((resolve, reject) => {
510
+ pending.set(id, { resolve, reject, method });
511
+ });
512
+ await handle.writeStdin(
513
+ `${JSON.stringify({ id, method, ...params === void 0 ? {} : { params } })}
514
+ `
515
+ );
516
+ let timer;
517
+ try {
518
+ return await Promise.race([
519
+ settled,
520
+ new Promise((_, reject) => {
521
+ timer = setTimeout(() => {
522
+ pending.delete(id);
523
+ reject(new Error(`codex app-server ${method} timed out after ${String(timeoutMs)}ms`));
524
+ }, timeoutMs);
525
+ })
526
+ ]);
527
+ } finally {
528
+ if (timer !== void 0) clearTimeout(timer);
529
+ }
530
+ };
531
+ const notify = async (method, params) => {
532
+ await handle.writeStdin(
533
+ `${JSON.stringify({ method, ...params === void 0 ? {} : { params } })}
534
+ `
535
+ );
536
+ };
537
+ const close = async () => {
538
+ try {
539
+ await handle.closeStdin();
540
+ } catch {
541
+ }
542
+ try {
543
+ await handle.kill();
544
+ } catch {
545
+ }
546
+ failAll("the client was closed");
547
+ await Promise.all([pumpStdout, pumpStderr]);
548
+ };
549
+ try {
550
+ await request("initialize", CODEX_INITIALIZE_PARAMS);
551
+ await notify("initialized");
552
+ } catch (error) {
553
+ await close();
554
+ throw error;
555
+ }
556
+ return { request, notify, close };
557
+ }
558
+
559
+ // src/codexSessionScan.ts
560
+ import * as NodePath2 from "node:path";
561
+ var PAGE_LIMIT = 50;
562
+ var MAX_PAGES = 10;
563
+ var MAX_ROWS2 = 50;
564
+ var PREVIEW_LIMIT2 = 200;
565
+ function truncate2(value) {
566
+ const trimmed = value?.trim() ?? "";
567
+ if (trimmed.length === 0) return null;
568
+ return trimmed.length <= PREVIEW_LIMIT2 ? trimmed : `${trimmed.slice(0, PREVIEW_LIMIT2)}\u2026`;
569
+ }
570
+ var asString = (value) => typeof value === "string" ? value : null;
571
+ var nativeModel = (value) => {
572
+ const model = asString(value)?.trim();
573
+ return model && model !== "<synthetic>" ? model : null;
574
+ };
575
+ var asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
576
+ function readListPage(response) {
577
+ if (!response || typeof response !== "object") return { threads: [], nextCursor: null };
578
+ const typed = response;
579
+ const rows = Array.isArray(typed.data) ? typed.data : [];
580
+ const threads = [];
581
+ for (const row of rows) {
582
+ if (!row || typeof row !== "object") continue;
583
+ const thread = row;
584
+ if (typeof thread.id !== "string" || typeof thread.cwd !== "string") continue;
585
+ threads.push({
586
+ id: thread.id,
587
+ cwd: thread.cwd,
588
+ name: asString(thread.name),
589
+ preview: asString(thread.preview),
590
+ model: asString(thread.model),
591
+ updatedAt: typeof thread.updatedAt === "number" ? thread.updatedAt : 0
592
+ });
593
+ }
594
+ return { threads, nextCursor: asString(typed.nextCursor) };
595
+ }
596
+ function readThreadTurns(response) {
597
+ if (!response || typeof response !== "object") return [];
598
+ const thread = response.thread;
599
+ if (!thread || typeof thread !== "object") return [];
600
+ const turns = thread.turns;
601
+ if (!Array.isArray(turns)) return [];
602
+ return turns.flatMap((turn) => {
603
+ if (!turn || typeof turn !== "object") return [];
604
+ const typed = turn;
605
+ return [
606
+ {
607
+ ...typeof typed.id === "string" && typed.id.length > 0 ? { id: typed.id } : {},
608
+ ...typeof typed.model === "string" && typed.model.length > 0 ? { model: typed.model } : {},
609
+ startedAt: typeof typed.startedAt === "number" ? typed.startedAt : null,
610
+ items: Array.isArray(typed.items) ? typed.items : []
611
+ }
612
+ ];
613
+ });
614
+ }
615
+ async function scanCodexSessions(input) {
616
+ const threads = [];
617
+ let cursor = null;
618
+ for (let page = 0; page < MAX_PAGES && threads.length < MAX_ROWS2; page += 1) {
619
+ const listed = readListPage(
620
+ await input.client.request("thread/list", {
621
+ limit: PAGE_LIMIT,
622
+ sortKey: "updated_at",
623
+ sortDirection: "desc",
624
+ ...cursor === null ? {} : { cursor }
625
+ })
626
+ );
627
+ for (const thread of listed.threads) {
628
+ if (!isWithinDirectory(thread.cwd, input.directory, input.caseInsensitive)) continue;
629
+ threads.push(thread);
630
+ if (threads.length >= MAX_ROWS2) break;
631
+ }
632
+ if (listed.nextCursor === null || listed.nextCursor.length === 0) break;
633
+ cursor = listed.nextCursor;
634
+ }
635
+ const rows = [];
636
+ for (const thread of threads) {
637
+ const turnCount = await input.client.request("thread/read", { threadId: thread.id, includeTurns: true }).then((response) => readThreadTurns(response).length).catch(() => 0);
638
+ rows.push({
639
+ threadId: thread.id,
640
+ cwd: thread.cwd,
641
+ title: truncate2(thread.name),
642
+ preview: truncate2(thread.preview),
643
+ // modelProvider 是供应商标识,不是生成回复的模型;缺模型字段就省略。
644
+ model: truncate2(thread.model),
645
+ turnCount,
646
+ // app-server 给的是 Unix 秒,列表行统一按毫秒走,跟 Claude 那边对齐。
647
+ updatedAt: thread.updatedAt * 1e3
648
+ });
649
+ }
650
+ return rows;
651
+ }
652
+ async function readCodexSession(input) {
653
+ const response = await input.client.request("thread/read", {
654
+ threadId: input.threadId,
655
+ includeTurns: true
656
+ });
657
+ const thread = asRecord(asRecord(response)?.thread);
658
+ const path = input.caseInsensitive ? NodePath2.win32 : NodePath2;
659
+ const sameDirectory = (left, right) => isWithinDirectory(path.resolve(left), path.resolve(right), input.caseInsensitive) && isWithinDirectory(path.resolve(right), path.resolve(left), input.caseInsensitive);
660
+ if (thread?.id !== input.threadId || typeof thread.cwd !== "string" || !path.isAbsolute(thread.cwd) || !path.isAbsolute(input.cwd) || !sameDirectory(thread.cwd, input.cwd)) {
661
+ throw new Error("The native Codex session does not match the requested session and directory.");
662
+ }
663
+ const model = nativeModel(thread.model);
664
+ return {
665
+ turns: readThreadTurns(response),
666
+ model: model ?? (input.readRolloutModel === false ? null : await readRolloutModel(input, thread.path, sameDirectory))
667
+ };
668
+ }
669
+ async function readRolloutModel(input, rolloutPath, sameDirectory) {
670
+ const path = input.caseInsensitive ? NodePath2.win32 : NodePath2;
671
+ if (typeof rolloutPath !== "string" || !path.isAbsolute(rolloutPath) || path.extname(rolloutPath).toLowerCase() !== ".jsonl")
672
+ return null;
673
+ const resolved = path.resolve(rolloutPath);
674
+ const roots = [
675
+ ...input.homePath ? [path.join(input.homePath, "sessions"), path.join(input.homePath, "archived_sessions")] : [],
676
+ ...input.transcriptDir ? [input.transcriptDir, path.join(path.dirname(input.transcriptDir), "archived_sessions")] : []
677
+ ];
678
+ if (!roots.some((root) => isWithinDirectory(resolved, path.resolve(root), input.caseInsensitive)))
679
+ return null;
680
+ let verified = false;
681
+ let model = null;
682
+ try {
683
+ for await (const line of input.fs.readLines(resolved)) {
684
+ if (!line.includes('"session_meta"') && !line.includes('"turn_context"')) continue;
685
+ let record3;
686
+ try {
687
+ record3 = asRecord(JSON.parse(line));
688
+ } catch {
689
+ continue;
690
+ }
691
+ const payload = asRecord(record3?.payload);
692
+ if (!record3 || !payload) continue;
693
+ if (record3.type === "session_meta" && !verified) {
694
+ if ((payload.id ?? payload.session_id) !== input.threadId || typeof payload.cwd !== "string" || !path.isAbsolute(payload.cwd) || !sameDirectory(payload.cwd, input.cwd))
695
+ return null;
696
+ verified = true;
697
+ } else if (record3.type === "turn_context" && verified) {
698
+ model = nativeModel(payload.model) ?? model;
699
+ }
700
+ }
701
+ } catch {
702
+ return null;
703
+ }
704
+ return model;
705
+ }
706
+
707
+ // src/reconcileImportedTimeline.ts
708
+ import * as NodeCrypto from "node:crypto";
709
+ function record(value) {
710
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
711
+ }
712
+ function sourceId(value, input) {
713
+ const source2 = record(value);
714
+ return source2?.kind === "native-session" && source2.provider === input.provider && source2.sessionId === input.nativeSessionId && typeof source2.messageId === "string" ? source2.messageId : void 0;
715
+ }
716
+ function stableId(input, kind, nativeId) {
717
+ const digest = NodeCrypto.createHash("sha256").update(JSON.stringify([input.threadId, input.provider, input.nativeSessionId, kind, nativeId])).digest("hex");
718
+ return `native-${kind}:${digest}`;
719
+ }
720
+ function sourceFor(input, item) {
721
+ return {
722
+ kind: "native-session",
723
+ provider: input.provider,
724
+ sessionId: input.nativeSessionId,
725
+ messageId: item.sourceId,
726
+ ...item.nativeTurnId ? { turnId: item.nativeTurnId } : {},
727
+ order: item.sourceOrder
728
+ };
729
+ }
730
+ function equalValue(left, right) {
731
+ if (left === right) return true;
732
+ if (Array.isArray(left) && Array.isArray(right)) {
733
+ return left.length === right.length && left.every((value, index) => equalValue(value, right[index]));
734
+ }
735
+ const leftRecord = record(left);
736
+ const rightRecord = record(right);
737
+ if (!leftRecord || !rightRecord) return false;
738
+ const keys = Object.keys(leftRecord);
739
+ return keys.length === Object.keys(rightRecord).length && keys.every(
740
+ (key) => Object.hasOwn(rightRecord, key) && equalValue(leftRecord[key], rightRecord[key])
741
+ );
742
+ }
743
+ function messageFingerprint(message) {
744
+ return JSON.stringify([message.role, message.text, message.createdAt]);
745
+ }
746
+ function toolCallId(activity) {
747
+ const value = record(activity.payload)?.toolCallId;
748
+ return activity.kind.startsWith("tool.") && typeof value === "string" ? value : void 0;
749
+ }
750
+ function reconcileImportedTimeline(input) {
751
+ const messages = [];
752
+ const activities = [];
753
+ const nativeMessages = [
754
+ ...new Map(input.transcript.messages.map((item) => [item.sourceId, item])).values()
755
+ ].sort((left, right) => left.sourceOrder - right.sourceOrder);
756
+ const existingBySource = /* @__PURE__ */ new Map();
757
+ const legacyByFingerprint = /* @__PURE__ */ new Map();
758
+ for (const message of input.existingMessages) {
759
+ const identity = sourceId(message.source, input);
760
+ if (identity) existingBySource.set(identity, message);
761
+ else if (!message.source) {
762
+ const fingerprint = messageFingerprint(message);
763
+ const bucket = legacyByFingerprint.get(fingerprint) ?? [];
764
+ bucket.push(message);
765
+ legacyByFingerprint.set(fingerprint, bucket);
766
+ }
767
+ }
768
+ const groups = /* @__PURE__ */ new Map();
769
+ for (const message of nativeMessages) {
770
+ const recordId = /^claude:(.+):block:\d+$/.exec(message.sourceId)?.[1];
771
+ if (input.provider !== "claude" || !recordId) continue;
772
+ const group = groups.get(recordId) ?? [];
773
+ group.push(message);
774
+ groups.set(recordId, group);
775
+ }
776
+ const units = [];
777
+ const coveredIds = /* @__PURE__ */ new Set();
778
+ for (const group of groups.values()) {
779
+ const first = group[0];
780
+ if (!first || group.length < 2) continue;
781
+ const combined = {
782
+ ...first,
783
+ text: group.map((message) => message.text).join("\n\n").trim()
784
+ };
785
+ const sourced = existingBySource.get(first.sourceId);
786
+ const legacy = legacyByFingerprint.get(messageFingerprint(combined));
787
+ if (sourced?.text === combined.text && sourced.role === combined.role || legacy?.length === 1) {
788
+ units.push({ message: combined, covered: group });
789
+ for (const message of group) coveredIds.add(message.sourceId);
790
+ }
791
+ }
792
+ for (const message of nativeMessages) {
793
+ if (coveredIds.has(message.sourceId)) continue;
794
+ const trimmed = { ...message, text: message.text.trim() };
795
+ const sourced = existingBySource.get(message.sourceId);
796
+ const wasTrimmed = message.text !== trimmed.text && (sourced?.text === trimmed.text && sourced.role === message.role || !legacyByFingerprint.has(messageFingerprint(message)) && legacyByFingerprint.has(messageFingerprint(trimmed)));
797
+ units.push({ message: wasTrimmed ? trimmed : message, covered: [message] });
798
+ }
799
+ units.sort((left, right) => left.message.sourceOrder - right.message.sourceOrder);
800
+ const nativeFingerprintCounts = /* @__PURE__ */ new Map();
801
+ for (const { message } of units) {
802
+ const fingerprint = messageFingerprint(message);
803
+ nativeFingerprintCounts.set(fingerprint, (nativeFingerprintCounts.get(fingerprint) ?? 0) + 1);
804
+ }
805
+ const aligned = /* @__PURE__ */ new Map();
806
+ const matchedExisting = /* @__PURE__ */ new Set();
807
+ let hasLegacyMatches = false;
808
+ for (const { message } of units) {
809
+ const sourced = existingBySource.get(message.sourceId);
810
+ const fingerprint = messageFingerprint(message);
811
+ const legacy = message.createdAt && nativeFingerprintCounts.get(fingerprint) === 1 ? legacyByFingerprint.get(fingerprint) : void 0;
812
+ const existing = sourced ?? (legacy?.length === 1 ? legacy[0] : void 0);
813
+ if (!existing || matchedExisting.has(existing.messageId)) continue;
814
+ aligned.set(message.sourceId, existing);
815
+ matchedExisting.add(existing.messageId);
816
+ if (!sourced) hasLegacyMatches = true;
817
+ }
818
+ const latest = input.existingMessages.at(-1);
819
+ const latestOwned = latest && matchedExisting.has(latest.messageId);
820
+ let suffixOrder = Number.POSITIVE_INFINITY;
821
+ const latestSource = record(latest?.source);
822
+ if (sourceId(latest?.source, input) && typeof latestSource?.order === "number") {
823
+ suffixOrder = latestSource.order;
824
+ }
825
+ if (latest && !latestOwned && !latest.source) {
826
+ for (let index = units.length - 1; index >= 0; index--) {
827
+ const unit = units[index];
828
+ if (unit?.message.role === latest.role && unit.message.text === latest.text) {
829
+ suffixOrder = Math.max(...unit.covered.map((message) => message.sourceOrder));
830
+ break;
831
+ }
832
+ }
833
+ }
834
+ const mayBackfill = !latest || latestOwned;
835
+ let appendedMessageCount = 0;
836
+ for (const { message } of units) {
837
+ const existing = aligned.get(message.sourceId);
838
+ if (!existing && legacyByFingerprint.has(messageFingerprint(message))) continue;
839
+ if (!existing && !mayBackfill && message.sourceOrder <= suffixOrder) continue;
840
+ const createdAt = message.createdAt || existing?.createdAt || input.fallbackIso;
841
+ const modelSelection = message.model?.trim() ? { instanceId: input.instanceId, model: message.model } : existing?.modelSelection;
842
+ const next = {
843
+ messageId: existing?.messageId ?? stableId(input, "message", message.sourceId),
844
+ role: message.role,
845
+ text: message.text,
846
+ createdAt,
847
+ updatedAt: existing?.text === message.text ? existing.updatedAt ?? createdAt : createdAt,
848
+ source: sourceFor(input, message),
849
+ ...modelSelection ? { modelSelection } : {}
850
+ };
851
+ if (existing && existing.role === next.role && existing.text === next.text && existing.createdAt === next.createdAt && equalValue(existing.source, next.source) && equalValue(existing.modelSelection, next.modelSelection))
852
+ continue;
853
+ messages.push(next);
854
+ if (!existing) appendedMessageCount++;
855
+ }
856
+ const activityBySource = /* @__PURE__ */ new Map();
857
+ const activityById = /* @__PURE__ */ new Map();
858
+ const activitiesByCall = /* @__PURE__ */ new Map();
859
+ for (const activity of input.existingActivities) {
860
+ const identity = sourceId(record(activity.payload)?.source, input);
861
+ if (identity) activityBySource.set(identity, activity);
862
+ const id = activity.id ?? activity.activityId;
863
+ if (id) activityById.set(id, activity);
864
+ const callId = toolCallId(activity);
865
+ if (callId) {
866
+ const bucket = activitiesByCall.get(callId) ?? [];
867
+ bucket.push(activity);
868
+ activitiesByCall.set(callId, bucket);
869
+ }
870
+ }
871
+ const nativeActivities = new Map(
872
+ input.transcript.activities.map((item) => [item.sourceId, item])
873
+ );
874
+ for (const activity of nativeActivities.values()) {
875
+ const generatedId = stableId(input, "activity", activity.sourceId);
876
+ const sourced = activityBySource.get(activity.sourceId) ?? activityById.get(generatedId);
877
+ const callId = toolCallId(activity);
878
+ const callMatches = callId ? activitiesByCall.get(callId) : void 0;
879
+ if (!sourced && callMatches?.some((match) => match.turnId !== null)) continue;
880
+ const legacyMatches = callMatches?.filter(
881
+ (match) => match.kind === activity.kind && !record(match.payload)?.source
882
+ );
883
+ const legacy = legacyMatches?.length === 1 ? legacyMatches[0] : void 0;
884
+ const existing = sourced ?? legacy;
885
+ if (!existing && callMatches?.length) continue;
886
+ if (!existing && !mayBackfill && !hasLegacyMatches && activity.sourceOrder <= suffixOrder)
887
+ continue;
888
+ const payload = {
889
+ ...record(existing?.payload),
890
+ ...record(activity.payload),
891
+ source: sourceFor(input, activity)
892
+ };
893
+ const next = {
894
+ id: existing?.id ?? existing?.activityId ?? generatedId,
895
+ kind: activity.kind,
896
+ tone: activity.tone,
897
+ summary: activity.summary,
898
+ payload,
899
+ createdAt: activity.createdAt || existing?.createdAt || input.fallbackIso,
900
+ turnId: null
901
+ };
902
+ if (existing && existing.kind === next.kind && existing.tone === next.tone && existing.summary === next.summary && existing.createdAt === next.createdAt && existing.turnId === null && equalValue(existing.payload, next.payload))
903
+ continue;
904
+ activities.push(next);
905
+ }
906
+ return { messages, activities, hasLegacyMatches, appendedMessageCount };
907
+ }
908
+
909
+ // src/sessionTranscript.ts
910
+ import * as NodeCrypto2 from "node:crypto";
911
+
912
+ // src/wireTypes.ts
913
+ var SESSION_IMPORT_MESSAGE_LIMIT = 200;
914
+ var SessionImportFailure = class extends Error {
915
+ code;
916
+ constructor(code, message) {
917
+ super(message);
918
+ this.name = "SessionImportFailure";
919
+ this.code = code;
920
+ }
921
+ };
922
+
923
+ // src/sessionTranscript.ts
924
+ var SESSION_IMPORT_ACTIVITY_LIMIT = 1e3;
925
+ var SESSION_IMPORT_ACTIVITY_TEXT_LIMIT = 32e3;
926
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
927
+ var nonempty = (value) => typeof value === "string" && value.trim().length > 0 ? value : void 0;
928
+ function nativeModel2(value) {
929
+ const model = nonempty(value);
930
+ return model?.trim() === "<synthetic>" ? void 0 : model;
931
+ }
932
+ function fallbackId(value) {
933
+ let text;
934
+ try {
935
+ text = JSON.stringify(value) ?? "";
936
+ } catch {
937
+ text = "";
938
+ }
939
+ return `record:${NodeCrypto2.createHash("sha256").update(text).digest("hex").slice(0, 20)}`;
940
+ }
941
+ function readableContent(value) {
942
+ if (typeof value === "string") return value;
943
+ if (!Array.isArray(value)) return "";
944
+ return value.flatMap((entry) => {
945
+ if (typeof entry === "string") return [entry];
946
+ const block = record2(entry);
947
+ return block && ["text", "input_text", "output_text", "summary_text", "reasoning_text"].includes(
948
+ String(block.type)
949
+ ) && typeof block.text === "string" ? [block.text] : [];
950
+ }).join("\n\n");
951
+ }
952
+ function toolInput(value) {
953
+ if (typeof value === "string") return value;
954
+ const input = record2(value);
955
+ if (input && Object.keys(input).length === 1 && typeof input.command === "string")
956
+ return input.command;
957
+ if (input && Object.keys(input).length === 1 && typeof input.cmd === "string") return input.cmd;
958
+ if (value === void 0 || value === null) return void 0;
959
+ try {
960
+ return JSON.stringify(value, null, 2);
961
+ } catch {
962
+ return void 0;
963
+ }
964
+ }
965
+ function toolType(name) {
966
+ const normalized = name.toLowerCase();
967
+ if (/^(bash|shell|command|terminal|exec_command|write_stdin)$/u.test(normalized))
968
+ return "command_execution";
969
+ if (/^(edit|write|multiedit|apply_patch|applypatch|patch)$/u.test(normalized))
970
+ return "file_change";
971
+ if (/^(agent|task|workflow)$/u.test(normalized)) return "dynamic_tool_call";
972
+ if (normalized.startsWith("mcp")) return "mcp_tool_call";
973
+ if (/^(websearch|webfetch|web_search)$/u.test(normalized)) return "web_search";
974
+ return "dynamic_tool_call";
975
+ }
976
+ function limit(messages, activities) {
977
+ const availableMessages = messages.length;
978
+ const retainedMessages = messages.slice(-SESSION_IMPORT_MESSAGE_LIMIT);
979
+ const floor = availableMessages > SESSION_IMPORT_MESSAGE_LIMIT ? retainedMessages[0]?.sourceOrder ?? 0 : 0;
980
+ const visibleActivities = activities.filter((activity) => activity.sourceOrder >= floor);
981
+ const retainedActivities = visibleActivities.slice(-SESSION_IMPORT_ACTIVITY_LIMIT);
982
+ return {
983
+ availableMessages,
984
+ availableActivities: activities.length,
985
+ ...retainedActivities.length < activities.length ? { activityTruncated: true } : {},
986
+ messages: retainedMessages,
987
+ activities: retainedActivities
988
+ };
989
+ }
990
+ function bounded(value, tail = false) {
991
+ if (value.length <= SESSION_IMPORT_ACTIVITY_TEXT_LIMIT) return { text: value, truncated: false };
992
+ const offset = tail ? value.length - SESSION_IMPORT_ACTIVITY_TEXT_LIMIT : SESSION_IMPORT_ACTIVITY_TEXT_LIMIT;
993
+ const character = value.charCodeAt(offset);
994
+ const previous = value.charCodeAt(offset - 1);
995
+ const splitPair = character >= 56320 && character <= 57343 && previous >= 55296 && previous <= 56319;
996
+ return {
997
+ text: tail ? value.slice(offset + (splitPair ? 1 : 0)) : value.slice(0, offset - (splitPair ? 1 : 0)),
998
+ truncated: true
999
+ };
1000
+ }
1001
+ function inputPayload(input) {
1002
+ if (input === void 0) return {};
1003
+ const result = bounded(input);
1004
+ return { input: result.text, ...result.truncated ? { inputTruncated: true } : {} };
1005
+ }
1006
+ function outputPayload(output) {
1007
+ if (output === void 0) return {};
1008
+ const result = bounded(output, true);
1009
+ return { output: result.text, ...result.truncated ? { outputTruncated: true } : {} };
1010
+ }
1011
+ function reasoning(identity, text, streamKind) {
1012
+ const result = bounded(text);
1013
+ return {
1014
+ ...identity,
1015
+ kind: "reasoning",
1016
+ tone: "info",
1017
+ summary: "Reasoning",
1018
+ payload: {
1019
+ text: result.text,
1020
+ streamKind,
1021
+ chars: result.text.length,
1022
+ streaming: false,
1023
+ ...result.truncated ? { truncated: true } : {}
1024
+ }
1025
+ };
1026
+ }
1027
+ function readClaudeTranscript(records) {
1028
+ const messages = [];
1029
+ const activities = [];
1030
+ const byUuid = /* @__PURE__ */ new Map();
1031
+ const tools = /* @__PURE__ */ new Map();
1032
+ const toolsById = /* @__PURE__ */ new Map();
1033
+ for (const raw of records) {
1034
+ const value = record2(raw);
1035
+ if (!value || value.type !== "user" && value.type !== "assistant") continue;
1036
+ if (value.isSidechain === true || value.isSideChain === true || value.isMeta === true || nonempty(value.parent_tool_use_id))
1037
+ continue;
1038
+ const uuid = nonempty(value.uuid) ?? fallbackId(raw);
1039
+ const previous = byUuid.get(uuid);
1040
+ const beforeMessage = record2(previous?.message);
1041
+ const nextMessage = record2(value.message);
1042
+ if (previous && beforeMessage && nextMessage && Array.isArray(beforeMessage.content) && Array.isArray(nextMessage.content)) {
1043
+ const beforeBlocks = beforeMessage.content;
1044
+ byUuid.set(uuid, {
1045
+ ...previous,
1046
+ ...value,
1047
+ message: {
1048
+ ...beforeMessage,
1049
+ ...nextMessage,
1050
+ content: nextMessage.content.map((entry, index) => {
1051
+ const block = record2(entry);
1052
+ const before = record2(beforeBlocks[index]);
1053
+ return block?.type === "tool_result" && before?.type === "tool_result" && block.tool_use_id === before.tool_use_id && block.is_error === void 0 && before.is_error === true ? { ...block, is_error: true } : entry;
1054
+ })
1055
+ }
1056
+ });
1057
+ } else byUuid.set(uuid, value);
1058
+ }
1059
+ let nativeTurnId;
1060
+ let sourceOrder = 0;
1061
+ for (const [uuid, value] of byUuid) {
1062
+ const message = record2(value.message);
1063
+ if (!message) continue;
1064
+ const content = message.content;
1065
+ const blocks = typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content : [];
1066
+ const hasToolResult = blocks.some((block) => record2(block)?.type === "tool_result");
1067
+ const hasUserText = value.type === "user" && !hasToolResult && blocks.some((block) => {
1068
+ const text = record2(block);
1069
+ return text?.type === "text" && nonempty(text.text) !== void 0;
1070
+ });
1071
+ const hasUserInput = hasUserText || value.type === "user" && !hasToolResult && blocks.some((block) => record2(block)?.type === "image");
1072
+ if (hasUserInput) nativeTurnId = nonempty(value.uuid);
1073
+ const createdAt = typeof value.timestamp === "string" ? value.timestamp : "";
1074
+ const model = nativeModel2(message.model) ?? nativeModel2(value.model);
1075
+ for (const [blockIndex, rawBlock] of blocks.entries()) {
1076
+ const block = record2(rawBlock);
1077
+ const identity = {
1078
+ sourceId: `claude:${uuid}:block:${blockIndex}`,
1079
+ sourceOrder: sourceOrder++,
1080
+ createdAt,
1081
+ ...nativeTurnId !== void 0 ? { nativeTurnId } : {},
1082
+ ...model !== void 0 ? { model } : {}
1083
+ };
1084
+ if (!block) continue;
1085
+ if (block.type === "text" && nonempty(block.text) !== void 0 && (value.type === "assistant" || hasUserText)) {
1086
+ messages.push({
1087
+ ...identity,
1088
+ role: value.type === "assistant" ? "assistant" : "user",
1089
+ text: block.text
1090
+ });
1091
+ } else if (value.type === "assistant" && block.type === "thinking" && nonempty(block.thinking) !== void 0) {
1092
+ activities.push(reasoning(identity, block.thinking, "reasoning_text"));
1093
+ } else if (value.type === "assistant" && (block.type === "tool_use" || block.type === "server_tool_use" || block.type === "mcp_tool_use")) {
1094
+ const toolCallId2 = nonempty(block.id);
1095
+ if (!toolCallId2) continue;
1096
+ const key = `${nativeTurnId ?? ""}\0${toolCallId2}`;
1097
+ const name = nonempty(block.name) ?? "Tool";
1098
+ const input = toolInput(block.input);
1099
+ const known = tools.get(key);
1100
+ if (known) {
1101
+ if (input !== void 0 && input !== "{}" && known.startIndex !== void 0) {
1102
+ const { inputTruncated: _inputTruncated, ...before } = known.start.payload;
1103
+ known.start = { ...known.start, payload: { ...before, ...inputPayload(input) } };
1104
+ activities[known.startIndex] = known.start;
1105
+ if (known.completionIndex !== void 0) {
1106
+ const completion = activities[known.completionIndex];
1107
+ const { inputTruncated: _completedTruncated, ...completedPayload } = completion.payload;
1108
+ activities[known.completionIndex] = {
1109
+ ...completion,
1110
+ payload: { ...completedPayload, ...inputPayload(input) }
1111
+ };
1112
+ }
1113
+ }
1114
+ continue;
1115
+ }
1116
+ const start = {
1117
+ ...identity,
1118
+ kind: "tool.started",
1119
+ tone: "tool",
1120
+ summary: name,
1121
+ payload: {
1122
+ toolCallId: toolCallId2,
1123
+ itemType: toolType(name),
1124
+ toolName: name,
1125
+ startedAt: createdAt,
1126
+ ...inputPayload(input)
1127
+ }
1128
+ };
1129
+ const startIndex = activities.push(start) - 1;
1130
+ const tool = { start, startIndex, completedId: `${identity.sourceId}:completed` };
1131
+ tools.set(key, tool);
1132
+ toolsById.set(toolCallId2, tool);
1133
+ } else if (block.type === "tool_result") {
1134
+ const toolCallId2 = nonempty(block.tool_use_id);
1135
+ if (!toolCallId2) continue;
1136
+ const key = `${nativeTurnId ?? ""}\0${toolCallId2}`;
1137
+ const tool = tools.get(key) ?? toolsById.get(toolCallId2);
1138
+ const output = readableContent(block.content);
1139
+ const previousStatus = tool?.completionIndex !== void 0 ? activities[tool.completionIndex]?.payload.status : void 0;
1140
+ const status = block.is_error === true || block.status === "failed" ? "failed" : block.is_error === false ? "completed" : previousStatus === "failed" ? "failed" : "completed";
1141
+ const completed = {
1142
+ ...identity,
1143
+ sourceId: tool?.completedId ?? `claude:tool:${toolCallId2}:completed`,
1144
+ ...tool?.start.nativeTurnId !== void 0 ? { nativeTurnId: tool.start.nativeTurnId } : {},
1145
+ ...tool?.start.model !== void 0 ? { model: tool.start.model } : {},
1146
+ kind: "tool.completed",
1147
+ tone: status === "failed" ? "error" : "tool",
1148
+ summary: tool?.start.summary ?? "Tool",
1149
+ payload: {
1150
+ ...tool?.start.payload,
1151
+ toolCallId: toolCallId2,
1152
+ itemType: tool?.start.payload.itemType ?? "dynamic_tool_call",
1153
+ status,
1154
+ ...outputPayload(output)
1155
+ }
1156
+ };
1157
+ if (tool?.completionIndex !== void 0) {
1158
+ const previous = activities[tool.completionIndex];
1159
+ activities[tool.completionIndex] = {
1160
+ ...completed,
1161
+ sourceOrder: previous.sourceOrder,
1162
+ createdAt: previous.createdAt
1163
+ };
1164
+ } else {
1165
+ const completionIndex = activities.push(completed) - 1;
1166
+ if (tool) tool.completionIndex = completionIndex;
1167
+ else {
1168
+ const orphan = { start: completed, completedId: completed.sourceId, completionIndex };
1169
+ tools.set(key, orphan);
1170
+ toolsById.set(toolCallId2, orphan);
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+ return limit(messages, activities);
1177
+ }
1178
+ function codexStatus(item) {
1179
+ if (item.status === "failed" || item.error != null) return "failed";
1180
+ if (item.status === "completed" || item.status === "done") return "completed";
1181
+ if (item.status === "declined") return "declined";
1182
+ if (item.status === "inProgress" || item.status === "in_progress" || item.status === "running")
1183
+ return "inProgress";
1184
+ if (typeof item.exitCode === "number" && Number.isInteger(item.exitCode))
1185
+ return item.exitCode === 0 ? "completed" : "failed";
1186
+ return void 0;
1187
+ }
1188
+ function codexOutput(item) {
1189
+ if (typeof item.aggregatedOutput === "string") return item.aggregatedOutput;
1190
+ if (typeof item.output === "string") return item.output;
1191
+ const result = record2(item.result);
1192
+ if (typeof item.result === "string") return item.result;
1193
+ if (result?.content !== void 0) return readableContent(result.content);
1194
+ if (item.contentItems !== void 0) return readableContent(item.contentItems);
1195
+ const error = record2(item.error);
1196
+ return typeof error?.message === "string" ? error.message : void 0;
1197
+ }
1198
+ function readCodexTranscript(turns) {
1199
+ const messages = [];
1200
+ const activities = [];
1201
+ const items = /* @__PURE__ */ new Map();
1202
+ for (const turn of turns) {
1203
+ for (const raw of turn.items) {
1204
+ const item = record2(raw);
1205
+ if (!item) continue;
1206
+ const sourceId2 = `codex:${turn.id ?? "unassigned"}:${nonempty(item.id) ?? fallbackId(raw)}`;
1207
+ const previous = items.get(sourceId2);
1208
+ items.set(sourceId2, {
1209
+ item: { ...previous?.item, ...item },
1210
+ turn: previous ? {
1211
+ ...previous.turn,
1212
+ ...turn,
1213
+ startedAt: turn.startedAt ?? previous.turn.startedAt,
1214
+ model: turn.model ?? previous.turn.model
1215
+ } : turn,
1216
+ sourceId: sourceId2
1217
+ });
1218
+ }
1219
+ }
1220
+ let sourceOrder = 0;
1221
+ for (const { item, turn, sourceId: sourceId2 } of items.values()) {
1222
+ const milliseconds = typeof turn.startedAt === "number" ? turn.startedAt * 1e3 : NaN;
1223
+ const createdAt = Number.isFinite(milliseconds) && Math.abs(milliseconds) <= 864e13 ? new Date(milliseconds).toISOString() : "";
1224
+ const model = nativeModel2(item.model) ?? nativeModel2(turn.model);
1225
+ const nativeTurnId = nonempty(turn.id);
1226
+ const identity = {
1227
+ sourceId: sourceId2,
1228
+ sourceOrder,
1229
+ createdAt,
1230
+ ...nativeTurnId !== void 0 ? { nativeTurnId } : {},
1231
+ ...model !== void 0 ? { model } : {}
1232
+ };
1233
+ sourceOrder += 4;
1234
+ if (item.type === "userMessage" || item.type === "agentMessage") {
1235
+ const text = item.type === "userMessage" ? readableContent(item.content) : typeof item.text === "string" ? item.text : "";
1236
+ if (text.trim().length > 0)
1237
+ messages.push({
1238
+ ...identity,
1239
+ role: item.type === "userMessage" ? "user" : "assistant",
1240
+ text
1241
+ });
1242
+ continue;
1243
+ }
1244
+ if (item.type === "reasoning") {
1245
+ const summary2 = readableContent(item.summary);
1246
+ const text = readableContent(item.content) || (typeof item.text === "string" ? item.text : "");
1247
+ if (summary2.trim().length > 0)
1248
+ activities.push(
1249
+ reasoning(
1250
+ { ...identity, sourceId: `${sourceId2}:summary` },
1251
+ summary2,
1252
+ "reasoning_summary_text"
1253
+ )
1254
+ );
1255
+ if (text.trim().length > 0 && text !== summary2)
1256
+ activities.push(
1257
+ reasoning(
1258
+ {
1259
+ ...identity,
1260
+ sourceId: `${sourceId2}:text`,
1261
+ sourceOrder: summary2 ? identity.sourceOrder + 1 : identity.sourceOrder
1262
+ },
1263
+ text,
1264
+ "reasoning_text"
1265
+ )
1266
+ );
1267
+ continue;
1268
+ }
1269
+ const itemTypes = {
1270
+ commandExecution: "command_execution",
1271
+ fileChange: "file_change",
1272
+ mcpToolCall: "mcp_tool_call",
1273
+ dynamicToolCall: "dynamic_tool_call",
1274
+ collabAgentToolCall: "dynamic_tool_call",
1275
+ webSearch: "web_search",
1276
+ imageView: "image_view"
1277
+ };
1278
+ const itemType = itemTypes[String(item.type)];
1279
+ const toolCallId2 = nonempty(item.id);
1280
+ if (!itemType || !toolCallId2) continue;
1281
+ const input = toolInput(item.command ?? item.arguments ?? item.input ?? item.changes);
1282
+ const output = codexOutput(item);
1283
+ const status = codexStatus(item);
1284
+ const summary = nonempty(item.tool) ?? nonempty(item.name) ?? (itemType === "command_execution" ? "Command" : itemType === "file_change" ? "File changes" : "Tool");
1285
+ const payload = {
1286
+ toolCallId: toolCallId2,
1287
+ itemType,
1288
+ startedAt: createdAt,
1289
+ ...inputPayload(input),
1290
+ ...status === "inProgress" ? { status } : {}
1291
+ };
1292
+ activities.push({
1293
+ ...identity,
1294
+ kind: "tool.started",
1295
+ tone: "tool",
1296
+ summary,
1297
+ payload: {
1298
+ ...payload,
1299
+ ...status === void 0 ? outputPayload(output) : {}
1300
+ }
1301
+ });
1302
+ if (status && status !== "inProgress")
1303
+ activities.push({
1304
+ ...identity,
1305
+ sourceId: `${sourceId2}:completed`,
1306
+ sourceOrder: identity.sourceOrder + 1,
1307
+ kind: "tool.completed",
1308
+ tone: status === "failed" ? "error" : "tool",
1309
+ summary,
1310
+ payload: {
1311
+ ...payload,
1312
+ status,
1313
+ ...outputPayload(output),
1314
+ ...typeof item.exitCode === "number" && Number.isInteger(item.exitCode) ? { exitCode: item.exitCode } : {}
1315
+ }
1316
+ });
1317
+ }
1318
+ return limit(messages, activities);
1319
+ }
1320
+
1321
+ // src/sessionImportRuntime.ts
1322
+ var NOT_IMPORTED = {
1323
+ status: "not-imported",
1324
+ appended: 0
1325
+ };
1326
+ var UNCHANGED = { status: "unchanged", appended: 0 };
1327
+ var SKIPPED_ACTIVE = {
1328
+ status: "skipped-active",
1329
+ appended: 0
1330
+ };
1331
+ var IMPORTED_TITLE_MAX_LENGTH = 80;
1332
+ var CLAUDE_DRIVER = "claudeAgent";
1333
+ var CODEX_DRIVER = "codex";
1334
+ var providerKindOfDriver = (driverKind) => driverKind === CLAUDE_DRIVER ? "claude" : driverKind === CODEX_DRIVER ? "codex" : void 0;
1335
+ var importedThreadKey = (provider, nativeSessionId) => `${provider}:${nativeSessionId}`;
1336
+ var caseInsensitivePaths = () => NodeProcess2.platform === "win32";
1337
+ var LAUNCH_ARGS_ENV = "YCODIUM_CODEX_LAUNCH_ARGS";
1338
+ var resolveCodexLaunchArgs = (instance) => instance.environment[LAUNCH_ARGS_ENV]?.trim() || instance.launchArgs.trim() || "";
1339
+ function tokenizeLaunchArgs(value) {
1340
+ const input = value.trim();
1341
+ if (input.length === 0) return [];
1342
+ const tokens = [];
1343
+ let current = "";
1344
+ let quote = null;
1345
+ let quoted = false;
1346
+ for (const char of input) {
1347
+ if (quote !== null) {
1348
+ if (char === quote) quote = null;
1349
+ else current += char;
1350
+ continue;
1351
+ }
1352
+ if (char === "'" || char === '"') {
1353
+ quote = char;
1354
+ quoted = true;
1355
+ continue;
1356
+ }
1357
+ if (/\s/u.test(char)) {
1358
+ if (current.length > 0 || quoted) tokens.push(current);
1359
+ current = "";
1360
+ quoted = false;
1361
+ continue;
1362
+ }
1363
+ current += char;
1364
+ }
1365
+ if (current.length > 0 || quoted) tokens.push(current);
1366
+ return tokens;
1367
+ }
1368
+ var nativeSessionIdOf = (provider, cursor) => {
1369
+ if (cursor === null || typeof cursor !== "object") return null;
1370
+ const record3 = cursor;
1371
+ const value = provider === "claude" ? record3.resume ?? record3.sessionId : record3.threadId;
1372
+ return typeof value === "string" && value.length > 0 ? value : null;
1373
+ };
1374
+ async function readImportedThreads(ctx) {
1375
+ const imported = /* @__PURE__ */ new Map();
1376
+ const bindings = await ctx.providers.sessions.list().catch(() => []);
1377
+ for (const binding of bindings) {
1378
+ const provider = providerKindOfDriver(binding.driver);
1379
+ if (provider === void 0) continue;
1380
+ const nativeId = nativeSessionIdOf(provider, binding.resumeCursor);
1381
+ if (nativeId === null) continue;
1382
+ imported.set(importedThreadKey(provider, nativeId), binding.threadId);
1383
+ }
1384
+ return imported;
1385
+ }
1386
+ var candidateOf = (input) => ({
1387
+ provider: input.provider,
1388
+ instanceId: input.instanceId,
1389
+ nativeSessionId: input.nativeSessionId,
1390
+ cwd: input.cwd,
1391
+ title: input.title,
1392
+ preview: input.preview,
1393
+ model: input.model,
1394
+ turnCount: input.turnCount,
1395
+ updatedAt: input.updatedAt,
1396
+ availability: input.importedThreadId !== void 0 ? "alreadyImported" : "importable",
1397
+ openElsewhere: input.openElsewhere,
1398
+ ...input.importedThreadId !== void 0 ? { importedThreadId: input.importedThreadId } : {}
1399
+ });
1400
+ async function openCodexFor(ctx, instance) {
1401
+ const binaryPath = ctx.config.codexBinaries?.[instance.instanceId];
1402
+ if (binaryPath === void 0 || binaryPath.length === 0) {
1403
+ throw new Error(`No codex binary is configured for provider instance ${instance.instanceId}.`);
1404
+ }
1405
+ return openCodexAppServer({
1406
+ process: ctx.process,
1407
+ // 白名单条目的键就是 instanceId:路径由宿主查,插件手上不留第二份。
1408
+ program: { key: instance.instanceId },
1409
+ args: [...tokenizeLaunchArgs(resolveCodexLaunchArgs(instance))],
1410
+ // 转录始终在 CODEX_HOME 下;实例的 home 是宿主解析过的那个(含 auth overlay)。
1411
+ ...instance.homePath === null ? {} : { env: { CODEX_HOME: instance.homePath } }
1412
+ });
1413
+ }
1414
+ async function withCodex(ctx, instance, use) {
1415
+ const client = await openCodexFor(ctx, instance);
1416
+ try {
1417
+ return await use(client);
1418
+ } finally {
1419
+ await client.close();
1420
+ }
1421
+ }
1422
+ async function listSessions(ctx, input) {
1423
+ const project = await ctx.projections.getProject(input.projectId);
1424
+ if (project === null) {
1425
+ throw new SessionImportFailure("notFound", "Project not found.");
1426
+ }
1427
+ const directory = input.directory ?? project.workspaceRoot;
1428
+ const caseInsensitive = caseInsensitivePaths();
1429
+ const instances = await ctx.providers.listInstances();
1430
+ const imported = await readImportedThreads(ctx);
1431
+ const sources = [];
1432
+ const scannedKeys = /* @__PURE__ */ new Set();
1433
+ for (const instance of instances) {
1434
+ if (!instance.enabled) continue;
1435
+ if (instance.driver === CODEX_DRIVER) {
1436
+ const binaryPath = ctx.config.codexBinaries?.[instance.instanceId];
1437
+ if (binaryPath === void 0) continue;
1438
+ const scanKey2 = `codex:${instance.homePath ?? ""}:${binaryPath}`;
1439
+ if (scannedKeys.has(scanKey2)) continue;
1440
+ scannedKeys.add(scanKey2);
1441
+ try {
1442
+ const rows = await withCodex(
1443
+ ctx,
1444
+ instance,
1445
+ (client) => scanCodexSessions({ client, directory, caseInsensitive })
1446
+ );
1447
+ sources.push({
1448
+ instanceId: instance.instanceId,
1449
+ provider: "codex",
1450
+ candidates: rows.map(
1451
+ (row) => candidateOf({
1452
+ provider: "codex",
1453
+ instanceId: instance.instanceId,
1454
+ nativeSessionId: row.threadId,
1455
+ cwd: row.cwd,
1456
+ title: row.title,
1457
+ preview: row.preview,
1458
+ model: row.model,
1459
+ turnCount: row.turnCount,
1460
+ updatedAt: row.updatedAt,
1461
+ // Codex 判不了活,双开由它自己在 resume 上挡。
1462
+ openElsewhere: false,
1463
+ importedThreadId: imported.get(importedThreadKey("codex", row.threadId))
1464
+ })
1465
+ ),
1466
+ unavailableReason: null
1467
+ });
1468
+ } catch (error) {
1469
+ ctx.log.warn("session-import could not read codex sessions", {
1470
+ instanceId: instance.instanceId,
1471
+ detail: describeError(error)
1472
+ });
1473
+ sources.push({
1474
+ instanceId: instance.instanceId,
1475
+ provider: "codex",
1476
+ candidates: [],
1477
+ unavailableReason: "Codex sessions could not be read."
1478
+ });
1479
+ }
1480
+ continue;
1481
+ }
1482
+ if (instance.driver !== CLAUDE_DRIVER) continue;
1483
+ if (instance.transcriptDir === null || instance.configDirPath === null) continue;
1484
+ const scanKey = `claude:${instance.transcriptDir}`;
1485
+ if (scannedKeys.has(scanKey)) continue;
1486
+ scannedKeys.add(scanKey);
1487
+ try {
1488
+ const rows = await scanClaudeSessions({
1489
+ fs: ctx.fs,
1490
+ cache: ctx.cache,
1491
+ transcriptDir: instance.transcriptDir,
1492
+ configDir: instance.configDirPath,
1493
+ directory,
1494
+ caseInsensitive
1495
+ });
1496
+ sources.push({
1497
+ instanceId: instance.instanceId,
1498
+ provider: "claude",
1499
+ candidates: rows.map(
1500
+ (row) => candidateOf({
1501
+ provider: "claude",
1502
+ instanceId: instance.instanceId,
1503
+ nativeSessionId: row.sessionId,
1504
+ cwd: row.cwd,
1505
+ title: row.title,
1506
+ preview: row.preview,
1507
+ model: row.model,
1508
+ turnCount: row.turnCount,
1509
+ updatedAt: row.updatedAt,
1510
+ openElsewhere: row.openElsewhere,
1511
+ importedThreadId: imported.get(importedThreadKey("claude", row.sessionId))
1512
+ })
1513
+ ),
1514
+ unavailableReason: null
1515
+ });
1516
+ } catch (error) {
1517
+ ctx.log.warn("session-import could not read claude sessions", {
1518
+ instanceId: instance.instanceId,
1519
+ detail: describeError(error)
1520
+ });
1521
+ sources.push({
1522
+ instanceId: instance.instanceId,
1523
+ provider: "claude",
1524
+ candidates: [],
1525
+ unavailableReason: "Claude sessions could not be read."
1526
+ });
1527
+ }
1528
+ }
1529
+ return { sources, directory };
1530
+ }
1531
+ var describeError = (error) => error instanceof Error ? error.message : String(error);
1532
+ function resolveImportedTitle(transcript) {
1533
+ const first = transcript.messages.find((message) => message.role === "user");
1534
+ const text = (first?.text ?? "").replace(/\s+/gu, " ").trim();
1535
+ if (text.length === 0) return "Imported session";
1536
+ return text.length > IMPORTED_TITLE_MAX_LENGTH ? `${text.slice(0, IMPORTED_TITLE_MAX_LENGTH - 1).trimEnd()}\u2026` : text;
1537
+ }
1538
+ function resolveImportedModelSelection(input) {
1539
+ if (input.projectDefault !== null && input.projectDefault.instanceId === input.instance.instanceId) {
1540
+ return input.projectDefault;
1541
+ }
1542
+ const models = input.instance.models;
1543
+ if (models === void 0) return null;
1544
+ const catalogModel = models.find((candidate2) => candidate2.isDefault) ?? models[0];
1545
+ if (catalogModel !== void 0) {
1546
+ return { instanceId: input.instance.instanceId, model: catalogModel.slug };
1547
+ }
1548
+ const transcript = input.transcript;
1549
+ if (transcript === void 0) return null;
1550
+ let model = transcript.model?.trim();
1551
+ if (model === "<synthetic>") model = void 0;
1552
+ if (!model) {
1553
+ let latestOrder = -1;
1554
+ for (const entry of [...transcript.messages, ...transcript.activities]) {
1555
+ const nativeModel3 = entry.model?.trim();
1556
+ if (nativeModel3 && nativeModel3 !== "<synthetic>" && entry.sourceOrder > latestOrder) {
1557
+ model = nativeModel3;
1558
+ latestOrder = entry.sourceOrder;
1559
+ }
1560
+ }
1561
+ }
1562
+ return model ? { instanceId: input.instance.instanceId, model } : null;
1563
+ }
1564
+ function importedHistoryBatches(history) {
1565
+ const entries = [
1566
+ ...history.messages.map((message) => ({ order: message.source.order, message })),
1567
+ ...history.activities.map((activity) => ({
1568
+ order: activity.payload.source.order,
1569
+ activity
1570
+ }))
1571
+ ].sort((left, right) => left.order - right.order);
1572
+ const batches = [];
1573
+ let batch = { messages: [], activities: [] };
1574
+ let bytes = 0;
1575
+ let count = 0;
1576
+ for (const entry of entries) {
1577
+ const size = new TextEncoder().encode(JSON.stringify(entry)).byteLength;
1578
+ if (size > 512e3) throw new Error("A native history record exceeds the import batch limit.");
1579
+ if (count > 0 && (count >= 64 || bytes + size > 512e3)) {
1580
+ batches.push(batch);
1581
+ batch = { messages: [], activities: [] };
1582
+ bytes = 0;
1583
+ count = 0;
1584
+ }
1585
+ if ("message" in entry) batch.messages.push(entry.message);
1586
+ else batch.activities.push(entry.activity);
1587
+ bytes += size;
1588
+ count++;
1589
+ }
1590
+ if (count > 0) batches.push(batch);
1591
+ return batches;
1592
+ }
1593
+ var historyCommandId = (threadId, batch) => `native-history:${NodeCrypto3.createHash("sha256").update(JSON.stringify([threadId, batch])).digest("hex")}`;
1594
+ var importsInFlight = /* @__PURE__ */ new WeakMap();
1595
+ async function runImport(ctx, input, emit) {
1596
+ let pending = importsInFlight.get(ctx);
1597
+ if (!pending) importsInFlight.set(ctx, pending = /* @__PURE__ */ new Map());
1598
+ const key = JSON.stringify([input.projectId, input.provider, input.nativeSessionId]);
1599
+ const previous = pending.get(key) ?? Promise.resolve();
1600
+ const operation = previous.catch(() => {
1601
+ }).then(() => runImportOnce(ctx, input, emit));
1602
+ pending.set(key, operation);
1603
+ try {
1604
+ await operation;
1605
+ } finally {
1606
+ if (pending.get(key) === operation) pending.delete(key);
1607
+ }
1608
+ }
1609
+ async function runImportOnce(ctx, input, emit) {
1610
+ let threadCreated = false;
1611
+ let phase2 = "read";
1612
+ const fail = (detail, code = "importFailed") => {
1613
+ emit({
1614
+ kind: "import_failed",
1615
+ threadId: input.threadId,
1616
+ phase: phase2,
1617
+ message: detail,
1618
+ threadCreated
1619
+ });
1620
+ throw new SessionImportFailure(code, detail);
1621
+ };
1622
+ const enterPhase = (next) => {
1623
+ phase2 = next;
1624
+ emit({ kind: "phase_started", threadId: input.threadId, phase: next });
1625
+ };
1626
+ emit({ kind: "import_started", threadId: input.threadId, phases: ["read", "create", "link"] });
1627
+ const project = await ctx.projections.getProject(input.projectId);
1628
+ if (project === null) return fail("Project not found.", "notFound");
1629
+ const instances = await ctx.providers.listInstances();
1630
+ const instance = instances.find((row) => row.instanceId === input.instanceId);
1631
+ if (instance === void 0 || !instance.enabled) {
1632
+ return fail(`This ${input.provider} provider instance is not enabled.`);
1633
+ }
1634
+ if (providerKindOfDriver(instance.driver) !== input.provider) {
1635
+ return fail("The selected provider does not own this native session.");
1636
+ }
1637
+ const bindings = await ctx.providers.sessions.list();
1638
+ const existing = await ctx.projections.getThread(input.threadId, { includeActivities: true });
1639
+ const binding = bindings.find((candidate2) => candidate2.threadId === input.threadId);
1640
+ if (existing !== null) {
1641
+ const savedSources = [
1642
+ ...existing.messages.map((message) => message.source),
1643
+ ...(existing.activities ?? []).map(
1644
+ (activity) => activity.payload !== null && typeof activity.payload === "object" ? activity.payload.source : void 0
1645
+ )
1646
+ ].filter((source2) => source2 !== void 0);
1647
+ const pendingLink = binding === void 0 && savedSources.length > 0 && savedSources.every(
1648
+ (source2) => source2 !== null && typeof source2 === "object" && source2.provider === input.provider && source2.sessionId === input.nativeSessionId
1649
+ );
1650
+ if (existing.projectId !== input.projectId || !pendingLink && (binding?.instanceId !== input.instanceId || providerKindOfDriver(binding.driver) !== input.provider || nativeSessionIdOf(input.provider, binding.resumeCursor) !== input.nativeSessionId))
1651
+ return fail("This thread is not linked to the selected native session.");
1652
+ if (existing.archivedAt !== null || existing.session?.activeTurnId != null || existing.session?.status === "running" || existing.session?.status === "starting")
1653
+ return fail(
1654
+ "The existing thread must be idle and unarchived before its history can be refreshed."
1655
+ );
1656
+ threadCreated = true;
1657
+ } else {
1658
+ for (const candidate2 of bindings) {
1659
+ if (providerKindOfDriver(candidate2.driver) !== input.provider || nativeSessionIdOf(input.provider, candidate2.resumeCursor) !== input.nativeSessionId)
1660
+ continue;
1661
+ const imported = await ctx.projections.getThread(candidate2.threadId);
1662
+ if (imported?.projectId === input.projectId)
1663
+ return fail(
1664
+ "This native session is already imported. Refresh its existing thread instead.",
1665
+ "alreadyImported"
1666
+ );
1667
+ }
1668
+ }
1669
+ enterPhase("read");
1670
+ const preferredModelSelection = existing?.modelSelection ?? resolveImportedModelSelection({ instance, projectDefault: project.defaultModelSelection });
1671
+ const transcript = await readNativeTranscript(
1672
+ ctx,
1673
+ instance,
1674
+ input,
1675
+ preferredModelSelection === null && instance.models !== void 0
1676
+ ).catch((error) => {
1677
+ ctx.log.warn("session-import could not read the native session", {
1678
+ instanceId: input.instanceId,
1679
+ detail: describeError(error)
1680
+ });
1681
+ return null;
1682
+ });
1683
+ if (transcript === null) return fail("The session could not be read from disk.");
1684
+ emit({
1685
+ kind: "session_read",
1686
+ threadId: input.threadId,
1687
+ availableMessages: transcript.availableMessages,
1688
+ restoredMessages: transcript.messages.length,
1689
+ availableActivities: transcript.availableActivities,
1690
+ restoredActivities: transcript.activities.length
1691
+ });
1692
+ enterPhase("create");
1693
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1694
+ const history = reconcileImportedTimeline({
1695
+ threadId: input.threadId,
1696
+ provider: input.provider,
1697
+ nativeSessionId: input.nativeSessionId,
1698
+ instanceId: input.instanceId,
1699
+ transcript,
1700
+ existingMessages: existing?.messages ?? [],
1701
+ existingActivities: existing?.activities ?? [],
1702
+ fallbackIso: now
1703
+ });
1704
+ const modelSelection = preferredModelSelection ?? resolveImportedModelSelection({
1705
+ instance,
1706
+ projectDefault: project.defaultModelSelection,
1707
+ // Codex 的续聊默认只认会话 API/rollout 读数,不借未经核实的 turn.model 回退。
1708
+ transcript: input.provider === "codex" ? { model: transcript.model ?? null, messages: [], activities: [] } : transcript
1709
+ });
1710
+ if (modelSelection === null) return fail("This provider instance has no model to start from.");
1711
+ let batches;
1712
+ try {
1713
+ batches = [...importedHistoryBatches(history)];
1714
+ } catch {
1715
+ return fail("A native history record is too large to import without truncating its text.");
1716
+ }
1717
+ if (existing === null && batches.length === 0) batches.push({ messages: [], activities: [] });
1718
+ try {
1719
+ for (const batch of batches) {
1720
+ await ctx.commands.dispatch({
1721
+ ...threadCreated ? { type: "thread.external-messages.append" } : {
1722
+ type: "thread.import",
1723
+ projectId: input.projectId,
1724
+ title: resolveImportedTitle(transcript),
1725
+ modelSelection,
1726
+ runtimeMode: "full-access",
1727
+ branch: null,
1728
+ worktreePath: null
1729
+ },
1730
+ commandId: historyCommandId(input.threadId, batch),
1731
+ threadId: input.threadId,
1732
+ ...batch,
1733
+ createdAt: now
1734
+ });
1735
+ threadCreated = true;
1736
+ }
1737
+ } catch (error) {
1738
+ ctx.log.warn("session-import could not create the thread", {
1739
+ threadId: input.threadId,
1740
+ detail: describeError(error)
1741
+ });
1742
+ return fail("The native session history could not be saved.");
1743
+ }
1744
+ enterPhase("link");
1745
+ const linked = await ctx.providers.sessions.setResumeCursor({
1746
+ instanceId: input.instanceId,
1747
+ threadId: input.threadId,
1748
+ cursor: input.provider === "claude" ? { threadId: input.threadId, resume: input.nativeSessionId } : { threadId: input.nativeSessionId }
1749
+ }).then(() => true).catch(() => false);
1750
+ if (!linked) return fail("The thread was created but could not be linked to the session.");
1751
+ emit({
1752
+ kind: "import_finished",
1753
+ threadId: input.threadId,
1754
+ restoredMessages: transcript.messages.length,
1755
+ restoredActivities: transcript.activities.length
1756
+ });
1757
+ }
1758
+ async function readNativeTranscript(ctx, instance, input, readRolloutModel2) {
1759
+ if (input.provider === "claude") {
1760
+ if (instance.transcriptDir === null) {
1761
+ throw new Error("This Claude instance has no transcript directory.");
1762
+ }
1763
+ return readClaudeTranscript(
1764
+ await readClaudeSessionRecords({
1765
+ fs: ctx.fs,
1766
+ transcriptDir: instance.transcriptDir,
1767
+ sessionId: input.nativeSessionId,
1768
+ cwd: input.cwd
1769
+ })
1770
+ );
1771
+ }
1772
+ const session = await withCodex(
1773
+ ctx,
1774
+ instance,
1775
+ (client) => readCodexSession({
1776
+ client,
1777
+ fs: ctx.fs,
1778
+ threadId: input.nativeSessionId,
1779
+ cwd: input.cwd,
1780
+ homePath: instance.homePath,
1781
+ transcriptDir: instance.transcriptDir,
1782
+ caseInsensitive: caseInsensitivePaths(),
1783
+ readRolloutModel: readRolloutModel2
1784
+ })
1785
+ );
1786
+ return { ...readCodexTranscript(session.turns), model: session.model };
1787
+ }
1788
+ var NativeSessionTail = class {
1789
+ /** 同一条 thread 的并发拾取串行化:打开会话与发消息可能同时打进来。 */
1790
+ inFlight = /* @__PURE__ */ new Map();
1791
+ ctx;
1792
+ constructor(ctx) {
1793
+ this.ctx = ctx;
1794
+ }
1795
+ /** 拾取一条 thread。任何失败都收成 `failed`,调用方不必 catch。 */
1796
+ async sync(threadId) {
1797
+ const previous = this.inFlight.get(threadId);
1798
+ if (previous !== void 0) return previous;
1799
+ const operation = this.syncOnce(threadId).catch((error) => {
1800
+ this.ctx.log.warn("session-import sync failed", {
1801
+ threadId,
1802
+ detail: describeError(error)
1803
+ });
1804
+ return { status: "failed", appended: 0 };
1805
+ });
1806
+ this.inFlight.set(threadId, operation);
1807
+ try {
1808
+ return await operation;
1809
+ } finally {
1810
+ if (this.inFlight.get(threadId) === operation) this.inFlight.delete(threadId);
1811
+ }
1812
+ }
1813
+ baselineKey(threadId) {
1814
+ return `native-session-tail:${threadId}`;
1815
+ }
1816
+ /** 缓存里那条基线;没有、或者形状不对都当作“没见过”。 */
1817
+ async readBaseline(threadId) {
1818
+ const cached = await this.ctx.cache.getJson(this.baselineKey(threadId)).catch(() => null);
1819
+ const value = cached?.value.modifiedAt;
1820
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1821
+ }
1822
+ async writeBaseline(threadId, modifiedAt) {
1823
+ await this.ctx.cache.setJson(this.baselineKey(threadId), { modifiedAt }).catch((error) => {
1824
+ this.ctx.log.warn("session-import could not record the tail baseline", {
1825
+ threadId,
1826
+ detail: describeError(error)
1827
+ });
1828
+ });
1829
+ }
1830
+ async syncOnce(threadId) {
1831
+ const binding = (await this.ctx.providers.sessions.list()).find(
1832
+ (candidate2) => candidate2.threadId === threadId && providerKindOfDriver(candidate2.driver) === "claude"
1833
+ );
1834
+ if (binding === void 0 || binding.instanceId === null) return NOT_IMPORTED;
1835
+ const nativeSessionId = nativeSessionIdOf("claude", binding.resumeCursor);
1836
+ if (nativeSessionId === null) return NOT_IMPORTED;
1837
+ const instance = (await this.ctx.providers.listInstances()).find(
1838
+ (row) => row.instanceId === binding.instanceId
1839
+ );
1840
+ const transcriptDir = instance?.transcriptDir ?? null;
1841
+ if (transcriptDir === null) return NOT_IMPORTED;
1842
+ const modifiedAt = await claudeSessionModifiedAt({
1843
+ fs: this.ctx.fs,
1844
+ transcriptDir,
1845
+ sessionId: nativeSessionId
1846
+ });
1847
+ if (modifiedAt === null) return NOT_IMPORTED;
1848
+ const seen = await this.readBaseline(threadId);
1849
+ if (seen !== void 0 && modifiedAt <= seen) return UNCHANGED;
1850
+ const thread = await this.ctx.projections.getThread(threadId, { includeActivities: true });
1851
+ if (thread === null || thread.archivedAt !== null) return NOT_IMPORTED;
1852
+ if (thread.session?.activeTurnId != null) return SKIPPED_ACTIVE;
1853
+ if (thread.session?.status === "running" || thread.session?.status === "starting")
1854
+ return SKIPPED_ACTIVE;
1855
+ const records = await readClaudeSessionRecords({
1856
+ fs: this.ctx.fs,
1857
+ transcriptDir,
1858
+ sessionId: nativeSessionId
1859
+ }).catch(() => null);
1860
+ if (records === null) return { status: "failed", appended: 0 };
1861
+ const appendedAt = (/* @__PURE__ */ new Date()).toISOString();
1862
+ const history = reconcileImportedTimeline({
1863
+ threadId,
1864
+ provider: "claude",
1865
+ nativeSessionId,
1866
+ instanceId: binding.instanceId,
1867
+ transcript: readClaudeTranscript(records),
1868
+ existingMessages: thread.messages,
1869
+ existingActivities: thread.activities ?? [],
1870
+ fallbackIso: appendedAt
1871
+ });
1872
+ const batches = importedHistoryBatches(history);
1873
+ if (batches.length === 0) {
1874
+ await this.writeBaseline(threadId, modifiedAt);
1875
+ return { status: "synced", appended: 0 };
1876
+ }
1877
+ try {
1878
+ for (const batch of batches) {
1879
+ await this.ctx.commands.dispatch({
1880
+ type: "thread.external-messages.append",
1881
+ commandId: historyCommandId(threadId, batch),
1882
+ threadId,
1883
+ ...batch,
1884
+ createdAt: appendedAt
1885
+ });
1886
+ }
1887
+ } catch (error) {
1888
+ this.ctx.log.warn("session-import sync append failed", {
1889
+ threadId,
1890
+ detail: describeError(error)
1891
+ });
1892
+ return { status: "failed", appended: 0 };
1893
+ }
1894
+ await this.writeBaseline(threadId, modifiedAt);
1895
+ this.ctx.log.info("session-import sync appended external messages", {
1896
+ threadId,
1897
+ appendedMessages: history.appendedMessageCount,
1898
+ updatedMessages: history.messages.length,
1899
+ updatedActivities: history.activities.length
1900
+ });
1901
+ if (history.appendedMessageCount === 0 && (history.hasLegacyMatches || history.activities.length === 0))
1902
+ return { status: "synced", appended: history.appendedMessageCount };
1903
+ const current = await this.ctx.projections.getThread(threadId);
1904
+ if (current?.session == null || current.session.status === "stopped" || current.session.activeTurnId !== null || current.session.status === "running" || current.session.status === "starting")
1905
+ return { status: "synced", appended: history.appendedMessageCount };
1906
+ await this.ctx.providers.sessions.stop(threadId).catch((error) => {
1907
+ this.ctx.log.warn("session-import sync could not stop the session", {
1908
+ threadId,
1909
+ detail: describeError(error)
1910
+ });
1911
+ });
1912
+ return { status: "synced", appended: history.appendedMessageCount };
1913
+ }
1914
+ };
1915
+
1916
+ // src/index.ts
1917
+ var index_default = definePlugin({
1918
+ id: "session-import",
1919
+ apiVersion: 2,
1920
+ activate: (ctx) => {
1921
+ ctx.command({
1922
+ name: "scan",
1923
+ description: "Native CLI sessions under a project directory that can be taken over.",
1924
+ scope: "read",
1925
+ payloadSchema: sessionImportListInputSchema,
1926
+ resultSchema: sessionImportListResultSchema,
1927
+ handle: (payload) => listSessions(ctx, payload)
1928
+ });
1929
+ ctx.stream({
1930
+ name: "import",
1931
+ description: "Takes over one native CLI session, reporting each phase as it goes.",
1932
+ scope: "operate",
1933
+ payloadSchema: sessionImportInputSchema,
1934
+ itemSchema: sessionImportProgressEventSchema,
1935
+ handle: async (payload, emit, signal) => {
1936
+ await runImport(ctx, payload, (event) => {
1937
+ if (!signal.aborted) emit(event);
1938
+ });
1939
+ }
1940
+ });
1941
+ const tail = new NativeSessionTail(ctx);
1942
+ ctx.command({
1943
+ name: "sync",
1944
+ description: "Picks up what a terminal wrote to one imported session since the last check.",
1945
+ scope: "operate",
1946
+ payloadSchema: sessionImportSyncInputSchema,
1947
+ resultSchema: sessionImportSyncResultSchema,
1948
+ handle: (payload) => tail.sync(payload.threadId)
1949
+ });
1950
+ }
1951
+ });
1952
+ export {
1953
+ index_default as default
1954
+ };