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