@basou/cli 0.71.1 → 0.71.2

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.
package/dist/program.js CHANGED
@@ -1040,6 +1040,8 @@ import {
1040
1040
  CLAUDE_IMPORT_SOURCE,
1041
1041
  CODEX_IMPORT_SOURCE,
1042
1042
  classifyFilesBySourceRoot,
1043
+ claudeTranscriptOwnRecords,
1044
+ claudeTranscriptRecordUuid,
1043
1045
  claudeTranscriptToImportPayload,
1044
1046
  codexRolloutToImportPayload,
1045
1047
  displayPath as displayPath2,
@@ -1140,24 +1142,43 @@ async function doRunImportClaudeCode(options, ctx) {
1140
1142
  });
1141
1143
  const projectsRoot = ctx.claudeProjectsDir ?? join2(homedir(), ".claude", "projects");
1142
1144
  const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
1145
+ const store = await loadImportStore(paths, options, CLAUDE_IMPORT_SOURCE);
1146
+ const conversations = await indexConversations(
1147
+ await listTranscriptFiles(projectsRoot, projectPaths),
1148
+ ctx.transcriptCreatedAtMs ?? birthTimeMs,
1149
+ (externalId) => earliestStart(store.byExternalId.get(externalId) ?? [])
1150
+ );
1143
1151
  const projectSet = new Set(projectPaths);
