@ycodium-ai/plugin-session-import 0.2.3018 → 0.2.3020

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 (2) hide show
  1. package/dist/index.js +148 -19
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -164,9 +164,10 @@ var sessionImportSyncResultSchema = {
164
164
  };
165
165
 
166
166
  // src/sessionImportRuntime.ts
167
- import * as NodeCrypto3 from "node:crypto";
167
+ import * as NodeCrypto4 from "node:crypto";
168
168
  import {
169
- projectOrNull as projectOrNull2
169
+ projectOrNull as projectOrNull2,
170
+ threadOrNull as threadOrNull2
170
171
  } from "@ycodium-ai/plugin-api";
171
172
 
172
173
  // src/claudeSessionScan.ts
@@ -1130,8 +1131,126 @@ async function readRolloutModel(input, rolloutPath, sameDirectory) {
1130
1131
  return model;
1131
1132
  }
1132
1133
 
1133
- // src/reconcileImportedTimeline.ts
1134
+ // src/existingTimeline.ts
1134
1135
  import * as NodeCrypto from "node:crypto";
1136
+ import {
1137
+ isThreadNotFound,
1138
+ threadOrNull,
1139
+ THREAD_ACTIVITIES_LIST_LIMIT,
1140
+ THREAD_MESSAGES_LIST_LIMIT,
1141
+ TOOL_LIFECYCLE_ACTIVITY_KINDS
1142
+ } from "@ycodium-ai/plugin-api";
1143
+ var MESSAGES_PAGE = THREAD_MESSAGES_LIST_LIMIT;
1144
+ var ACTIVITIES_PAGE = THREAD_ACTIVITIES_LIST_LIMIT;
1145
+ var TOOL_KINDS = [...TOOL_LIFECYCLE_ACTIVITY_KINDS];
1146
+ var IMPORTED_KINDS = [...TOOL_KINDS, "reasoning"];
1147
+ var MAX_FULL_READS = 4;
1148
+ var TimelineKeptChangingError = class extends Error {
1149
+ constructor() {
1150
+ super("The thread's timeline kept changing while it was being read.");
1151
+ this.name = "TimelineKeptChangingError";
1152
+ }
1153
+ };
1154
+ async function readEveryPage(listPage) {
1155
+ const rows = [];
1156
+ let cursor = null;
1157
+ let sequence;
1158
+ let steady = true;
1159
+ do {
1160
+ const page = await listPage(cursor);
1161
+ if (sequence !== void 0 && page.snapshotSequence !== sequence) steady = false;
1162
+ sequence = page.snapshotSequence;
1163
+ rows.push(...page.items);
1164
+ cursor = page.nextCursor;
1165
+ } while (cursor !== null);
1166
+ return { rows, steady };
1167
+ }
1168
+ function digestOfRows(rows) {
1169
+ const hash = NodeCrypto.createHash("sha256");
1170
+ for (const row of rows) hash.update(JSON.stringify(row)).update("\n");
1171
+ return hash.digest("hex");
1172
+ }
1173
+ async function readStable(listPage) {
1174
+ let previousDigest;
1175
+ for (let attempt = 0; attempt < MAX_FULL_READS; attempt++) {
1176
+ const { rows, steady } = await readEveryPage(listPage);
1177
+ if (new Set(rows.map((row) => row.id)).size !== rows.length) {
1178
+ previousDigest = void 0;
1179
+ continue;
1180
+ }
1181
+ if (steady) return rows;
1182
+ const digest = digestOfRows(rows);
1183
+ if (digest === previousDigest) return rows;
1184
+ previousDigest = digest;
1185
+ }
1186
+ throw new TimelineKeptChangingError();
1187
+ }
1188
+ async function listAllMessages(threads, threadId) {
1189
+ const newestFirst = await readStable(
1190
+ (cursor) => threads.messages.list({ threadId, limit: MESSAGES_PAGE, cursor })
1191
+ );
1192
+ return newestFirst.toReversed().map((item) => ({
1193
+ messageId: item.id,
1194
+ role: item.role,
1195
+ text: item.text,
1196
+ createdAt: item.createdAt,
1197
+ ...item.source === void 0 ? {} : { source: item.source },
1198
+ ...item.modelSelection === void 0 ? {} : { modelSelection: item.modelSelection }
1199
+ }));
1200
+ }
1201
+ async function listAllActivities(threads, threadId, query) {
1202
+ const newestFirst = await readStable(
1203
+ (cursor) => threads.activities.list({ threadId, ...query, limit: ACTIVITIES_PAGE, cursor })
1204
+ );
1205
+ return newestFirst.toReversed();
1206
+ }
1207
+ var activityOf = (item, payload) => ({
1208
+ id: item.id,
1209
+ kind: item.kind,
1210
+ tone: item.tone,
1211
+ summary: item.summary,
1212
+ payload,
1213
+ createdAt: item.createdAt,
1214
+ turnId: item.turnId
1215
+ });
1216
+ async function readExistingThread(threads, threadId) {
1217
+ const shell = await threadOrNull(threads, threadId);
1218
+ if (shell === null) return null;
1219
+ const timeline = await readExistingTimeline(threads, threadId);
1220
+ return timeline === null ? null : { ...shell, ...timeline };
1221
+ }
1222
+ async function readExistingTimeline(threads, threadId) {
1223
+ try {
1224
+ const messages = await listAllMessages(threads, threadId);
1225
+ const unturned = await listAllActivities(threads, threadId, {
1226
+ kinds: IMPORTED_KINDS,
1227
+ inTurn: "none"
1228
+ });
1229
+ const turned = await listAllActivities(threads, threadId, {
1230
+ kinds: TOOL_KINDS,
1231
+ inTurn: "any",
1232
+ maxPayloadBytes: 0
1233
+ });
1234
+ const runtimeCalls = turned.filter((item) => item.turnId !== null).map((item) => {
1235
+ const toolCallId2 = "toolCallId" in item ? item.toolCallId : void 0;
1236
+ return activityOf(item, toolCallId2 === void 0 ? void 0 : { toolCallId: toolCallId2 });
1237
+ });
1238
+ return {
1239
+ messages,
1240
+ activities: [
1241
+ // 这一趟没限载荷大小,行都带载荷;只有被 `maxPayloadBytes` 省掉的行才没有。
1242
+ ...unturned.map((item) => activityOf(item, "payload" in item ? item.payload : void 0)),
1243
+ ...runtimeCalls
1244
+ ]
1245
+ };
1246
+ } catch (error) {
1247
+ if (isThreadNotFound(error)) return null;
1248
+ throw error;
1249
+ }
1250
+ }
1251
+
1252
+ // src/reconcileImportedTimeline.ts
1253
+ import * as NodeCrypto2 from "node:crypto";
1135
1254
  function record(value) {
1136
1255
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1137
1256
  }
