@caupulican/pi-agent-core 0.93.18 → 0.94.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/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +50 -17
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +5 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +4 -0
- package/dist/agent.js.map +1 -1
- package/dist/provider-request-estimator.d.ts.map +1 -1
- package/dist/provider-request-estimator.js +27 -17
- package/dist/provider-request-estimator.js.map +1 -1
- package/dist/provider-request-planner.d.ts +7 -1
- package/dist/provider-request-planner.d.ts.map +1 -1
- package/dist/provider-request-planner.js +20 -1
- package/dist/provider-request-planner.js.map +1 -1
- package/dist/session/lifecycle-ledger.d.ts +173 -0
- package/dist/session/lifecycle-ledger.d.ts.map +1 -0
- package/dist/session/lifecycle-ledger.js +648 -0
- package/dist/session/lifecycle-ledger.js.map +1 -0
- package/dist/session/session-manager.d.ts +52 -4
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +434 -39
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/session-tree.d.ts +10 -0
- package/dist/session/session-tree.d.ts.map +1 -0
- package/dist/session/session-tree.js +33 -0
- package/dist/session/session-tree.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -8,27 +8,16 @@ import { StreamingLineDecoder } from "@caupulican/pi-ai/streaming-lines";
|
|
|
8
8
|
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
9
9
|
import { normalizePath, resolvePath } from "../utils/paths.js";
|
|
10
10
|
import { uuidv7 } from "../uuid.js";
|
|
11
|
+
import { encodeSessionEntry, indexSessionLifecycle, inspectSessionLifecycle, isSessionLifecycleEntry, planSessionLifecycleRepair, sessionLifecycleToolIdentityKey, validateLoadedLifecycleEntries, validateSessionLifecycleEntry, } from "./lifecycle-ledger.js";
|
|
11
12
|
import { compactToolResultDetailsForRetention } from "./message-retention.js";
|
|
12
|
-
|
|
13
|
+
import { invalidSessionParentCycle, visitSessionAncestry } from "./session-tree.js";
|
|
14
|
+
export * from "./lifecycle-ledger.js";
|
|
15
|
+
export const CURRENT_SESSION_VERSION = 4;
|
|
13
16
|
/** Maximum borrowed entries one non-copying SessionManager visit may inspect. */
|
|
14
17
|
export const MAX_SESSION_ENTRY_VISIT_COUNT = 1_024;
|
|
18
|
+
/** Maximum normal message/custom-message records in one atomic append batch. */
|
|
19
|
+
export const MAX_SESSION_MESSAGE_BATCH_ENTRIES = 256;
|
|
15
20
|
const MAX_SESSION_LINEAGE_DEPTH = 64;
|
|
16
|
-
function invalidSessionParentCycle(entryId) {
|
|
17
|
-
return new Error(`Invalid session entry graph: parent cycle detected at entry "${entryId}".`);
|
|
18
|
-
}
|
|
19
|
-
/** Visit leaf-to-root ancestry through one bounded, cycle-rejecting implementation path. */
|
|
20
|
-
function visitSessionAncestry(start, byId, visitor) {
|
|
21
|
-
let current = start;
|
|
22
|
-
// One external start plus every indexed node is the longest possible acyclic walk.
|
|
23
|
-
let remainingEntries = byId.size + 1;
|
|
24
|
-
while (current) {
|
|
25
|
-
if (remainingEntries-- === 0)
|
|
26
|
-
throw invalidSessionParentCycle(current.id);
|
|
27
|
-
if (visitor(current) === false)
|
|
28
|
-
return;
|
|
29
|
-
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
21
|
const synthesizedSessionContextMessages = new WeakSet();
|
|
33
22
|
function retainSynthesizedSessionContextMessage(message) {
|
|
34
23
|
synthesizedSessionContextMessages.add(message);
|
|
@@ -62,6 +51,9 @@ function generateId(byId) {
|
|
|
62
51
|
// Fallback to full UUID if somehow we have collisions
|
|
63
52
|
return randomUUID();
|
|
64
53
|
}
|
|
54
|
+
function assistantToolIdentityKey(assistantMessageEntryId, callId) {
|
|
55
|
+
return JSON.stringify([assistantMessageEntryId, callId]);
|
|
56
|
+
}
|
|
65
57
|
/** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */
|
|
66
58
|
function migrateV1ToV2(entries) {
|
|
67
59
|
const ids = new Set();
|
|
@@ -104,6 +96,13 @@ function migrateV2ToV3(entries) {
|
|
|
104
96
|
}
|
|
105
97
|
}
|
|
106
98
|
}
|
|
99
|
+
/** Migrate v3 → v4: reserve the version for the typed lifecycle ledger. */
|
|
100
|
+
function migrateV3ToV4(entries) {
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.type === "session")
|
|
103
|
+
entry.version = 4;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
107
106
|
/**
|
|
108
107
|
* Run all necessary migrations to bring entries to current version.
|
|
109
108
|
* Mutates entries in place. Returns true if any migration was applied.
|
|
@@ -117,6 +116,8 @@ function migrateToCurrentVersion(entries) {
|
|
|
117
116
|
migrateV1ToV2(entries);
|
|
118
117
|
if (version < 3)
|
|
119
118
|
migrateV2ToV3(entries);
|
|
119
|
+
if (version < 4)
|
|
120
|
+
migrateV3ToV4(entries);
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
122
123
|
/** Exported for testing */
|
|
@@ -367,7 +368,9 @@ function loadEntriesFromFileInternal(filePath, options, onEntry) {
|
|
|
367
368
|
}
|
|
368
369
|
/** Exported for testing */
|
|
369
370
|
export function loadEntriesFromFile(filePath, options) {
|
|
370
|
-
|
|
371
|
+
const entries = loadEntriesFromFileInternal(filePath, options).entries;
|
|
372
|
+
validateLoadedLifecycleEntries(entries);
|
|
373
|
+
return entries;
|
|
371
374
|
}
|
|
372
375
|
const ENTRY_ID_PREFIX_BYTES = 64 * 1024;
|
|
373
376
|
const COMPACTED_PAYLOAD_RELEASE_MIN_CHARS = 16 * 1024;
|
|
@@ -854,6 +857,7 @@ export class SessionManager {
|
|
|
854
857
|
this.inheritedSessionIds = collectParentSessionIds(header?.parentSession, dirname(this.sessionFile));
|
|
855
858
|
this._ensureEntryFileLocations(this.coldPayloadEntryIds);
|
|
856
859
|
const migrated = migrateToCurrentVersion(this.fileEntries);
|
|
860
|
+
validateLoadedLifecycleEntries(this.fileEntries);
|
|
857
861
|
// Validate and index the complete parent graph before a migration rewrite can
|
|
858
862
|
// mutate the source file. Malformed cycles must fail synchronously on cold open.
|
|
859
863
|
this._buildIndex();
|
|
@@ -895,6 +899,7 @@ export class SessionManager {
|
|
|
895
899
|
this.labelsById.clear();
|
|
896
900
|
this.labelTimestampsById.clear();
|
|
897
901
|
this.leafId = null;
|
|
902
|
+
this.lifecycleActiveCache = undefined;
|
|
898
903
|
this.flushed = false;
|
|
899
904
|
this.persistenceStateUncertain = false;
|
|
900
905
|
this._invalidateSessionContextCache();
|
|
@@ -967,6 +972,7 @@ export class SessionManager {
|
|
|
967
972
|
this.labelsById = labelsById;
|
|
968
973
|
this.labelTimestampsById = labelTimestampsById;
|
|
969
974
|
this.leafId = leafId;
|
|
975
|
+
this.lifecycleActiveCache = undefined;
|
|
970
976
|
for (const id of this.coldPayloadEntryIds) {
|
|
971
977
|
if (!byId.has(id))
|
|
972
978
|
this.coldPayloadEntryIds.delete(id);
|
|
@@ -988,7 +994,7 @@ export class SessionManager {
|
|
|
988
994
|
const fd = openSync(tempFile, "wx");
|
|
989
995
|
try {
|
|
990
996
|
for (const entry of this.fileEntries) {
|
|
991
|
-
writeFileSync(fd, `${JSON.stringify(entry)}\n`);
|
|
997
|
+
writeFileSync(fd, `${entry.type === "session" ? JSON.stringify(entry) : encodeSessionEntry(entry)}\n`);
|
|
992
998
|
}
|
|
993
999
|
}
|
|
994
1000
|
finally {
|
|
@@ -1185,56 +1191,159 @@ export class SessionManager {
|
|
|
1185
1191
|
this._releaseMessageProperty(release.entry, property);
|
|
1186
1192
|
}
|
|
1187
1193
|
}
|
|
1188
|
-
|
|
1194
|
+
_persistEntries(entries, encodedEntries) {
|
|
1189
1195
|
if (!this.persist || !this.sessionFile)
|
|
1190
1196
|
return;
|
|
1197
|
+
if (entries.length !== encodedEntries.length) {
|
|
1198
|
+
throw new Error("Session persistence encoding count does not match the entry batch.");
|
|
1199
|
+
}
|
|
1191
1200
|
if (this.persistenceStateUncertain) {
|
|
1192
1201
|
throw new Error("Session persistence state is uncertain after a failed write; reopen the session file or start a new session before appending.");
|
|
1193
1202
|
}
|
|
1194
1203
|
try {
|
|
1195
|
-
const hasAssistant = (entry.type === "message" && entry.message.role === "assistant")
|
|
1196
|
-
|
|
1197
|
-
if (!hasAssistant) {
|
|
1198
|
-
if (this.flushed) {
|
|
1204
|
+
const hasAssistant = this.fileEntries.some((candidate) => candidate.type === "message" && candidate.message.role === "assistant") || entries.some((entry) => entry.type === "message" && entry.message.role === "assistant");
|
|
1205
|
+
const forceFlush = entries.some((entry) => isSessionLifecycleEntry(entry));
|
|
1206
|
+
if (!hasAssistant && !forceFlush) {
|
|
1207
|
+
if (this.flushed && encodedEntries.length > 0) {
|
|
1199
1208
|
this._ensureSessionFileParent(this.sessionFile);
|
|
1200
|
-
appendFileSync(this.sessionFile, `${
|
|
1209
|
+
appendFileSync(this.sessionFile, `${encodedEntries.join("\n")}\n`);
|
|
1201
1210
|
}
|
|
1202
1211
|
return;
|
|
1203
1212
|
}
|
|
1213
|
+
const encodedPayload = `${encodedEntries.join("\n")}\n`;
|
|
1204
1214
|
if (!this.flushed) {
|
|
1205
1215
|
this._ensureSessionFileParent(this.sessionFile);
|
|
1206
1216
|
const fd = openSync(this.sessionFile, "wx");
|
|
1207
1217
|
try {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
writeFileSync(fd, `${JSON.stringify(entry)}\n`);
|
|
1218
|
+
const prefix = this.fileEntries.map((candidate) => candidate.type === "session" ? JSON.stringify(candidate) : encodeSessionEntry(candidate));
|
|
1219
|
+
writeFileSync(fd, `${prefix.concat(encodedEntries).join("\n")}\n`);
|
|
1212
1220
|
}
|
|
1213
1221
|
finally {
|
|
1214
1222
|
closeSync(fd);
|
|
1215
1223
|
}
|
|
1216
1224
|
this.flushed = true;
|
|
1217
1225
|
}
|
|
1218
|
-
else {
|
|
1226
|
+
else if (encodedEntries.length > 0) {
|
|
1219
1227
|
this._ensureSessionFileParent(this.sessionFile);
|
|
1220
|
-
appendFileSync(this.sessionFile,
|
|
1228
|
+
appendFileSync(this.sessionFile, encodedPayload);
|
|
1221
1229
|
}
|
|
1222
1230
|
}
|
|
1223
1231
|
catch (error) {
|
|
1224
1232
|
// append/write/close failures may leave a partial JSONL suffix. Do not publish the
|
|
1225
|
-
//
|
|
1233
|
+
// entries in memory, and fence every later append until an explicit reload owns the
|
|
1226
1234
|
// surviving canonical prefix.
|
|
1227
1235
|
this.persistenceStateUncertain = true;
|
|
1228
1236
|
throw error;
|
|
1229
1237
|
}
|
|
1230
1238
|
}
|
|
1239
|
+
_persist(entry) {
|
|
1240
|
+
const encodedEntry = encodeSessionEntry(entry);
|
|
1241
|
+
this._persistEntries([entry], [encodedEntry]);
|
|
1242
|
+
}
|
|
1243
|
+
_appendEntries(entries, preencodedEntries) {
|
|
1244
|
+
const encodedEntries = preencodedEntries ??
|
|
1245
|
+
(this.persist && this.sessionFile
|
|
1246
|
+
? entries.map((entry) => {
|
|
1247
|
+
if (isSessionLifecycleEntry(entry))
|
|
1248
|
+
validateSessionLifecycleEntry(entry);
|
|
1249
|
+
return encodeSessionEntry(entry);
|
|
1250
|
+
})
|
|
1251
|
+
: []);
|
|
1252
|
+
if (preencodedEntries && preencodedEntries.length !== entries.length) {
|
|
1253
|
+
throw new Error("Session append encoding count does not match the entry batch.");
|
|
1254
|
+
}
|
|
1255
|
+
if (encodedEntries.length > 0) {
|
|
1256
|
+
this._persistEntries(entries, encodedEntries);
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
for (const entry of entries) {
|
|
1260
|
+
if (isSessionLifecycleEntry(entry))
|
|
1261
|
+
encodeSessionEntry(entry);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
for (const entry of entries) {
|
|
1265
|
+
this.fileEntries.push(entry);
|
|
1266
|
+
this.entries.push(entry);
|
|
1267
|
+
this.byId.set(entry.id, entry);
|
|
1268
|
+
this.leafId = entry.id;
|
|
1269
|
+
this._advanceLifecycleActiveCache(entry);
|
|
1270
|
+
this._advanceSessionContextCache(entry);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1231
1273
|
_appendEntry(entry) {
|
|
1232
|
-
this.
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
this.
|
|
1236
|
-
|
|
1237
|
-
|
|
1274
|
+
this._appendEntries([entry]);
|
|
1275
|
+
}
|
|
1276
|
+
_getLifecycleActiveCache() {
|
|
1277
|
+
if (this.lifecycleActiveCache?.leafId === this.leafId)
|
|
1278
|
+
return this.lifecycleActiveCache;
|
|
1279
|
+
const branch = [];
|
|
1280
|
+
const leaf = this.leafId === null ? undefined : this.byId.get(this.leafId);
|
|
1281
|
+
visitSessionAncestry(leaf, this.byId, (entry) => {
|
|
1282
|
+
branch.push(entry);
|
|
1283
|
+
});
|
|
1284
|
+
branch.reverse();
|
|
1285
|
+
const cache = {
|
|
1286
|
+
leafId: this.leafId,
|
|
1287
|
+
positions: new Map(),
|
|
1288
|
+
entryTypes: new Map(),
|
|
1289
|
+
requestIds: new Set(),
|
|
1290
|
+
startsByIdentity: new Set(),
|
|
1291
|
+
terminalsByIdentity: new Set(),
|
|
1292
|
+
compactionStarts: new Map(),
|
|
1293
|
+
compactionEnds: new Set(),
|
|
1294
|
+
assistantToolsByIdentity: new Map(),
|
|
1295
|
+
};
|
|
1296
|
+
for (let position = 0; position < branch.length; position += 1) {
|
|
1297
|
+
this._applyLifecycleCacheEntry(cache, branch[position], position);
|
|
1298
|
+
}
|
|
1299
|
+
this.lifecycleActiveCache = cache;
|
|
1300
|
+
return cache;
|
|
1301
|
+
}
|
|
1302
|
+
/** Apply one canonical branch entry to both rebuilt and incrementally advanced lifecycle caches. */
|
|
1303
|
+
_applyLifecycleCacheEntry(cache, entry, position) {
|
|
1304
|
+
cache.positions.set(entry.id, position);
|
|
1305
|
+
cache.entryTypes.set(entry.id, entry.type);
|
|
1306
|
+
if (entry.type === "request_snapshot") {
|
|
1307
|
+
cache.currentRequestId = entry.requestId;
|
|
1308
|
+
cache.requestIds.add(entry.requestId);
|
|
1309
|
+
}
|
|
1310
|
+
else if (entry.type === "foreground_tool_start") {
|
|
1311
|
+
cache.startsByIdentity.add(sessionLifecycleToolIdentityKey(entry.requestId, entry.assistantMessageEntryId, entry.callId));
|
|
1312
|
+
}
|
|
1313
|
+
else if (entry.type === "foreground_tool_terminal") {
|
|
1314
|
+
cache.terminalsByIdentity.add(sessionLifecycleToolIdentityKey(entry.requestId, entry.assistantMessageEntryId, entry.callId));
|
|
1315
|
+
}
|
|
1316
|
+
else if (entry.type === "compaction_start" && !cache.compactionStarts.has(entry.compactionId)) {
|
|
1317
|
+
cache.compactionStarts.set(entry.compactionId, entry);
|
|
1318
|
+
}
|
|
1319
|
+
else if (entry.type === "compaction_end") {
|
|
1320
|
+
cache.compactionEnds.add(entry.compactionId);
|
|
1321
|
+
}
|
|
1322
|
+
else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
1323
|
+
for (const block of entry.message.content) {
|
|
1324
|
+
if (block.type !== "toolCall")
|
|
1325
|
+
continue;
|
|
1326
|
+
const key = assistantToolIdentityKey(entry.id, block.id);
|
|
1327
|
+
if (cache.assistantToolsByIdentity.has(key)) {
|
|
1328
|
+
cache.assistantToolsByIdentity.set(key, "duplicate");
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
cache.assistantToolsByIdentity.set(key, {
|
|
1332
|
+
requestId: cache.currentRequestId,
|
|
1333
|
+
toolName: block.name,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
cache.leafId = entry.id;
|
|
1339
|
+
}
|
|
1340
|
+
_advanceLifecycleActiveCache(entry) {
|
|
1341
|
+
const cache = this.lifecycleActiveCache;
|
|
1342
|
+
if (!cache || cache.leafId !== entry.parentId) {
|
|
1343
|
+
this.lifecycleActiveCache = undefined;
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
this._applyLifecycleCacheEntry(cache, entry, cache.positions.size);
|
|
1238
1347
|
}
|
|
1239
1348
|
_invalidateSessionContextCache() {
|
|
1240
1349
|
this.sessionContextCache = undefined;
|
|
@@ -1287,6 +1396,63 @@ export class SessionManager {
|
|
|
1287
1396
|
this._appendEntry(entry);
|
|
1288
1397
|
return entry.id;
|
|
1289
1398
|
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Append a bounded mixed message/custom-message batch as one durable publication.
|
|
1401
|
+
* Every item is encoded before the session tree or persistence state is mutated.
|
|
1402
|
+
*/
|
|
1403
|
+
appendMessageBatch(batch) {
|
|
1404
|
+
if (batch.length === 0)
|
|
1405
|
+
return [];
|
|
1406
|
+
if (batch.length > MAX_SESSION_MESSAGE_BATCH_ENTRIES) {
|
|
1407
|
+
throw new RangeError(`Session message batch cannot contain more than ${MAX_SESSION_MESSAGE_BATCH_ENTRIES} entries.`);
|
|
1408
|
+
}
|
|
1409
|
+
const entries = [];
|
|
1410
|
+
const ids = new Set(this.byId.keys());
|
|
1411
|
+
let parentId = this.leafId;
|
|
1412
|
+
for (const item of batch) {
|
|
1413
|
+
if (!item || typeof item !== "object") {
|
|
1414
|
+
throw new TypeError("Session message batch items must be objects.");
|
|
1415
|
+
}
|
|
1416
|
+
const id = generateId(ids);
|
|
1417
|
+
let entry;
|
|
1418
|
+
if (item.kind === "message") {
|
|
1419
|
+
if (!item.message || typeof item.message !== "object") {
|
|
1420
|
+
throw new TypeError("Session message batch message items require a message object.");
|
|
1421
|
+
}
|
|
1422
|
+
entry = {
|
|
1423
|
+
type: "message",
|
|
1424
|
+
id,
|
|
1425
|
+
parentId,
|
|
1426
|
+
timestamp: new Date().toISOString(),
|
|
1427
|
+
message: item.message,
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
else if (item.kind === "custom") {
|
|
1431
|
+
if (!item.message || typeof item.message !== "object") {
|
|
1432
|
+
throw new TypeError("Session message batch custom items require a message object.");
|
|
1433
|
+
}
|
|
1434
|
+
entry = {
|
|
1435
|
+
type: "custom_message",
|
|
1436
|
+
customType: item.message.customType,
|
|
1437
|
+
content: item.message.content,
|
|
1438
|
+
display: item.message.display,
|
|
1439
|
+
details: item.message.details,
|
|
1440
|
+
id,
|
|
1441
|
+
parentId,
|
|
1442
|
+
timestamp: new Date().toISOString(),
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
else {
|
|
1446
|
+
throw new TypeError("Session message batch items must use kind message or custom.");
|
|
1447
|
+
}
|
|
1448
|
+
entries.push(entry);
|
|
1449
|
+
ids.add(id);
|
|
1450
|
+
parentId = id;
|
|
1451
|
+
}
|
|
1452
|
+
const encodedEntries = entries.map((entry) => encodeSessionEntry(entry));
|
|
1453
|
+
this._appendEntries(entries, encodedEntries);
|
|
1454
|
+
return entries.map((entry) => entry.id);
|
|
1455
|
+
}
|
|
1290
1456
|
/** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
|
|
1291
1457
|
appendThinkingLevelChange(thinkingLevel) {
|
|
1292
1458
|
const entry = {
|
|
@@ -1332,7 +1498,220 @@ export class SessionManager {
|
|
|
1332
1498
|
this._releaseCompactedMessagePayloads(firstKeptEntryId, compactionParentId, retention, entry.id);
|
|
1333
1499
|
return entry.id;
|
|
1334
1500
|
}
|
|
1335
|
-
|
|
1501
|
+
_findAssistantToolCall(assistantMessageEntryId, callId) {
|
|
1502
|
+
const cache = this._getLifecycleActiveCache();
|
|
1503
|
+
if (!cache.positions.has(assistantMessageEntryId)) {
|
|
1504
|
+
throw new Error(`Assistant message is outside the active branch: ${assistantMessageEntryId}.`);
|
|
1505
|
+
}
|
|
1506
|
+
const assistant = this.byId.get(assistantMessageEntryId);
|
|
1507
|
+
if (!assistant || assistant.type !== "message") {
|
|
1508
|
+
throw new Error(`Foreground tool lifecycle identity requires an assistant message: ${assistantMessageEntryId}.`);
|
|
1509
|
+
}
|
|
1510
|
+
const reference = cache.assistantToolsByIdentity.get(assistantToolIdentityKey(assistantMessageEntryId, callId));
|
|
1511
|
+
if (reference === "duplicate") {
|
|
1512
|
+
throw new Error(`Foreground tool lifecycle identity must resolve to exactly one assistant tool call: ${assistantMessageEntryId}/${callId}.`);
|
|
1513
|
+
}
|
|
1514
|
+
if (!reference) {
|
|
1515
|
+
throw new Error(`Foreground tool lifecycle identity must resolve to exactly one assistant tool call: ${assistantMessageEntryId}/${callId}.`);
|
|
1516
|
+
}
|
|
1517
|
+
return reference;
|
|
1518
|
+
}
|
|
1519
|
+
_validateRequestSnapshot(entry) {
|
|
1520
|
+
const cache = this._getLifecycleActiveCache();
|
|
1521
|
+
if (cache.requestIds.has(entry.requestId)) {
|
|
1522
|
+
throw new Error(`Duplicate request snapshot identity: ${entry.requestId}.`);
|
|
1523
|
+
}
|
|
1524
|
+
for (const messageEntryId of entry.messageEntryIds) {
|
|
1525
|
+
const messageEntry = this.byId.get(messageEntryId);
|
|
1526
|
+
if (!messageEntry || messageEntry.type !== "message") {
|
|
1527
|
+
throw new Error(`Request snapshot references a non-message entry: ${messageEntryId}.`);
|
|
1528
|
+
}
|
|
1529
|
+
if (!cache.positions.has(messageEntryId)) {
|
|
1530
|
+
throw new Error(`Request snapshot message is outside the active branch: ${messageEntryId}.`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
_validateForegroundToolStartBatch(entries) {
|
|
1535
|
+
const cache = this._getLifecycleActiveCache();
|
|
1536
|
+
const seen = new Set();
|
|
1537
|
+
for (const entry of entries) {
|
|
1538
|
+
validateSessionLifecycleEntry(entry);
|
|
1539
|
+
const call = this._findAssistantToolCall(entry.assistantMessageEntryId, entry.callId);
|
|
1540
|
+
if (call.requestId !== undefined && call.requestId !== entry.requestId) {
|
|
1541
|
+
throw new Error(`Foreground tool start request does not match its assistant request: ${entry.id}.`);
|
|
1542
|
+
}
|
|
1543
|
+
const key = sessionLifecycleToolIdentityKey(entry.requestId, entry.assistantMessageEntryId, entry.callId);
|
|
1544
|
+
if (seen.has(key) || cache.startsByIdentity.has(key)) {
|
|
1545
|
+
throw new Error(`Duplicate foreground tool start identity: ${entry.requestId}/${entry.assistantMessageEntryId}/${entry.callId}.`);
|
|
1546
|
+
}
|
|
1547
|
+
seen.add(key);
|
|
1548
|
+
if (call.toolName !== entry.toolName) {
|
|
1549
|
+
throw new Error(`Foreground tool start name does not match its assistant call: ${entry.id}.`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
_validateForegroundToolTerminal(entry) {
|
|
1554
|
+
validateSessionLifecycleEntry(entry);
|
|
1555
|
+
const call = this._findAssistantToolCall(entry.assistantMessageEntryId, entry.callId);
|
|
1556
|
+
if (call.requestId !== undefined && call.requestId !== entry.requestId) {
|
|
1557
|
+
throw new Error(`Foreground tool terminal request does not match its assistant request: ${entry.id}.`);
|
|
1558
|
+
}
|
|
1559
|
+
if (call.toolName !== entry.toolName) {
|
|
1560
|
+
throw new Error(`Foreground tool terminal name does not match its assistant call: ${entry.id}.`);
|
|
1561
|
+
}
|
|
1562
|
+
const cache = this._getLifecycleActiveCache();
|
|
1563
|
+
const identityKey = sessionLifecycleToolIdentityKey(entry.requestId, entry.assistantMessageEntryId, entry.callId);
|
|
1564
|
+
if (cache.terminalsByIdentity.has(identityKey)) {
|
|
1565
|
+
throw new Error(`Duplicate foreground tool terminal identity: ${entry.id}.`);
|
|
1566
|
+
}
|
|
1567
|
+
if (!cache.startsByIdentity.has(identityKey)) {
|
|
1568
|
+
throw new Error(`Foreground tool terminal has no matching durable start: ${entry.id}.`);
|
|
1569
|
+
}
|
|
1570
|
+
const resultEntry = this.byId.get(entry.resultMessageEntryId);
|
|
1571
|
+
if (!resultEntry ||
|
|
1572
|
+
!cache.positions.has(entry.resultMessageEntryId) ||
|
|
1573
|
+
resultEntry.type !== "message" ||
|
|
1574
|
+
resultEntry.message.role !== "toolResult") {
|
|
1575
|
+
throw new Error(`Foreground tool terminal references a non-result entry: ${entry.resultMessageEntryId}.`);
|
|
1576
|
+
}
|
|
1577
|
+
const resultErrorKind = resultEntry.message.isError
|
|
1578
|
+
? (resultEntry.message.errorKind ?? "tool_failure")
|
|
1579
|
+
: undefined;
|
|
1580
|
+
if (resultEntry.message.toolCallId !== entry.callId ||
|
|
1581
|
+
resultEntry.message.toolName !== entry.toolName ||
|
|
1582
|
+
entry.outcome !== (resultEntry.message.isError ? "error" : "success") ||
|
|
1583
|
+
(resultEntry.message.errorKind !== undefined && !resultEntry.message.isError) ||
|
|
1584
|
+
entry.errorKind !== resultErrorKind) {
|
|
1585
|
+
throw new Error(`Foreground tool terminal metadata contradicts its canonical result: ${entry.id}.`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
_validateCompactionStart(entry) {
|
|
1589
|
+
validateSessionLifecycleEntry(entry);
|
|
1590
|
+
const cache = this._getLifecycleActiveCache();
|
|
1591
|
+
if (cache.compactionStarts.has(entry.compactionId)) {
|
|
1592
|
+
throw new Error(`Duplicate compaction start identity: ${entry.compactionId}.`);
|
|
1593
|
+
}
|
|
1594
|
+
const firstKeptPosition = cache.positions.get(entry.firstKeptEntryId);
|
|
1595
|
+
const leafPosition = this.leafId === null ? undefined : cache.positions.get(this.leafId);
|
|
1596
|
+
if (firstKeptPosition === undefined || leafPosition === undefined || firstKeptPosition > leafPosition) {
|
|
1597
|
+
throw new Error(`Compaction start firstKeptEntryId is not an earlier active-branch entry: ${entry.firstKeptEntryId}.`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
_validateCompactionEnd(entry) {
|
|
1601
|
+
validateSessionLifecycleEntry(entry);
|
|
1602
|
+
const cache = this._getLifecycleActiveCache();
|
|
1603
|
+
const start = cache.compactionStarts.get(entry.compactionId);
|
|
1604
|
+
if (!start)
|
|
1605
|
+
throw new Error(`Compaction end has no matching start: ${entry.compactionId}.`);
|
|
1606
|
+
if (cache.compactionEnds.has(entry.compactionId)) {
|
|
1607
|
+
throw new Error(`Duplicate compaction end identity: ${entry.compactionId}.`);
|
|
1608
|
+
}
|
|
1609
|
+
if (entry.outcome !== "success")
|
|
1610
|
+
return;
|
|
1611
|
+
const compactionPosition = entry.compactionEntryId ? cache.positions.get(entry.compactionEntryId) : undefined;
|
|
1612
|
+
const compactionEntry = entry.compactionEntryId ? this.byId.get(entry.compactionEntryId) : undefined;
|
|
1613
|
+
const finalFirstKeptPosition = compactionEntry?.type === "compaction" ? cache.positions.get(compactionEntry.firstKeptEntryId) : undefined;
|
|
1614
|
+
if (!entry.compactionEntryId ||
|
|
1615
|
+
cache.entryTypes.get(entry.compactionEntryId) !== "compaction" ||
|
|
1616
|
+
compactionPosition === undefined ||
|
|
1617
|
+
compactionPosition <= (cache.positions.get(start.id) ?? -1) ||
|
|
1618
|
+
compactionEntry?.type !== "compaction" ||
|
|
1619
|
+
finalFirstKeptPosition === undefined ||
|
|
1620
|
+
finalFirstKeptPosition >= compactionPosition) {
|
|
1621
|
+
throw new Error(`Compaction end references an invalid summary entry: ${entry.compactionEntryId ?? "missing"}.`);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
/** Append a bounded provider request snapshot before its transport starts. */
|
|
1625
|
+
appendRequestSnapshot(snapshot) {
|
|
1626
|
+
const entry = {
|
|
1627
|
+
type: "request_snapshot",
|
|
1628
|
+
id: generateId(this.byId),
|
|
1629
|
+
parentId: this.leafId,
|
|
1630
|
+
timestamp: new Date().toISOString(),
|
|
1631
|
+
...snapshot,
|
|
1632
|
+
messageEntryIds: [...snapshot.messageEntryIds],
|
|
1633
|
+
};
|
|
1634
|
+
validateSessionLifecycleEntry(entry);
|
|
1635
|
+
this._validateRequestSnapshot(entry);
|
|
1636
|
+
this._appendEntry(entry);
|
|
1637
|
+
return entry.id;
|
|
1638
|
+
}
|
|
1639
|
+
/** Append a bounded batch of foreground tool execution start markers in one durable publication. */
|
|
1640
|
+
appendForegroundToolStarts(starts) {
|
|
1641
|
+
if (starts.length === 0)
|
|
1642
|
+
return [];
|
|
1643
|
+
const entries = [];
|
|
1644
|
+
let parentId = this.leafId;
|
|
1645
|
+
const ids = new Set(this.byId.keys());
|
|
1646
|
+
for (const start of starts) {
|
|
1647
|
+
const entry = {
|
|
1648
|
+
type: "foreground_tool_start",
|
|
1649
|
+
id: generateId(ids),
|
|
1650
|
+
parentId,
|
|
1651
|
+
timestamp: new Date().toISOString(),
|
|
1652
|
+
...start,
|
|
1653
|
+
};
|
|
1654
|
+
entries.push(entry);
|
|
1655
|
+
ids.add(entry.id);
|
|
1656
|
+
parentId = entry.id;
|
|
1657
|
+
}
|
|
1658
|
+
this._validateForegroundToolStartBatch(entries);
|
|
1659
|
+
this._appendEntries(entries);
|
|
1660
|
+
return entries.map((entry) => entry.id);
|
|
1661
|
+
}
|
|
1662
|
+
/** Append one bounded foreground tool execution start marker. */
|
|
1663
|
+
appendForegroundToolStart(requestId, assistantMessageEntryId, callId, toolName) {
|
|
1664
|
+
return this.appendForegroundToolStarts([{ requestId, assistantMessageEntryId, callId, toolName }])[0];
|
|
1665
|
+
}
|
|
1666
|
+
/** Append a bounded foreground tool terminal marker. */
|
|
1667
|
+
appendForegroundToolTerminal(requestId, assistantMessageEntryId, callId, toolName, outcome, metadata) {
|
|
1668
|
+
const entry = {
|
|
1669
|
+
type: "foreground_tool_terminal",
|
|
1670
|
+
id: generateId(this.byId),
|
|
1671
|
+
parentId: this.leafId,
|
|
1672
|
+
timestamp: new Date().toISOString(),
|
|
1673
|
+
requestId,
|
|
1674
|
+
assistantMessageEntryId,
|
|
1675
|
+
callId,
|
|
1676
|
+
toolName,
|
|
1677
|
+
outcome,
|
|
1678
|
+
...metadata,
|
|
1679
|
+
};
|
|
1680
|
+
this._validateForegroundToolTerminal(entry);
|
|
1681
|
+
this._appendEntry(entry);
|
|
1682
|
+
return entry.id;
|
|
1683
|
+
}
|
|
1684
|
+
/** Append a bounded compaction transaction start marker. */
|
|
1685
|
+
appendCompactionStart(compactionId, firstKeptEntryId, tokensBefore) {
|
|
1686
|
+
const entry = {
|
|
1687
|
+
type: "compaction_start",
|
|
1688
|
+
id: generateId(this.byId),
|
|
1689
|
+
parentId: this.leafId,
|
|
1690
|
+
timestamp: new Date().toISOString(),
|
|
1691
|
+
compactionId,
|
|
1692
|
+
firstKeptEntryId,
|
|
1693
|
+
tokensBefore,
|
|
1694
|
+
};
|
|
1695
|
+
this._validateCompactionStart(entry);
|
|
1696
|
+
this._appendEntry(entry);
|
|
1697
|
+
return entry.id;
|
|
1698
|
+
}
|
|
1699
|
+
/** Append a bounded compaction transaction end marker. */
|
|
1700
|
+
appendCompactionEnd(compactionId, outcome, metadata = {}) {
|
|
1701
|
+
const entry = {
|
|
1702
|
+
type: "compaction_end",
|
|
1703
|
+
id: generateId(this.byId),
|
|
1704
|
+
parentId: this.leafId,
|
|
1705
|
+
timestamp: new Date().toISOString(),
|
|
1706
|
+
compactionId,
|
|
1707
|
+
outcome,
|
|
1708
|
+
...metadata,
|
|
1709
|
+
};
|
|
1710
|
+
this._validateCompactionEnd(entry);
|
|
1711
|
+
this._appendEntry(entry);
|
|
1712
|
+
return entry.id;
|
|
1713
|
+
}
|
|
1714
|
+
/** Append a custom entry (for extensions) as child of current leaf, then advance leaf. */
|
|
1336
1715
|
appendCustomEntry(customType, data) {
|
|
1337
1716
|
const entry = {
|
|
1338
1717
|
type: "custom",
|
|
@@ -1370,6 +1749,18 @@ export class SessionManager {
|
|
|
1370
1749
|
}
|
|
1371
1750
|
return undefined;
|
|
1372
1751
|
}
|
|
1752
|
+
/** Return lifecycle records on one branch without exposing lifecycle data to model context. */
|
|
1753
|
+
getSessionLifecycleIndex(fromId) {
|
|
1754
|
+
return indexSessionLifecycle(this.entries, fromId === undefined ? this.leafId : fromId);
|
|
1755
|
+
}
|
|
1756
|
+
/** Inspect lifecycle balance on one branch without mutating or appending repair records. */
|
|
1757
|
+
inspectSessionLifecycle(fromId) {
|
|
1758
|
+
return inspectSessionLifecycle(this.entries, fromId === undefined ? this.leafId : fromId);
|
|
1759
|
+
}
|
|
1760
|
+
/** Plan deterministic lifecycle repair for one branch without mutating the session. */
|
|
1761
|
+
planSessionLifecycleRepair(fromId) {
|
|
1762
|
+
return planSessionLifecycleRepair(this.entries, fromId === undefined ? this.leafId : fromId);
|
|
1763
|
+
}
|
|
1373
1764
|
/**
|
|
1374
1765
|
* Append a custom message entry (for extensions) that participates in LLM context.
|
|
1375
1766
|
* @param customType Extension identifier for filtering on reload
|
|
@@ -1742,6 +2133,7 @@ export class SessionManager {
|
|
|
1742
2133
|
}
|
|
1743
2134
|
this._invalidateSessionContextCache();
|
|
1744
2135
|
this.leafId = branchFromId;
|
|
2136
|
+
this.lifecycleActiveCache = undefined;
|
|
1745
2137
|
}
|
|
1746
2138
|
/**
|
|
1747
2139
|
* Reset the leaf pointer to null (before any entries).
|
|
@@ -1751,6 +2143,7 @@ export class SessionManager {
|
|
|
1751
2143
|
resetLeaf() {
|
|
1752
2144
|
this._invalidateSessionContextCache();
|
|
1753
2145
|
this.leafId = null;
|
|
2146
|
+
this.lifecycleActiveCache = undefined;
|
|
1754
2147
|
}
|
|
1755
2148
|
/**
|
|
1756
2149
|
* Start a new branch with a summary of the abandoned path.
|
|
@@ -1763,6 +2156,7 @@ export class SessionManager {
|
|
|
1763
2156
|
}
|
|
1764
2157
|
this._invalidateSessionContextCache();
|
|
1765
2158
|
this.leafId = branchFromId;
|
|
2159
|
+
this.lifecycleActiveCache = undefined;
|
|
1766
2160
|
const entry = {
|
|
1767
2161
|
type: "branch_summary",
|
|
1768
2162
|
id: generateId(this.byId),
|
|
@@ -1869,6 +2263,7 @@ export class SessionManager {
|
|
|
1869
2263
|
this.labelsById = branched.labelsById;
|
|
1870
2264
|
this.labelTimestampsById = branched.labelTimestampsById;
|
|
1871
2265
|
this.leafId = branched.leafId;
|
|
2266
|
+
this.lifecycleActiveCache = undefined;
|
|
1872
2267
|
this.persistenceStateUncertain = branched.persistenceStateUncertain;
|
|
1873
2268
|
this.inheritedSessionIds = new Set(branched.inheritedSessionIds);
|
|
1874
2269
|
this.coldPayloadEntryIds.clear();
|