1144
1152
  const candidates = files.map((file) => {
1145
1153
  const externalId = basename3(file, ".jsonl");
1154
+ const { root, older } = conversations.place(file);
1146
1155
  return {
1147
1156
  externalId,
1148
1157
  sourcePath: file,
1149
- toPayload: async () => {
1158
+ // An earlier import that took the copies too began the session at the
1159
+ // conversation's first record or before it; one without them begins after
1160
+ // the last copy.
1161
+ holdsCopies: (prior) => older.length > 0 && root?.timestamp !== void 0 && Date.parse(prior.startedAt) <= Date.parse(root.timestamp),
1162
+ toPayload: async (notBefore) => {
1150
1163
  const { records, sizeBytes } = await readJsonlRecords(file);
1151
1164
  const cwd = firstTranscriptCwd(records);
1152
- if (cwd === void 0 || !projectSet.has(cwd)) return null;
1165
+ if (cwd === void 0 || !projectSet.has(cwd))
1166
+ return { payload: null, leftOutCopies: false };
1153
1167
  const observation2 = await readSessionObservation(paths.observations, externalId);
1154
1168
  const observedFiles = observation2 === null ? [] : observedFilesOf(observation2);
1155
- return claudeTranscriptToImportPayload(records, {
1156
- workspaceId: manifest.workspace.id,
1157
- externalId,
1158
- sourceSizeBytes: sizeBytes,
1159
- ...observedFiles.length > 0 ? { observedFiles } : {}
1169
+ const own = claudeTranscriptOwnRecords(records, {
1170
+ copiedUuids: await conversations.uuidsOf(older),
1171
+ ...notBefore !== void 0 ? { notBefore } : {}
1160
1172
  });
1173
+ return {
1174
+ payload: claudeTranscriptToImportPayload(own, {
1175
+ workspaceId: manifest.workspace.id,
1176
+ externalId,
1177
+ sourceSizeBytes: sizeBytes,
1178
+ ...observedFiles.length > 0 ? { observedFiles } : {}
1179
+ }),
1180
+ leftOutCopies: own.length < records.length
1181
+ };
1161
1182
  }
1162
1183
  };
1163
1184
  });
@@ -1165,7 +1186,7 @@ async function doRunImportClaudeCode(options, ctx) {
1165
1186
  paths,
1166
1187
  manifest,
1167
1188
  options,
1168
- CLAUDE_IMPORT_SOURCE,
1189
+ store,
1169
1190
  candidates,
1170
1191
  projectPaths,
1171
1192
  hasDeclaredBoundary(options, manifest)
@@ -1187,18 +1208,21 @@ async function doRunImportCodex(options, ctx) {
1187
1208
  sourcePath: file,
1188
1209
  toPayload: async () => {
1189
1210
  const { records, sizeBytes } = await readJsonlRecords(file);
1190
- return codexRolloutToImportPayload(records, {
1191
- workspaceId: manifest.workspace.id,
1192
- externalId,
1193
- sourceSizeBytes: sizeBytes
1194
- });
1211
+ return {
1212
+ payload: codexRolloutToImportPayload(records, {
1213
+ workspaceId: manifest.workspace.id,
1214
+ externalId,
1215
+ sourceSizeBytes: sizeBytes
1216
+ }),
1217
+ leftOutCopies: false
1218
+ };
1195
1219
  }
1196
1220
  }));
1197
1221
  await importDerivedSessions(
1198
1222
  paths,
1199
1223
  manifest,
1200
1224
  options,
1201
- CODEX_IMPORT_SOURCE,
1225
+ await loadImportStore(paths, options, CODEX_IMPORT_SOURCE),
1202
1226
  candidates,
1203
1227
  projectPaths,
1204
1228
  hasDeclaredBoundary(options, manifest)
@@ -1223,14 +1247,8 @@ async function resolveImportTarget(ctx) {
1223
1247
  const manifest = await readManifest2(paths);
1224
1248
  return { repositoryRoot, paths, manifest };
1225
1249
  }
1226
- async function importDerivedSessions(paths, manifest, options, sourceKind, candidates, projectPaths, boundaryDeclared) {
1227
- await assertSessionStoreSafe(paths);
1228
- if (options.dryRun !== true) await assertLockStoreSafe2(paths);
1229
- const {
1230
- byExternalId: existingByExternalId,
1231
- notDirectories,
1232
- yamlNotFiles
1233
- } = await loadExistingByExternalId(paths, sourceKind);
1250
+ async function importDerivedSessions(paths, manifest, options, store, candidates, projectPaths, boundaryDeclared) {
1251
+ const { byExternalId: existingByExternalId, notDirectories, yamlNotFiles } = store;
1234
1252
  let notDirectoriesWarned = false;
1235
1253
  const seenThisRun = /* @__PURE__ */ new Set();
1236
1254
  const crossProjectCheck = boundaryDeclared;
@@ -1258,7 +1276,8 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1258
1276
  skippedLegacy: 0,
1259
1277
  skippedDecreased: 0,
1260
1278
  skippedDuplicate: 0,
1261
- skippedUnverifiable: 0
1279
+ skippedUnverifiable: 0,
1280
+ droppedCopies: 0
1262
1281
  };
1263
1282
  let sanitizedPaths = 0;
1264
1283
  const validate = (payload) => {
@@ -1274,30 +1293,44 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1274
1293
  }
1275
1294
  return parsed.data;
1276
1295
  };
1277
- for (const { externalId, sourcePath, toPayload } of candidates) {
1296
+ for (const { externalId, sourcePath, toPayload, holdsCopies } of candidates) {
1278
1297
  if (seenThisRun.has(externalId)) {
1279
1298
  counts.skippedExisting++;
1280
1299
  continue;
1281
1300
  }
1282
1301
  const priors = existingByExternalId.get(externalId) ?? [];
1283
1302
  if (priors.length > 0 && options.force !== true) {
1284
- const prior = await classifyReimport(priors, sourcePath, externalId, counts);
1285
- if (prior === null) continue;
1286
- const payload2 = validate(await toPayload());
1303
+ const decision = await classifyReimport(priors, sourcePath, externalId, counts, holdsCopies);
1304
+ if (decision === null) continue;
1305
+ const { prior, reason } = decision;
1306
+ const derived = await toPayload(prior.startedAt);
1307
+ const payload2 = validate(derived.payload);
1287
1308
  if (payload2 === null) {
1309
+ if (reason === "holds_copies") {
1310
+ console.error(
1311
+ `Import: ${externalId} added no action of its own after the records it copied from an older transcript; its session, imported with those records, is left as it is`
1312
+ );
1313
+ }
1288
1314
  counts.skippedNoAction++;
1289
1315
  continue;
1290
1316
  }
1317
+ if (reason === "holds_copies" && !derived.leftOutCopies) {
1318
+ counts.skippedExisting++;
1319
+ continue;
1320
+ }
1291
1321
  const readSize = payload2.session.source.source_size_bytes;
1292
- if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && readSize <= prior.sourceSizeBytes) {
1322
+ if (prior.sourceSizeBytes !== void 0 && readSize !== void 0 && (reason === "grew" ? readSize <= prior.sourceSizeBytes : readSize < prior.sourceSizeBytes)) {
1293
1323
  console.error(
1294
- `Import: ${externalId} source changed during read (now ${readSize} <= ${prior.sourceSizeBytes} bytes); re-import skipped`
1324
+ `Import: ${externalId} source changed during read (now ${readSize} bytes, was ${prior.sourceSizeBytes}); re-import skipped`
1295
1325
  );
1296
1326
  counts.skippedDecreased++;
1297
1327
  continue;
1298
1328
  }
1299
1329
  const outcome = await reimportPreservingId(paths, manifest, prior.sessionId, payload2, {
1300
- dryRun: options.dryRun === true
1330
+ dryRun: options.dryRun === true,
1331
+ // What an earlier import derived from the copies predates the session's
1332
+ // fresh start; anything later that is not reproduced still refuses.
1333
+ ...derived.leftOutCopies ? { dropDerivedBefore: payload2.session.started_at } : {}
1301
1334
  });
1302
1335
  if (outcome.status === "skipped") {
1303
1336
  const detail = outcome.reason === "prior_events_unreadable" ? "prior events.jsonl has unreadable lines" : outcome.reason === "prior_chain_broken" ? "prior events.jsonl failed hash-chain verification (run 'basou verify')" : outcome.reason === "prior_yaml_invalid" ? "prior session.yaml does not load as a whole document" : "source changed in a non-append way (derived events would be dropped)";
@@ -1306,11 +1339,12 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1306
1339
  continue;
1307
1340
  }
1308
1341
  counts.reimported++;
1342
+ if (holdsCopies?.(prior) === true) counts.droppedCopies++;
1309
1343
  seenThisRun.add(externalId);
1310
1344
  await noteCrossProject(externalId, payload2);
1311
1345
  continue;
1312
1346
  }
1313
- const payload = validate(await toPayload());
1347
+ const payload = validate((await toPayload(earliestStart(priors))).payload);
1314
1348
  if (payload === null) {
1315
1349
  counts.skippedNoAction++;
1316
1350
  continue;
@@ -1361,7 +1395,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
1361
1395
  }
1362
1396
  printImportResult(options, results, counts);
1363
1397
  }
1364
- async function classifyReimport(priors, sourcePath, externalId, counts) {
1398
+ async function classifyReimport(priors, sourcePath, externalId, counts, holdsCopies) {
1365
1399
  if (priors.length > 1) {
1366
1400
  console.error(
1367
1401
  `Import: ${externalId} has ${priors.length} prior sessions; re-import skipped (use --force)`
@@ -1384,6 +1418,7 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
1384
1418
  return null;
1385
1419
  }
1386
1420
  if (currentSize === prior.sourceSizeBytes) {
1421
+ if (holdsCopies?.(prior) === true) return { prior, reason: "holds_copies" };
1387
1422
  counts.skippedExisting++;
1388
1423
  return null;
1389
1424
  }
@@ -1394,7 +1429,15 @@ async function classifyReimport(priors, sourcePath, externalId, counts) {
1394
1429
  counts.skippedDecreased++;
1395
1430
  return null;
1396
1431
  }
1397
- return prior;
1432
+ return { prior, reason: "grew" };
1433
+ }
1434
+ function earliestStart(priors) {
1435
+ let earliest;
1436
+ for (const { startedAt } of priors) {
1437
+ if (earliest === void 0 || Date.parse(startedAt) < Date.parse(earliest))
1438
+ earliest = startedAt;
1439
+ }
1440
+ return earliest;
1398
1441
  }
1399
1442
  function encodeProjectDir(projectPath) {
1400
1443
  return projectPath.replace(/[^a-zA-Z0-9]/g, "-");
@@ -1406,6 +1449,11 @@ function firstTranscriptCwd(records) {
1406
1449
  }
1407
1450
  return void 0;
1408
1451
  }
1452
+ async function loadImportStore(paths, options, sourceKind) {
1453
+ await assertSessionStoreSafe(paths);
1454
+ if (options.dryRun !== true) await assertLockStoreSafe2(paths);
1455
+ return loadExistingByExternalId(paths, sourceKind);
1456
+ }
1409
1457
  async function loadExistingByExternalId(paths, sourceKind) {
1410
1458
  const byExternalId = /* @__PURE__ */ new Map();
1411
1459
  const add = (externalId, prior) => {
@@ -1431,7 +1479,8 @@ async function loadExistingByExternalId(paths, sourceKind) {
1431
1479
  }
1432
1480
  if (session.session.source.kind !== sourceKind) continue;
1433
1481
  const sourceSizeBytes = session.session.source.source_size_bytes;
1434
- const prior = sourceSizeBytes !== void 0 ? { sessionId, sourceSizeBytes } : { sessionId };
1482
+ const startedAt = session.session.started_at;
1483
+ const prior = sourceSizeBytes !== void 0 ? { sessionId, startedAt, sourceSizeBytes } : { sessionId, startedAt };
1435
1484
  const ext = session.session.source.external_id;
1436
1485
  if (typeof ext === "string" && ext.length > 0) {
1437
1486
  add(ext, prior);
@@ -1476,6 +1525,153 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
1476
1525
  }
1477
1526
  return [...new Set(files)].sort();
1478
1527
  }
1528
+ async function listTranscriptFiles(projectsRoot, projectPaths) {
1529
+ const files = [];
1530
+ for (const projectPath of projectPaths) {
1531
+ const transcriptDir = join2(projectsRoot, encodeProjectDir(projectPath));
1532
+ let entries;
1533
+ try {
1534
+ entries = await readdir(transcriptDir);
1535
+ } catch (error) {
1536
+ if (findErrorCode2(error, "ENOENT")) continue;
1537
+ throw new Error("Failed to read Claude transcript directory", { cause: error });
1538
+ }
1539
+ for (const name of entries) {
1540
+ if (name.endsWith(".jsonl")) files.push(join2(transcriptDir, name));
1541
+ }
1542
+ }
1543
+ return [...new Set(files)].sort();
1544
+ }
1545
+ async function indexConversations(files, createdAtMs, importedStartedAt) {
1546
+ const uuidsByFile = /* @__PURE__ */ new Map();
1547
+ const uuidsIn = async (file) => {
1548
+ let held = uuidsByFile.get(file);
1549
+ if (held === void 0) {
1550
+ held = await readTranscriptUuids(file);
1551
+ uuidsByFile.set(file, held);
1552
+ }
1553
+ return held;
1554
+ };
1555
+ const rootOf = /* @__PURE__ */ new Map();
1556
+ const byRoot = /* @__PURE__ */ new Map();
1557
+ for (const file of files) {
1558
+ const root = await readConversationRoot(file);
1559
+ if (root === void 0) continue;
1560
+ rootOf.set(file, root);
1561
+ const entry = { file, externalId: basename3(file, ".jsonl"), createdAtMs: Number.NaN };
1562
+ const list = byRoot.get(root.uuid);
1563
+ if (list === void 0) byRoot.set(root.uuid, [entry]);
1564
+ else list.push(entry);
1565
+ }
1566
+ for (const list of byRoot.values()) {
1567
+ if (list.length < 2) continue;
1568
+ for (const entry of list) entry.createdAtMs = await createdAtMs(entry.file);
1569
+ list.sort((a, b) => {
1570
+ const knownA = Number.isFinite(a.createdAtMs) && a.createdAtMs > 0;
1571
+ const knownB = Number.isFinite(b.createdAtMs) && b.createdAtMs > 0;
1572
+ if (knownA && knownB && a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs;
1573
+ if (knownA !== knownB) return knownA ? -1 : 1;
1574
+ return a.externalId < b.externalId ? -1 : a.externalId > b.externalId ? 1 : 0;
1575
+ });
1576
+ const startedAtMs = /* @__PURE__ */ new Map();
1577
+ for (const entry of list) {
1578
+ const ms = Date.parse(importedStartedAt(entry.externalId) ?? "");
1579
+ if (!Number.isNaN(ms)) startedAtMs.set(entry, ms);
1580
+ }
1581
+ const imported = [...startedAtMs.keys()].sort(
1582
+ (a, b) => (startedAtMs.get(a) ?? 0) - (startedAtMs.get(b) ?? 0)
1583
+ );
1584
+ list.forEach((entry, index) => {
1585
+ if (startedAtMs.has(entry)) list[index] = imported.shift() ?? entry;
1586
+ });
1587
+ const rootMs = Date.parse(rootOf.get(list[0]?.file ?? "")?.timestamp ?? "");
1588
+ const forks = list.filter((entry) => (startedAtMs.get(entry) ?? Number.NaN) > rootMs);
1589
+ if (forks.length === 0) continue;
1590
+ const rank = new Map(list.map((entry, index) => [entry, index]));
1591
+ const countedBy = /* @__PURE__ */ new Map();
1592
+ for (const [index, entry] of list.entries()) {
1593
+ if (startedAtMs.has(entry)) continue;
1594
+ for (const fork of forks) {
1595
+ const forkRank = rank.get(fork) ?? index;
1596
+ if (forkRank < index) continue;
1597
+ let counted = countedBy.get(fork);
1598
+ if (counted === void 0) {
1599
+ counted = await readTranscriptUuids(fork.file, importedStartedAt(fork.externalId));
1600
+ countedBy.set(fork, counted);
1601
+ }
1602
+ if (counted.size === 0) continue;
1603
+ const held = await uuidsIn(entry.file);
1604
+ if ([...counted].some((uuid) => held.has(uuid))) {
1605
+ rank.set(entry, Math.max(rank.get(entry) ?? index, forkRank + 0.5));
1606
+ }
1607
+ }
1608
+ }
1609
+ list.sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0));
1610
+ }
1611
+ return {
1612
+ place(file) {
1613
+ const root = rootOf.get(file);
1614
+ if (root === void 0) return { root: void 0, older: [] };
1615
+ const list = byRoot.get(root.uuid) ?? [];
1616
+ const at = list.findIndex((entry) => entry.file === file);
1617
+ return { root, older: list.slice(0, Math.max(at, 0)).map((entry) => entry.file) };
1618
+ },
1619
+ async uuidsOf(older) {
1620
+ const uuids = /* @__PURE__ */ new Set();
1621
+ for (const file of older) {
1622
+ for (const uuid of await uuidsIn(file)) uuids.add(uuid);
1623
+ }
1624
+ return uuids;
1625
+ }
1626
+ };
1627
+ }
1628
+ async function readTranscriptUuids(file, notBefore) {
1629
+ const uuids = /* @__PURE__ */ new Set();
1630
+ try {
1631
+ const { records } = await readJsonlRecords(file);
1632
+ const kept = notBefore === void 0 ? records : claudeTranscriptOwnRecords(records, { notBefore });
1633
+ for (const record of kept) {
1634
+ const uuid = claudeTranscriptRecordUuid(record);
1635
+ if (uuid !== void 0) uuids.add(uuid);
1636
+ }
1637
+ } catch {
1638
+ }
1639
+ return uuids;
1640
+ }
1641
+ async function readConversationRoot(file) {
1642
+ const stream = createReadStream(file, { encoding: "utf8" });
1643
+ const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
1644
+ try {
1645
+ for await (const line of rl) {
1646
+ const trimmed = line.trim();
1647
+ if (trimmed.length === 0) continue;
1648
+ let parsed;
1649
+ try {
1650
+ parsed = JSON.parse(trimmed);
1651
+ } catch {
1652
+ continue;
1653
+ }
1654
+ if (!isObject(parsed)) continue;
1655
+ const uuid = claudeTranscriptRecordUuid(parsed);
1656
+ if (uuid === void 0) continue;
1657
+ const timestamp = parsed.timestamp;
1658
+ return typeof timestamp === "string" && timestamp.length > 0 ? { uuid, timestamp } : { uuid };
1659
+ }
1660
+ return void 0;
1661
+ } catch {
1662
+ return void 0;
1663
+ } finally {
1664
+ rl.close();
1665
+ stream.destroy();
1666
+ }
1667
+ }
1668
+ async function birthTimeMs(file) {
1669
+ try {
1670
+ return (await stat2(file)).birthtimeMs;
1671
+ } catch {
1672
+ return Number.NaN;
1673
+ }
1674
+ }
1479
1675
  async function pathExists(file) {
1480
1676
  try {
1481
1677
  await stat2(file);
@@ -1611,7 +1807,8 @@ function printImportResult(options, results, counts) {
1611
1807
  skippedLegacy,
1612
1808
  skippedDecreased,
1613
1809
  skippedDuplicate,
1614
- skippedUnverifiable
1810
+ skippedUnverifiable,
1811
+ droppedCopies
1615
1812
  } = counts;
1616
1813
  if (options.json === true) {
1617
1814
  console.log(
@@ -1625,6 +1822,7 @@ function printImportResult(options, results, counts) {
1625
1822
  imported_count: results.length,
1626
1823
  replaced_count: replaced,
1627
1824
  reimported_count: reimported,
1825
+ dropped_copies_count: droppedCopies,
1628
1826
  skipped_no_action: skippedNoAction,
1629
1827
  skipped_already_imported: skippedExisting,
1630
1828
  skipped_legacy_untracked: skippedLegacy,
@@ -1647,10 +1845,11 @@ function printImportResult(options, results, counts) {
1647
1845
  skipParts.push(`${skippedUnverifiable} unverifiable (run 'basou verify')`);
1648
1846
  const skipSuffix = skipParts.length > 0 ? `; skipped ${skipParts.join(", ")}` : "";
1649
1847
  const eventsPart = replaced > 0 ? `${eventTotal} events, ${replaced} replaced` : `${eventTotal} events`;
1848
+ const copiesPart = droppedCopies > 0 ? ` (${droppedCopies} without the records ${droppedCopies === 1 ? "its transcript" : "their transcripts"} copied from an older one)` : "";
1650
1849
  if (isDry) {
1651
1850
  const parts = [];
1652
1851
  if (results.length > 0) parts.push(`import ${results.length} session(s) (${eventsPart})`);
1653
- if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)`);
1852
+ if (reimported > 0) parts.push(`re-import ${reimported} changed session(s)${copiesPart}`);
1654
1853
  const head = parts.length > 0 ? `Dry run: would ${parts.join(", ")}` : "Dry run: no changes";
1655
1854
  console.log(`${head}${skipSuffix}`);
1656
1855
  return;
@@ -1668,7 +1867,7 @@ function printImportResult(options, results, counts) {
1668
1867
  }
1669
1868
  if (reimported > 0) {
1670
1869
  segments.push(
1671
- `${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)`
1870
+ `${results.length > 0 ? "re-imported" : "Re-imported"} ${reimported} changed session(s)${copiesPart}`
1672
1871
  );
1673
1872
  }
1674
1873
  console.log(`${segments.join(", ")}${skipSuffix}`);
@@ -1754,6 +1953,7 @@ async function runImport(adapter, fn) {
1754
1953
  importedCount: readCount(json.imported_count),
1755
1954
  replacedCount: readCount(json.replaced_count),
1756
1955
  reimportedCount: readCount(json.reimported_count),
1956
+ droppedCopiesCount: readCount(json.dropped_copies_count),
1757
1957
  skippedNoAction: readCount(json.skipped_no_action),
1758
1958
  skippedAlreadyImported: readCount(json.skipped_already_imported),
1759
1959
  skippedLegacyUntracked: readCount(json.skipped_legacy_untracked),
@@ -2195,9 +2395,9 @@ async function checkAndRecord(options, ctx) {
2195
2395
  ${parsed.errors.map((e) => ` - ${displayPath3(e)}`).join("\n")}`
2196
2396
  );
2197
2397
  }
2198
- const recordedAt = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
2199
2398
  const previous = await previousOf(loaded);
2200
2399
  const measurement = await measureLoaded(loaded, ctx, parsed.input.judged_by.model, previous);
2400
+ const recordedAt = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
2201
2401
  if (measurement.digest !== parsed.input.measure_digest) {
2202
2402
  throw new RecordRefusal(
2203
2403
  `measure_digest is ${parsed.input.measure_digest}, but the board measures ${measurement.digest} now: something it measures or board.yaml changed since the judgement, or basou was rebuilt or upgraded since that measurement. Measure again and judge that; nothing was written.`
@@ -2751,7 +2951,8 @@ function printMeasurementText(m, hasRecords2) {
2751
2951
  const width = Math.max(...measures.map(([id]) => id.length));
2752
2952
  lines.push("", "Measures:");
2753
2953
  for (const [id, measure] of measures) {
2754
- const unit = measure.value === null ? "" : ` ${displayPath3(measure.unit)}`;
2954
+ const counted = typeof measure.value === "number" || Array.isArray(measure.value);
2955
+ const unit = counted ? ` ${displayPath3(measure.unit)}` : "";
2755
2956
  const listed = Array.isArray(measure.value) ? " (paths in --json)" : "";
2756
2957
  const lane = measure.lane === void 0 ? "" : ` [${measure.lane.join(", ")}]`;
2757
2958
  lines.push(` ${id.padEnd(width)} ${shownValue(measure.value)}${unit}${listed}${lane}`);
@@ -9994,7 +10195,10 @@ function describeImport(outcome) {
9994
10195
  }
9995
10196
  const verb = outcome.dryRun ? "would import" : "imported";
9996
10197
  const parts = [`${outcome.importedCount} session(s)`, `${outcome.eventTotal} events`];
9997
- if (outcome.reimportedCount > 0) parts.push(`${outcome.reimportedCount} re-imported`);
10198
+ if (outcome.reimportedCount > 0) {
10199
+ const copies = outcome.droppedCopiesCount > 0 ? ` (${outcome.droppedCopiesCount} without records copied from an older transcript)` : "";
10200
+ parts.push(`${outcome.reimportedCount} re-imported${copies}`);
10201
+ }
9998
10202
  if (outcome.replacedCount > 0) parts.push(`${outcome.replacedCount} replaced`);
9999
10203
  if (outcome.skippedAlreadyImported > 0)
10000
10204
  parts.push(`${outcome.skippedAlreadyImported} already imported`);
@@ -14321,7 +14525,8 @@ var BOARD_HTML = `<!doctype html>
14321
14525
  el('ul', null, m.value.map(function (p) { return el('li', { text: p }); }))
14322
14526
  ])]);
14323
14527
  }
14324
- return el('li', { text: m.id + ': ' + num(m.value) + (m.value === null ? '' : ' ' + m.unit) });
14528
+ // A string (a file's name, a capture) is no amount: it takes no unit.
14529
+ return el('li', { text: m.id + ': ' + num(m.value) + (typeof m.value === 'number' ? ' ' + m.unit : '') });
14325
14530
  })));
14326
14531
  }
14327
14532
  return el('div', { class: 'lane', 'data-lane': lane.id }, body);
@@ -16129,7 +16334,7 @@ function installExperimentalNotice(program, names = EXPERIMENTAL_COMMANDS, write
16129
16334
  function readBuildStamp() {
16130
16335
  if (false) return void 0;
16131
16336
  try {
16132
- return JSON.parse('{"version":"0.71.1","commit":"5097e32","committedAt":"2026-10-19T07:43:16+09:00"}');
16337
+ return JSON.parse('{"version":"0.71.2","commit":"c7fc0ed","committedAt":"2026-10-19T20:00:28+09:00"}');
16133
16338
  } catch {
16134
16339
  return void 0;
16135
16340
  }