@@ -1140,7 +1259,7 @@ function sourceId(value, input) {
1140
1259
  return source2?.kind === "native-session" && source2.provider === input.provider && source2.sessionId === input.nativeSessionId && typeof source2.messageId === "string" ? source2.messageId : void 0;
1141
1260
  }
1142
1261
  function stableId(input, kind, nativeId) {
1143
- const digest = NodeCrypto.createHash("sha256").update(JSON.stringify([input.threadId, input.provider, input.nativeSessionId, kind, nativeId])).digest("hex");
1262
+ const digest = NodeCrypto2.createHash("sha256").update(JSON.stringify([input.threadId, input.provider, input.nativeSessionId, kind, nativeId])).digest("hex");
1144
1263
  return `native-${kind}:${digest}`;
1145
1264
  }
1146
1265
  function sourceFor(input, item) {
@@ -1745,7 +1864,7 @@ async function listSessions(ctx, input) {
1745
1864
  var describeError = (error) => error instanceof Error ? error.message : String(error);
1746
1865
 
1747
1866
  // src/sessionTranscript.ts
1748
- import * as NodeCrypto2 from "node:crypto";
1867
+ import * as NodeCrypto3 from "node:crypto";
1749
1868
  var SESSION_IMPORT_ACTIVITY_LIMIT = 1e3;
1750
1869
  var SESSION_IMPORT_ACTIVITY_TEXT_LIMIT = 32e3;
1751
1870
  var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -1761,7 +1880,7 @@ function fallbackId(value) {
1761
1880
  } catch {
1762
1881
  text = "";
1763
1882
  }
1764
- return `record:${NodeCrypto2.createHash("sha256").update(text).digest("hex").slice(0, 20)}`;
1883
+ return `record:${NodeCrypto3.createHash("sha256").update(text).digest("hex").slice(0, 20)}`;
1765
1884
  }
1766
1885
  function readableContent(value) {
1767
1886
  if (typeof value === "string") return value;
@@ -2153,6 +2272,7 @@ var SKIPPED_ACTIVE = {
2153
2272
  status: "skipped-active",
2154
2273
  appended: 0
2155
2274
  };
2275
+ var EXISTING_THREAD_NOT_IDLE = "The existing thread must be idle and unarchived before its history can be refreshed.";
2156
2276
  var IMPORTED_TITLE_MAX_LENGTH = 80;
2157
2277
  function resolveImportedTitle(transcript) {
2158
2278
  const first = transcript.messages.find((message) => message.role === "user");
@@ -2215,7 +2335,7 @@ function importedHistoryBatches(history) {
2215
2335
  if (count > 0) batches.push(batch);
2216
2336
  return batches;
2217
2337
  }
2218
- var historyCommandId = (threadId, batch) => `native-history:${NodeCrypto3.createHash("sha256").update(JSON.stringify([threadId, batch])).digest("hex")}`;
2338
+ var historyCommandId = (threadId, batch) => `native-history:${NodeCrypto4.createHash("sha256").update(JSON.stringify([threadId, batch])).digest("hex")}`;
2219
2339
  var importsInFlight = /* @__PURE__ */ new WeakMap();
2220
2340
  async function runImport(ctx, input, emit) {
2221
2341
  let pending = importsInFlight.get(ctx);
@@ -2260,12 +2380,15 @@ async function runImportOnce(ctx, input, emit) {
2260
2380
  return fail("The selected provider does not own this native session.");
2261
2381
  }
2262
2382
  const bindings = await ctx.providers.sessions.list();
2263
- const existing = await ctx.projections.getThread(input.threadId, { includeActivities: true });
2383
+ const existing = await readExistingThread(ctx.threads, input.threadId).catch((error) => {
2384
+ if (error instanceof TimelineKeptChangingError) return fail(EXISTING_THREAD_NOT_IDLE);
2385
+ throw error;
2386
+ });
2264
2387
  const binding = bindings.find((candidate2) => candidate2.threadId === input.threadId);
2265
2388
  if (existing !== null) {
2266
2389
  const savedSources = [
2267
2390
  ...existing.messages.map((message) => message.source),
2268
- ...(existing.activities ?? []).map(
2391
+ ...existing.activities.map(
2269
2392
  (activity) => activity.payload !== null && typeof activity.payload === "object" ? activity.payload.source : void 0
2270
2393
  )
2271
2394
  ].filter((source2) => source2 !== void 0);
@@ -2275,15 +2398,13 @@ async function runImportOnce(ctx, input, emit) {
2275
2398
  if (existing.projectId !== input.projectId || !pendingLink && (binding?.instanceId !== input.instanceId || providerKindOfDriver(binding.driver) !== input.provider || nativeSessionIdOf(input.provider, binding.resumeCursor) !== input.nativeSessionId))
2276
2399
  return fail("This thread is not linked to the selected native session.");
2277
2400
  if (existing.archivedAt !== null || existing.session?.activeTurnId != null || existing.session?.status === "running" || existing.session?.status === "starting")
2278
- return fail(
2279
- "The existing thread must be idle and unarchived before its history can be refreshed."
2280
- );
2401
+ return fail(EXISTING_THREAD_NOT_IDLE);
2281
2402
  threadCreated = true;
2282
2403
  } else {
2283
2404
  for (const candidate2 of bindings) {
2284
2405
  if (providerKindOfDriver(candidate2.driver) !== input.provider || nativeSessionIdOf(input.provider, candidate2.resumeCursor) !== input.nativeSessionId)
2285
2406
  continue;
2286
- const imported = await ctx.projections.getThread(candidate2.threadId);
2407
+ const imported = await threadOrNull2(ctx.threads, candidate2.threadId);
2287
2408
  if (imported?.projectId === input.projectId)
2288
2409
  return fail(
2289
2410
  "This native session is already imported. Refresh its existing thread instead.",
@@ -2472,11 +2593,19 @@ var NativeSessionTail = class {
2472
2593
  if (modifiedAt === null) return NOT_IMPORTED;
2473
2594
  const seen = await this.readBaseline(threadId);
2474
2595
  if (seen !== void 0 && modifiedAt <= seen) return UNCHANGED;
2475
- const thread = await this.ctx.projections.getThread(threadId, { includeActivities: true });
2476
- if (thread === null || thread.archivedAt !== null) return NOT_IMPORTED;
2596
+ const thread = await threadOrNull2(this.ctx.threads, threadId);
2597
+ if (thread === null) return NOT_IMPORTED;
2477
2598
  if (thread.session?.activeTurnId != null) return SKIPPED_ACTIVE;
2478
2599
  if (thread.session?.status === "running" || thread.session?.status === "starting")
2479
2600
  return SKIPPED_ACTIVE;
2601
+ const timeline = await readExistingTimeline(this.ctx.threads, threadId).catch(
2602
+ (error) => {
2603
+ if (error instanceof TimelineKeptChangingError) return "busy";
2604
+ throw error;
2605
+ }
2606
+ );
2607
+ if (timeline === "busy") return SKIPPED_ACTIVE;
2608
+ if (timeline === null) return NOT_IMPORTED;
2480
2609
  const records = await readClaudeSessionRecords({
2481
2610
  fs: this.ctx.fs,
2482
2611
  transcriptDir,
@@ -2490,8 +2619,8 @@ var NativeSessionTail = class {
2490
2619
  nativeSessionId,
2491
2620
  instanceId: binding.instanceId,
2492
2621
  transcript: readClaudeTranscript(records),
2493
- existingMessages: thread.messages,
2494
- existingActivities: thread.activities ?? [],
2622
+ existingMessages: timeline.messages,
2623
+ existingActivities: timeline.activities,
2495
2624
  fallbackIso: appendedAt
2496
2625
  });
2497
2626
  const batches = importedHistoryBatches(history);
@@ -2525,7 +2654,7 @@ var NativeSessionTail = class {
2525
2654
  });
2526
2655
  if (history.appendedMessageCount === 0 && (history.hasLegacyMatches || history.activities.length === 0))
2527
2656
  return { status: "synced", appended: history.appendedMessageCount };
2528
- const current = await this.ctx.projections.getThread(threadId);
2657
+ const current = await threadOrNull2(this.ctx.threads, threadId);
2529
2658
  if (current?.session == null || current.session.status === "stopped" || current.session.activeTurnId !== null || current.session.status === "running" || current.session.status === "starting")
2530
2659
  return { status: "synced", appended: history.appendedMessageCount };
2531
2660
  await this.ctx.providers.sessions.stop(threadId).catch((error) => {
@@ -2541,7 +2670,7 @@ var NativeSessionTail = class {
2541
2670
  // src/index.ts
2542
2671
  var index_default = definePlugin({
2543
2672
  id: "session-import",
2544
- apiVersion: 8,
2673
+ apiVersion: 9,
2545
2674
  activate: (ctx) => {
2546
2675
  ctx.command({
2547
2676
  name: "scan",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/plugin-session-import",
3
- "version": "0.2.3018",
3
+ "version": "0.2.3020",
4
4
  "description": "Brings a session you started in a terminal into Ycodium.",
5
5
  "keywords": [
6
6
  "session-import",
@@ -20,7 +20,7 @@
20
20
  "effect": "4.0.0-beta.103",
21
21
  "esbuild": "0.28.1",
22
22
  "vite-plus": "0.2.2",
23
- "@ycodium-ai/plugin-api": "0.2.3018",
23
+ "@ycodium-ai/plugin-api": "0.2.3020",
24
24
  "ycodium-jsonl-stream": "0.0.0"
25
25
  },
26
26
  "ycodium": {