@ouro.bot/cli 0.1.0-alpha.813 → 0.1.0-alpha.815
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/changelog.json +14 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +16 -12
- package/dist/heart/session-events.js +125 -61
- package/dist/heart/session-redaction-repair.js +12 -3
- package/dist/heart/session-stats.js +3 -2
- package/dist/mailbox-ui/assets/{index-J8zDwHmE.js → index-Cyv4mw3Y.js} +1 -1
- package/dist/mailbox-ui/index.html +1 -1
- package/dist/mind/context.js +31 -1
- package/dist/senses/sanctuary-interactive-control.js +160 -56
- package/dist/senses/shared-turn.js +18 -5
- package/dist/senses/telegram-effect-adapter.js +10 -2
- package/dist/senses/telegram.js +24 -21
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.815",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Protect Sanctuary's resident control socket from transient Telegram app disposal, and safely retry interrupted startup and cleanup without stealing another listener.",
|
|
8
|
+
"Preserve the order of overlapping startup and shutdown requests, and check socket modification time without mistaking permission changes for replacement."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.814",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Preserve native session history and delivery references while keeping model context and active outputs bounded.",
|
|
15
|
+
"Keep precommitted ingress validation strict when loading effective projections."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.813",
|
|
6
20
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.815</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
package/dist/heart/core.js
CHANGED
|
@@ -1336,14 +1336,16 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1336
1336
|
contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
|
|
1337
1337
|
};
|
|
1338
1338
|
const canonicalBudget = (0, prompt_budget_1.applyPromptBudget)({ ...budgetOptions, messages });
|
|
1339
|
-
|
|
1340
|
-
messages.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1339
|
+
const currentUserMessage = budgetOptions.requiredPromptEvidence?.currentUserMessage
|
|
1340
|
+
?? canonicalBudget.messages.findLast((message) => message.role === "user");
|
|
1341
|
+
// Keep retry controls attached to the genuine request, not to canonical history.
|
|
1342
|
+
let attemptMessages = rejectedAttempt.length > 0
|
|
1343
|
+
? [...(0, prompt_budget_1.applyPromptBudget)({
|
|
1344
|
+
...budgetOptions,
|
|
1345
|
+
requiredPromptEvidence: budgetOptions.requiredPromptEvidence ?? (currentUserMessage ? { currentUserMessage } : undefined),
|
|
1346
|
+
messages: [...canonicalBudget.messages, ...rejectedAttempt, ...nextAttemptControls],
|
|
1347
|
+
}).messages]
|
|
1348
|
+
: [...canonicalBudget.messages];
|
|
1347
1349
|
if (rejectedAttempt.length > 0 || canonicalBudget.status !== "within_budget")
|
|
1348
1350
|
providerRuntime.resetTurnState(attemptMessages);
|
|
1349
1351
|
const turnCallbackBufferRef = { current: null };
|
|
@@ -1384,20 +1386,22 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1384
1386
|
throw error;
|
|
1385
1387
|
if (isContextOverflow(error) && !overflowRetried) {
|
|
1386
1388
|
overflowRetried = true;
|
|
1387
|
-
const
|
|
1389
|
+
const protectedStart = currentUserMessage ? Math.max(0, attemptMessages.indexOf(currentUserMessage)) : 0;
|
|
1390
|
+
const currentAttempt = attemptMessages.slice(protectedStart);
|
|
1391
|
+
const overflowMessages = attemptMessages.slice(0, protectedStart).map((message) => message.role === "assistant" ? { ...message } : message);
|
|
1388
1392
|
stripLastToolCalls(overflowMessages);
|
|
1393
|
+
overflowMessages.push(...currentAttempt);
|
|
1389
1394
|
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
1390
1395
|
const trimmed = (0, context_1.trimMessages)(overflowMessages, maxTokens, contextMargin, maxTokens * 2);
|
|
1391
1396
|
const requiredEvidence = options?.requiredPromptEvidence;
|
|
1392
1397
|
const requiredMessages = new Set([
|
|
1393
1398
|
...(requiredEvidence?.verifiedPredecessorMessage ? [requiredEvidence.verifiedPredecessorMessage] : []),
|
|
1394
|
-
...
|
|
1399
|
+
...currentAttempt,
|
|
1395
1400
|
]);
|
|
1396
1401
|
const trimmedMessages = new Set(trimmed);
|
|
1397
|
-
|
|
1402
|
+
attemptMessages = requiredMessages.size === 0
|
|
1398
1403
|
? trimmed
|
|
1399
1404
|
: overflowMessages.filter((message) => trimmedMessages.has(message) || requiredMessages.has(message));
|
|
1400
|
-
messages.splice(0, messages.length, ...overflowRetryMessages);
|
|
1401
1405
|
providerRuntime.resetTurnState(attemptMessages);
|
|
1402
1406
|
callbacks.onError(new Error("context trimmed, retrying..."), "transient");
|
|
1403
1407
|
return callProviderTurn();
|
|
@@ -239,6 +239,13 @@ function removeApprovalSuspensionCheckpoint(envelope, approvalId) {
|
|
|
239
239
|
});
|
|
240
240
|
return { ...envelope, approvalSuspensions };
|
|
241
241
|
}
|
|
242
|
+
// Native origins survive replay normalization, but never enter serialized provider or session data.
|
|
243
|
+
const messageSourceEventIds = Symbol("messageSourceEventIds");
|
|
244
|
+
function withMessageSourceEventIds(message, eventIds) {
|
|
245
|
+
if (eventIds)
|
|
246
|
+
Object.defineProperty(message, messageSourceEventIds, { value: eventIds });
|
|
247
|
+
return message;
|
|
248
|
+
}
|
|
242
249
|
exports.EVENT_CONTENT_MAX_CHARS = 256 * 1024;
|
|
243
250
|
function truncateLargeEventContent(content, maxChars) {
|
|
244
251
|
if (typeof content !== "string") {
|
|
@@ -383,6 +390,7 @@ function normalizeMessage(message) {
|
|
|
383
390
|
const record = message;
|
|
384
391
|
const role = normalizeRole(record.role);
|
|
385
392
|
const normalizedContent = sanitizeConversationContent(role, normalizeContent(record.content));
|
|
393
|
+
const sourceEventIds = message[messageSourceEventIds];
|
|
386
394
|
if (role === "assistant") {
|
|
387
395
|
return {
|
|
388
396
|
role,
|
|
@@ -391,6 +399,7 @@ function normalizeMessage(message) {
|
|
|
391
399
|
toolCallId: null,
|
|
392
400
|
toolCalls: normalizeToolCalls(record.tool_calls),
|
|
393
401
|
hadToolCallsField: Array.isArray(record.tool_calls),
|
|
402
|
+
sourceEventIds,
|
|
394
403
|
};
|
|
395
404
|
}
|
|
396
405
|
if (role === "tool") {
|
|
@@ -401,6 +410,7 @@ function normalizeMessage(message) {
|
|
|
401
410
|
toolCallId: typeof record.tool_call_id === "string" ? record.tool_call_id : null,
|
|
402
411
|
toolCalls: [],
|
|
403
412
|
hadToolCallsField: false,
|
|
413
|
+
sourceEventIds,
|
|
404
414
|
};
|
|
405
415
|
}
|
|
406
416
|
return {
|
|
@@ -410,6 +420,7 @@ function normalizeMessage(message) {
|
|
|
410
420
|
toolCallId: null,
|
|
411
421
|
toolCalls: [],
|
|
412
422
|
hadToolCallsField: false,
|
|
423
|
+
sourceEventIds,
|
|
413
424
|
};
|
|
414
425
|
}
|
|
415
426
|
function contentText(content) {
|
|
@@ -442,33 +453,38 @@ function toProviderMessage(message) {
|
|
|
442
453
|
},
|
|
443
454
|
}));
|
|
444
455
|
}
|
|
445
|
-
return assistant;
|
|
456
|
+
return withMessageSourceEventIds(assistant, message.sourceEventIds);
|
|
446
457
|
}
|
|
447
458
|
if (message.role === "tool") {
|
|
448
|
-
return {
|
|
459
|
+
return withMessageSourceEventIds({
|
|
449
460
|
role: "tool",
|
|
450
461
|
content: typeof message.content === "string" ? message.content : contentText(message.content),
|
|
451
462
|
tool_call_id: message.toolCallId ?? "",
|
|
452
|
-
};
|
|
463
|
+
}, message.sourceEventIds);
|
|
453
464
|
}
|
|
454
465
|
if (message.role === "system") {
|
|
455
|
-
return {
|
|
466
|
+
return withMessageSourceEventIds({
|
|
456
467
|
role: "system",
|
|
457
468
|
content: typeof message.content === "string" ? message.content : contentText(message.content),
|
|
458
469
|
...(message.name ? { name: message.name } : {}),
|
|
459
|
-
};
|
|
470
|
+
}, message.sourceEventIds);
|
|
460
471
|
}
|
|
461
|
-
return {
|
|
472
|
+
return withMessageSourceEventIds({
|
|
462
473
|
role: "user",
|
|
463
474
|
content: (typeof message.content === "string" || Array.isArray(message.content) ? message.content : ""),
|
|
464
475
|
...(message.name ? { name: message.name } : {}),
|
|
465
|
-
};
|
|
476
|
+
}, message.sourceEventIds);
|
|
477
|
+
}
|
|
478
|
+
function messageSourcesAgree(left, right) {
|
|
479
|
+
const leftIds = left[messageSourceEventIds];
|
|
480
|
+
const rightIds = right[messageSourceEventIds];
|
|
481
|
+
return !leftIds || !rightIds || leftIds.length === rightIds.length && leftIds.every((id, index) => id === rightIds[index]);
|
|
466
482
|
}
|
|
467
|
-
function messageFingerprint(message) {
|
|
483
|
+
function messageFingerprint(message, captureContent = false) {
|
|
468
484
|
const normalized = normalizeMessage(message);
|
|
469
485
|
return JSON.stringify({
|
|
470
486
|
role: normalized.role,
|
|
471
|
-
content: normalized.content,
|
|
487
|
+
content: captureContent ? truncateLargeEventContent(normalized.content, exports.EVENT_CONTENT_MAX_CHARS).content : normalized.content,
|
|
472
488
|
name: normalized.name,
|
|
473
489
|
tool_call_id: normalized.toolCallId,
|
|
474
490
|
tool_calls: normalized.toolCalls,
|
|
@@ -582,6 +598,8 @@ function repairSessionMessages(messages) {
|
|
|
582
598
|
if (msg.role === "assistant" && result.length > 0) {
|
|
583
599
|
const prev = result[result.length - 1];
|
|
584
600
|
if (prev.role === "assistant" && prev.toolCalls.length === 0) {
|
|
601
|
+
if (prev.sourceEventIds)
|
|
602
|
+
prev.sourceEventIds = [...new Set([...prev.sourceEventIds, ...(msg.sourceEventIds ?? [])])];
|
|
585
603
|
const prevContent = contentText(prev.content);
|
|
586
604
|
const curContent = contentText(msg.content);
|
|
587
605
|
// Drop the second of two consecutive assistants when the content is
|
|
@@ -805,7 +823,7 @@ function repairInlineReasoningOnReplay(messages, inlineReasoningStrippedCallIds)
|
|
|
805
823
|
repaired++;
|
|
806
824
|
for (const tc of a.tool_calls)
|
|
807
825
|
inlineReasoningStrippedCallIds.add(tc.id);
|
|
808
|
-
return { ...a, content: stripped.length > 0 ? stripped : null };
|
|
826
|
+
return withMessageSourceEventIds({ ...a, content: stripped.length > 0 ? stripped : null }, msg[messageSourceEventIds]);
|
|
809
827
|
});
|
|
810
828
|
if (repaired > 0) {
|
|
811
829
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -949,27 +967,30 @@ function buildEventFromMessage(message, sequence, recordedAt, captureKind, sourc
|
|
|
949
967
|
};
|
|
950
968
|
}
|
|
951
969
|
function projectedSessionEventIds(envelope) {
|
|
952
|
-
return envelope.
|
|
953
|
-
? envelope.projection.eventIds
|
|
954
|
-
: envelope.events.map((event) => event.id);
|
|
970
|
+
return providerProjectionEvents(envelope).map((event) => event.id);
|
|
955
971
|
}
|
|
956
972
|
function providerProjectionEvents(envelope) {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
973
|
+
const events = selectEffectiveSessionEvents(envelope.events);
|
|
974
|
+
if (envelope.projection.eventIds.length === 0)
|
|
975
|
+
return envelope.projection.trimmed ? [] : events;
|
|
976
|
+
const byId = new Map(events.map((event) => [event.id, event]));
|
|
977
|
+
return envelope.projection.eventIds
|
|
960
978
|
.map((id) => byId.get(id))
|
|
961
979
|
.filter((event) => Boolean(event));
|
|
962
980
|
}
|
|
963
|
-
function
|
|
964
|
-
return
|
|
965
|
-
.map((event) => toProviderMessage({
|
|
981
|
+
function providerMessageFromEvent(event) {
|
|
982
|
+
return toProviderMessage({
|
|
966
983
|
role: event.role,
|
|
967
984
|
content: event.content,
|
|
968
985
|
name: event.name,
|
|
969
986
|
toolCallId: event.toolCallId,
|
|
970
987
|
toolCalls: event.toolCalls,
|
|
971
988
|
hadToolCallsField: event.toolCalls.length > 0,
|
|
972
|
-
|
|
989
|
+
sourceEventIds: [event.id],
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
function projectProviderMessages(envelope) {
|
|
993
|
+
return providerProjectionEvents(envelope).map(providerMessageFromEvent);
|
|
973
994
|
}
|
|
974
995
|
/**
|
|
975
996
|
* Annotate user and assistant messages with a relative time offset tag.
|
|
@@ -1262,7 +1283,7 @@ function parseSessionEnvelope(raw, options = {}) {
|
|
|
1262
1283
|
// still resolves predictably.
|
|
1263
1284
|
const events = dedupeEventsByIdLastWins(rawEvents);
|
|
1264
1285
|
const projection = record.projection;
|
|
1265
|
-
|
|
1286
|
+
const envelope = {
|
|
1266
1287
|
version: 2,
|
|
1267
1288
|
events,
|
|
1268
1289
|
projection: {
|
|
@@ -1273,11 +1294,12 @@ function parseSessionEnvelope(raw, options = {}) {
|
|
|
1273
1294
|
inputTokens: typeof projection.inputTokens === "number" ? projection.inputTokens : null,
|
|
1274
1295
|
projectedAt: typeof projection.projectedAt === "string" ? projection.projectedAt : null,
|
|
1275
1296
|
},
|
|
1276
|
-
structuredOutputs: (0, structured_output_1.extractStructuredOutputsFromEvents)(selectEffectiveSessionEvents(events), { emitTelemetry: false }),
|
|
1277
1297
|
lastUsage: normalizeUsage(record.lastUsage),
|
|
1278
1298
|
state: normalizeContinuityState(record.state),
|
|
1279
1299
|
approvalSuspensions: normalizeApprovalSuspensions(record.approvalSuspensions),
|
|
1280
1300
|
};
|
|
1301
|
+
envelope.structuredOutputs = (0, structured_output_1.extractStructuredOutputsFromEvents)(providerProjectionEvents(envelope), { emitTelemetry: false });
|
|
1302
|
+
return envelope;
|
|
1281
1303
|
}
|
|
1282
1304
|
function loadSessionEnvelopeFile(filePath) {
|
|
1283
1305
|
try {
|
|
@@ -1310,12 +1332,14 @@ function filterNonSystem(messages) {
|
|
|
1310
1332
|
* System messages (whose content changes every turn due to live world-state)
|
|
1311
1333
|
* are excluded so that prefix matching is not defeated by system prompt updates.
|
|
1312
1334
|
*/
|
|
1313
|
-
function findCommonPrefixLength(a, b) {
|
|
1335
|
+
function findCommonPrefixLength(a, b, captureContent = false) {
|
|
1314
1336
|
const aNonSys = filterNonSystem(a);
|
|
1315
1337
|
const bNonSys = filterNonSystem(b);
|
|
1316
1338
|
const max = Math.min(aNonSys.length, bNonSys.length);
|
|
1317
1339
|
for (let i = 0; i < max; i++) {
|
|
1318
|
-
if (
|
|
1340
|
+
if (!messageSourcesAgree(aNonSys[i], bNonSys[i]))
|
|
1341
|
+
return i;
|
|
1342
|
+
if (messageFingerprint(aNonSys[i], captureContent) !== messageFingerprint(bNonSys[i], captureContent))
|
|
1319
1343
|
return i;
|
|
1320
1344
|
}
|
|
1321
1345
|
return max;
|
|
@@ -1324,101 +1348,139 @@ function selectProjectedEventIds(currentMessages, currentEventIds, trimmedMessag
|
|
|
1324
1348
|
if (trimmedMessages.length === 0)
|
|
1325
1349
|
return [];
|
|
1326
1350
|
const result = [];
|
|
1351
|
+
const currentIdentities = new Set(currentMessages);
|
|
1327
1352
|
let needle = 0;
|
|
1328
1353
|
let trimmedFingerprint = null;
|
|
1329
1354
|
for (let i = 0; i < currentMessages.length && needle < trimmedMessages.length; i++) {
|
|
1330
1355
|
const currentMessage = currentMessages[i];
|
|
1331
1356
|
const trimmedMessage = trimmedMessages[needle];
|
|
1332
1357
|
if (currentMessage !== trimmedMessage) {
|
|
1358
|
+
if (currentIdentities.has(trimmedMessage))
|
|
1359
|
+
continue;
|
|
1360
|
+
if (!messageSourcesAgree(currentMessage, trimmedMessage))
|
|
1361
|
+
continue;
|
|
1333
1362
|
trimmedFingerprint ??= messageFingerprint(trimmedMessage);
|
|
1334
1363
|
if (messageFingerprint(currentMessage) !== trimmedFingerprint)
|
|
1335
1364
|
continue;
|
|
1336
1365
|
}
|
|
1337
|
-
result.push(currentEventIds[i]);
|
|
1366
|
+
result.push(...currentEventIds[i]);
|
|
1338
1367
|
needle++;
|
|
1339
1368
|
trimmedFingerprint = null;
|
|
1340
1369
|
}
|
|
1341
1370
|
return result;
|
|
1342
1371
|
}
|
|
1372
|
+
function retainedSourceEventIds(message, existing, used) {
|
|
1373
|
+
const ids = message[messageSourceEventIds];
|
|
1374
|
+
if (!ids?.length || ids.some((id) => !existing.has(id) || used.has(id)))
|
|
1375
|
+
return null;
|
|
1376
|
+
const originals = ids.map((id) => providerMessageFromEvent(existing.get(id)));
|
|
1377
|
+
const fingerprint = messageFingerprint(message, true);
|
|
1378
|
+
const matches = (candidate) => (candidate[messageSourceEventIds] !== undefined
|
|
1379
|
+
&& messageSourcesAgree(candidate, message)
|
|
1380
|
+
&& messageFingerprint(candidate, true) === fingerprint);
|
|
1381
|
+
return originals.some(matches) || sanitizeProviderMessages(originals).some(matches) ? ids : null;
|
|
1382
|
+
}
|
|
1343
1383
|
function buildCanonicalSessionEnvelope(options) {
|
|
1344
1384
|
const existing = options.existing;
|
|
1345
1385
|
// Callers pass pre-sanitized messages + pre-captured ingress times.
|
|
1346
1386
|
const currentIngressTimes = options.currentIngressTimes ?? options.currentMessages.map(getIngressTime);
|
|
1347
1387
|
const currentIngressRelations = options.currentIngressRelations ?? options.currentMessages.map(getIngressRelations);
|
|
1348
|
-
|
|
1388
|
+
let previousMessages = options.previousMessages;
|
|
1349
1389
|
const currentMessages = options.currentMessages;
|
|
1350
1390
|
const trimmedMessages = options.trimmedMessages;
|
|
1351
|
-
|
|
1391
|
+
let previousProjectionGroups = existing ? providerProjectionEvents(existing).map((event) => [event.id]) : [];
|
|
1352
1392
|
// Compare only non-system messages to find the common prefix.
|
|
1353
1393
|
// System messages change every turn (live world-state in system prompt)
|
|
1354
1394
|
// and must not defeat prefix matching of the actual conversation.
|
|
1355
|
-
|
|
1395
|
+
let nonSystemPrefix = findCommonPrefixLength(previousMessages, currentMessages, true);
|
|
1396
|
+
// A repaired provider message can represent several original native records.
|
|
1397
|
+
const repairedPrevious = sanitizeProviderMessages(previousMessages.map((message, index) => withMessageSourceEventIds({ ...message }, previousProjectionGroups[index])));
|
|
1398
|
+
const repairedPrefix = findCommonPrefixLength(repairedPrevious, currentMessages);
|
|
1399
|
+
if (repairedPrefix > nonSystemPrefix) {
|
|
1400
|
+
previousMessages = repairedPrevious;
|
|
1401
|
+
previousProjectionGroups = repairedPrevious.map((message) => message[messageSourceEventIds] ?? []);
|
|
1402
|
+
nonSystemPrefix = repairedPrefix;
|
|
1403
|
+
}
|
|
1356
1404
|
// Build a lookup of non-system previous projection IDs.
|
|
1357
1405
|
const prevNonSystemIds = [];
|
|
1358
1406
|
for (let i = 0; i < previousMessages.length; i++) {
|
|
1359
1407
|
if (messageRole(previousMessages[i]) !== "system") {
|
|
1360
|
-
prevNonSystemIds.push(
|
|
1408
|
+
prevNonSystemIds.push(previousProjectionGroups[i]);
|
|
1361
1409
|
}
|
|
1362
1410
|
}
|
|
1363
1411
|
// Walk currentMessages and build currentEventIds + new events.
|
|
1364
1412
|
// Non-system messages within the prefix reuse old event IDs.
|
|
1365
|
-
//
|
|
1413
|
+
// Other messages reuse a matching native origin or create a new event.
|
|
1366
1414
|
const events = [...(existing?.events ?? [])];
|
|
1415
|
+
const sourceEvents = new Map(selectEffectiveSessionEvents(events).map((event) => [event.id, event]));
|
|
1416
|
+
const excludedSourceIds = new Set(events.filter((event) => !sourceEvents.has(event.id)).map((event) => event.id));
|
|
1417
|
+
const usedEventIds = new Set();
|
|
1367
1418
|
const currentEventIds = [];
|
|
1368
1419
|
let nonSystemSeen = 0;
|
|
1369
1420
|
for (let i = 0; i < currentMessages.length; i++) {
|
|
1421
|
+
const sourceIds = currentMessages[i][messageSourceEventIds];
|
|
1422
|
+
if (sourceIds?.some((id) => excludedSourceIds.has(id))) {
|
|
1423
|
+
throw new Error("cannot persist messages derived from redacted session events");
|
|
1424
|
+
}
|
|
1370
1425
|
const role = messageRole(currentMessages[i]);
|
|
1371
1426
|
const isSystem = role === "system";
|
|
1372
1427
|
const inPrefix = !isSystem && nonSystemSeen < nonSystemPrefix;
|
|
1373
|
-
if (inPrefix) {
|
|
1428
|
+
if (inPrefix && prevNonSystemIds[nonSystemSeen].every((id) => !usedEventIds.has(id))) {
|
|
1374
1429
|
// Reuse existing event ID for this matched non-system message
|
|
1375
1430
|
currentEventIds.push(prevNonSystemIds[nonSystemSeen]);
|
|
1376
1431
|
nonSystemSeen++;
|
|
1377
1432
|
}
|
|
1378
1433
|
else if (isSystem && i < previousMessages.length
|
|
1379
1434
|
&& messageRole(previousMessages[i]) === "system"
|
|
1380
|
-
&&
|
|
1435
|
+
&& messageSourcesAgree(currentMessages[i], previousMessages[i])
|
|
1436
|
+
&& previousProjectionGroups[i].every((id) => !usedEventIds.has(id))
|
|
1437
|
+
&& messageFingerprint(currentMessages[i], true) === messageFingerprint(previousMessages[i], true)) {
|
|
1381
1438
|
// System message at same position with identical content -- reuse event ID
|
|
1382
|
-
currentEventIds.push(
|
|
1439
|
+
currentEventIds.push(previousProjectionGroups[i]);
|
|
1383
1440
|
}
|
|
1384
1441
|
else {
|
|
1385
1442
|
if (!isSystem)
|
|
1386
1443
|
nonSystemSeen++;
|
|
1444
|
+
const retainedIds = retainedSourceEventIds(currentMessages[i], sourceEvents, usedEventIds);
|
|
1445
|
+
if (retainedIds) {
|
|
1446
|
+
currentEventIds.push(retainedIds);
|
|
1447
|
+
for (const id of retainedIds)
|
|
1448
|
+
usedEventIds.add(id);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1387
1451
|
// Create a new event. Use nextEventSequence(events) instead of
|
|
1388
1452
|
// `events.length + 1` so that any gap (from pruning, archive replay,
|
|
1389
1453
|
// or self-heal dedup) cannot collide with an existing id.
|
|
1390
1454
|
const event = buildEventFromMessage(currentMessages[i], nextEventSequence(events), options.recordedAt, "live", null, null, currentIngressTimes[i], currentIngressRelations[i]);
|
|
1391
1455
|
events.push(event);
|
|
1392
|
-
currentEventIds.push(event.id);
|
|
1456
|
+
currentEventIds.push([event.id]);
|
|
1393
1457
|
}
|
|
1458
|
+
for (const id of currentEventIds.at(-1))
|
|
1459
|
+
usedEventIds.add(id);
|
|
1394
1460
|
}
|
|
1395
1461
|
const projectionEventIds = selectProjectedEventIds(currentMessages, currentEventIds, trimmedMessages);
|
|
1396
|
-
//
|
|
1397
|
-
// Events not in projection are returned as evicted for archiving.
|
|
1462
|
+
// Projection omissions are not authority to delete native history.
|
|
1398
1463
|
const projectionIdSet = new Set(projectionEventIds);
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
inputTokens: options.projectionBasis.inputTokens,
|
|
1413
|
-
projectedAt: options.recordedAt,
|
|
1414
|
-
},
|
|
1415
|
-
structuredOutputs: (0, structured_output_1.extractStructuredOutputsFromEvents)(selectEffectiveSessionEvents(prunedEvents)),
|
|
1416
|
-
lastUsage: normalizeUsage(options.lastUsage),
|
|
1417
|
-
state: normalizeContinuityState(options.state),
|
|
1418
|
-
approvalSuspensions: structuredClone(options.existing?.approvalSuspensions ?? []),
|
|
1464
|
+
const effectiveEvents = selectEffectiveSessionEvents(events);
|
|
1465
|
+
const effectiveIds = new Set(effectiveEvents.map((event) => event.id));
|
|
1466
|
+
const evictedEvents = effectiveEvents.filter((event) => !projectionIdSet.has(event.id));
|
|
1467
|
+
const envelope = {
|
|
1468
|
+
version: 2,
|
|
1469
|
+
events,
|
|
1470
|
+
projection: {
|
|
1471
|
+
eventIds: projectionEventIds.filter((id) => effectiveIds.has(id)),
|
|
1472
|
+
trimmed: evictedEvents.length > 0,
|
|
1473
|
+
maxTokens: options.projectionBasis.maxTokens,
|
|
1474
|
+
contextMargin: options.projectionBasis.contextMargin,
|
|
1475
|
+
inputTokens: options.projectionBasis.inputTokens,
|
|
1476
|
+
projectedAt: options.recordedAt,
|
|
1419
1477
|
},
|
|
1420
|
-
|
|
1478
|
+
lastUsage: normalizeUsage(options.lastUsage),
|
|
1479
|
+
state: normalizeContinuityState(options.state),
|
|
1480
|
+
approvalSuspensions: structuredClone(options.existing?.approvalSuspensions ?? []),
|
|
1421
1481
|
};
|
|
1482
|
+
envelope.structuredOutputs = (0, structured_output_1.extractStructuredOutputsFromEvents)(providerProjectionEvents(envelope));
|
|
1483
|
+
return { envelope, evictedEvents };
|
|
1422
1484
|
}
|
|
1423
1485
|
function appendSyntheticAssistantEvent(envelope, content, recordedAt) {
|
|
1424
1486
|
// Use nextEventSequence(events) instead of `events.length + 1` so any gap
|
|
@@ -1426,15 +1488,17 @@ function appendSyntheticAssistantEvent(envelope, content, recordedAt) {
|
|
|
1426
1488
|
// an existing event id. Same fix pattern as line 1046.
|
|
1427
1489
|
const sequence = nextEventSequence(envelope.events);
|
|
1428
1490
|
const event = buildEventFromMessage({ role: "assistant", content }, sequence, recordedAt, "synthetic", null, null);
|
|
1429
|
-
|
|
1491
|
+
const priorIds = projectedSessionEventIds(envelope);
|
|
1492
|
+
const updated = {
|
|
1430
1493
|
...envelope,
|
|
1431
1494
|
events: [...envelope.events, event],
|
|
1432
|
-
structuredOutputs: (0, structured_output_1.extractStructuredOutputsFromEvents)(selectEffectiveSessionEvents([...envelope.events, event])),
|
|
1433
1495
|
projection: {
|
|
1434
1496
|
...envelope.projection,
|
|
1435
|
-
eventIds: [...
|
|
1497
|
+
eventIds: [...priorIds, event.id],
|
|
1436
1498
|
projectedAt: recordedAt,
|
|
1437
|
-
trimmed:
|
|
1499
|
+
trimmed: new Set(priorIds).size < selectEffectiveSessionEvents(envelope.events).length,
|
|
1438
1500
|
},
|
|
1439
1501
|
};
|
|
1502
|
+
updated.structuredOutputs = (0, structured_output_1.extractStructuredOutputsFromEvents)(providerProjectionEvents(updated));
|
|
1503
|
+
return updated;
|
|
1440
1504
|
}
|
|
@@ -258,6 +258,11 @@ function rawEnvelope(bytes) {
|
|
|
258
258
|
}
|
|
259
259
|
return envelope;
|
|
260
260
|
}
|
|
261
|
+
function projectedStructuredOutputs(envelope) {
|
|
262
|
+
const byId = new Map(envelope.events.map((event) => [event.id, event]));
|
|
263
|
+
const events = (0, session_events_1.projectedSessionEventIds)(envelope).map((id) => byId.get(id));
|
|
264
|
+
return (0, structured_output_1.extractStructuredOutputsFromEvents)(events, { emitTelemetry: false });
|
|
265
|
+
}
|
|
261
266
|
function compute(bytes, relative, capturedAt) {
|
|
262
267
|
const envelope = rawEnvelope(bytes);
|
|
263
268
|
const targets = selectA003LegacyRequiredCorrections(envelope.events);
|
|
@@ -275,11 +280,15 @@ function compute(bytes, relative, capturedAt) {
|
|
|
275
280
|
if (!entries.every((entry) => (0, session_events_1.isExactRawSessionRedactionMarker)(entry.marker, events)))
|
|
276
281
|
refuse("invalid computed marker block");
|
|
277
282
|
const removed = new Set(entries.flatMap((entry) => [entry.target.id, entry.marker.id]));
|
|
283
|
+
const selected = new Set((0, session_events_1.projectedSessionEventIds)(envelope).filter((id) => !removed.has(id)));
|
|
278
284
|
const postimage = {
|
|
279
285
|
...envelope, events,
|
|
280
|
-
projection: {
|
|
281
|
-
|
|
286
|
+
projection: {
|
|
287
|
+
...envelope.projection, eventIds: envelope.projection.eventIds.filter((id) => !removed.has(id)),
|
|
288
|
+
trimmed: envelope.projection.trimmed || (0, session_events_1.selectEffectiveSessionEvents)(events).some((event) => !selected.has(event.id)),
|
|
289
|
+
},
|
|
282
290
|
};
|
|
291
|
+
postimage.structuredOutputs = projectedStructuredOutputs(postimage);
|
|
283
292
|
const postBytes = JSON.stringify(postimage, null, 2);
|
|
284
293
|
if (Buffer.byteLength(postBytes) > SESSION_LIMIT)
|
|
285
294
|
refuse("postimage exceeds 32 MiB");
|
|
@@ -449,7 +458,7 @@ function alreadyApplied(envelope, manifest, currentRevision) {
|
|
|
449
458
|
const effective = (0, session_events_1.selectEffectiveSessionEvents)(envelope.events);
|
|
450
459
|
const visible = new Set(effective.map((event) => event.id));
|
|
451
460
|
return envelope.projection.eventIds.every((id) => visible.has(id))
|
|
452
|
-
&& (0, node_util_1.isDeepStrictEqual)(envelope.structuredOutputs, (
|
|
461
|
+
&& (0, node_util_1.isDeepStrictEqual)(envelope.structuredOutputs, projectedStructuredOutputs(envelope));
|
|
453
462
|
}
|
|
454
463
|
async function reconcileWrite(sessionPath, lease, value, before, after, success) {
|
|
455
464
|
try {
|
|
@@ -55,6 +55,7 @@ function eventTimeMs(event) {
|
|
|
55
55
|
}
|
|
56
56
|
function computeSessionStats(envelope, sessionPath) {
|
|
57
57
|
const byRole = emptyByRole();
|
|
58
|
+
const projectionCount = (0, session_events_1.projectedSessionEventIds)(envelope).length;
|
|
58
59
|
let toolCallTotal = 0;
|
|
59
60
|
let attachments = 0;
|
|
60
61
|
const toolNameCounts = new Map();
|
|
@@ -97,8 +98,8 @@ function computeSessionStats(envelope, sessionPath) {
|
|
|
97
98
|
durationMs: earliestMs !== null && latestMs !== null ? latestMs - earliestMs : null,
|
|
98
99
|
},
|
|
99
100
|
projection: {
|
|
100
|
-
eventCount:
|
|
101
|
-
omittedFromProjection: Math.max(0, envelope.events.length -
|
|
101
|
+
eventCount: projectionCount,
|
|
102
|
+
omittedFromProjection: Math.max(0, envelope.events.length - projectionCount),
|
|
102
103
|
inputTokens: envelope.projection.inputTokens,
|
|
103
104
|
maxTokens: envelope.projection.maxTokens,
|
|
104
105
|
trimmed: envelope.projection.trimmed,
|