@alsdd/t3-import 0.0.0-stage → 0.1.1-alsd.1

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 (97) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/assets/fork-demo.gif +0 -0
  5. package/assets/fork-demo.png +0 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +399 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/core/checkpoint.d.ts +25 -0
  10. package/dist/core/checkpoint.js +82 -0
  11. package/dist/core/checkpoint.js.map +1 -0
  12. package/dist/core/errors.d.ts +12 -0
  13. package/dist/core/errors.js +17 -0
  14. package/dist/core/errors.js.map +1 -0
  15. package/dist/core/jsonl.d.ts +7 -0
  16. package/dist/core/jsonl.js +37 -0
  17. package/dist/core/jsonl.js.map +1 -0
  18. package/dist/core/providers.d.ts +6 -0
  19. package/dist/core/providers.js +17 -0
  20. package/dist/core/providers.js.map +1 -0
  21. package/dist/core/types.d.ts +247 -0
  22. package/dist/core/types.js +4 -0
  23. package/dist/core/types.js.map +1 -0
  24. package/dist/core/util.d.ts +14 -0
  25. package/dist/core/util.js +61 -0
  26. package/dist/core/util.js.map +1 -0
  27. package/dist/sources/claude.d.ts +6 -0
  28. package/dist/sources/claude.js +576 -0
  29. package/dist/sources/claude.js.map +1 -0
  30. package/dist/sources/codex.d.ts +11 -0
  31. package/dist/sources/codex.js +603 -0
  32. package/dist/sources/codex.js.map +1 -0
  33. package/dist/sources/codexAppServer.d.ts +24 -0
  34. package/dist/sources/codexAppServer.js +190 -0
  35. package/dist/sources/codexAppServer.js.map +1 -0
  36. package/dist/sources/files.d.ts +1 -0
  37. package/dist/sources/files.js +24 -0
  38. package/dist/sources/files.js.map +1 -0
  39. package/dist/sources/index.d.ts +3 -0
  40. package/dist/sources/index.js +15 -0
  41. package/dist/sources/index.js.map +1 -0
  42. package/dist/sources/opencode.d.ts +6 -0
  43. package/dist/sources/opencode.js +443 -0
  44. package/dist/sources/opencode.js.map +1 -0
  45. package/dist/target/cache.d.ts +14 -0
  46. package/dist/target/cache.js +155 -0
  47. package/dist/target/cache.js.map +1 -0
  48. package/dist/target/config.d.ts +22 -0
  49. package/dist/target/config.js +90 -0
  50. package/dist/target/config.js.map +1 -0
  51. package/dist/target/destination.d.ts +21 -0
  52. package/dist/target/destination.js +77 -0
  53. package/dist/target/destination.js.map +1 -0
  54. package/dist/target/importer.d.ts +86 -0
  55. package/dist/target/importer.js +597 -0
  56. package/dist/target/importer.js.map +1 -0
  57. package/dist/target/launch.d.ts +6 -0
  58. package/dist/target/launch.js +77 -0
  59. package/dist/target/launch.js.map +1 -0
  60. package/dist/target/ledger.d.ts +35 -0
  61. package/dist/target/ledger.js +174 -0
  62. package/dist/target/ledger.js.map +1 -0
  63. package/dist/target/reconciliation.d.ts +60 -0
  64. package/dist/target/reconciliation.js +259 -0
  65. package/dist/target/reconciliation.js.map +1 -0
  66. package/dist/target/replace.d.ts +9 -0
  67. package/dist/target/replace.js +341 -0
  68. package/dist/target/replace.js.map +1 -0
  69. package/dist/target/schema.d.ts +22 -0
  70. package/dist/target/schema.js +108 -0
  71. package/dist/target/schema.js.map +1 -0
  72. package/dist/target/sync.d.ts +34 -0
  73. package/dist/target/sync.js +879 -0
  74. package/dist/target/sync.js.map +1 -0
  75. package/dist/target/syncAll.d.ts +4 -0
  76. package/dist/target/syncAll.js +98 -0
  77. package/dist/target/syncAll.js.map +1 -0
  78. package/dist/target/v2.d.ts +18 -0
  79. package/dist/target/v2.js +267 -0
  80. package/dist/target/v2.js.map +1 -0
  81. package/dist/target/v2Profiles.d.ts +5 -0
  82. package/dist/target/v2Profiles.js +14 -0
  83. package/dist/target/v2Profiles.js.map +1 -0
  84. package/dist/tui.d.ts +49 -0
  85. package/dist/tui.js +560 -0
  86. package/dist/tui.js.map +1 -0
  87. package/package.json +93 -4
  88. package/vendor/t3-v010/LICENSE +21 -0
  89. package/vendor/t3-v010/THIRD-PARTY-NOTICES.txt +2274 -0
  90. package/vendor/t3-v010/reference.json +6 -0
  91. package/vendor/t3-v010/runtime.mjs +60364 -0
  92. package/vendor/t3-v010/schema-columns.json +568 -0
  93. package/vendor/t3-v011/LICENSE +21 -0
  94. package/vendor/t3-v011/THIRD-PARTY-NOTICES.txt +2274 -0
  95. package/vendor/t3-v011/reference.json +6 -0
  96. package/vendor/t3-v011/runtime.mjs +60993 -0
  97. package/vendor/t3-v011/schema-columns.json +526 -0
