@basou/core 0.7.0 → 0.9.0

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
@@ -224,7 +224,8 @@ function claudeTranscriptToImportPayload(records, options) {
224
224
  source: {
225
225
  kind: CLAUDE_IMPORT_SOURCE,
226
226
  version: "0.1.0",
227
- ...externalId !== void 0 ? { external_id: externalId } : {}
227
+ ...externalId !== void 0 ? { external_id: externalId } : {},
228
+ ...options.sourceSizeBytes !== void 0 ? { source_size_bytes: options.sourceSizeBytes } : {}
228
229
  },
229
230
  started_at: minTs,
230
231
  ended_at: maxTs,
@@ -457,7 +458,8 @@ function codexRolloutToImportPayload(records, options) {
457
458
  source: {
458
459
  kind: CODEX_IMPORT_SOURCE,
459
460
  version: "0.1.0",
460
- ...externalId !== void 0 ? { external_id: externalId } : {}
461
+ ...externalId !== void 0 ? { external_id: externalId } : {},
462
+ ...options.sourceSizeBytes !== void 0 ? { source_size_bytes: options.sourceSizeBytes } : {}
461
463
  },
462
464
  started_at: minTs,
463
465
  ended_at: maxTs,
@@ -779,7 +781,14 @@ var BaseEventSchema = z3.object({
779
781
  id: EventIdSchema,
780
782
  session_id: SessionIdSchema,
781
783
  occurred_at: IsoTimestampSchema,
782
- source: EventSourceSchema
784
+ source: EventSourceSchema,
785
+ // Tamper-evidence back-pointer (hex sha-256 of the PREVIOUS event line's
786
+ // written bytes; the first line carries the session-bound genesis hash).
787
+ // Present only on sessions written with chaining enabled (import paths);
788
+ // live/ad-hoc sessions omit it. Declared on the base so the `.strict()`
789
+ // variants below treat it as a known key. Additive optional => no
790
+ // schema_version bump.
791
+ prev_hash: z3.string().optional()
783
792
  });
784
793
  var SessionStartedEventSchema = BaseEventSchema.extend({
785
794
  type: z3.literal("session_started")
@@ -1032,7 +1041,15 @@ var SessionSourceSchema = z4.object({
1032
1041
  // Optional id of the originating session in the SOURCE tool's own
1033
1042
  // namespace (e.g. the Claude Code session UUID for a `claude-code-import`).
1034
1043
  // Lets re-imports of the same source be deduplicated; absent for live runs.
1035
- external_id: z4.string().optional()
1044
+ external_id: z4.string().optional(),
1045
+ // Byte size of the source native log at import time, recorded so a later
1046
+ // import can detect that an append-only transcript GREW and re-import it
1047
+ // (scoped, preserving the session id) instead of skipping it as already
1048
+ // imported. Additive optional => no schema_version bump (precedent:
1049
+ // external_id, metrics). Absent on sessions imported before this field
1050
+ // existed (treated as legacy: never auto-re-imported, populated on the next
1051
+ // fresh import or `--force`).
1052
+ source_size_bytes: z4.number().int().nonnegative().optional()
1036
1053
  });
1037
1054
  var InvocationSchema = z4.object({
1038
1055
  command: z4.string().min(1),
@@ -1052,6 +1069,10 @@ var SessionMetricsSchema = z4.object({
1052
1069
  active_time_method: z4.string().optional(),
1053
1070
  machine_active_time_ms: z4.number().int().nonnegative().optional()
1054
1071
  });
1072
+ var SessionIntegritySchema = z4.object({
1073
+ head_hash: z4.string(),
1074
+ event_count: z4.number().int().nonnegative()
1075
+ }).strict();
1055
1076
  var SessionInnerSchema = z4.object({
1056
1077
  id: SessionIdSchema,
1057
1078
  label: z4.string().optional(),
@@ -1067,7 +1088,8 @@ var SessionInnerSchema = z4.object({
1067
1088
  related_files: z4.array(z4.string()).default([]),
1068
1089
  events_log: z4.string().default("events.jsonl"),
1069
1090
  summary: z4.string().nullable().optional(),
1070
- metrics: SessionMetricsSchema.optional()
1091
+ metrics: SessionMetricsSchema.optional(),
1092
+ integrity: SessionIntegritySchema.optional()
1071
1093
  });
1072
1094
  var SessionSchema = z4.object({
1073
1095
  schema_version: SchemaVersionSchema,
@@ -1276,9 +1298,50 @@ function shortDecisionSessionId(sessionId) {
1276
1298
  return sessionId.slice(0, 10);
1277
1299
  }
1278
1300
 
1301
+ // src/events/chain.ts
1302
+ import { createHash } from "crypto";
1303
+ var GENESIS_PREFIX = "basou:event-chain:v1:";
1304
+ function genesisHash(sessionId) {
1305
+ return createHash("sha256").update(`${GENESIS_PREFIX}${sessionId}`, "utf8").digest("hex");
1306
+ }
1307
+ function lineHash(rawLine) {
1308
+ const hash = createHash("sha256");
1309
+ if (typeof rawLine === "string") {
1310
+ hash.update(rawLine, "utf8");
1311
+ } else {
1312
+ hash.update(rawLine);
1313
+ }
1314
+ return hash.digest("hex");
1315
+ }
1316
+ function serializeEventLine(event) {
1317
+ return JSON.stringify(event);
1318
+ }
1319
+ function chainEvents(events, sessionId) {
1320
+ let prev = genesisHash(sessionId);
1321
+ const lines = [];
1322
+ for (const event of events) {
1323
+ const chained = { ...event, prev_hash: prev };
1324
+ const line = serializeEventLine(chained);
1325
+ lines.push(line);
1326
+ prev = lineHash(line);
1327
+ }
1328
+ return { lines, headHash: prev, count: lines.length };
1329
+ }
1330
+ function chainRawJsonLines(rawLines, sessionId) {
1331
+ let prev = genesisHash(sessionId);
1332
+ const lines = [];
1333
+ for (const rawLine of rawLines) {
1334
+ const parsed = JSON.parse(rawLine);
1335
+ const line = JSON.stringify({ ...parsed, prev_hash: prev });
1336
+ lines.push(line);
1337
+ prev = lineHash(line);
1338
+ }
1339
+ return { lines, headHash: prev, count: lines.length };
1340
+ }
1341
+
1279
1342
  // src/events/event-writer.ts
1280
1343
  import { appendFile } from "fs/promises";
1281
- import { join as join5 } from "path";
1344
+ import { basename, join as join5 } from "path";
1282
1345
  async function appendEvent(sessionDir, event) {
1283
1346
  let validated;
1284
1347
  try {
@@ -1286,7 +1349,7 @@ async function appendEvent(sessionDir, event) {
1286
1349
  } catch (error) {
1287
1350
  throw new Error("Invalid Basou event payload", { cause: error });
1288
1351
  }
1289
- const line = `${JSON.stringify(validated)}
1352
+ const line = `${serializeEventLine(validated)}
1290
1353
  `;
1291
1354
  try {
1292
1355
  await appendFile(join5(sessionDir, "events.jsonl"), line, "utf8");
@@ -1294,7 +1357,7 @@ async function appendEvent(sessionDir, event) {
1294
1357
  throw new Error("Failed to append event to events.jsonl", { cause: error });
1295
1358
  }
1296
1359
  }
1297
- async function writeEventsBulk(sessionDir, events) {
1360
+ async function writeEventsBulk(sessionDir, events, options = {}) {
1298
1361
  const validated = [];
1299
1362
  try {
1300
1363
  for (const event of events) {
@@ -1304,13 +1367,153 @@ async function writeEventsBulk(sessionDir, events) {
1304
1367
  throw new Error("Invalid Basou event payload", { cause: error });
1305
1368
  }
1306
1369
  const filePath = join5(sessionDir, "events.jsonl");
1307
- const body = validated.length > 0 ? `${validated.map((e) => JSON.stringify(e)).join("\n")}
1370
+ let body;
1371
+ let result = null;
1372
+ if (options.chain === true) {
1373
+ const { lines, headHash, count } = chainEvents(validated, basename(sessionDir));
1374
+ body = lines.length > 0 ? `${lines.join("\n")}
1375
+ ` : "";
1376
+ result = count > 0 ? { headHash, count } : null;
1377
+ } else {
1378
+ body = validated.length > 0 ? `${validated.map(serializeEventLine).join("\n")}
1308
1379
  ` : "";
1380
+ }
1309
1381
  try {
1310
1382
  await atomicReplace(filePath, body);
1311
1383
  } catch (error) {
1312
1384
  throw new Error("Failed to write events.jsonl", { cause: error });
1313
1385
  }
1386
+ return result;
1387
+ }
1388
+
1389
+ // src/events/verify.ts
1390
+ import { readFile as readFile2 } from "fs/promises";
1391
+ import { join as join6 } from "path";
1392
+ async function verifyEventsChain(paths, sessionId) {
1393
+ const sessionDir = join6(paths.sessions, sessionId);
1394
+ let raw = null;
1395
+ try {
1396
+ raw = await readFile2(join6(sessionDir, "events.jsonl"));
1397
+ } catch (error) {
1398
+ if (!findErrorCode(error, "ENOENT")) {
1399
+ throw new Error("Failed to read events.jsonl", { cause: error });
1400
+ }
1401
+ }
1402
+ let anchor;
1403
+ try {
1404
+ const session = await readSessionYaml(paths, sessionId);
1405
+ anchor = { kind: "present", integrity: session.session.integrity };
1406
+ } catch (error) {
1407
+ if (error instanceof Error && error.message === "YAML file not found") {
1408
+ anchor = { kind: "absent" };
1409
+ } else {
1410
+ anchor = { kind: "unreadable" };
1411
+ }
1412
+ }
1413
+ const terminated = raw === null || raw.length === 0 || raw[raw.length - 1] === 10;
1414
+ const segments = raw === null ? [] : splitLinesBytes(raw);
1415
+ const tailFragment = !terminated && segments.length > 0 ? segments.pop() : null;
1416
+ const lines = segments;
1417
+ const carriesPrevHash = (s) => {
1418
+ try {
1419
+ const obj = JSON.parse(s.toString("utf8"));
1420
+ return typeof obj === "object" && obj !== null && "prev_hash" in obj;
1421
+ } catch {
1422
+ return false;
1423
+ }
1424
+ };
1425
+ const chained = lines.some((l) => l.length > 0 && carriesPrevHash(l)) || tailFragment !== null && carriesPrevHash(tailFragment);
1426
+ if (!chained) {
1427
+ if (anchor.kind === "present" && anchor.integrity !== void 0) {
1428
+ return {
1429
+ status: "tampered",
1430
+ eventCount: lines.length,
1431
+ reason: "anchor_without_chain"
1432
+ };
1433
+ }
1434
+ if (raw === null || raw.length === 0) {
1435
+ return { status: "empty", eventCount: 0 };
1436
+ }
1437
+ return { status: "unchained", eventCount: lines.length };
1438
+ }
1439
+ let expected = genesisHash(sessionId);
1440
+ for (let i = 0; i < lines.length; i++) {
1441
+ const line = lines[i];
1442
+ const lineNo = i + 1;
1443
+ if (line.length === 0) {
1444
+ return { status: "tampered", eventCount: lines.length, reason: "blank_line", line: lineNo };
1445
+ }
1446
+ let parsed;
1447
+ try {
1448
+ parsed = JSON.parse(line.toString("utf8"));
1449
+ } catch {
1450
+ return {
1451
+ status: "tampered",
1452
+ eventCount: lines.length,
1453
+ reason: "malformed_line",
1454
+ line: lineNo
1455
+ };
1456
+ }
1457
+ const record = parsed;
1458
+ if (typeof record.prev_hash !== "string") {
1459
+ return {
1460
+ status: "tampered",
1461
+ eventCount: lines.length,
1462
+ reason: "missing_prev_hash",
1463
+ line: lineNo
1464
+ };
1465
+ }
1466
+ if (record.prev_hash !== expected) {
1467
+ return {
1468
+ status: "tampered",
1469
+ eventCount: lines.length,
1470
+ reason: i === 0 ? "genesis_mismatch" : "broken_link",
1471
+ line: lineNo
1472
+ };
1473
+ }
1474
+ if (record.session_id !== sessionId) {
1475
+ return {
1476
+ status: "tampered",
1477
+ eventCount: lines.length,
1478
+ reason: "session_id_mismatch",
1479
+ line: lineNo
1480
+ };
1481
+ }
1482
+ expected = lineHash(line);
1483
+ }
1484
+ if (tailFragment !== null || !terminated) {
1485
+ return {
1486
+ status: "tampered",
1487
+ eventCount: lines.length,
1488
+ reason: "torn_tail",
1489
+ line: lines.length + 1
1490
+ };
1491
+ }
1492
+ if (anchor.kind === "absent") {
1493
+ return { status: "incomplete", eventCount: lines.length, reason: "yaml_missing" };
1494
+ }
1495
+ if (anchor.kind === "unreadable") {
1496
+ return { status: "tampered", eventCount: lines.length, reason: "yaml_unreadable" };
1497
+ }
1498
+ if (anchor.integrity === void 0) {
1499
+ return { status: "tampered", eventCount: lines.length, reason: "anchor_missing" };
1500
+ }
1501
+ if (anchor.integrity.event_count !== lines.length || anchor.integrity.head_hash !== expected) {
1502
+ return { status: "tampered", eventCount: lines.length, reason: "anchor_mismatch" };
1503
+ }
1504
+ return { status: "verified", eventCount: lines.length };
1505
+ }
1506
+ function splitLinesBytes(buf) {
1507
+ const out = [];
1508
+ let start = 0;
1509
+ for (let i = 0; i < buf.length; i++) {
1510
+ if (buf[i] === 10) {
1511
+ out.push(buf.subarray(start, i));
1512
+ start = i + 1;
1513
+ }
1514
+ }
1515
+ if (start < buf.length) out.push(buf.subarray(start));
1516
+ return out;
1314
1517
  }
1315
1518
 
1316
1519
  // src/git/snapshot.ts
@@ -1607,12 +1810,12 @@ function parseDiffNameStatus(raw) {
1607
1810
  }
1608
1811
 
1609
1812
  // src/handoff/handoff-renderer.ts
1610
- import { join as join10 } from "path";
1813
+ import { join as join11 } from "path";
1611
1814
 
1612
1815
  // src/storage/tasks.ts
1613
- import { createHash } from "crypto";
1614
- import { mkdir as mkdir2, readdir as readdir3, readFile as readFile5, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
1615
- import { join as join9 } from "path";
1816
+ import { createHash as createHash2 } from "crypto";
1817
+ import { mkdir as mkdir3, readdir as readdir3, readFile as readFile6, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
1818
+ import { join as join10 } from "path";
1616
1819
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1617
1820
  import { z as z8 } from "zod";
1618
1821
 
@@ -1656,7 +1859,7 @@ var TaskSchema = z6.object({
1656
1859
  // src/storage/ad-hoc-session.ts
1657
1860
  import { mkdir, rm } from "fs/promises";
1658
1861
  import { homedir } from "os";
1659
- import { join as join6 } from "path";
1862
+ import { join as join7 } from "path";
1660
1863
 
1661
1864
  // src/lib/path-sanitizer.ts
1662
1865
  import { posix as path } from "path";
@@ -1736,13 +1939,13 @@ async function createAdHocSessionWithEvent(input) {
1736
1939
  taskId: input.taskId ?? null
1737
1940
  })
1738
1941
  );
1739
- const sessionDir = join6(input.paths.sessions, sessionId);
1942
+ const sessionDir = join7(input.paths.sessions, sessionId);
1740
1943
  try {
1741
1944
  await mkdir(sessionDir, { recursive: true });
1742
1945
  } catch (error) {
1743
1946
  throw new Error("Failed to create session directory", { cause: error });
1744
1947
  }
1745
- const sessionYamlPath = join6(sessionDir, "session.yaml");
1948
+ const sessionYamlPath = join7(sessionDir, "session.yaml");
1746
1949
  try {
1747
1950
  await linkYamlFile(sessionYamlPath, initialSession);
1748
1951
  } catch (error) {
@@ -1865,7 +2068,7 @@ async function appendEventToExistingSession(input) {
1865
2068
  }
1866
2069
  const eventId = prefixedUlid("evt");
1867
2070
  const event = assertTargetEventIdentity(input.eventBuilder(eventId), input.sessionId, eventId);
1868
- const sessionDir = join6(input.paths.sessions, input.sessionId);
2071
+ const sessionDir = join7(input.paths.sessions, input.sessionId);
1869
2072
  await appendEvent(sessionDir, event);
1870
2073
  return { eventId, sessionStatus: status };
1871
2074
  }
@@ -1880,8 +2083,8 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
1880
2083
  }
1881
2084
 
1882
2085
  // src/storage/lockfile.ts
1883
- import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
1884
- import { join as join7 } from "path";
2086
+ import { mkdir as mkdir2, readFile as readFile4, unlink as unlink2 } from "fs/promises";
2087
+ import { dirname as dirname2, join as join8 } from "path";
1885
2088
  var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
1886
2089
  async function acquireLock(paths, scope, resourceId) {
1887
2090
  const lockPath = lockfilePath(paths, scope, resourceId);
@@ -1893,6 +2096,19 @@ async function acquireLock(paths, scope, resourceId) {
1893
2096
  try {
1894
2097
  await atomicCreate(lockPath, serialised);
1895
2098
  } catch (error) {
2099
+ if (findErrorCode(error, "ENOENT")) {
2100
+ try {
2101
+ await mkdir2(dirname2(lockPath), { recursive: true });
2102
+ await atomicCreate(lockPath, serialised);
2103
+ return {
2104
+ release: async () => {
2105
+ await unlink2(lockPath).catch(() => void 0);
2106
+ }
2107
+ };
2108
+ } catch (retryError) {
2109
+ throw new Error("Failed to acquire lock", { cause: retryError });
2110
+ }
2111
+ }
1896
2112
  if (!findErrorCode(error, "EEXIST")) {
1897
2113
  throw error;
1898
2114
  }
@@ -1916,7 +2132,7 @@ async function acquireLock(paths, scope, resourceId) {
1916
2132
  async function isStaleLock(lockPath) {
1917
2133
  let body;
1918
2134
  try {
1919
- const raw = await readFile3(lockPath, "utf8");
2135
+ const raw = await readFile4(lockPath, "utf8");
1920
2136
  const parsed = JSON.parse(raw);
1921
2137
  if (typeof parsed !== "object" || parsed === null) return true;
1922
2138
  const candidate = parsed;
@@ -1942,12 +2158,12 @@ async function isStaleLock(lockPath) {
1942
2158
  function lockfilePath(paths, scope, resourceId) {
1943
2159
  const sep = resourceId.indexOf("_");
1944
2160
  const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
1945
- return join7(paths.locks, `${scope}_${ulid2}.lock`);
2161
+ return join8(paths.locks, `${scope}_${ulid2}.lock`);
1946
2162
  }
1947
2163
 
1948
2164
  // src/storage/task-index.ts
1949
- import { readFile as readFile4 } from "fs/promises";
1950
- import { join as join8 } from "path";
2165
+ import { readFile as readFile5 } from "fs/promises";
2166
+ import { join as join9 } from "path";
1951
2167
 
1952
2168
  // src/schemas/task-index.schema.ts
1953
2169
  import { z as z7 } from "zod";
@@ -1966,13 +2182,13 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
1966
2182
 
1967
2183
  // src/storage/task-index.ts
1968
2184
  function taskIndexPath(paths) {
1969
- return join8(paths.tasks, "index.json");
2185
+ return join9(paths.tasks, "index.json");
1970
2186
  }
1971
2187
  async function readTaskIndex(paths) {
1972
2188
  const filePath = taskIndexPath(paths);
1973
2189
  let raw;
1974
2190
  try {
1975
- raw = await readFile4(filePath, "utf8");
2191
+ raw = await readFile5(filePath, "utf8");
1976
2192
  } catch (error) {
1977
2193
  if (findErrorCode(error, "ENOENT")) {
1978
2194
  throw new Error("Task index not found", { cause: error });
@@ -2080,10 +2296,10 @@ function splitFrontMatter(raw) {
2080
2296
  return { yamlText, body };
2081
2297
  }
2082
2298
  async function readTaskFile(paths, taskId) {
2083
- const filePath = join9(paths.tasks, `${taskId}.md`);
2299
+ const filePath = join10(paths.tasks, `${taskId}.md`);
2084
2300
  let raw;
2085
2301
  try {
2086
- raw = await readFile5(filePath, "utf8");
2302
+ raw = await readFile6(filePath, "utf8");
2087
2303
  } catch (error) {
2088
2304
  if (findErrorCode(error, "ENOENT")) {
2089
2305
  throw new Error("Task file not found", { cause: error });
@@ -2113,7 +2329,7 @@ async function readTaskFile(paths, taskId) {
2113
2329
  }
2114
2330
  async function writeTaskFile(paths, taskId, doc, options) {
2115
2331
  const validated = TaskSchema.parse(doc.task);
2116
- const filePath = join9(paths.tasks, `${taskId}.md`);
2332
+ const filePath = join10(paths.tasks, `${taskId}.md`);
2117
2333
  const yamlText = stringifyYaml(validated);
2118
2334
  const trimmedBody = doc.body.length === 0 ? "" : `
2119
2335
  ${doc.body.endsWith("\n") ? doc.body : `${doc.body}
@@ -2198,7 +2414,7 @@ async function safeUpdateTaskIndex(paths, op) {
2198
2414
  }
2199
2415
  var ARCHIVE_DIR_NAME = "archive";
2200
2416
  function archiveTasksDir(paths) {
2201
- return join9(paths.tasks, ARCHIVE_DIR_NAME);
2417
+ return join10(paths.tasks, ARCHIVE_DIR_NAME);
2202
2418
  }
2203
2419
  async function enumerateArchivedTaskIds(paths) {
2204
2420
  let entries;
@@ -2228,10 +2444,10 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
2228
2444
  throw error;
2229
2445
  }
2230
2446
  }
2231
- const archiveFilePath = join9(archiveTasksDir(paths), `${taskId}.md`);
2447
+ const archiveFilePath = join10(archiveTasksDir(paths), `${taskId}.md`);
2232
2448
  let raw;
2233
2449
  try {
2234
- raw = await readFile5(archiveFilePath, "utf8");
2450
+ raw = await readFile6(archiveFilePath, "utf8");
2235
2451
  } catch (error) {
2236
2452
  if (findErrorCode(error, "ENOENT")) {
2237
2453
  throw new Error("Task file not found", { cause: error });
@@ -2522,7 +2738,7 @@ async function createTaskAttachLocked(input) {
2522
2738
  ...sessionDoc,
2523
2739
  session: { ...sessionDoc.session, task_id: input.taskId }
2524
2740
  };
2525
- await overwriteYamlFile(join9(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2741
+ await overwriteYamlFile(join10(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2526
2742
  } catch (error) {
2527
2743
  throw new TaskWriteAfterEventError({
2528
2744
  taskId: input.taskId,
@@ -2781,17 +2997,17 @@ function buildUpdatedDoc(input) {
2781
2997
  return { task: next, body: input.currentDoc.body };
2782
2998
  }
2783
2999
  async function computeTaskMdSnapshot(paths, taskId) {
2784
- const filePath = join9(paths.tasks, `${taskId}.md`);
2785
- const [stats, raw] = await Promise.all([stat2(filePath), readFile5(filePath)]);
2786
- const hash = createHash("sha256").update(raw).digest("hex");
3000
+ const filePath = join10(paths.tasks, `${taskId}.md`);
3001
+ const [stats, raw] = await Promise.all([stat2(filePath), readFile6(filePath)]);
3002
+ const hash = createHash2("sha256").update(raw).digest("hex");
2787
3003
  return { mtimeMs: stats.mtimeMs, hash };
2788
3004
  }
2789
3005
  async function readTaskFileWithSnapshot(paths, taskId) {
2790
- const filePath = join9(paths.tasks, `${taskId}.md`);
3006
+ const filePath = join10(paths.tasks, `${taskId}.md`);
2791
3007
  let rawBuffer;
2792
3008
  let stats;
2793
3009
  try {
2794
- [rawBuffer, stats] = await Promise.all([readFile5(filePath), stat2(filePath)]);
3010
+ [rawBuffer, stats] = await Promise.all([readFile6(filePath), stat2(filePath)]);
2795
3011
  } catch (error) {
2796
3012
  if (findErrorCode(error, "ENOENT")) {
2797
3013
  throw new Error("Task file not found", { cause: error });
@@ -2799,7 +3015,7 @@ async function readTaskFileWithSnapshot(paths, taskId) {
2799
3015
  throw new Error("Failed to read task file", { cause: error });
2800
3016
  }
2801
3017
  const raw = rawBuffer.toString("utf8");
2802
- const hash = createHash("sha256").update(rawBuffer).digest("hex");
3018
+ const hash = createHash2("sha256").update(rawBuffer).digest("hex");
2803
3019
  let split;
2804
3020
  try {
2805
3021
  split = splitFrontMatter(raw);
@@ -3279,7 +3495,7 @@ async function deleteTaskLocked(input) {
3279
3495
  });
3280
3496
  const eventId = adHoc.targetEventIds[0];
3281
3497
  try {
3282
- await unlink3(join9(input.paths.tasks, `${input.taskId}.md`));
3498
+ await unlink3(join10(input.paths.tasks, `${input.taskId}.md`));
3283
3499
  } catch (error) {
3284
3500
  throw new TaskWriteAfterEventError({
3285
3501
  taskId: input.taskId,
@@ -3349,10 +3565,10 @@ async function archiveTaskLocked(input) {
3349
3565
  { task: next, body: doc.body },
3350
3566
  { mode: "overwrite" }
3351
3567
  );
3352
- await mkdir2(archiveTasksDir(input.paths), { recursive: true });
3568
+ await mkdir3(archiveTasksDir(input.paths), { recursive: true });
3353
3569
  await rename2(
3354
- join9(input.paths.tasks, `${input.taskId}.md`),
3355
- join9(archiveTasksDir(input.paths), `${input.taskId}.md`)
3570
+ join10(input.paths.tasks, `${input.taskId}.md`),
3571
+ join10(archiveTasksDir(input.paths), `${input.taskId}.md`)
3356
3572
  );
3357
3573
  } catch (error) {
3358
3574
  throw new TaskWriteAfterEventError({
@@ -3388,7 +3604,7 @@ async function renderHandoff(input) {
3388
3604
  const tasksCreated = [];
3389
3605
  const tasksStatusChanged = [];
3390
3606
  for (const entry of entries) {
3391
- const sessionDir = join10(input.paths.sessions, entry.sessionId);
3607
+ const sessionDir = join11(input.paths.sessions, entry.sessionId);
3392
3608
  try {
3393
3609
  for await (const ev of replayEvents(sessionDir, {
3394
3610
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -3922,7 +4138,13 @@ var SessionInnerImportSchema = z10.object({
3922
4138
  version: z10.literal("0.1.0"),
3923
4139
  // Source-tool-native id (e.g. Claude Code session UUID), retained so
3924
4140
  // re-imports of the same source can be deduplicated.
3925
- external_id: z10.string().optional()
4141
+ external_id: z10.string().optional(),
4142
+ // Byte size of the source native log at import time. Declared here too
4143
+ // (not only in session.schema.ts) because this inner `source` object is
4144
+ // a plain z.object: zod strips keys it does not declare, so a field
4145
+ // absent here would be dropped from the parsed payload before persist
4146
+ // and the size could never be stored.
4147
+ source_size_bytes: z10.number().int().nonnegative().optional()
3926
4148
  }),
3927
4149
  started_at: IsoTimestampSchema,
3928
4150
  ended_at: IsoTimestampSchema.optional(),
@@ -3936,7 +4158,12 @@ var SessionInnerImportSchema = z10.object({
3936
4158
  related_files: z10.array(z10.string()).default([]),
3937
4159
  events_log: z10.string().optional(),
3938
4160
  summary: z10.string().nullable().optional(),
3939
- metrics: SessionMetricsSchema.optional()
4161
+ metrics: SessionMetricsSchema.optional(),
4162
+ // Accepted so a payload assembled from an on-disk chained session.yaml
4163
+ // round-trips, and DISCARDED by the importer (buildSessionRecord never
4164
+ // copies it): the integrity anchor is computed at write time, never
4165
+ // imported. Mirrors the accept-and-discard of `prev_hash` on events.
4166
+ integrity: SessionIntegritySchema.optional()
3940
4167
  }).strict();
3941
4168
  var SessionImportPayloadSchema = z10.object({
3942
4169
  schema_version: z10.string(),
@@ -4018,7 +4245,7 @@ function serializeJsonSchema(schema) {
4018
4245
  }
4019
4246
 
4020
4247
  // src/stats/work-stats.ts
4021
- import { join as join11 } from "path";
4248
+ import { join as join12 } from "path";
4022
4249
  function resolveTimeZone(timeZone) {
4023
4250
  if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4024
4251
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -4049,7 +4276,7 @@ async function computeWorkStats(input) {
4049
4276
  const events = [];
4050
4277
  let eventsUnreadable = false;
4051
4278
  try {
4052
- for await (const ev of replayEvents(join11(input.paths.sessions, entry.sessionId), {
4279
+ for await (const ev of replayEvents(join12(input.paths.sessions, entry.sessionId), {
4053
4280
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4054
4281
  })) {
4055
4282
  events.push(ev);
@@ -4304,28 +4531,28 @@ function tzDate(ms, timeZone) {
4304
4531
  }
4305
4532
 
4306
4533
  // src/storage/basou-dir.ts
4307
- import { lstat as lstat3, mkdir as mkdir3 } from "fs/promises";
4308
- import { join as join12 } from "path";
4534
+ import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
4535
+ import { join as join13 } from "path";
4309
4536
  function basouPaths(repositoryRoot) {
4310
- const root = join12(repositoryRoot, ".basou");
4311
- const approvalsBase = join12(root, "approvals");
4537
+ const root = join13(repositoryRoot, ".basou");
4538
+ const approvalsBase = join13(root, "approvals");
4312
4539
  return {
4313
4540
  root,
4314
- sessions: join12(root, "sessions"),
4315
- tasks: join12(root, "tasks"),
4541
+ sessions: join13(root, "sessions"),
4542
+ tasks: join13(root, "tasks"),
4316
4543
  approvals: {
4317
- pending: join12(approvalsBase, "pending"),
4318
- resolved: join12(approvalsBase, "resolved")
4544
+ pending: join13(approvalsBase, "pending"),
4545
+ resolved: join13(approvalsBase, "resolved")
4319
4546
  },
4320
- locks: join12(root, "locks"),
4321
- logs: join12(root, "logs"),
4322
- raw: join12(root, "raw"),
4323
- tmp: join12(root, "tmp"),
4547
+ locks: join13(root, "locks"),
4548
+ logs: join13(root, "logs"),
4549
+ raw: join13(root, "raw"),
4550
+ tmp: join13(root, "tmp"),
4324
4551
  files: {
4325
- manifest: join12(root, "manifest.yaml"),
4326
- status: join12(root, "status.json"),
4327
- handoff: join12(root, "handoff.md"),
4328
- decisions: join12(root, "decisions.md")
4552
+ manifest: join13(root, "manifest.yaml"),
4553
+ status: join13(root, "status.json"),
4554
+ handoff: join13(root, "handoff.md"),
4555
+ decisions: join13(root, "decisions.md")
4329
4556
  }
4330
4557
  };
4331
4558
  }
@@ -4366,7 +4593,7 @@ async function ensureBasouDirectory(repositoryRoot) {
4366
4593
  }
4367
4594
  async function mkdirLabeled(target, label) {
4368
4595
  try {
4369
- await mkdir3(target, { recursive: true });
4596
+ await mkdir4(target, { recursive: true });
4370
4597
  } catch (error) {
4371
4598
  if (hasErrorCode3(error) && (error.code === "ENOTDIR" || error.code === "EEXIST")) {
4372
4599
  throw new Error(`${label} exists but is not a directory`, { cause: error });
@@ -4381,16 +4608,16 @@ function hasErrorCode3(error) {
4381
4608
  }
4382
4609
 
4383
4610
  // src/storage/gitignore.ts
4384
- import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
4385
- import { join as join13 } from "path";
4611
+ import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
4612
+ import { join as join14 } from "path";
4386
4613
  var MARKER = "# Basou - default ignore";
4387
4614
  var BASOU_GITIGNORE_BLOCK = "# Basou - default ignore\n.basou/logs/\n.basou/raw/\n.basou/tmp/\n.basou/locks/\n.basou/status.json\n.basou/sessions/*/events.jsonl\n.basou/sessions/*/artifacts/\n.basou/approvals/pending/\n.basou/approvals/resolved/\n\n# Basou - default commit\n# .basou/manifest.yaml\n# .basou/handoff.md\n# .basou/decisions.md\n# .basou/tasks/\n# .basou/sessions/*/session.yaml\n# .basou/sessions/*/transcript.md\n# .basou/sessions/*/changed-files.json\n";
4388
4615
  async function appendBasouGitignore(repositoryRoot) {
4389
- const gitignorePath = join13(repositoryRoot, ".gitignore");
4616
+ const gitignorePath = join14(repositoryRoot, ".gitignore");
4390
4617
  let body;
4391
4618
  let existed;
4392
4619
  try {
4393
- body = await readFile6(gitignorePath, "utf8");
4620
+ body = await readFile7(gitignorePath, "utf8");
4394
4621
  existed = true;
4395
4622
  } catch (error) {
4396
4623
  if (hasErrorCode4(error) && error.code === "ENOENT") {
@@ -4496,12 +4723,12 @@ function hasErrorCode5(error) {
4496
4723
  }
4497
4724
 
4498
4725
  // src/storage/markdown-store.ts
4499
- import { readFile as readFile7 } from "fs/promises";
4726
+ import { readFile as readFile8 } from "fs/promises";
4500
4727
  var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
4501
4728
  var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
4502
4729
  async function readMarkdownFile(filePath) {
4503
4730
  try {
4504
- return await readFile7(filePath, "utf8");
4731
+ return await readFile8(filePath, "utf8");
4505
4732
  } catch (error) {
4506
4733
  if (hasErrorCode6(error) && error.code === "ENOENT") return null;
4507
4734
  throw new Error("Failed to read markdown file", { cause: error });
@@ -4599,9 +4826,9 @@ function hasErrorCode6(error) {
4599
4826
  }
4600
4827
 
4601
4828
  // src/storage/session-import.ts
4602
- import { mkdir as mkdir4, rm as rm2 } from "fs/promises";
4829
+ import { mkdir as mkdir5, readFile as readFile9, rm as rm2 } from "fs/promises";
4603
4830
  import { homedir as homedir2 } from "os";
4604
- import { join as join14 } from "path";
4831
+ import { join as join15 } from "path";
4605
4832
  async function importSessionFromJson(paths, manifest, payload, options) {
4606
4833
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
4607
4834
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -4626,21 +4853,22 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4626
4853
  pathSanitizeReport
4627
4854
  };
4628
4855
  }
4629
- const sessionDir = join14(paths.sessions, newSessionId);
4856
+ const sessionDir = join15(paths.sessions, newSessionId);
4630
4857
  try {
4631
- await mkdir4(sessionDir, { recursive: true });
4858
+ await mkdir5(sessionDir, { recursive: true });
4632
4859
  } catch (error) {
4633
4860
  throw new Error("Failed to create session directory", { cause: error });
4634
4861
  }
4862
+ let chainResult;
4635
4863
  try {
4636
- await writeEventsBulk(sessionDir, rewrittenEvents);
4864
+ chainResult = await writeEventsBulk(sessionDir, rewrittenEvents, { chain: true });
4637
4865
  } catch (error) {
4638
4866
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
4639
4867
  throw error;
4640
4868
  }
4641
4869
  try {
4642
- const sessionYamlPath = join14(sessionDir, "session.yaml");
4643
- await linkYamlFile(sessionYamlPath, sessionRecord);
4870
+ const sessionYamlPath = join15(sessionDir, "session.yaml");
4871
+ await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
4644
4872
  } catch (error) {
4645
4873
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
4646
4874
  if (findErrorCode(error, "EEXIST")) {
@@ -4697,6 +4925,16 @@ function assertChronologicalOrder(events) {
4697
4925
  }
4698
4926
  }
4699
4927
  }
4928
+ function withIntegrity(record, chainResult) {
4929
+ if (chainResult === null) return record;
4930
+ return {
4931
+ ...record,
4932
+ session: {
4933
+ ...record.session,
4934
+ integrity: { head_hash: chainResult.headHash, event_count: chainResult.count }
4935
+ }
4936
+ };
4937
+ }
4700
4938
  function buildSessionRecord(input, manifest, newSessionId, options) {
4701
4939
  const home = homedir2();
4702
4940
  const workingDirectoryRaw = input.working_directory;
@@ -4731,6 +4969,243 @@ function buildSessionRecord(input, manifest, newSessionId, options) {
4731
4969
  }
4732
4970
  };
4733
4971
  }
4972
+ var IMPORT_DERIVED_SOURCES = /* @__PURE__ */ new Set([
4973
+ "claude-code-import",
4974
+ "codex-import"
4975
+ ]);
4976
+ function isImportDerivedSource(source) {
4977
+ return IMPORT_DERIVED_SOURCES.has(source);
4978
+ }
4979
+ function derivedEventContentKey(event) {
4980
+ const base = `${event.type}\0${event.occurred_at}`;
4981
+ switch (event.type) {
4982
+ case "command_executed":
4983
+ return `${base}\0${event.command}\0${event.args.join("")}\0${event.cwd}`;
4984
+ case "file_changed":
4985
+ return `${base}\0${event.path}\0${event.change_type}`;
4986
+ case "decision_recorded":
4987
+ return `${base}\0${event.title}`;
4988
+ default:
4989
+ return base;
4990
+ }
4991
+ }
4992
+ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
4993
+ const priorStarted = priorDerived.find((e) => e.type === "session_started");
4994
+ const priorEnded = priorDerived.find((e) => e.type === "session_ended");
4995
+ let startedUsed = false;
4996
+ let endedUsed = false;
4997
+ const middleByKey = /* @__PURE__ */ new Map();
4998
+ for (const e of priorDerived) {
4999
+ if (e.type === "session_started" || e.type === "session_ended") continue;
5000
+ const key = derivedEventContentKey(e);
5001
+ const list = middleByKey.get(key);
5002
+ if (list === void 0) middleByKey.set(key, [e]);
5003
+ else list.push(e);
5004
+ }
5005
+ let reusedIdCount = 0;
5006
+ const withReusedId = (fresh, prior) => {
5007
+ reusedIdCount++;
5008
+ if (fresh.type === "decision_recorded" && prior.type === "decision_recorded") {
5009
+ return { ...fresh, id: prior.id, session_id: sessionId, decision_id: prior.decision_id };
5010
+ }
5011
+ return { ...fresh, id: prior.id, session_id: sessionId };
5012
+ };
5013
+ const events = freshDerived.map((fresh) => {
5014
+ if (fresh.type === "session_started") {
5015
+ if (priorStarted !== void 0) {
5016
+ startedUsed = true;
5017
+ return withReusedId(fresh, priorStarted);
5018
+ }
5019
+ return { ...fresh, id: prefixedUlid("evt"), session_id: sessionId };
5020
+ }
5021
+ if (fresh.type === "session_ended") {
5022
+ if (priorEnded !== void 0) {
5023
+ endedUsed = true;
5024
+ return withReusedId(fresh, priorEnded);
5025
+ }
5026
+ return { ...fresh, id: prefixedUlid("evt"), session_id: sessionId };
5027
+ }
5028
+ const match = middleByKey.get(derivedEventContentKey(fresh))?.shift();
5029
+ if (match !== void 0) return withReusedId(fresh, match);
5030
+ return { ...fresh, id: prefixedUlid("evt"), session_id: sessionId };
5031
+ });
5032
+ const droppedPriorDerived = priorStarted !== void 0 && !startedUsed || priorEnded !== void 0 && !endedUsed || [...middleByKey.values()].some((q) => q.length > 0);
5033
+ return { events, reusedIdCount, droppedPriorDerived };
5034
+ }
5035
+ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
5036
+ const sessionId = priorSessionId;
5037
+ const importSource = freshPayload.session.source.kind;
5038
+ const sessionDir = join15(paths.sessions, priorSessionId);
5039
+ const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
5040
+ try {
5041
+ const priorVerdict = await verifyEventsChain(paths, priorSessionId);
5042
+ if (priorVerdict.status === "tampered") {
5043
+ return { status: "skipped", reason: "prior_chain_broken" };
5044
+ }
5045
+ let priorUnreadable = false;
5046
+ const priorEvents = await readAllEvents(sessionDir, {
5047
+ onWarning: () => {
5048
+ priorUnreadable = true;
5049
+ }
5050
+ });
5051
+ if (priorUnreadable) {
5052
+ return { status: "skipped", reason: "prior_events_unreadable" };
5053
+ }
5054
+ const priorDerived = priorEvents.filter((e) => e.source === importSource);
5055
+ const preserved = priorEvents.filter((e) => e.source !== importSource);
5056
+ const {
5057
+ events: rederived,
5058
+ reusedIdCount,
5059
+ droppedPriorDerived
5060
+ } = reuseDerivedIds(priorDerived, freshPayload.events, sessionId);
5061
+ if (droppedPriorDerived) {
5062
+ return { status: "skipped", reason: "prior_derived_dropped" };
5063
+ }
5064
+ const mergedEvents = [...rederived, ...preserved].sort(
5065
+ (a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at)
5066
+ );
5067
+ assertChronologicalOrder(mergedEvents);
5068
+ const prior = await readSessionYaml(paths, priorSessionId);
5069
+ const { record } = buildSessionRecord(freshPayload.session, manifest, sessionId, {});
5070
+ const preservedInner = {
5071
+ ...record.session,
5072
+ // Defensive: keep any task_id already present on the prior yaml so a
5073
+ // re-derive never drops a link, whatever wrote it.
5074
+ task_id: prior.session.task_id ?? null,
5075
+ // Re-derivation always yields a null summary; keep a prior non-null one.
5076
+ summary: prior.session.summary ?? record.session.summary ?? null
5077
+ };
5078
+ const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
5079
+ if (options.dryRun !== true) {
5080
+ const eventsPath = join15(sessionDir, "events.jsonl");
5081
+ let priorEventsRaw = null;
5082
+ try {
5083
+ priorEventsRaw = await readFile9(eventsPath);
5084
+ } catch (error) {
5085
+ if (!findErrorCode(error, "ENOENT")) {
5086
+ throw new Error("Failed to read events.jsonl", { cause: error });
5087
+ }
5088
+ }
5089
+ const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
5090
+ try {
5091
+ await overwriteYamlFile(
5092
+ join15(sessionDir, "session.yaml"),
5093
+ withIntegrity(updatedRecord, chainResult)
5094
+ );
5095
+ } catch (error) {
5096
+ if (priorEventsRaw !== null) {
5097
+ await atomicReplace(eventsPath, priorEventsRaw).catch(() => void 0);
5098
+ } else {
5099
+ await rm2(eventsPath, { force: true }).catch(() => void 0);
5100
+ }
5101
+ throw error;
5102
+ }
5103
+ }
5104
+ return {
5105
+ status: "reimported",
5106
+ sessionId,
5107
+ eventCount: mergedEvents.length,
5108
+ preservedCount: preserved.length,
5109
+ reusedIdCount
5110
+ };
5111
+ } finally {
5112
+ await lock?.release();
5113
+ }
5114
+ }
5115
+ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5116
+ const sessionDir = join15(paths.sessions, sessionId);
5117
+ let lock;
5118
+ try {
5119
+ lock = await acquireLock(paths, "session", sessionId);
5120
+ } catch (error) {
5121
+ if (error instanceof Error && error.message === "Lock is held by another process") {
5122
+ throw error;
5123
+ }
5124
+ throw new Error("Failed to acquire lock", { cause: error });
5125
+ }
5126
+ try {
5127
+ let record;
5128
+ try {
5129
+ record = await readSessionYaml(paths, sessionId);
5130
+ } catch (error) {
5131
+ if (error instanceof Error && error.message === "YAML file not found") {
5132
+ return { status: "skipped", reason: "yaml_missing" };
5133
+ }
5134
+ return { status: "skipped", reason: "yaml_unreadable" };
5135
+ }
5136
+ if (record.session.status !== "imported") {
5137
+ return { status: "skipped", reason: "not_imported" };
5138
+ }
5139
+ const verdict = await verifyEventsChain(paths, sessionId);
5140
+ if (verdict.status === "verified") {
5141
+ return { status: "skipped", reason: "already_chained" };
5142
+ }
5143
+ if (verdict.status === "empty") {
5144
+ return { status: "skipped", reason: "empty" };
5145
+ }
5146
+ if (verdict.status !== "unchained") {
5147
+ return { status: "skipped", reason: "tampered" };
5148
+ }
5149
+ const eventsPath = join15(sessionDir, "events.jsonl");
5150
+ let priorRaw;
5151
+ try {
5152
+ priorRaw = await readFile9(eventsPath);
5153
+ } catch (error) {
5154
+ throw new Error("Failed to read events.jsonl", { cause: error });
5155
+ }
5156
+ if (priorRaw.length === 0 || priorRaw[priorRaw.length - 1] !== 10) {
5157
+ return { status: "skipped", reason: "events_unreadable" };
5158
+ }
5159
+ const text = priorRaw.toString("utf8");
5160
+ if (!priorRaw.equals(Buffer.from(text, "utf8"))) {
5161
+ return { status: "skipped", reason: "events_unreadable" };
5162
+ }
5163
+ const rawLines = text.slice(0, -1).split("\n");
5164
+ for (const line of rawLines) {
5165
+ if (line.trim().length === 0) {
5166
+ return { status: "skipped", reason: "events_unreadable" };
5167
+ }
5168
+ let parsed;
5169
+ try {
5170
+ parsed = JSON.parse(line);
5171
+ } catch {
5172
+ return { status: "skipped", reason: "events_unreadable" };
5173
+ }
5174
+ if (JSON.stringify(parsed) !== line) {
5175
+ return { status: "skipped", reason: "events_unreadable" };
5176
+ }
5177
+ if (!EventSchema.safeParse(parsed).success) {
5178
+ return { status: "skipped", reason: "events_unreadable" };
5179
+ }
5180
+ if (parsed.session_id !== sessionId) {
5181
+ return { status: "skipped", reason: "session_id_mismatch" };
5182
+ }
5183
+ }
5184
+ if (options.dryRun === true) {
5185
+ return { status: "rechained", eventCount: rawLines.length };
5186
+ }
5187
+ const chainResult = chainRawJsonLines(rawLines, sessionId);
5188
+ const body = `${chainResult.lines.join("\n")}
5189
+ `;
5190
+ try {
5191
+ await atomicReplace(eventsPath, body);
5192
+ } catch (error) {
5193
+ throw new Error("Failed to write events.jsonl", { cause: error });
5194
+ }
5195
+ try {
5196
+ await overwriteYamlFile(
5197
+ join15(sessionDir, "session.yaml"),
5198
+ withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
5199
+ );
5200
+ } catch (error) {
5201
+ await atomicReplace(eventsPath, priorRaw).catch(() => void 0);
5202
+ throw error;
5203
+ }
5204
+ return { status: "rechained", eventCount: chainResult.count };
5205
+ } finally {
5206
+ await lock.release();
5207
+ }
5208
+ }
4734
5209
 
4735
5210
  // src/index.ts
4736
5211
  var BASOU_CORE_VERSION = "0.1.0";
@@ -4760,6 +5235,7 @@ export {
4760
5235
  SessionIdSchema,
4761
5236
  SessionImportPayloadSchema,
4762
5237
  SessionInnerImportSchema,
5238
+ SessionIntegritySchema,
4763
5239
  SessionMetricsSchema,
4764
5240
  SessionSchema,
4765
5241
  SessionSourceKindSchema,
@@ -4779,6 +5255,8 @@ export {
4779
5255
  basouPaths,
4780
5256
  buildJsonSchemas,
4781
5257
  buildStatusSnapshot,
5258
+ chainEvents,
5259
+ chainRawJsonLines,
4782
5260
  classifySuspect,
4783
5261
  claudeCodeAdapterMetadata,
4784
5262
  claudeTranscriptToImportPayload,
@@ -4795,11 +5273,14 @@ export {
4795
5273
  enumerateSessionDirs,
4796
5274
  enumerateTaskIds,
4797
5275
  findErrorCode,
5276
+ genesisHash,
4798
5277
  getDiff,
4799
5278
  getSnapshot,
4800
5279
  importSessionFromJson,
5280
+ isImportDerivedSource,
4801
5281
  isLazyExpired,
4802
5282
  isValidPrefixedId,
5283
+ lineHash,
4803
5284
  linkYamlFile,
4804
5285
  loadApproval,
4805
5286
  loadSessionEntries,
@@ -4816,9 +5297,11 @@ export {
4816
5297
  readTaskFile,
4817
5298
  readTaskFileWithArchiveFallback,
4818
5299
  readYamlFile,
5300
+ rechainSessionInPlace,
4819
5301
  reconcileAllTasks,
4820
5302
  reconcileTask,
4821
5303
  refreshTaskLinkedSessions,
5304
+ reimportPreservingId,
4822
5305
  renderDecisions,
4823
5306
  renderHandoff,
4824
5307
  renderWithMarkers,
@@ -4830,12 +5313,14 @@ export {
4830
5313
  sanitizePath,
4831
5314
  sanitizeRelatedFiles,
4832
5315
  sanitizeWorkingDirectory,
5316
+ serializeEventLine,
4833
5317
  serializeJsonSchema,
4834
5318
  sessionWorkStatsFromEvents,
4835
5319
  summarizeAdapterOutput,
4836
5320
  tryRemoteUrl,
4837
5321
  ulid,
4838
5322
  updateTaskStatusWithEvent,
5323
+ verifyEventsChain,
4839
5324
  writeEventsBulk,
4840
5325
  writeManifest,
4841
5326
  writeMarkdownFile,