@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.d.ts +144 -14
- package/dist/index.js +412 -278
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1391
|
-
import { join as
|
|
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
|
|
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
|
|
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 = {
|
|
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 ? [] :
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
1818
|
-
import { join as
|
|
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
|
|
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 =
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
-
|
|
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
|
|
2166
|
-
import { join as
|
|
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
|
|
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
|
|
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 =
|
|
2429
|
+
const filePath = join11(paths.tasks, `${taskId}.md`);
|
|
2300
2430
|
let raw;
|
|
2301
2431
|
try {
|
|
2302
|
-
raw = await
|
|
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 =
|
|
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
|
|
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 =
|
|
2577
|
+
const archiveFilePath = join11(archiveTasksDir(paths), `${taskId}.md`);
|
|
2448
2578
|
let raw;
|
|
2449
2579
|
try {
|
|
2450
|
-
raw = await
|
|
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(
|
|
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 =
|
|
3001
|
-
const [stats, raw] = await Promise.all([stat2(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 =
|
|
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([
|
|
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(
|
|
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
|
-
|
|
3571
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
4665
|
+
import { join as join14 } from "path";
|
|
4536
4666
|
function basouPaths(repositoryRoot) {
|
|
4537
|
-
const root =
|
|
4538
|
-
const approvalsBase =
|
|
4667
|
+
const root = join14(repositoryRoot, ".basou");
|
|
4668
|
+
const approvalsBase = join14(root, "approvals");
|
|
4539
4669
|
return {
|
|
4540
4670
|
root,
|
|
4541
|
-
sessions:
|
|
4542
|
-
tasks:
|
|
4671
|
+
sessions: join14(root, "sessions"),
|
|
4672
|
+
tasks: join14(root, "tasks"),
|
|
4543
4673
|
approvals: {
|
|
4544
|
-
pending:
|
|
4545
|
-
resolved:
|
|
4674
|
+
pending: join14(approvalsBase, "pending"),
|
|
4675
|
+
resolved: join14(approvalsBase, "resolved")
|
|
4546
4676
|
},
|
|
4547
|
-
locks:
|
|
4548
|
-
logs:
|
|
4549
|
-
raw:
|
|
4550
|
-
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:
|
|
4553
|
-
status:
|
|
4554
|
-
handoff:
|
|
4555
|
-
decisions:
|
|
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
|
|
4612
|
-
import { join as
|
|
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 =
|
|
4746
|
+
const gitignorePath = join15(repositoryRoot, ".gitignore");
|
|
4617
4747
|
let body;
|
|
4618
4748
|
let existed;
|
|
4619
4749
|
try {
|
|
4620
|
-
body = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5210
|
+
const eventsPath = join16(sessionDir, "events.jsonl");
|
|
5081
5211
|
let priorEventsRaw = null;
|
|
5082
5212
|
try {
|
|
5083
|
-
priorEventsRaw = await
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
5279
|
+
const eventsPath = join16(sessionDir, "events.jsonl");
|
|
5150
5280
|
let priorRaw;
|
|
5151
5281
|
try {
|
|
5152
|
-
priorRaw = await
|
|
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
|
-
|
|
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,
|