@turns/dev 0.0.26 → 0.0.28

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.
@@ -964,6 +964,45 @@ function firstRecords(descriptor, limit = 32, maxBytes = 1024 * 1024) {
964
964
  return [];
965
965
  }
966
966
  }
967
+ /** Claude copies a backgrounded session into a new transcript and ends the
968
+ * old one with `{"type":"continued-in","continuedInSessionId":...}`, followed
969
+ * only by small bookkeeping rows. Read just the file's tail for that marker. */
970
+ function claudeContinuedInSessionId(file) {
971
+ let handle;
972
+ try {
973
+ const stat = fs.statSync(file);
974
+ const bytesToRead = Math.min(stat.size, 16 * 1024);
975
+ handle = fs.openSync(file, 'r');
976
+ const buffer = Buffer.alloc(bytesToRead);
977
+ const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, stat.size - bytesToRead);
978
+ const text = buffer.subarray(0, bytesRead).toString('utf8');
979
+ if (!text.includes('"continued-in"'))
980
+ return '';
981
+ let continuedIn = '';
982
+ for (const line of text.split('\n')) {
983
+ if (!line.includes('"continued-in"'))
984
+ continue;
985
+ try {
986
+ const record = jsonObject(JSON.parse(line));
987
+ const id = String(record?.continuedInSessionId || '').trim();
988
+ if (record?.type === 'continued-in' && /^[0-9a-f-]{8,64}$/iu.test(id))
989
+ continuedIn = id;
990
+ }
991
+ catch { }
992
+ }
993
+ return continuedIn;
994
+ }
995
+ catch {
996
+ return '';
997
+ }
998
+ finally {
999
+ try {
1000
+ if (handle !== undefined)
1001
+ fs.closeSync(handle);
1002
+ }
1003
+ catch { }
1004
+ }
1005
+ }
967
1006
  /** Native identity and parentage carried by a bounded transcript prefix. */
968
1007
  export function nativeDescriptorLineage(descriptor) {
969
1008
  const sourcePath = String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/');
@@ -1042,10 +1081,14 @@ export function nativeDescriptorLineage(descriptor) {
1042
1081
  }
1043
1082
  if (parentNativeSessionId)
1044
1083
  isSubagent = true;
1084
+ const continuedInNativeSessionId = descriptor?.harness === 'claude' && !isSubagent && descriptor?.file
1085
+ ? claudeContinuedInSessionId(String(descriptor.file))
1086
+ : '';
1045
1087
  const value = {
1046
1088
  nativeSessionId,
1047
1089
  parentNativeSessionId,
1048
1090
  isSubagent,
1091
+ ...(continuedInNativeSessionId ? { continuedInNativeSessionId } : {}),
1049
1092
  ...(structural?.uploadable === false ? { uploadable: false } : {}),
1050
1093
  ...(parentSourcePath ? { parentSourcePath } : {}),
1051
1094
  ...(claudeMetadata?.description ? { nativeTitle: claudeMetadata.description } : {}),
@@ -317,6 +317,19 @@ export function linkNativeSessionParents(machine, descriptors) {
317
317
  candidates.push(record.descriptor);
318
318
  byNativeId.set(key, candidates);
319
319
  }
320
+ // A continued transcript names its successor; key it by the successor's
321
+ // store directory so equal native IDs in other projects never match.
322
+ const nativeDirectory = (descriptor) => path.posix.dirname(String(descriptor?.sourcePath || '').replaceAll('\\', '/'));
323
+ const predecessorsBySuccessor = new Map();
324
+ for (const record of records) {
325
+ const successorId = record.lineage.continuedInNativeSessionId;
326
+ if (!successorId || successorId === record.lineage.nativeSessionId)
327
+ continue;
328
+ const key = `${record.descriptor.harness}\0${nativeDirectory(record.descriptor)}\0${successorId}`;
329
+ const candidates = predecessorsBySuccessor.get(key) || [];
330
+ candidates.push(record.descriptor);
331
+ predecessorsBySuccessor.set(key, candidates);
332
+ }
320
333
  const grokParentsByChildId = new Map();
321
334
  for (const record of records) {
322
335
  if (record.descriptor.harness !== 'grok')
@@ -367,6 +380,10 @@ export function linkNativeSessionParents(machine, descriptors) {
367
380
  // native upload until its store exposes enough lineage to nest it safely.
368
381
  if (isSubagent && !parentSourcePath)
369
382
  return [];
383
+ const predecessors = !isSubagent && lineage.nativeSessionId
384
+ ? predecessorsBySuccessor.get(`${descriptor.harness}\0${nativeDirectory(descriptor)}\0${lineage.nativeSessionId}`) || []
385
+ : [];
386
+ const predecessor = predecessors.length === 1 ? predecessors[0] : null;
370
387
  return [
371
388
  {
372
389
  ...descriptor,
@@ -379,6 +396,9 @@ export function linkNativeSessionParents(machine, descriptors) {
379
396
  parentSessionId: stableNativeSessionId(machine, descriptor.harness, parentSourcePath),
380
397
  }
381
398
  : {}),
399
+ ...(predecessor
400
+ ? { supersedesSessionId: stableNativeSessionId(machine, descriptor.harness, predecessor.sourcePath) }
401
+ : {}),
382
402
  },
383
403
  ];
384
404
  });
@@ -391,6 +411,8 @@ export function nativeDescriptorFingerprint(descriptor) {
391
411
  String(descriptor.parentSessionId || ''),
392
412
  NATIVE_DESCRIPTOR_VERSION,
393
413
  nativeDescriptorMetadataFingerprint(descriptor),
414
+ // Appended only when present so unrelated sessions keep their fingerprint.
415
+ ...(descriptor.supersedesSessionId ? [String(descriptor.supersedesSessionId)] : []),
394
416
  ]);
395
417
  }
396
418
  function nativeDescriptorMetadataFingerprint(descriptor) {
@@ -994,6 +1016,7 @@ export async function uploadNativeSession(rpc, machine, descriptor, cursor = {},
994
1016
  ...(descriptor.readOnly === true ? { readOnly: true } : {}),
995
1017
  ...(agentSessionId ? { agentSessionId } : {}),
996
1018
  ...(descriptor.parentSessionId ? { parentSessionId: descriptor.parentSessionId } : {}),
1019
+ ...(descriptor.supersedesSessionId ? { supersedesSessionId: descriptor.supersedesSessionId } : {}),
997
1020
  },
998
1021
  });
999
1022
  if (opened.type === 'session_tombstoned') {
@@ -1592,7 +1615,8 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
1592
1615
  Number(previous?.[1]) === Number(descriptor.mtimeMs) &&
1593
1616
  String(previous?.[3] || '') === String(descriptor.parentSessionId || '') &&
1594
1617
  Number(previous?.[4]) === NATIVE_DESCRIPTOR_VERSION &&
1595
- String(previous?.[5] || '') === nativeDescriptorMetadataFingerprint(descriptor)) {
1618
+ String(previous?.[5] || '') === nativeDescriptorMetadataFingerprint(descriptor) &&
1619
+ String(previous?.[6] || '') === String(descriptor.supersedesSessionId || '')) {
1596
1620
  fingerprint = previousFingerprint;
1597
1621
  }
1598
1622
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",