@basou/core 0.9.0 → 0.10.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
@@ -767,7 +767,7 @@ function isLazyExpired(approval, now) {
767
767
 
768
768
  // src/decisions/decisions-renderer.ts
769
769
  import { lstat } from "fs/promises";
770
- import { dirname, join as join4, resolve } from "path";
770
+ import { dirname as dirname2, join as join6, resolve } from "path";
771
771
 
772
772
  // src/events/event-replay.ts
773
773
  import { createReadStream } from "fs";
@@ -1013,7 +1013,208 @@ async function readAllEvents(sessionDir, options = {}) {
1013
1013
 
1014
1014
  // src/storage/sessions.ts
1015
1015
  import { readdir as readdir2 } from "fs/promises";
1016
- import { join as join3 } from "path";
1016
+ import { join as join5 } from "path";
1017
+
1018
+ // src/events/chained-append.ts
1019
+ import { appendFile, readFile as readFile3 } from "fs/promises";
1020
+ import { join as join4 } from "path";
1021
+
1022
+ // src/storage/lockfile.ts
1023
+ import { mkdir, readFile as readFile2, unlink as unlink2 } from "fs/promises";
1024
+ import { dirname, join as join3 } from "path";
1025
+ var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
1026
+ async function acquireLock(paths, scope, resourceId) {
1027
+ const lockPath = lockfilePath(paths, scope, resourceId);
1028
+ const body = {
1029
+ pid: process.pid,
1030
+ acquired_at: (/* @__PURE__ */ new Date()).toISOString()
1031
+ };
1032
+ const serialised = JSON.stringify(body);
1033
+ try {
1034
+ await atomicCreate(lockPath, serialised);
1035
+ } catch (error) {
1036
+ if (findErrorCode(error, "ENOENT")) {
1037
+ try {
1038
+ await mkdir(dirname(lockPath), { recursive: true });
1039
+ await atomicCreate(lockPath, serialised);
1040
+ return {
1041
+ release: async () => {
1042
+ await unlink2(lockPath).catch(() => void 0);
1043
+ }
1044
+ };
1045
+ } catch (retryError) {
1046
+ throw new Error("Failed to acquire lock", { cause: retryError });
1047
+ }
1048
+ }
1049
+ if (!findErrorCode(error, "EEXIST")) {
1050
+ throw error;
1051
+ }
1052
+ const stale = await isStaleLock(lockPath);
1053
+ if (!stale) {
1054
+ throw new Error("Lock is held by another process", { cause: error });
1055
+ }
1056
+ await unlink2(lockPath).catch(() => void 0);
1057
+ try {
1058
+ await atomicCreate(lockPath, serialised);
1059
+ } catch (retryError) {
1060
+ throw new Error("Lock is held by another process", { cause: retryError });
1061
+ }
1062
+ }
1063
+ return {
1064
+ release: async () => {
1065
+ await unlink2(lockPath).catch(() => void 0);
1066
+ }
1067
+ };
1068
+ }
1069
+ async function isStaleLock(lockPath) {
1070
+ let body;
1071
+ try {
1072
+ const raw = await readFile2(lockPath, "utf8");
1073
+ const parsed = JSON.parse(raw);
1074
+ if (typeof parsed !== "object" || parsed === null) return true;
1075
+ const candidate = parsed;
1076
+ if (typeof candidate.pid !== "number" || typeof candidate.acquired_at !== "string") {
1077
+ return true;
1078
+ }
1079
+ body = { pid: candidate.pid, acquired_at: candidate.acquired_at };
1080
+ } catch {
1081
+ return true;
1082
+ }
1083
+ const ageMs = Date.now() - Date.parse(body.acquired_at);
1084
+ if (!Number.isFinite(ageMs) || ageMs > STALE_LOCK_MAX_AGE_MS) {
1085
+ return true;
1086
+ }
1087
+ try {
1088
+ process.kill(body.pid, 0);
1089
+ return false;
1090
+ } catch (error) {
1091
+ if (findErrorCode(error, "ESRCH")) return true;
1092
+ return false;
1093
+ }
1094
+ }
1095
+ function lockfilePath(paths, scope, resourceId) {
1096
+ const sep = resourceId.indexOf("_");
1097
+ const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
1098
+ return join3(paths.locks, `${scope}_${ulid2}.lock`);
1099
+ }
1100
+
1101
+ // src/events/chain.ts
1102
+ import { createHash } from "crypto";
1103
+ var GENESIS_PREFIX = "basou:event-chain:v1:";
1104
+ function genesisHash(sessionId) {
1105
+ return createHash("sha256").update(`${GENESIS_PREFIX}${sessionId}`, "utf8").digest("hex");
1106
+ }
1107
+ function lineHash(rawLine) {
1108
+ const hash = createHash("sha256");
1109
+ if (typeof rawLine === "string") {
1110
+ hash.update(rawLine, "utf8");
1111
+ } else {
1112
+ hash.update(rawLine);
1113
+ }
1114
+ return hash.digest("hex");
1115
+ }
1116
+ function serializeEventLine(event) {
1117
+ return JSON.stringify(event);
1118
+ }
1119
+ function chainEvents(events, sessionId) {
1120
+ let prev = genesisHash(sessionId);
1121
+ const lines = [];
1122
+ for (const event of events) {
1123
+ const chained = { ...event, prev_hash: prev };
1124
+ const line = serializeEventLine(chained);
1125
+ lines.push(line);
1126
+ prev = lineHash(line);
1127
+ }
1128
+ return { lines, headHash: prev, count: lines.length };
1129
+ }
1130
+ function chainRawJsonLines(rawLines, sessionId) {
1131
+ let prev = genesisHash(sessionId);
1132
+ const lines = [];
1133
+ for (const rawLine of rawLines) {
1134
+ const parsed = JSON.parse(rawLine);
1135
+ const line = JSON.stringify({ ...parsed, prev_hash: prev });
1136
+ lines.push(line);
1137
+ prev = lineHash(line);
1138
+ }
1139
+ return { lines, headHash: prev, count: lines.length };
1140
+ }
1141
+
1142
+ // src/events/chained-append.ts
1143
+ function splitLinesBytes(buf) {
1144
+ const out = [];
1145
+ let start = 0;
1146
+ for (let i = 0; i < buf.length; i++) {
1147
+ if (buf[i] === 10) {
1148
+ out.push(buf.subarray(start, i));
1149
+ start = i + 1;
1150
+ }
1151
+ }
1152
+ if (start < buf.length) out.push(buf.subarray(start));
1153
+ return out;
1154
+ }
1155
+ function carriesPrevHash(line) {
1156
+ try {
1157
+ const obj = JSON.parse(line.toString("utf8"));
1158
+ return typeof obj === "object" && obj !== null && "prev_hash" in obj;
1159
+ } catch {
1160
+ return false;
1161
+ }
1162
+ }
1163
+ async function inspectChainTail(paths, sessionId) {
1164
+ const filePath = join4(paths.sessions, sessionId, "events.jsonl");
1165
+ let raw;
1166
+ try {
1167
+ raw = await readFile3(filePath);
1168
+ } catch (error) {
1169
+ if (findErrorCode(error, "ENOENT")) {
1170
+ return { chained: true, head: genesisHash(sessionId), count: 0 };
1171
+ }
1172
+ throw new Error("Failed to read events.jsonl", { cause: error });
1173
+ }
1174
+ if (raw.length === 0) {
1175
+ return { chained: true, head: genesisHash(sessionId), count: 0 };
1176
+ }
1177
+ if (raw[raw.length - 1] !== 10) {
1178
+ throw new Error("Unterminated final line in events.jsonl");
1179
+ }
1180
+ const lines = splitLinesBytes(raw);
1181
+ const first = lines[0];
1182
+ const last = lines[lines.length - 1];
1183
+ const firstChained = carriesPrevHash(first);
1184
+ if (firstChained !== carriesPrevHash(last)) {
1185
+ throw new Error("events.jsonl is partially chained");
1186
+ }
1187
+ return {
1188
+ chained: firstChained,
1189
+ head: firstChained ? lineHash(last) : genesisHash(sessionId),
1190
+ count: lines.length
1191
+ };
1192
+ }
1193
+ async function appendChainedEventLocked(paths, sessionId, event) {
1194
+ let validated;
1195
+ try {
1196
+ validated = EventSchema.parse(event);
1197
+ } catch (error) {
1198
+ throw new Error("Invalid Basou event payload", { cause: error });
1199
+ }
1200
+ const tail = await inspectChainTail(paths, sessionId);
1201
+ const line = tail.chained ? serializeEventLine({ ...validated, prev_hash: tail.head }) : serializeEventLine(validated);
1202
+ try {
1203
+ await appendFile(join4(paths.sessions, sessionId, "events.jsonl"), `${line}
1204
+ `, "utf8");
1205
+ } catch (error) {
1206
+ throw new Error("Failed to append event to events.jsonl", { cause: error });
1207
+ }
1208
+ return { chained: tail.chained };
1209
+ }
1210
+ async function appendChainedEvent(paths, sessionId, event) {
1211
+ const lock = await acquireLock(paths, "session", sessionId);
1212
+ try {
1213
+ return await appendChainedEventLocked(paths, sessionId, event);
1214
+ } finally {
1215
+ await lock.release();
1216
+ }
1217
+ }
1017
1218
 
1018
1219
  // src/schemas/session.schema.ts
1019
1220
  import { z as z4 } from "zod";
@@ -1108,7 +1309,7 @@ async function enumerateSessionDirs(paths) {
1108
1309
  }
1109
1310
  }
1110
1311
  async function readSessionYaml(paths, sessionId) {
1111
- const filePath = join3(paths.sessions, sessionId, "session.yaml");
1312
+ const filePath = join5(paths.sessions, sessionId, "session.yaml");
1112
1313
  let raw;
1113
1314
  try {
1114
1315
  raw = await readYamlFile(filePath);
@@ -1122,11 +1323,26 @@ async function readSessionYaml(paths, sessionId) {
1122
1323
  }
1123
1324
  return result.data;
1124
1325
  }
1326
+ async function finalizeSessionYaml(paths, sessionId, mutate) {
1327
+ const lock = await acquireLock(paths, "session", sessionId);
1328
+ try {
1329
+ const session = await readSessionYaml(paths, sessionId);
1330
+ mutate(session);
1331
+ const tail = await inspectChainTail(paths, sessionId);
1332
+ if (tail.chained && tail.count > 0) {
1333
+ session.session.integrity = { head_hash: tail.head, event_count: tail.count };
1334
+ }
1335
+ const validated = SessionSchema.parse(session);
1336
+ await overwriteYamlFile(join5(paths.sessions, sessionId, "session.yaml"), validated);
1337
+ } finally {
1338
+ await lock.release();
1339
+ }
1340
+ }
1125
1341
  async function classifySuspect(paths, sessionId, session, now, onWarning) {
1126
1342
  if (session.session.status !== "running") {
1127
1343
  return { suspect: false, suspectReason: null };
1128
1344
  }
1129
- const sessionDir = join3(paths.sessions, sessionId);
1345
+ const sessionDir = join5(paths.sessions, sessionId);
1130
1346
  let endedFound = false;
1131
1347
  let lastEventOccurredAt = null;
1132
1348
  const replayOpts = onWarning !== void 0 ? { onWarning } : {};
@@ -1194,7 +1410,7 @@ async function renderDecisions(input) {
1194
1410
  const decisions = [];
1195
1411
  const knownEventIds = /* @__PURE__ */ new Set();
1196
1412
  for (const entry of entries) {
1197
- const sessionDir = join4(input.paths.sessions, entry.sessionId);
1413
+ const sessionDir = join6(input.paths.sessions, entry.sessionId);
1198
1414
  try {
1199
1415
  for await (const ev of replayEvents(sessionDir, {
1200
1416
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -1224,7 +1440,7 @@ async function renderDecisions(input) {
1224
1440
  const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
1225
1441
  return c !== 0 ? c : a.decisionId.localeCompare(b.decisionId);
1226
1442
  });
1227
- const repoRoot = dirname(input.paths.root);
1443
+ const repoRoot = dirname2(input.paths.root);
1228
1444
  const fileExistenceCache = /* @__PURE__ */ new Map();
1229
1445
  async function fileExists(relPath) {
1230
1446
  const cached = fileExistenceCache.get(relPath);
@@ -1298,50 +1514,9 @@ function shortDecisionSessionId(sessionId) {
1298
1514
  return sessionId.slice(0, 10);
1299
1515
  }
1300
1516
 
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
-
1342
1517
  // src/events/event-writer.ts
1343
- import { appendFile } from "fs/promises";
1344
- import { basename, join as join5 } from "path";
1518
+ import { appendFile as appendFile2 } from "fs/promises";
1519
+ import { basename, join as join7 } from "path";
1345
1520
  async function appendEvent(sessionDir, event) {
1346
1521
  let validated;
1347
1522
  try {
@@ -1352,7 +1527,7 @@ async function appendEvent(sessionDir, event) {
1352
1527
  const line = `${serializeEventLine(validated)}
1353
1528
  `;
1354
1529
  try {
1355
- await appendFile(join5(sessionDir, "events.jsonl"), line, "utf8");
1530
+ await appendFile2(join7(sessionDir, "events.jsonl"), line, "utf8");
1356
1531
  } catch (error) {
1357
1532
  throw new Error("Failed to append event to events.jsonl", { cause: error });
1358
1533
  }
@@ -1366,7 +1541,7 @@ async function writeEventsBulk(sessionDir, events, options = {}) {
1366
1541
  } catch (error) {
1367
1542
  throw new Error("Invalid Basou event payload", { cause: error });
1368
1543
  }
1369
- const filePath = join5(sessionDir, "events.jsonl");
1544
+ const filePath = join7(sessionDir, "events.jsonl");
1370
1545
  let body;
1371
1546
  let result = null;
1372
1547
  if (options.chain === true) {
@@ -1387,13 +1562,30 @@ async function writeEventsBulk(sessionDir, events, options = {}) {
1387
1562
  }
1388
1563
 
1389
1564
  // src/events/verify.ts
1390
- import { readFile as readFile2 } from "fs/promises";
1391
- import { join as join6 } from "path";
1565
+ import { readFile as readFile4 } from "fs/promises";
1566
+ import { join as join8 } from "path";
1567
+ var STRICT_STATUSES = /* @__PURE__ */ new Set([
1568
+ "completed",
1569
+ "failed",
1570
+ "interrupted",
1571
+ "imported",
1572
+ "archived"
1573
+ ]);
1574
+ function isLiveStatus(status) {
1575
+ return !STRICT_STATUSES.has(status);
1576
+ }
1392
1577
  async function verifyEventsChain(paths, sessionId) {
1393
- const sessionDir = join6(paths.sessions, sessionId);
1578
+ const first = await verifyOnce(paths, sessionId);
1579
+ if (first.status === "tampered" && first.reason === "anchor_mismatch") {
1580
+ return await verifyOnce(paths, sessionId);
1581
+ }
1582
+ return first;
1583
+ }
1584
+ async function verifyOnce(paths, sessionId) {
1585
+ const sessionDir = join8(paths.sessions, sessionId);
1394
1586
  let raw = null;
1395
1587
  try {
1396
- raw = await readFile2(join6(sessionDir, "events.jsonl"));
1588
+ raw = await readFile4(join8(sessionDir, "events.jsonl"));
1397
1589
  } catch (error) {
1398
1590
  if (!findErrorCode(error, "ENOENT")) {
1399
1591
  throw new Error("Failed to read events.jsonl", { cause: error });
@@ -1402,7 +1594,11 @@ async function verifyEventsChain(paths, sessionId) {
1402
1594
  let anchor;
1403
1595
  try {
1404
1596
  const session = await readSessionYaml(paths, sessionId);
1405
- anchor = { kind: "present", integrity: session.session.integrity };
1597
+ anchor = {
1598
+ kind: "present",
1599
+ integrity: session.session.integrity,
1600
+ status: session.session.status
1601
+ };
1406
1602
  } catch (error) {
1407
1603
  if (error instanceof Error && error.message === "YAML file not found") {
1408
1604
  anchor = { kind: "absent" };
@@ -1411,10 +1607,10 @@ async function verifyEventsChain(paths, sessionId) {
1411
1607
  }
1412
1608
  }
1413
1609
  const terminated = raw === null || raw.length === 0 || raw[raw.length - 1] === 10;
1414
- const segments = raw === null ? [] : splitLinesBytes(raw);
1610
+ const segments = raw === null ? [] : splitLinesBytes2(raw);
1415
1611
  const tailFragment = !terminated && segments.length > 0 ? segments.pop() : null;
1416
1612
  const lines = segments;
1417
- const carriesPrevHash = (s) => {
1613
+ const carriesPrevHash2 = (s) => {
1418
1614
  try {
1419
1615
  const obj = JSON.parse(s.toString("utf8"));
1420
1616
  return typeof obj === "object" && obj !== null && "prev_hash" in obj;
@@ -1422,7 +1618,7 @@ async function verifyEventsChain(paths, sessionId) {
1422
1618
  return false;
1423
1619
  }
1424
1620
  };
1425
- const chained = lines.some((l) => l.length > 0 && carriesPrevHash(l)) || tailFragment !== null && carriesPrevHash(tailFragment);
1621
+ const chained = lines.some((l) => l.length > 0 && carriesPrevHash2(l)) || tailFragment !== null && carriesPrevHash2(tailFragment);
1426
1622
  if (!chained) {
1427
1623
  if (anchor.kind === "present" && anchor.integrity !== void 0) {
1428
1624
  return {
@@ -1481,7 +1677,11 @@ async function verifyEventsChain(paths, sessionId) {
1481
1677
  }
1482
1678
  expected = lineHash(line);
1483
1679
  }
1680
+ const live = anchor.kind === "present" && isLiveStatus(anchor.status);
1484
1681
  if (tailFragment !== null || !terminated) {
1682
+ if (live) {
1683
+ return { status: "in_progress", eventCount: lines.length };
1684
+ }
1485
1685
  return {
1486
1686
  status: "tampered",
1487
1687
  eventCount: lines.length,
@@ -1495,6 +1695,9 @@ async function verifyEventsChain(paths, sessionId) {
1495
1695
  if (anchor.kind === "unreadable") {
1496
1696
  return { status: "tampered", eventCount: lines.length, reason: "yaml_unreadable" };
1497
1697
  }
1698
+ if (live) {
1699
+ return { status: "in_progress", eventCount: lines.length };
1700
+ }
1498
1701
  if (anchor.integrity === void 0) {
1499
1702
  return { status: "tampered", eventCount: lines.length, reason: "anchor_missing" };
1500
1703
  }
@@ -1503,7 +1706,7 @@ async function verifyEventsChain(paths, sessionId) {
1503
1706
  }
1504
1707
  return { status: "verified", eventCount: lines.length };
1505
1708
  }
1506
- function splitLinesBytes(buf) {
1709
+ function splitLinesBytes2(buf) {
1507
1710
  const out = [];
1508
1711
  let start = 0;
1509
1712
  for (let i = 0; i < buf.length; i++) {
@@ -1810,12 +2013,12 @@ function parseDiffNameStatus(raw) {
1810
2013
  }
1811
2014
 
1812
2015
  // src/handoff/handoff-renderer.ts
1813
- import { join as join11 } from "path";
2016
+ import { join as join12 } from "path";
1814
2017
 
1815
2018
  // src/storage/tasks.ts
1816
2019
  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";
2020
+ import { mkdir as mkdir3, readdir as readdir3, readFile as readFile7, rename as rename2, stat as stat2, unlink as unlink3 } from "fs/promises";
2021
+ import { join as join11 } from "path";
1819
2022
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1820
2023
  import { z as z8 } from "zod";
1821
2024
 
@@ -1857,9 +2060,9 @@ var TaskSchema = z6.object({
1857
2060
  });
1858
2061
 
1859
2062
  // src/storage/ad-hoc-session.ts
1860
- import { mkdir, rm } from "fs/promises";
2063
+ import { mkdir as mkdir2, rm } from "fs/promises";
1861
2064
  import { homedir } from "os";
1862
- import { join as join7 } from "path";
2065
+ import { join as join9 } from "path";
1863
2066
 
1864
2067
  // src/lib/path-sanitizer.ts
1865
2068
  import { posix as path } from "path";
@@ -1939,87 +2142,94 @@ async function createAdHocSessionWithEvent(input) {
1939
2142
  taskId: input.taskId ?? null
1940
2143
  })
1941
2144
  );
1942
- const sessionDir = join7(input.paths.sessions, sessionId);
1943
- try {
1944
- await mkdir(sessionDir, { recursive: true });
1945
- } catch (error) {
1946
- throw new Error("Failed to create session directory", { cause: error });
1947
- }
1948
- const sessionYamlPath = join7(sessionDir, "session.yaml");
2145
+ const sessionDir = join9(input.paths.sessions, sessionId);
2146
+ const sessionYamlPath = join9(sessionDir, "session.yaml");
2147
+ const lock = await acquireLock(input.paths, "session", sessionId);
2148
+ let bulkResult = null;
1949
2149
  try {
1950
- await linkYamlFile(sessionYamlPath, initialSession);
1951
- } catch (error) {
1952
- await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
1953
- if (findErrorCode(error, "EEXIST")) {
1954
- throw new Error("Session directory collision (retry the command)", {
1955
- cause: error
1956
- });
2150
+ try {
2151
+ await mkdir2(sessionDir, { recursive: true });
2152
+ } catch (error) {
2153
+ throw new Error("Failed to create session directory", { cause: error });
1957
2154
  }
1958
- throw error;
1959
- }
1960
- try {
1961
- const targetEvents = input.targetEventBuilders.map((build, index) => {
1962
- const targetEventId = targetEventIds[index];
1963
- return assertTargetEventIdentity(build(sessionId, targetEventId), sessionId, targetEventId);
1964
- });
1965
- const events = [
1966
- {
1967
- schema_version: "0.1.0",
1968
- id: startedEventId,
1969
- session_id: sessionId,
1970
- occurred_at: input.occurredAt,
1971
- source: "local-cli",
1972
- type: "session_started"
1973
- },
1974
- {
1975
- schema_version: "0.1.0",
1976
- id: statusToRunningEventId,
1977
- session_id: sessionId,
1978
- occurred_at: input.occurredAt,
1979
- source: "local-cli",
1980
- type: "session_status_changed",
1981
- from: "initialized",
1982
- to: "running"
1983
- },
1984
- ...targetEvents,
1985
- {
1986
- schema_version: "0.1.0",
1987
- id: statusToCompletedEventId,
1988
- session_id: sessionId,
1989
- occurred_at: input.occurredAt,
1990
- source: "local-cli",
1991
- type: "session_status_changed",
1992
- from: "running",
1993
- to: "completed"
1994
- },
1995
- {
1996
- schema_version: "0.1.0",
1997
- id: endedEventId,
1998
- session_id: sessionId,
1999
- occurred_at: input.occurredAt,
2000
- source: "local-cli",
2001
- type: "session_ended",
2002
- exit_code: 0
2003
- }
2004
- ];
2005
- await writeEventsBulk(sessionDir, events);
2006
- } catch (error) {
2007
- await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
2008
- throw error;
2009
- }
2010
- try {
2011
- const finalSession = SessionSchema.parse({
2012
- ...initialSession,
2013
- session: {
2014
- ...initialSession.session,
2015
- status: "completed",
2016
- ended_at: input.occurredAt,
2017
- invocation: { ...initialSession.session.invocation, exit_code: 0 }
2155
+ try {
2156
+ await linkYamlFile(sessionYamlPath, initialSession);
2157
+ } catch (error) {
2158
+ await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
2159
+ if (findErrorCode(error, "EEXIST")) {
2160
+ throw new Error("Session directory collision (retry the command)", {
2161
+ cause: error
2162
+ });
2018
2163
  }
2019
- });
2020
- await overwriteYamlFile(sessionYamlPath, finalSession);
2021
- } catch (error) {
2022
- throw new FailedToFinalizeError(sessionId, targetEventIds, error);
2164
+ throw error;
2165
+ }
2166
+ try {
2167
+ const targetEvents = input.targetEventBuilders.map((build, index) => {
2168
+ const targetEventId = targetEventIds[index];
2169
+ return assertTargetEventIdentity(build(sessionId, targetEventId), sessionId, targetEventId);
2170
+ });
2171
+ const events = [
2172
+ {
2173
+ schema_version: "0.1.0",
2174
+ id: startedEventId,
2175
+ session_id: sessionId,
2176
+ occurred_at: input.occurredAt,
2177
+ source: "local-cli",
2178
+ type: "session_started"
2179
+ },
2180
+ {
2181
+ schema_version: "0.1.0",
2182
+ id: statusToRunningEventId,
2183
+ session_id: sessionId,
2184
+ occurred_at: input.occurredAt,
2185
+ source: "local-cli",
2186
+ type: "session_status_changed",
2187
+ from: "initialized",
2188
+ to: "running"
2189
+ },
2190
+ ...targetEvents,
2191
+ {
2192
+ schema_version: "0.1.0",
2193
+ id: statusToCompletedEventId,
2194
+ session_id: sessionId,
2195
+ occurred_at: input.occurredAt,
2196
+ source: "local-cli",
2197
+ type: "session_status_changed",
2198
+ from: "running",
2199
+ to: "completed"
2200
+ },
2201
+ {
2202
+ schema_version: "0.1.0",
2203
+ id: endedEventId,
2204
+ session_id: sessionId,
2205
+ occurred_at: input.occurredAt,
2206
+ source: "local-cli",
2207
+ type: "session_ended",
2208
+ exit_code: 0
2209
+ }
2210
+ ];
2211
+ bulkResult = await writeEventsBulk(sessionDir, events, { chain: true });
2212
+ } catch (error) {
2213
+ await rm(sessionDir, { recursive: true, force: true }).catch(() => void 0);
2214
+ throw error;
2215
+ }
2216
+ try {
2217
+ const finalSession = SessionSchema.parse({
2218
+ ...initialSession,
2219
+ session: {
2220
+ ...initialSession.session,
2221
+ status: "completed",
2222
+ ended_at: input.occurredAt,
2223
+ invocation: { ...initialSession.session.invocation, exit_code: 0 },
2224
+ ...bulkResult !== null ? { integrity: { head_hash: bulkResult.headHash, event_count: bulkResult.count } } : {}
2225
+ }
2226
+ });
2227
+ await overwriteYamlFile(sessionYamlPath, finalSession);
2228
+ } catch (error) {
2229
+ throw new FailedToFinalizeError(sessionId, targetEventIds, error);
2230
+ }
2231
+ } finally {
2232
+ await lock.release();
2023
2233
  }
2024
2234
  return {
2025
2235
  sessionId,
@@ -2068,8 +2278,7 @@ async function appendEventToExistingSession(input) {
2068
2278
  }
2069
2279
  const eventId = prefixedUlid("evt");
2070
2280
  const event = assertTargetEventIdentity(input.eventBuilder(eventId), input.sessionId, eventId);
2071
- const sessionDir = join7(input.paths.sessions, input.sessionId);
2072
- await appendEvent(sessionDir, event);
2281
+ await appendChainedEventLocked(input.paths, input.sessionId, event);
2073
2282
  return { eventId, sessionStatus: status };
2074
2283
  }
2075
2284
  function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
@@ -2082,88 +2291,9 @@ function assertTargetEventIdentity(event, expectedSessionId, expectedEventId) {
2082
2291
  return event;
2083
2292
  }
2084
2293
 
2085
- // src/storage/lockfile.ts
2086
- import { mkdir as mkdir2, readFile as readFile4, unlink as unlink2 } from "fs/promises";
2087
- import { dirname as dirname2, join as join8 } from "path";
2088
- var STALE_LOCK_MAX_AGE_MS = 60 * 60 * 1e3;
2089
- async function acquireLock(paths, scope, resourceId) {
2090
- const lockPath = lockfilePath(paths, scope, resourceId);
2091
- const body = {
2092
- pid: process.pid,
2093
- acquired_at: (/* @__PURE__ */ new Date()).toISOString()
2094
- };
2095
- const serialised = JSON.stringify(body);
2096
- try {
2097
- await atomicCreate(lockPath, serialised);
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
- }
2112
- if (!findErrorCode(error, "EEXIST")) {
2113
- throw error;
2114
- }
2115
- const stale = await isStaleLock(lockPath);
2116
- if (!stale) {
2117
- throw new Error("Lock is held by another process", { cause: error });
2118
- }
2119
- await unlink2(lockPath).catch(() => void 0);
2120
- try {
2121
- await atomicCreate(lockPath, serialised);
2122
- } catch (retryError) {
2123
- throw new Error("Lock is held by another process", { cause: retryError });
2124
- }
2125
- }
2126
- return {
2127
- release: async () => {
2128
- await unlink2(lockPath).catch(() => void 0);
2129
- }
2130
- };
2131
- }
2132
- async function isStaleLock(lockPath) {
2133
- let body;
2134
- try {
2135
- const raw = await readFile4(lockPath, "utf8");
2136
- const parsed = JSON.parse(raw);
2137
- if (typeof parsed !== "object" || parsed === null) return true;
2138
- const candidate = parsed;
2139
- if (typeof candidate.pid !== "number" || typeof candidate.acquired_at !== "string") {
2140
- return true;
2141
- }
2142
- body = { pid: candidate.pid, acquired_at: candidate.acquired_at };
2143
- } catch {
2144
- return true;
2145
- }
2146
- const ageMs = Date.now() - Date.parse(body.acquired_at);
2147
- if (!Number.isFinite(ageMs) || ageMs > STALE_LOCK_MAX_AGE_MS) {
2148
- return true;
2149
- }
2150
- try {
2151
- process.kill(body.pid, 0);
2152
- return false;
2153
- } catch (error) {
2154
- if (findErrorCode(error, "ESRCH")) return true;
2155
- return false;
2156
- }
2157
- }
2158
- function lockfilePath(paths, scope, resourceId) {
2159
- const sep = resourceId.indexOf("_");
2160
- const ulid2 = sep >= 0 ? resourceId.slice(sep + 1) : resourceId;
2161
- return join8(paths.locks, `${scope}_${ulid2}.lock`);
2162
- }
2163
-
2164
2294
  // src/storage/task-index.ts
2165
- import { readFile as readFile5 } from "fs/promises";
2166
- import { join as join9 } from "path";
2295
+ import { readFile as readFile6 } from "fs/promises";
2296
+ import { join as join10 } from "path";
2167
2297
 
2168
2298
  // src/schemas/task-index.schema.ts
2169
2299
  import { z as z7 } from "zod";
@@ -2182,13 +2312,13 @@ var TASK_INDEX_SCHEMA_VERSION = "0.1.0";
2182
2312
 
2183
2313
  // src/storage/task-index.ts
2184
2314
  function taskIndexPath(paths) {
2185
- return join9(paths.tasks, "index.json");
2315
+ return join10(paths.tasks, "index.json");
2186
2316
  }
2187
2317
  async function readTaskIndex(paths) {
2188
2318
  const filePath = taskIndexPath(paths);
2189
2319
  let raw;
2190
2320
  try {
2191
- raw = await readFile5(filePath, "utf8");
2321
+ raw = await readFile6(filePath, "utf8");
2192
2322
  } catch (error) {
2193
2323
  if (findErrorCode(error, "ENOENT")) {
2194
2324
  throw new Error("Task index not found", { cause: error });
@@ -2296,10 +2426,10 @@ function splitFrontMatter(raw) {
2296
2426
  return { yamlText, body };
2297
2427
  }
2298
2428
  async function readTaskFile(paths, taskId) {
2299
- const filePath = join10(paths.tasks, `${taskId}.md`);
2429
+ const filePath = join11(paths.tasks, `${taskId}.md`);
2300
2430
  let raw;
2301
2431
  try {
2302
- raw = await readFile6(filePath, "utf8");
2432
+ raw = await readFile7(filePath, "utf8");
2303
2433
  } catch (error) {
2304
2434
  if (findErrorCode(error, "ENOENT")) {
2305
2435
  throw new Error("Task file not found", { cause: error });
@@ -2329,7 +2459,7 @@ async function readTaskFile(paths, taskId) {
2329
2459
  }
2330
2460
  async function writeTaskFile(paths, taskId, doc, options) {
2331
2461
  const validated = TaskSchema.parse(doc.task);
2332
- const filePath = join10(paths.tasks, `${taskId}.md`);
2462
+ const filePath = join11(paths.tasks, `${taskId}.md`);
2333
2463
  const yamlText = stringifyYaml(validated);
2334
2464
  const trimmedBody = doc.body.length === 0 ? "" : `
2335
2465
  ${doc.body.endsWith("\n") ? doc.body : `${doc.body}
@@ -2414,7 +2544,7 @@ async function safeUpdateTaskIndex(paths, op) {
2414
2544
  }
2415
2545
  var ARCHIVE_DIR_NAME = "archive";
2416
2546
  function archiveTasksDir(paths) {
2417
- return join10(paths.tasks, ARCHIVE_DIR_NAME);
2547
+ return join11(paths.tasks, ARCHIVE_DIR_NAME);
2418
2548
  }
2419
2549
  async function enumerateArchivedTaskIds(paths) {
2420
2550
  let entries;
@@ -2444,10 +2574,10 @@ async function readTaskFileWithArchiveFallback(paths, taskId) {
2444
2574
  throw error;
2445
2575
  }
2446
2576
  }
2447
- const archiveFilePath = join10(archiveTasksDir(paths), `${taskId}.md`);
2577
+ const archiveFilePath = join11(archiveTasksDir(paths), `${taskId}.md`);
2448
2578
  let raw;
2449
2579
  try {
2450
- raw = await readFile6(archiveFilePath, "utf8");
2580
+ raw = await readFile7(archiveFilePath, "utf8");
2451
2581
  } catch (error) {
2452
2582
  if (findErrorCode(error, "ENOENT")) {
2453
2583
  throw new Error("Task file not found", { cause: error });
@@ -2738,7 +2868,7 @@ async function createTaskAttachLocked(input) {
2738
2868
  ...sessionDoc,
2739
2869
  session: { ...sessionDoc.session, task_id: input.taskId }
2740
2870
  };
2741
- await overwriteYamlFile(join10(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2871
+ await overwriteYamlFile(join11(input.paths.sessions, input.sessionId, "session.yaml"), updated);
2742
2872
  } catch (error) {
2743
2873
  throw new TaskWriteAfterEventError({
2744
2874
  taskId: input.taskId,
@@ -2997,17 +3127,17 @@ function buildUpdatedDoc(input) {
2997
3127
  return { task: next, body: input.currentDoc.body };
2998
3128
  }
2999
3129
  async function computeTaskMdSnapshot(paths, taskId) {
3000
- const filePath = join10(paths.tasks, `${taskId}.md`);
3001
- const [stats, raw] = await Promise.all([stat2(filePath), readFile6(filePath)]);
3130
+ const filePath = join11(paths.tasks, `${taskId}.md`);
3131
+ const [stats, raw] = await Promise.all([stat2(filePath), readFile7(filePath)]);
3002
3132
  const hash = createHash2("sha256").update(raw).digest("hex");
3003
3133
  return { mtimeMs: stats.mtimeMs, hash };
3004
3134
  }
3005
3135
  async function readTaskFileWithSnapshot(paths, taskId) {
3006
- const filePath = join10(paths.tasks, `${taskId}.md`);
3136
+ const filePath = join11(paths.tasks, `${taskId}.md`);
3007
3137
  let rawBuffer;
3008
3138
  let stats;
3009
3139
  try {
3010
- [rawBuffer, stats] = await Promise.all([readFile6(filePath), stat2(filePath)]);
3140
+ [rawBuffer, stats] = await Promise.all([readFile7(filePath), stat2(filePath)]);
3011
3141
  } catch (error) {
3012
3142
  if (findErrorCode(error, "ENOENT")) {
3013
3143
  throw new Error("Task file not found", { cause: error });
@@ -3495,7 +3625,7 @@ async function deleteTaskLocked(input) {
3495
3625
  });
3496
3626
  const eventId = adHoc.targetEventIds[0];
3497
3627
  try {
3498
- await unlink3(join10(input.paths.tasks, `${input.taskId}.md`));
3628
+ await unlink3(join11(input.paths.tasks, `${input.taskId}.md`));
3499
3629
  } catch (error) {
3500
3630
  throw new TaskWriteAfterEventError({
3501
3631
  taskId: input.taskId,
@@ -3567,8 +3697,8 @@ async function archiveTaskLocked(input) {
3567
3697
  );
3568
3698
  await mkdir3(archiveTasksDir(input.paths), { recursive: true });
3569
3699
  await rename2(
3570
- join10(input.paths.tasks, `${input.taskId}.md`),
3571
- join10(archiveTasksDir(input.paths), `${input.taskId}.md`)
3700
+ join11(input.paths.tasks, `${input.taskId}.md`),
3701
+ join11(archiveTasksDir(input.paths), `${input.taskId}.md`)
3572
3702
  );
3573
3703
  } catch (error) {
3574
3704
  throw new TaskWriteAfterEventError({
@@ -3604,7 +3734,7 @@ async function renderHandoff(input) {
3604
3734
  const tasksCreated = [];
3605
3735
  const tasksStatusChanged = [];
3606
3736
  for (const entry of entries) {
3607
- const sessionDir = join11(input.paths.sessions, entry.sessionId);
3737
+ const sessionDir = join12(input.paths.sessions, entry.sessionId);
3608
3738
  try {
3609
3739
  for await (const ev of replayEvents(sessionDir, {
3610
3740
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -4245,7 +4375,7 @@ function serializeJsonSchema(schema) {
4245
4375
  }
4246
4376
 
4247
4377
  // src/stats/work-stats.ts
4248
- import { join as join12 } from "path";
4378
+ import { join as join13 } from "path";
4249
4379
  function resolveTimeZone(timeZone) {
4250
4380
  if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4251
4381
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -4276,7 +4406,7 @@ async function computeWorkStats(input) {
4276
4406
  const events = [];
4277
4407
  let eventsUnreadable = false;
4278
4408
  try {
4279
- for await (const ev of replayEvents(join12(input.paths.sessions, entry.sessionId), {
4409
+ for await (const ev of replayEvents(join13(input.paths.sessions, entry.sessionId), {
4280
4410
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4281
4411
  })) {
4282
4412
  events.push(ev);
@@ -4532,27 +4662,27 @@ function tzDate(ms, timeZone) {
4532
4662
 
4533
4663
  // src/storage/basou-dir.ts
4534
4664
  import { lstat as lstat3, mkdir as mkdir4 } from "fs/promises";
4535
- import { join as join13 } from "path";
4665
+ import { join as join14 } from "path";
4536
4666
  function basouPaths(repositoryRoot) {
4537
- const root = join13(repositoryRoot, ".basou");
4538
- const approvalsBase = join13(root, "approvals");
4667
+ const root = join14(repositoryRoot, ".basou");
4668
+ const approvalsBase = join14(root, "approvals");
4539
4669
  return {
4540
4670
  root,
4541
- sessions: join13(root, "sessions"),
4542
- tasks: join13(root, "tasks"),
4671
+ sessions: join14(root, "sessions"),
4672
+ tasks: join14(root, "tasks"),
4543
4673
  approvals: {
4544
- pending: join13(approvalsBase, "pending"),
4545
- resolved: join13(approvalsBase, "resolved")
4674
+ pending: join14(approvalsBase, "pending"),
4675
+ resolved: join14(approvalsBase, "resolved")
4546
4676
  },
4547
- locks: join13(root, "locks"),
4548
- logs: join13(root, "logs"),
4549
- raw: join13(root, "raw"),
4550
- tmp: join13(root, "tmp"),
4677
+ locks: join14(root, "locks"),
4678
+ logs: join14(root, "logs"),
4679
+ raw: join14(root, "raw"),
4680
+ tmp: join14(root, "tmp"),
4551
4681
  files: {
4552
- manifest: join13(root, "manifest.yaml"),
4553
- status: join13(root, "status.json"),
4554
- handoff: join13(root, "handoff.md"),
4555
- decisions: join13(root, "decisions.md")
4682
+ manifest: join14(root, "manifest.yaml"),
4683
+ status: join14(root, "status.json"),
4684
+ handoff: join14(root, "handoff.md"),
4685
+ decisions: join14(root, "decisions.md")
4556
4686
  }
4557
4687
  };
4558
4688
  }
@@ -4608,16 +4738,16 @@ function hasErrorCode3(error) {
4608
4738
  }
4609
4739
 
4610
4740
  // src/storage/gitignore.ts
4611
- import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
4612
- import { join as join14 } from "path";
4741
+ import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
4742
+ import { join as join15 } from "path";
4613
4743
  var MARKER = "# Basou - default ignore";
4614
4744
  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";
4615
4745
  async function appendBasouGitignore(repositoryRoot) {
4616
- const gitignorePath = join14(repositoryRoot, ".gitignore");
4746
+ const gitignorePath = join15(repositoryRoot, ".gitignore");
4617
4747
  let body;
4618
4748
  let existed;
4619
4749
  try {
4620
- body = await readFile7(gitignorePath, "utf8");
4750
+ body = await readFile8(gitignorePath, "utf8");
4621
4751
  existed = true;
4622
4752
  } catch (error) {
4623
4753
  if (hasErrorCode4(error) && error.code === "ENOENT") {
@@ -4723,12 +4853,12 @@ function hasErrorCode5(error) {
4723
4853
  }
4724
4854
 
4725
4855
  // src/storage/markdown-store.ts
4726
- import { readFile as readFile8 } from "fs/promises";
4856
+ import { readFile as readFile9 } from "fs/promises";
4727
4857
  var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
4728
4858
  var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
4729
4859
  async function readMarkdownFile(filePath) {
4730
4860
  try {
4731
- return await readFile8(filePath, "utf8");
4861
+ return await readFile9(filePath, "utf8");
4732
4862
  } catch (error) {
4733
4863
  if (hasErrorCode6(error) && error.code === "ENOENT") return null;
4734
4864
  throw new Error("Failed to read markdown file", { cause: error });
@@ -4826,9 +4956,9 @@ function hasErrorCode6(error) {
4826
4956
  }
4827
4957
 
4828
4958
  // src/storage/session-import.ts
4829
- import { mkdir as mkdir5, readFile as readFile9, rm as rm2 } from "fs/promises";
4959
+ import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
4830
4960
  import { homedir as homedir2 } from "os";
4831
- import { join as join15 } from "path";
4961
+ import { join as join16 } from "path";
4832
4962
  async function importSessionFromJson(paths, manifest, payload, options) {
4833
4963
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
4834
4964
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -4853,7 +4983,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4853
4983
  pathSanitizeReport
4854
4984
  };
4855
4985
  }
4856
- const sessionDir = join15(paths.sessions, newSessionId);
4986
+ const sessionDir = join16(paths.sessions, newSessionId);
4857
4987
  try {
4858
4988
  await mkdir5(sessionDir, { recursive: true });
4859
4989
  } catch (error) {
@@ -4867,7 +4997,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
4867
4997
  throw error;
4868
4998
  }
4869
4999
  try {
4870
- const sessionYamlPath = join15(sessionDir, "session.yaml");
5000
+ const sessionYamlPath = join16(sessionDir, "session.yaml");
4871
5001
  await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
4872
5002
  } catch (error) {
4873
5003
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
@@ -5035,7 +5165,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
5035
5165
  async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
5036
5166
  const sessionId = priorSessionId;
5037
5167
  const importSource = freshPayload.session.source.kind;
5038
- const sessionDir = join15(paths.sessions, priorSessionId);
5168
+ const sessionDir = join16(paths.sessions, priorSessionId);
5039
5169
  const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
5040
5170
  try {
5041
5171
  const priorVerdict = await verifyEventsChain(paths, priorSessionId);
@@ -5077,10 +5207,10 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5077
5207
  };
5078
5208
  const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
5079
5209
  if (options.dryRun !== true) {
5080
- const eventsPath = join15(sessionDir, "events.jsonl");
5210
+ const eventsPath = join16(sessionDir, "events.jsonl");
5081
5211
  let priorEventsRaw = null;
5082
5212
  try {
5083
- priorEventsRaw = await readFile9(eventsPath);
5213
+ priorEventsRaw = await readFile10(eventsPath);
5084
5214
  } catch (error) {
5085
5215
  if (!findErrorCode(error, "ENOENT")) {
5086
5216
  throw new Error("Failed to read events.jsonl", { cause: error });
@@ -5089,7 +5219,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5089
5219
  const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
5090
5220
  try {
5091
5221
  await overwriteYamlFile(
5092
- join15(sessionDir, "session.yaml"),
5222
+ join16(sessionDir, "session.yaml"),
5093
5223
  withIntegrity(updatedRecord, chainResult)
5094
5224
  );
5095
5225
  } catch (error) {
@@ -5113,7 +5243,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5113
5243
  }
5114
5244
  }
5115
5245
  async function rechainSessionInPlace(paths, sessionId, options = {}) {
5116
- const sessionDir = join15(paths.sessions, sessionId);
5246
+ const sessionDir = join16(paths.sessions, sessionId);
5117
5247
  let lock;
5118
5248
  try {
5119
5249
  lock = await acquireLock(paths, "session", sessionId);
@@ -5146,10 +5276,10 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5146
5276
  if (verdict.status !== "unchained") {
5147
5277
  return { status: "skipped", reason: "tampered" };
5148
5278
  }
5149
- const eventsPath = join15(sessionDir, "events.jsonl");
5279
+ const eventsPath = join16(sessionDir, "events.jsonl");
5150
5280
  let priorRaw;
5151
5281
  try {
5152
- priorRaw = await readFile9(eventsPath);
5282
+ priorRaw = await readFile10(eventsPath);
5153
5283
  } catch (error) {
5154
5284
  throw new Error("Failed to read events.jsonl", { cause: error });
5155
5285
  }
@@ -5194,7 +5324,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5194
5324
  }
5195
5325
  try {
5196
5326
  await overwriteYamlFile(
5197
- join15(sessionDir, "session.yaml"),
5327
+ join16(sessionDir, "session.yaml"),
5198
5328
  withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
5199
5329
  );
5200
5330
  } catch (error) {
@@ -5248,6 +5378,8 @@ export {
5248
5378
  WorkspaceIdSchema,
5249
5379
  acquireLock,
5250
5380
  appendBasouGitignore,
5381
+ appendChainedEvent,
5382
+ appendChainedEventLocked,
5251
5383
  appendEvent,
5252
5384
  appendEventToExistingSession,
5253
5385
  archiveTask,
@@ -5272,11 +5404,13 @@ export {
5272
5404
  enumerateArchivedTaskIds,
5273
5405
  enumerateSessionDirs,
5274
5406
  enumerateTaskIds,
5407
+ finalizeSessionYaml,
5275
5408
  findErrorCode,
5276
5409
  genesisHash,
5277
5410
  getDiff,
5278
5411
  getSnapshot,
5279
5412
  importSessionFromJson,
5413
+ inspectChainTail,
5280
5414
  isImportDerivedSource,
5281
5415
  isLazyExpired,
5282
5416
  isValidPrefixedId,