@@ -0,0 +1,597 @@
1
+ import Database from "better-sqlite3";
2
+ import { randomUUID } from "node:crypto";
3
+ import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
4
+ import { basename, extname, join } from "node:path";
5
+ import { z } from "zod";
6
+ import { canonicalPath, deterministicUuid, isObject, sha256, truncate } from "../core/util.js";
7
+ import { checkpointForThread } from "../core/checkpoint.js";
8
+ import { sourceDriver } from "../core/providers.js";
9
+ import { safetyError, writeError } from "../core/errors.js";
10
+ import { resolveProviderSelection } from "./config.js";
11
+ import { assertT3Closed, validateTargetDatabase } from "./schema.js";
12
+ import { recordImports } from "./ledger.js";
13
+ import { importDestination, threadWorkingDirectory, validateDestinationOptions } from "./destination.js";
14
+ import { isV2Database } from "./schema.js";
15
+ import { prepareV2Write, v2ProviderThreads, v2Thread } from "./v2.js";
16
+ const EVENT_TYPES = [
17
+ "project.created",
18
+ "thread.created",
19
+ "thread.message-sent",
20
+ "thread.turn-start-requested",
21
+ "thread.session-set",
22
+ "thread.proposed-plan-upserted",
23
+ "thread.activity-appended",
24
+ "thread.meta-updated",
25
+ "thread.deleted",
26
+ ];
27
+ const plannedEventSchema = z.object({
28
+ eventId: z.string().min(1),
29
+ aggregateKind: z.enum(["project", "thread"]),
30
+ streamId: z.string().min(1),
31
+ type: z.enum(EVENT_TYPES),
32
+ occurredAt: z.iso.datetime(),
33
+ actorKind: z.enum(["client", "server", "provider"]),
34
+ payload: z.record(z.string(), z.unknown()),
35
+ metadata: z.record(z.string(), z.unknown()),
36
+ });
37
+ export const MAX_SAFE_IMPORT_EVENTS = 900;
38
+ export const IMPORTER_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
39
+ const MAX_LAST_ERROR_LENGTH = 500;
40
+ function settledSession(turn) {
41
+ if (turn.status === "completed")
42
+ return { status: "ready", lastError: null };
43
+ if (turn.status === "failed")
44
+ return { status: "error", lastError: truncate(turn.terminalError ?? turn.terminalReason ?? "Imported provider turn failed.", MAX_LAST_ERROR_LENGTH) };
45
+ return { status: "interrupted", lastError: null };
46
+ }
47
+ function activityPriority(activity) {
48
+ if (activity.tone === "error" || activity.payload.status === "failed")
49
+ return 100;
50
+ switch (activity.payload.itemType) {
51
+ case "file_change": return 90;
52
+ case "mcp_tool_call":
53
+ case "dynamic_tool_call": return 80;
54
+ case "command_execution": return 70;
55
+ case "web_search": return 60;
56
+ case "image_view": return 50;
57
+ default: return 40;
58
+ }
59
+ }
60
+ function compactTurnActivities(activities) {
61
+ if (activities.length === 0)
62
+ return activities;
63
+ const selected = new Set();
64
+ let latestReasoning = -1;
65
+ let latestPlan = -1;
66
+ let representativeTool = -1;
67
+ const latestOtherByKind = new Map();
68
+ activities.forEach((activity, index) => {
69
+ if (activity.kind === "context-window.updated")
70
+ return;
71
+ if (activity.kind === "reasoning.summary") {
72
+ latestReasoning = index;
73
+ return;
74
+ }
75
+ if (activity.kind === "turn.plan.updated") {
76
+ latestPlan = index;
77
+ return;
78
+ }
79
+ if (activity.kind === "tool.completed") {
80
+ if (representativeTool < 0 ||
81
+ activityPriority(activity) >= activityPriority(activities[representativeTool]))
82
+ representativeTool = index;
83
+ return;
84
+ }
85
+ if (activity.kind === "context-compaction" ||
86
+ activity.tone === "error" ||
87
+ activity.tone === "approval") {
88
+ selected.add(index);
89
+ return;
90
+ }
91
+ latestOtherByKind.set(activity.kind, index);
92
+ });
93
+ if (latestReasoning >= 0)
94
+ selected.add(latestReasoning);
95
+ if (latestPlan >= 0)
96
+ selected.add(latestPlan);
97
+ if (representativeTool >= 0)
98
+ selected.add(representativeTool);
99
+ for (const index of latestOtherByKind.values())
100
+ selected.add(index);
101
+ return activities.filter((_activity, index) => selected.has(index));
102
+ }
103
+ export function compactThreadForImport(thread) {
104
+ const turns = thread.turns.map((turn) => ({
105
+ ...turn,
106
+ activities: compactTurnActivities(turn.activities),
107
+ }));
108
+ const before = thread.turns.reduce((sum, turn) => sum + turn.activities.length, 0);
109
+ const after = turns.reduce((sum, turn) => sum + turn.activities.length, 0);
110
+ if (after === before)
111
+ return { ...thread, turns };
112
+ return {
113
+ ...thread,
114
+ turns,
115
+ warnings: [
116
+ ...thread.warnings,
117
+ `Compacted ${before} source activities to ${after} import activities for T3 startup compatibility.`,
118
+ ],
119
+ };
120
+ }
121
+ export function projectionBacklog(db) {
122
+ if (isV2Database(db)) {
123
+ // V2's official sink advances this cursor with its thread projections.
124
+ // Project events share the sequence but do not advance that cursor.
125
+ const latestSequence = db.prepare("SELECT COALESCE(MAX(sequence),0) value FROM orchestration_events WHERE application_event_version=2 AND aggregate_kind='thread'").get().value;
126
+ const projectedSequence = db.prepare("SELECT last_sequence value FROM orchestration_v2_projection_metadata WHERE projection_name='thread-projections'").get()?.value ?? 0;
127
+ return { latestSequence, projectedSequence, backlog: Math.max(0, latestSequence - projectedSequence) };
128
+ }
129
+ const latestSequence = Number(db.prepare("SELECT COALESCE(MAX(sequence), 0) value FROM orchestration_events").get().value);
130
+ // Newer T3 versions keep a separate attachment-cleanup retry cursor. It only
131
+ // advances at bootstrap, not for live events, and is not a read-model cursor.
132
+ const projected = db.prepare("SELECT MIN(last_applied_sequence) value FROM projection_state WHERE projector <> 'projection.attachment-cleanup'").get();
133
+ const projectedSequence = projected.value ?? 0;
134
+ return {
135
+ latestSequence,
136
+ projectedSequence,
137
+ backlog: Math.max(0, latestSequence - projectedSequence),
138
+ };
139
+ }
140
+ function safeAttachmentSegment(threadId) {
141
+ return threadId.trim().toLowerCase().replace(/[^a-z0-9_-]+/giu, "-").replace(/-+/gu, "-").replace(/^[-_]+|[-_]+$/gu, "").slice(0, 80).replace(/[-_]+$/gu, "");
142
+ }
143
+ const imageExtension = {
144
+ "image/png": ".png",
145
+ "image/jpeg": ".jpg",
146
+ "image/gif": ".gif",
147
+ "image/webp": ".webp",
148
+ };
149
+ function attachmentName(name, extension) {
150
+ const clean = basename(name).replace(/[\u0000-\u001f]/gu, "").slice(0, 240) || "image";
151
+ return extname(clean) ? clean : `${clean}${extension}`;
152
+ }
153
+ function planMessageAttachments(message, threadId, paths, warnings) {
154
+ const descriptors = [];
155
+ const assets = [];
156
+ const notes = [];
157
+ for (const attachment of message.attachments.slice(0, 8)) {
158
+ const extension = imageExtension[attachment.mimeType];
159
+ if (!extension || attachment.sizeBytes > 10 * 1024 * 1024 || attachment.remoteUrl) {
160
+ const reason = attachment.remoteUrl ? "remote images are not downloaded" : !extension ? `unsupported MIME type ${attachment.mimeType}` : "image exceeds 10 MB";
161
+ warnings.push(`Skipped ${attachment.name}: ${reason}.`);
162
+ notes.push(`[Attachment not imported: ${attachment.remoteUrl ?? attachment.name}]`);
163
+ continue;
164
+ }
165
+ const id = `${safeAttachmentSegment(threadId)}-${deterministicUuid(`t3-import:attachment:${threadId}:${attachment.sourceId}`)}`;
166
+ const descriptor = { type: "image", id, name: attachmentName(attachment.name, extension), mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes };
167
+ descriptors.push(descriptor);
168
+ assets.push({ descriptor, ...(attachment.data ? { data: attachment.data } : {}), ...(attachment.path ? { sourcePath: attachment.path } : {}), finalPath: join(paths.attachmentsDir, `${id}${extension}`) });
169
+ }
170
+ if (message.attachments.length > 8)
171
+ warnings.push(`Message ${message.sourceId} has more than eight images; extras were skipped.`);
172
+ return { descriptors, assets, text: [message.text, ...notes].filter(Boolean).join("\n\n") };
173
+ }
174
+ export function event(seed, key, aggregateKind, streamId, type, occurredAt, actorKind, payload, metadata = {}) {
175
+ const value = { key, eventId: deterministicUuid(`t3-import:event:${seed}:${key}`), aggregateKind, streamId, type, occurredAt, actorKind, payload, metadata };
176
+ plannedEventSchema.parse(value);
177
+ return value;
178
+ }
179
+ export function modelSelection(provider) {
180
+ return { instanceId: provider.instanceId, model: provider.model, options: provider.options };
181
+ }
182
+ export function planThread(thread, threadId, projectId, paths, provider, seed, projectEvent, resumable, fingerprint, turnOffset = 0, includeThreadCreated = true, threadCreatedMetadata = {}) {
183
+ const events = projectEvent ? [projectEvent] : [];
184
+ const assets = [];
185
+ const warnings = [...thread.warnings, ...(provider.warning ? [provider.warning] : [])];
186
+ if (thread.turns.some((turn) => turn.status === "inProgress"))
187
+ warnings.push("Included an active provider snapshot as an interrupted historical turn; it will not be treated as pending in T3.");
188
+ const selection = modelSelection(provider);
189
+ const latestAt = thread.updatedAt;
190
+ if (includeThreadCreated)
191
+ events.push(event(seed, "thread.created", "thread", threadId, "thread.created", thread.createdAt, "server", {
192
+ threadId, projectId, title: thread.title, modelSelection: selection, runtimeMode: "full-access", interactionMode: "default", branch: thread.gitBranch ?? null, worktreePath: null, createdAt: thread.createdAt, updatedAt: thread.createdAt,
193
+ }, threadCreatedMetadata));
194
+ if (includeThreadCreated && !thread.currentBranch) {
195
+ const activityId = deterministicUuid(`t3-import:activity:${seed}:historical-branch`);
196
+ events.push(event(seed, "historical-branch", "thread", threadId, "thread.activity-appended", thread.createdAt, "provider", {
197
+ threadId,
198
+ activity: { id: activityId, tone: "info", kind: "import.historical-branch", summary: "Historical Claude branch", payload: { detail: warnings[0] }, turnId: null, createdAt: thread.createdAt },
199
+ }, { adapterKey: provider.adapterKey }));
200
+ }
201
+ thread.turns.forEach((turn, turnIndex) => {
202
+ const turnNumber = turnOffset + turnIndex;
203
+ const attachmentPlan = planMessageAttachments(turn.user, threadId, paths, warnings);
204
+ assets.push(...attachmentPlan.assets);
205
+ const userMessageId = deterministicUuid(`t3-import:message:${seed}:${turn.id}:user:${turn.user.sourceId}`);
206
+ events.push(event(seed, `turn.${turnNumber}.user`, "thread", threadId, "thread.message-sent", turn.user.timestamp, "client", {
207
+ threadId, messageId: userMessageId, role: "user", text: attachmentPlan.text, attachments: attachmentPlan.descriptors, turnId: null, streaming: false, createdAt: turn.user.timestamp, updatedAt: turn.user.timestamp,
208
+ }));
209
+ events.push(event(seed, `turn.${turnNumber}.start`, "thread", threadId, "thread.turn-start-requested", turn.startedAt, "client", {
210
+ threadId, messageId: userMessageId, modelSelection: selection, titleSeed: truncate(turn.user.text, 120), runtimeMode: "full-access", interactionMode: "default", createdAt: turn.startedAt,
211
+ }));
212
+ events.push(event(seed, `turn.${turnNumber}.running`, "thread", threadId, "thread.session-set", turn.startedAt, "provider", {
213
+ threadId,
214
+ session: { threadId, status: "running", providerName: provider.providerName, providerInstanceId: provider.instanceId, runtimeMode: "full-access", activeTurnId: turn.id, lastError: null, updatedAt: turn.startedAt },
215
+ }, { adapterKey: provider.adapterKey, providerTurnId: turn.id }));
216
+ // Bootstrap replays each projector independently. The turn projector sees
217
+ // the session projector's final state, so a non-streaming message can mark
218
+ // a turn completed before its terminal session event is read. Settle first;
219
+ // subsequent messages preserve interrupted/error states and their timestamps.
220
+ const settledAt = turn.completedAt ?? turn.assistant.at(-1)?.timestamp ?? turn.user.timestamp;
221
+ const settled = settledSession(turn);
222
+ events.push(event(seed, `turn.${turnNumber}.settled`, "thread", threadId, "thread.session-set", settledAt, "provider", {
223
+ threadId,
224
+ session: { threadId, status: settled.status, providerName: provider.providerName, providerInstanceId: provider.instanceId, runtimeMode: "full-access", activeTurnId: null, lastError: settled.lastError, updatedAt: settledAt },
225
+ }, { adapterKey: provider.adapterKey, providerTurnId: turn.id }));
226
+ const timeline = [
227
+ ...turn.assistant.map((value) => ({ timestamp: value.timestamp, sourceId: value.sourceId, type: "message", value })),
228
+ ...turn.activities.map((value) => ({ timestamp: value.timestamp, sourceId: value.sourceId, type: "activity", value })),
229
+ ...turn.plans.map((value) => ({ timestamp: value.timestamp, sourceId: value.sourceId, type: "plan", value })),
230
+ ].sort((a, b) => a.timestamp.localeCompare(b.timestamp) || a.sourceId.localeCompare(b.sourceId));
231
+ timeline.forEach((entry, entryIndex) => {
232
+ if (entry.type === "message") {
233
+ const message = entry.value;
234
+ const messageId = deterministicUuid(`t3-import:message:${seed}:${turn.id}:assistant:${message.sourceId}`);
235
+ events.push(event(seed, `turn.${turnNumber}.entry.${entryIndex}.message`, "thread", threadId, "thread.message-sent", message.timestamp, "provider", {
236
+ threadId, messageId, role: "assistant", text: message.text, turnId: turn.id, streaming: false, createdAt: message.timestamp, updatedAt: message.timestamp,
237
+ }, { adapterKey: provider.adapterKey, providerTurnId: turn.id }));
238
+ }
239
+ else if (entry.type === "activity") {
240
+ const activity = entry.value;
241
+ const activityId = deterministicUuid(`t3-import:activity:${seed}:${turn.id}:${activity.sourceId}`);
242
+ events.push(event(seed, `turn.${turnNumber}.entry.${entryIndex}.activity`, "thread", threadId, "thread.activity-appended", activity.timestamp, "provider", {
243
+ threadId,
244
+ activity: { id: activityId, tone: activity.tone, kind: activity.kind, summary: activity.summary || "Activity", payload: activity.payload, turnId: turn.id, sequence: entryIndex, createdAt: activity.timestamp },
245
+ }, { adapterKey: provider.adapterKey, providerTurnId: turn.id, providerItemId: activity.sourceId }));
246
+ }
247
+ else {
248
+ const plan = entry.value;
249
+ const planId = deterministicUuid(`t3-import:plan:${seed}:${turn.id}:${plan.sourceId}`);
250
+ events.push(event(seed, `turn.${turnNumber}.entry.${entryIndex}.plan`, "thread", threadId, "thread.proposed-plan-upserted", plan.timestamp, "provider", {
251
+ threadId,
252
+ proposedPlan: { id: planId, turnId: turn.id, planMarkdown: plan.markdown, implementedAt: null, implementationThreadId: null, createdAt: plan.timestamp, updatedAt: plan.timestamp },
253
+ }, { adapterKey: provider.adapterKey, providerTurnId: turn.id, providerItemId: plan.sourceId }));
254
+ }
255
+ });
256
+ });
257
+ return { thread, threadId, projectId, provider, events, assets, resumable, warnings, alreadyImported: false, fingerprint };
258
+ }
259
+ function existingProjectId(db, workspace) {
260
+ const normalized = canonicalPath(workspace);
261
+ const rows = db.prepare("SELECT project_id projectId, workspace_root workspaceRoot FROM projection_projects WHERE deleted_at IS NULL").all();
262
+ const projected = rows.find((row) => canonicalPath(row.workspaceRoot) === normalized)?.projectId;
263
+ if (projected)
264
+ return projected;
265
+ const pending = db.prepare("SELECT stream_id streamId, payload_json payload FROM orchestration_events WHERE event_type = 'project.created'").all();
266
+ return pending.find((row) => {
267
+ try {
268
+ const value = JSON.parse(row.payload);
269
+ return value.workspaceRoot ? canonicalPath(value.workspaceRoot) === normalized : false;
270
+ }
271
+ catch {
272
+ return false;
273
+ }
274
+ })?.streamId;
275
+ }
276
+ function isImported(db, sourceKey, thread, threadId) {
277
+ const marker = deterministicUuid(`t3-import:event:${sourceKey}:thread.created`);
278
+ const found = db.prepare("SELECT 1 found FROM orchestration_events WHERE event_id = ? OR (stream_id = ? AND event_type = 'thread.created') LIMIT 1").get(marker, threadId);
279
+ if (found)
280
+ return true;
281
+ if (thread.source === "codex") {
282
+ const legacy = deterministicUuid(`t3-import:event:${thread.sourceSessionId}:thread.created`);
283
+ return Boolean(db.prepare("SELECT 1 found FROM orchestration_events WHERE event_id = ? OR (stream_id = ? AND event_type = 'thread.created') LIMIT 1").get(legacy, thread.sourceSessionId));
284
+ }
285
+ return false;
286
+ }
287
+ function pidAlive(pid) {
288
+ try {
289
+ process.kill(pid, 0);
290
+ return true;
291
+ }
292
+ catch {
293
+ return false;
294
+ }
295
+ }
296
+ export function acquireLock(paths) {
297
+ const lockPath = join(paths.stateDir, ".t3-import.lock");
298
+ if (existsSync(lockPath)) {
299
+ try {
300
+ const value = JSON.parse(readFileSync(lockPath, "utf8"));
301
+ if (typeof value.pid === "number" && pidAlive(value.pid))
302
+ throw safetyError(`Another t3-import process is running (PID ${value.pid}).`);
303
+ unlinkSync(lockPath);
304
+ }
305
+ catch (error) {
306
+ if (error instanceof Error && error.name === "ImporterError")
307
+ throw error;
308
+ try {
309
+ unlinkSync(lockPath);
310
+ }
311
+ catch {
312
+ throw safetyError(`Cannot acquire importer lock: ${lockPath}`);
313
+ }
314
+ }
315
+ }
316
+ let descriptor;
317
+ try {
318
+ descriptor = openSync(lockPath, "wx");
319
+ writeFileSync(descriptor, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
320
+ closeSync(descriptor);
321
+ }
322
+ catch (error) {
323
+ throw safetyError(`Cannot acquire importer lock: ${error instanceof Error ? error.message : String(error)}`);
324
+ }
325
+ return () => { try {
326
+ unlinkSync(lockPath);
327
+ }
328
+ catch { /* best effort */ } };
329
+ }
330
+ export function createBackup(db, paths) {
331
+ const directory = join(paths.stateDir, "t3-import-backups");
332
+ mkdirSync(directory, { recursive: true });
333
+ const stamp = new Date().toISOString().replace(/[:.]/gu, "-");
334
+ const destination = join(directory, `state-before-import-${stamp}.sqlite`);
335
+ db.prepare("VACUUM INTO ?").run(destination);
336
+ const backup = new Database(destination, { readonly: true, fileMustExist: true });
337
+ try {
338
+ const integrity = String(backup.pragma("integrity_check", { simple: true }));
339
+ if (integrity !== "ok")
340
+ throw writeError(`Backup integrity check failed: ${destination}`);
341
+ }
342
+ finally {
343
+ backup.close();
344
+ }
345
+ return destination;
346
+ }
347
+ export function materializeAssets(plans, paths, runId) {
348
+ const staging = join(paths.attachmentsDir, `.t3-import-staging-${runId}`);
349
+ mkdirSync(staging, { recursive: true });
350
+ const created = [];
351
+ try {
352
+ for (const asset of plans.flatMap((plan) => plan.assets)) {
353
+ if (existsSync(asset.finalPath))
354
+ continue;
355
+ const temporary = join(staging, basename(asset.finalPath));
356
+ if (asset.data)
357
+ writeFileSync(temporary, asset.data);
358
+ else if (asset.sourcePath)
359
+ copyFileSync(asset.sourcePath, temporary);
360
+ else
361
+ throw writeError(`Attachment ${asset.descriptor.name} has no local data.`);
362
+ renameSync(temporary, asset.finalPath);
363
+ created.push(asset.finalPath);
364
+ }
365
+ return { created, cleanup: () => rmSync(staging, { recursive: true, force: true }) };
366
+ }
367
+ catch (error) {
368
+ for (const path of created)
369
+ rmSync(path, { force: true });
370
+ rmSync(staging, { recursive: true, force: true });
371
+ throw error;
372
+ }
373
+ }
374
+ function aggregateStatus(results, dryRun) {
375
+ if (dryRun)
376
+ return "dry-run";
377
+ const statuses = new Set(results.map((result) => result.status));
378
+ if (statuses.size > 1)
379
+ return "mixed";
380
+ return statuses.has("already-imported") ? "already-imported" : "imported";
381
+ }
382
+ export async function importConversations(selections, paths, options) {
383
+ validateDestinationOptions(options);
384
+ if (!existsSync(paths.dbPath))
385
+ throw safetyError(`T3 database not found: ${paths.dbPath}`);
386
+ await assertT3Closed(paths);
387
+ const releaseLock = options.dryRun ? () => { } : acquireLock(paths);
388
+ const db = new Database(paths.dbPath, { readonly: options.dryRun, fileMustExist: true });
389
+ let backup = null;
390
+ let v2Committed = false;
391
+ const runWarnings = [];
392
+ try {
393
+ const schema = validateTargetDatabase(db);
394
+ const planned = [];
395
+ const projectIds = new Map();
396
+ const plannedProjectEvents = new Set();
397
+ for (const selection of selections) {
398
+ for (const sourceThread of selection.conversation.threads) {
399
+ const thread = compactThreadForImport(sourceThread);
400
+ if (thread.source === "opencode" && !isV2Database(db))
401
+ throw safetyError("OpenCode imports require T3 Orchestrator V2.");
402
+ if (thread.turns.length === 0)
403
+ throw writeError(`Conversation ${thread.sourceSessionId} has no terminal turns to import. Use --include-incomplete to import an active snapshot as interrupted history.`);
404
+ const threadId = thread.source === "codex" && /^[0-9a-f-]{36}$/iu.test(thread.sourceSessionId)
405
+ ? thread.sourceSessionId
406
+ : deterministicUuid(`t3-import:thread:${thread.sourceKey}`);
407
+ const imported = isImported(db, thread.sourceKey, thread, threadId);
408
+ if (isV2Database(db) && !imported) {
409
+ const linked = v2ProviderThreads(db).filter((row) => isObject(row.nativeThreadRef) && row.nativeThreadRef.driver === sourceDriver(thread.source) && row.nativeThreadRef.nativeId === thread.sourceSessionId && typeof row.appThreadId === "string" && !v2Thread(db, row.appThreadId)?.deletedAt);
410
+ if (linked.length)
411
+ throw safetyError("This source session is already linked to a V2 thread. Inspect synchronization instead of creating a duplicate import.");
412
+ }
413
+ const destination = importDestination(db, paths, thread.workspace, options);
414
+ const workspaceKey = canonicalPath(destination?.projectRoot ?? thread.workspace);
415
+ let projectId = projectIds.get(workspaceKey) ?? existingProjectId(db, thread.workspace);
416
+ if (destination)
417
+ projectId = destination.projectId;
418
+ if (imported) {
419
+ const original = db.prepare("SELECT payload_json payload FROM orchestration_events WHERE stream_id=? AND event_type='thread.created' ORDER BY sequence LIMIT 1").get(threadId);
420
+ if (original) {
421
+ const created = JSON.parse(original.payload);
422
+ const current = threadWorkingDirectory(db, threadId, thread.workspace);
423
+ if (destination && (created.projectId !== destination.projectId || current.worktreePath !== destination.worktreePath))
424
+ throw safetyError("This thread is already imported into another destination. Import does not move existing threads; use a supported T3 project/workspace operation.");
425
+ projectId = created.projectId;
426
+ }
427
+ }
428
+ if (!projectId)
429
+ projectId = deterministicUuid(`t3-import:project:${workspaceKey}`);
430
+ projectIds.set(workspaceKey, projectId);
431
+ const provider = resolveProviderSelection(paths, thread.source, thread.model, thread.effort, options.providerInstance, thread.modelOptions);
432
+ const seed = thread.sourceKey;
433
+ const needsProject = destination ? destination.createProject : !existingProjectId(db, thread.workspace);
434
+ const projectEvent = !imported && needsProject && !plannedProjectEvents.has(projectId)
435
+ ? event(`project:${workspaceKey}`, "project.created", "project", projectId, "project.created", thread.createdAt, "client", {
436
+ projectId, title: options.noProject ? "No project" : basename(thread.workspace), workspaceRoot: destination?.projectRoot ?? thread.workspace, defaultModelSelection: modelSelection(provider), scripts: [], createdAt: thread.createdAt, updatedAt: thread.updatedAt,
437
+ })
438
+ : undefined;
439
+ if (projectEvent)
440
+ plannedProjectEvents.add(projectId);
441
+ const resumable = options.resume && selection.resume && thread.currentBranch && Boolean(thread.resumeCursor);
442
+ const plan = planThread(thread, threadId, projectId, paths, provider, seed, projectEvent, resumable, selection.conversation.fingerprint);
443
+ if (destination) {
444
+ plan.workingDirectory = destination.workingDirectory;
445
+ const created = plan.events.find((entry) => entry.type === "thread.created");
446
+ if (created)
447
+ created.payload.worktreePath = destination.worktreePath;
448
+ }
449
+ plan.alreadyImported = imported;
450
+ planned.push(plan);
451
+ }
452
+ }
453
+ const results = planned.map((plan) => ({
454
+ source: plan.thread.source,
455
+ sourceId: plan.thread.sourceSessionId,
456
+ sourceKey: plan.thread.sourceKey,
457
+ status: plan.alreadyImported ? "already-imported" : options.dryRun ? "dry-run" : "imported",
458
+ threadId: plan.threadId,
459
+ projectId: plan.projectId,
460
+ sourceWorkspace: plan.thread.workspace,
461
+ workingDirectory: plan.workingDirectory ?? (plan.alreadyImported ? threadWorkingDirectory(db, plan.threadId, plan.thread.workspace).cwd : plan.thread.workspace),
462
+ title: plan.thread.title,
463
+ turns: plan.thread.turns.length,
464
+ messages: plan.thread.turns.reduce((sum, turn) => sum + 1 + turn.assistant.length, 0),
465
+ activities: plan.thread.turns.reduce((sum, turn) => sum + turn.activities.length, 0) + (plan.thread.currentBranch ? 0 : 1),
466
+ events: plan.events.length,
467
+ attachments: plan.assets.length,
468
+ resumable: plan.resumable,
469
+ warnings: plan.warnings,
470
+ }));
471
+ const toWrite = planned.filter((plan) => !plan.alreadyImported);
472
+ const v2Write = isV2Database(db) && toWrite.length ? await prepareV2Write(db, toWrite) : undefined;
473
+ if (v2Write)
474
+ for (const plan of toWrite)
475
+ results.find((result) => result.threadId === plan.threadId).events = plan.targetEventCount;
476
+ if (toWrite.length > 0) {
477
+ const projection = projectionBacklog(db);
478
+ if (projection.backlog > 0) {
479
+ throw safetyError(`T3 has ${projection.backlog} unprojected event${projection.backlog === 1 ? "" : "s"} ` +
480
+ `(event sequence ${projection.latestSequence}, projection sequence ${projection.projectedSequence}). ` +
481
+ "Open T3 and let it finish loading before importing more conversations.");
482
+ }
483
+ const plannedEventCount = toWrite.reduce((sum, plan) => sum + (plan.targetEventCount ?? plan.events.length), 0);
484
+ if (plannedEventCount > MAX_SAFE_IMPORT_EVENTS) {
485
+ throw safetyError(`The compact import would append ${plannedEventCount} events, exceeding the safe one-launch limit of ${MAX_SAFE_IMPORT_EVENTS}. ` +
486
+ "Import fewer conversations at a time or select shorter histories.");
487
+ }
488
+ }
489
+ if (options.dryRun || toWrite.length === 0) {
490
+ return { schemaVersion: 1, status: aggregateStatus(results, options.dryRun), target: paths.dbPath, migration: schema.migration, backup: null, results, warnings: runWarnings };
491
+ }
492
+ backup = createBackup(db, paths);
493
+ mkdirSync(paths.attachmentsDir, { recursive: true });
494
+ const assets = materializeAssets(toWrite, paths, randomUUID());
495
+ const ledger = [];
496
+ try {
497
+ const v2Written = v2Write ? await v2Write.write() : undefined;
498
+ v2Committed = Boolean(v2Written);
499
+ const nextVersion = db.prepare("SELECT COALESCE(MAX(stream_version), -1) + 1 nextVersion FROM orchestration_events WHERE aggregate_kind = ? AND stream_id = ?");
500
+ const insert = db.prepare(`
501
+ INSERT INTO orchestration_events (
502
+ event_id, aggregate_kind, stream_id, stream_version, event_type,
503
+ occurred_at, command_id, causation_event_id, correlation_id,
504
+ actor_kind, payload_json, metadata_json
505
+ ) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, ?)
506
+ `);
507
+ const upsertRuntime = db.prepare(`
508
+ INSERT INTO provider_session_runtime (
509
+ thread_id, provider_name, provider_instance_id, adapter_key,
510
+ runtime_mode, status, last_seen_at, resume_cursor_json, runtime_payload_json
511
+ ) VALUES (?, ?, ?, ?, 'full-access', 'stopped', ?, ?, ?)
512
+ ON CONFLICT(thread_id) DO UPDATE SET
513
+ provider_name=excluded.provider_name,
514
+ provider_instance_id=excluded.provider_instance_id,
515
+ adapter_key=excluded.adapter_key,
516
+ runtime_mode=excluded.runtime_mode,
517
+ status=excluded.status,
518
+ last_seen_at=excluded.last_seen_at,
519
+ resume_cursor_json=excluded.resume_cursor_json,
520
+ runtime_payload_json=excluded.runtime_payload_json
521
+ `);
522
+ const streamVersions = new Map();
523
+ const collectRecords = () => {
524
+ for (const plan of toWrite) {
525
+ let firstSequence = Number.MAX_SAFE_INTEGER;
526
+ let lastSequence = 0;
527
+ if (v2Written) {
528
+ const sequence = v2Written.get(plan);
529
+ firstSequence = sequence.first;
530
+ lastSequence = sequence.last;
531
+ }
532
+ for (const plannedEvent of v2Written ? [] : plan.events) {
533
+ const streamKey = `${plannedEvent.aggregateKind}:${plannedEvent.streamId}`;
534
+ const version = streamVersions.get(streamKey) ?? nextVersion.get(plannedEvent.aggregateKind, plannedEvent.streamId).nextVersion;
535
+ const result = insert.run(plannedEvent.eventId, plannedEvent.aggregateKind, plannedEvent.streamId, version, plannedEvent.type, plannedEvent.occurredAt, plannedEvent.actorKind, JSON.stringify(plannedEvent.payload), JSON.stringify(plannedEvent.metadata));
536
+ const sequence = Number(result.lastInsertRowid);
537
+ firstSequence = Math.min(firstSequence, sequence);
538
+ lastSequence = Math.max(lastSequence, sequence);
539
+ streamVersions.set(streamKey, version + 1);
540
+ }
541
+ if (!v2Written && plan.resumable && plan.thread.resumeCursor) {
542
+ const cursor = plan.thread.source === "claude" ? { threadId: plan.threadId, ...plan.thread.resumeCursor } : plan.thread.resumeCursor;
543
+ upsertRuntime.run(plan.threadId, plan.provider.providerName, plan.provider.instanceId, plan.provider.adapterKey, plan.thread.updatedAt, JSON.stringify(cursor), JSON.stringify({ cwd: plan.thread.workspace, model: plan.provider.model, activeTurnId: null, lastError: null, modelSelection: modelSelection(plan.provider), lastRuntimeEvent: `t3-import.${plan.thread.source}`, lastRuntimeEventAt: plan.thread.updatedAt }));
544
+ }
545
+ const result = results.find((entry) => entry.threadId === plan.threadId);
546
+ result.sequence = { first: firstSequence, last: lastSequence };
547
+ ledger.push({
548
+ targetId: sha256(canonicalPath(paths.dbPath)), source: plan.thread.source,
549
+ sourceSessionId: plan.thread.sourceSessionId, sourceKey: plan.thread.sourceKey,
550
+ sourceFingerprint: plan.fingerprint, projectId: plan.projectId, threadId: plan.threadId,
551
+ importedAt: new Date().toISOString(), importerVersion: IMPORTER_VERSION,
552
+ migration: schema.migration, firstSequence, lastSequence,
553
+ resumable: plan.resumable, backupPath: backup, warnings: plan.warnings,
554
+ identitySeed: plan.thread.sourceKey, currentSourceKey: plan.thread.sourceKey,
555
+ ...(plan.thread.leafId ? { sourceLeafId: plan.thread.leafId } : {}),
556
+ sourceTitle: plan.thread.title, checkpoint: checkpointForThread(plan.thread),
557
+ isCanonical: true,
558
+ });
559
+ }
560
+ };
561
+ if (v2Written)
562
+ collectRecords();
563
+ else
564
+ db.transaction(collectRecords)();
565
+ assets.cleanup();
566
+ }
567
+ catch (error) {
568
+ if (!v2Committed)
569
+ for (const path of assets.created)
570
+ rmSync(path, { force: true });
571
+ assets.cleanup();
572
+ throw error;
573
+ }
574
+ const integrity = String(db.pragma("integrity_check", { simple: true }));
575
+ if (integrity !== "ok")
576
+ throw writeError(`Post-import integrity check failed: ${integrity}`);
577
+ try {
578
+ recordImports(ledger);
579
+ }
580
+ catch (error) {
581
+ runWarnings.push(`Import succeeded, but the external ledger could not be updated: ${error instanceof Error ? error.message : String(error)}`);
582
+ }
583
+ return { schemaVersion: 1, status: aggregateStatus(results, false), target: paths.dbPath, migration: schema.migration, backup, results, warnings: runWarnings };
584
+ }
585
+ catch (error) {
586
+ if (v2Committed)
587
+ throw writeError(`V2 import committed, but post-write verification or bookkeeping failed: ${error instanceof Error ? error.message : String(error)}. Attachments were retained. Inspect the target and recover the external ledger before further writes.`, error);
588
+ if (error instanceof Error && error.name === "ImporterError")
589
+ throw error;
590
+ throw writeError(`Import failed: ${error instanceof Error ? error.message : String(error)}`, error);
591
+ }
592
+ finally {
593
+ db.close();
594
+ releaseLock();
595
+ }
596
+ }
597
+ //# sourceMappingURL=importer.js.map