@sema-agent/core 5.55.0 → 5.56.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/CHANGELOG.md +66 -0
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +34 -23
- package/dist/core/runner/runtask.js +158 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +37 -11
- package/dist/core/untrusted-text.js +8 -0
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +10 -4
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { resolveAgentCoreCompleteFn, } from "../loop/runtime-deps.js";
|
|
|
2
2
|
import { asAgentMessage, convertToLlm, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
|
|
3
3
|
import { buildSessionContext } from "../session/session.js";
|
|
4
4
|
import { CompactionError, err, ok, } from "../harness/types.js";
|
|
5
|
-
import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, extractPersistedOutputRefs, formatFileOperations, formatPersistedOutputRefs, PERSISTED_OUTPUT_REFS_MAX_ENTRIES, readElidedMessages, readPersistedOutputRefs, readRetainedInvokedSkills, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
|
|
5
|
+
import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, extractPersistedOutputRefs, formatFileOperations, formatPersistedOutputRefs, PERSISTED_OUTPUT_REFS_MAX_ENTRIES, readElidedMessages, readPersistedOutputRefs, readRetainedInvokedSkills, readUnsummarizedMessages, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
|
|
6
6
|
function safeJsonStringify(value) {
|
|
7
7
|
try {
|
|
8
8
|
return JSON.stringify(value) ?? "undefined";
|
|
@@ -434,6 +434,58 @@ Then, after </analysis>, write the summary. Your summary should include the foll
|
|
|
434
434
|
9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
|
|
435
435
|
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
|
|
436
436
|
|
|
437
|
+
Here's an example of how your output should be structured:
|
|
438
|
+
|
|
439
|
+
<example>
|
|
440
|
+
<analysis>
|
|
441
|
+
[Your thought process, ensuring all points are covered thoroughly and accurately]
|
|
442
|
+
</analysis>
|
|
443
|
+
|
|
444
|
+
<summary>
|
|
445
|
+
1. Primary Request and Intent:
|
|
446
|
+
[Detailed description]
|
|
447
|
+
|
|
448
|
+
2. Key Technical Concepts:
|
|
449
|
+
- [Concept 1]
|
|
450
|
+
- [Concept 2]
|
|
451
|
+
- [...]
|
|
452
|
+
|
|
453
|
+
3. Files and Code Sections:
|
|
454
|
+
- [File Name 1]
|
|
455
|
+
- [Summary of why this file is important]
|
|
456
|
+
- [Summary of the changes made to this file, if any]
|
|
457
|
+
- [Important Code Snippet]
|
|
458
|
+
- [File Name 2]
|
|
459
|
+
- [Important Code Snippet]
|
|
460
|
+
- [...]
|
|
461
|
+
|
|
462
|
+
4. Errors and fixes:
|
|
463
|
+
- [Detailed description of error 1]:
|
|
464
|
+
- [How you fixed the error]
|
|
465
|
+
- [User feedback on the error if any]
|
|
466
|
+
- [...]
|
|
467
|
+
|
|
468
|
+
5. Problem Solving:
|
|
469
|
+
[Description of solved problems and ongoing troubleshooting]
|
|
470
|
+
|
|
471
|
+
6. All user messages:
|
|
472
|
+
- [Detailed non tool use user message]
|
|
473
|
+
- [...]
|
|
474
|
+
|
|
475
|
+
7. Pending Tasks:
|
|
476
|
+
- [Task 1]
|
|
477
|
+
- [Task 2]
|
|
478
|
+
- [...]
|
|
479
|
+
|
|
480
|
+
8. Current Work:
|
|
481
|
+
[Precise description of current work]
|
|
482
|
+
|
|
483
|
+
9. Optional Next Step:
|
|
484
|
+
[Optional Next step to take]
|
|
485
|
+
|
|
486
|
+
</summary>
|
|
487
|
+
</example>
|
|
488
|
+
|
|
437
489
|
Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
|
|
438
490
|
|
|
439
491
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
@@ -617,13 +669,28 @@ function splitMessageGroups(messages) {
|
|
|
617
669
|
}
|
|
618
670
|
return groups;
|
|
619
671
|
}
|
|
672
|
+
const SUMMARY_SCRATCH_PREFIX_RE = /^\s*<analysis>[\s\S]*?<\/analysis>\s*/i;
|
|
673
|
+
const SUMMARY_ENVELOPE_RE = /^<summary>([\s\S]*)<\/summary>$/i;
|
|
674
|
+
function stripSummaryEnvelope(text) {
|
|
675
|
+
const scratch = SUMMARY_SCRATCH_PREFIX_RE.exec(text);
|
|
676
|
+
const head = scratch === null ? "" : scratch[0];
|
|
677
|
+
const m = SUMMARY_ENVELOPE_RE.exec(text.slice(head.length).trim());
|
|
678
|
+
if (m === null)
|
|
679
|
+
return text;
|
|
680
|
+
return head + (m[1] ?? "").trim();
|
|
681
|
+
}
|
|
620
682
|
async function summarizeWithPtlRetry(req) {
|
|
621
|
-
const disclose = (droppedChars, keptChars) => {
|
|
683
|
+
const disclose = (droppedChars, keptChars, droppedMessages) => {
|
|
622
684
|
if (droppedChars <= 0) {
|
|
623
685
|
return;
|
|
624
686
|
}
|
|
625
687
|
try {
|
|
626
|
-
req.onInputTruncated?.({
|
|
688
|
+
req.onInputTruncated?.({
|
|
689
|
+
label: req.truncationLabel,
|
|
690
|
+
droppedChars,
|
|
691
|
+
keptChars,
|
|
692
|
+
...(droppedMessages !== undefined && droppedMessages > 0 ? { droppedMessages } : {}),
|
|
693
|
+
});
|
|
627
694
|
}
|
|
628
695
|
catch {
|
|
629
696
|
}
|
|
@@ -659,7 +726,11 @@ async function summarizeWithPtlRetry(req) {
|
|
|
659
726
|
thrown = e;
|
|
660
727
|
}
|
|
661
728
|
if (!didThrow && result && result.ok) {
|
|
662
|
-
|
|
729
|
+
const unwrapped = stripSummaryEnvelope(result.value);
|
|
730
|
+
if (unwrapped.trim() === "") {
|
|
731
|
+
return err(markEmptySummaryClass(new CompactionError("summarization_failed", `${req.label} produced an empty summary (the response was an empty <summary> envelope)`)));
|
|
732
|
+
}
|
|
733
|
+
return ok(unwrapped);
|
|
663
734
|
}
|
|
664
735
|
const failureMessage = didThrow
|
|
665
736
|
? thrown instanceof Error
|
|
@@ -688,12 +759,14 @@ async function summarizeWithPtlRetry(req) {
|
|
|
688
759
|
const beforeChars = serialized.length;
|
|
689
760
|
const rollbackTargetChars = Math.max(SUMMARY_INPUT_MIN_KEEP_CHARS, beforeChars - Math.max(gapChars, 1));
|
|
690
761
|
let remainingChars = beforeChars;
|
|
762
|
+
let droppedMessages = 0;
|
|
691
763
|
while (groups.length > 1 && remainingChars > rollbackTargetChars) {
|
|
692
764
|
const dropped = groups.shift();
|
|
765
|
+
droppedMessages += dropped.length;
|
|
693
766
|
remainingChars -= serializeConversation(dropped).length + 2;
|
|
694
767
|
}
|
|
695
768
|
droppedForRetry = true;
|
|
696
|
-
disclose(beforeChars - Math.max(remainingChars, 0), Math.max(remainingChars, 0));
|
|
769
|
+
disclose(beforeChars - Math.max(remainingChars, 0), Math.max(remainingChars, 0), droppedMessages);
|
|
697
770
|
}
|
|
698
771
|
}
|
|
699
772
|
function markEmptySummaryClass(e) {
|
|
@@ -896,6 +969,9 @@ export function prepareCompaction(pathEntries, settings, charsPerToken = DEFAULT
|
|
|
896
969
|
? readElidedMessages(pathEntries[prevCompactionIndex].details)
|
|
897
970
|
: undefined;
|
|
898
971
|
const elidedMessages = (prevElided ?? 0) + messagesToSummarize.length + turnPrefixMessages.length;
|
|
972
|
+
const carriedUnsummarizedMessages = prevCompactionIndex >= 0 && !pathEntries[prevCompactionIndex].fromHook
|
|
973
|
+
? readUnsummarizedMessages(pathEntries[prevCompactionIndex].details)
|
|
974
|
+
: undefined;
|
|
899
975
|
const [summarizeReady, prefixReady] = replaceInvokedSkillBodiesForSummary([messagesToSummarize, turnPrefixMessages], new Set(invokedSkills.map((s) => s.name)));
|
|
900
976
|
return ok({
|
|
901
977
|
firstKeptEntryId,
|
|
@@ -908,6 +984,9 @@ export function prepareCompaction(pathEntries, settings, charsPerToken = DEFAULT
|
|
|
908
984
|
invokedSkills,
|
|
909
985
|
persistedOutputRefs,
|
|
910
986
|
elidedMessages,
|
|
987
|
+
...(carriedUnsummarizedMessages !== undefined && carriedUnsummarizedMessages > 0
|
|
988
|
+
? { carriedUnsummarizedMessages }
|
|
989
|
+
: {}),
|
|
911
990
|
settings,
|
|
912
991
|
});
|
|
913
992
|
}
|
|
@@ -927,7 +1006,14 @@ Summarize the prefix to provide context for the retained suffix:
|
|
|
927
1006
|
Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
928
1007
|
export { computeFileLists, serializeConversation } from "./utils.js";
|
|
929
1008
|
export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry, forkContext) {
|
|
930
|
-
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, persistedOutputRefs, elidedMessages, settings, } = preparation;
|
|
1009
|
+
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, persistedOutputRefs, elidedMessages, carriedUnsummarizedMessages, settings, } = preparation;
|
|
1010
|
+
let uncoveredThisPass = 0;
|
|
1011
|
+
const observeInputTruncated = (info) => {
|
|
1012
|
+
if (info.droppedMessages !== undefined && info.droppedMessages > 0) {
|
|
1013
|
+
uncoveredThisPass += info.droppedMessages;
|
|
1014
|
+
}
|
|
1015
|
+
onInputTruncated?.(info);
|
|
1016
|
+
};
|
|
931
1017
|
if (!firstKeptEntryId) {
|
|
932
1018
|
return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
|
|
933
1019
|
}
|
|
@@ -957,10 +1043,10 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
957
1043
|
else if (isSplitTurn && turnPrefixMessages.length > 0) {
|
|
958
1044
|
const [historyResult, turnPrefixResult] = await Promise.all([
|
|
959
1045
|
messagesToSummarize.length > 0
|
|
960
|
-
? generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken,
|
|
1046
|
+
? generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken, observeInputTruncated, onPtlRetry)
|
|
961
1047
|
:
|
|
962
1048
|
Promise.resolve(ok(previousSummary ? stripFileOperationsFooter(previousSummary) : "No prior history.")),
|
|
963
|
-
generateTurnPrefixSummary(turnPrefixMessages, model, summaryBudget, apiKey, headers, signal, customInstructions, thinkingLevel, streamFn, runtime, charsPerToken,
|
|
1049
|
+
generateTurnPrefixSummary(turnPrefixMessages, model, summaryBudget, apiKey, headers, signal, customInstructions, thinkingLevel, streamFn, runtime, charsPerToken, observeInputTruncated, onPtlRetry),
|
|
964
1050
|
]);
|
|
965
1051
|
if (!historyResult.ok) {
|
|
966
1052
|
return err(historyResult.error);
|
|
@@ -971,7 +1057,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
971
1057
|
summary = `${historyResult.value}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`;
|
|
972
1058
|
}
|
|
973
1059
|
else {
|
|
974
|
-
const summaryResult = await generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken,
|
|
1060
|
+
const summaryResult = await generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken, observeInputTruncated, onPtlRetry);
|
|
975
1061
|
if (!summaryResult.ok) {
|
|
976
1062
|
return err(summaryResult.error);
|
|
977
1063
|
}
|
|
@@ -991,6 +1077,9 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
991
1077
|
...(invokedSkills !== undefined && invokedSkills.length > 0 ? { invokedSkills } : {}),
|
|
992
1078
|
...(persistedOutputRefs !== undefined && persistedOutputRefs.length > 0 ? { persistedOutputRefs } : {}),
|
|
993
1079
|
...(elidedMessages !== undefined && elidedMessages > 0 ? { elidedMessages } : {}),
|
|
1080
|
+
...((carriedUnsummarizedMessages ?? 0) + uncoveredThisPass > 0
|
|
1081
|
+
? { unsummarizedMessages: (carriedUnsummarizedMessages ?? 0) + uncoveredThisPass }
|
|
1082
|
+
: {}),
|
|
994
1083
|
},
|
|
995
1084
|
});
|
|
996
1085
|
}
|
|
@@ -94,6 +94,10 @@ export declare function extractPersistedOutputRefs(messages: AgentMessage[], int
|
|
|
94
94
|
* safe integer, else absent (pre-field sessions / foreign shapes yield undefined ⇒ no wrapper
|
|
95
95
|
* disclosure — the byte-identical legacy render). */
|
|
96
96
|
export declare function readElidedMessages(details: unknown): number | undefined;
|
|
97
|
+
/** Defensive reader for persisted `CompactionDetails.unsummarizedMessages` (PTL-shortfall accounting): same contract as
|
|
98
|
+
* {@link readElidedMessages} — a non-negative safe integer, else absent (pre-field sessions /
|
|
99
|
+
* foreign shapes ⇒ no uncovered-scale clause, byte-identical wrapper). */
|
|
100
|
+
export declare function readUnsummarizedMessages(details: unknown): number | undefined;
|
|
97
101
|
/** Defensive reader for persisted `CompactionDetails.activeTools` (RB-402, blackboard [2132]③c/
|
|
98
102
|
* [2133]): the ToolSearch activation snapshot each landed compaction restates. details is
|
|
99
103
|
* `unknown` on the wire — pre-field sessions and foreign shapes yield an empty list; non-string/
|
|
@@ -205,6 +205,12 @@ export function readElidedMessages(details) {
|
|
|
205
205
|
const raw = details.elidedMessages;
|
|
206
206
|
return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 0 ? raw : undefined;
|
|
207
207
|
}
|
|
208
|
+
export function readUnsummarizedMessages(details) {
|
|
209
|
+
if (details === null || typeof details !== "object")
|
|
210
|
+
return undefined;
|
|
211
|
+
const raw = details.unsummarizedMessages;
|
|
212
|
+
return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 0 ? raw : undefined;
|
|
213
|
+
}
|
|
208
214
|
export function readCompactionActiveTools(details) {
|
|
209
215
|
if (details === null || typeof details !== "object")
|
|
210
216
|
return [];
|
|
@@ -38,6 +38,15 @@ export interface UserMessageProvenance {
|
|
|
38
38
|
* projection point `projectHumanInput`) BEFORE the frame wrapping — the harness never rewrites
|
|
39
39
|
* text, so annotation stays adjacent to the frame it attributes. */
|
|
40
40
|
actor?: ActorAssertion;
|
|
41
|
+
/** backlog #389 — an OPAQUE record the caller rides on a CALLER-AUTHORED queued frame so a durable
|
|
42
|
+
* park can carry the instruction across the suspend instead of losing it. Same sidecar posture as
|
|
43
|
+
* {@link UserMessageProvenance.enginePayload} (a WeakMap entry, never a message field, never
|
|
44
|
+
* serialized, never interpreted here) but a different lane and a different fate: an engine payload
|
|
45
|
+
* is handed back for per-session redelivery, a park record is handed to the checkpoint mint through
|
|
46
|
+
* {@link AgentHarness.readParkableUserInputs}. The harness holds the FRAMED text; the record is what
|
|
47
|
+
* the parked-steer queue needs (pre-framing text + trust + inputId), which only the minting seam
|
|
48
|
+
* knows — hence opaque. */
|
|
49
|
+
parkRecord?: unknown;
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
52
|
* Harness-level recovery wiring (design/118 ④b/⑤). The loop's prompt-too-long seam wants a
|
|
@@ -80,6 +89,23 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
80
89
|
* boundary so the loop exits CLEANLY (emit `agent_end` + return) WITHOUT an abort/interrupt — the path a
|
|
81
90
|
* resource-slice suspend needs (an abort would trip the orphan-[INTERRUPTED] reconcile = corrupt resume). */
|
|
82
91
|
private _stopAfterTurn;
|
|
92
|
+
/** backlog #389 — one-way latch set by {@link abort}. Read by `drainQueuedMessages`: no boundary may
|
|
93
|
+
* hand a queued frame to the model once the run is unwinding, and — crucially — none may SPLICE one
|
|
94
|
+
* out of the queue either (that would delete the terminal undrained account the abort path owes it). */
|
|
95
|
+
private aborting;
|
|
96
|
+
/** backlog #389 (codex adversarial r1, HIGH-1) — frames a boundary has REMOVED from a queue but that
|
|
97
|
+
* have not yet entered the model's context.
|
|
98
|
+
*
|
|
99
|
+
* The abort latch alone leaves a real window: `drainQueuedMessages` splices first and then awaits
|
|
100
|
+
* `emitQueueUpdate`, and even after it returns the loop has one more decision point before it injects
|
|
101
|
+
* anything. An `abort()` landing in either gap finds both queues empty, so the terminal account —
|
|
102
|
+
* which reads the queues — reports zero for a frame that will never reach the model. That is the very
|
|
103
|
+
* silent loss this ticket exists to close, one layer deeper.
|
|
104
|
+
*
|
|
105
|
+
* A frame sits here from the splice until its `message_end` (the moment it is in the transcript);
|
|
106
|
+
* the terminal account, the abort snapshot and the durable-park migration all read it alongside the
|
|
107
|
+
* queues, so "accepted" is covered end to end rather than only while it happens to be in an array. */
|
|
108
|
+
private drainedPendingInjection;
|
|
83
109
|
private runAbortController?;
|
|
84
110
|
private runPromise?;
|
|
85
111
|
private pendingSessionWrites;
|
|
@@ -189,6 +215,11 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
189
215
|
private createContext;
|
|
190
216
|
private createStreamFn;
|
|
191
217
|
private drainQueuedMessages;
|
|
218
|
+
/** #389 — drop these frames from the in-flight account (they went back on a queue, or they landed). */
|
|
219
|
+
private unbookInFlight;
|
|
220
|
+
/** #389 — a queued frame has reached the transcript: it is delivered, so it leaves the in-flight
|
|
221
|
+
* account and gives up its park record (a suspend must never re-park what the model has read). */
|
|
222
|
+
private settleInjectedFrame;
|
|
192
223
|
private createLoopConfig;
|
|
193
224
|
/**
|
|
194
225
|
* design/118 ④b/⑤: wire (or clear) the loop's recovery chains. The runner calls this after
|
|
@@ -279,6 +310,59 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
279
310
|
* the queue is empty.
|
|
280
311
|
*/
|
|
281
312
|
flushQueuedSessionWrites(): Promise<void>;
|
|
313
|
+
/**
|
|
314
|
+
* backlog #389 — the accepted-but-undelivered CALLER frames a durable suspend can carry forward,
|
|
315
|
+
* in delivery order (steer before followUp, each queue forward — the order the live loop serves).
|
|
316
|
+
* NON-DESTRUCTIVE by design: the mint that reads this may still fail to commit, and a frame dropped
|
|
317
|
+
* for a checkpoint that never landed would be lost twice over. {@link dropParkedUserInputs} is the
|
|
318
|
+
* second half, called only once the checkpoint is durably committed.
|
|
319
|
+
*
|
|
320
|
+
* Only frames carrying a park record are returned — engine-authored notes have their own lossless
|
|
321
|
+
* lane (`onUndrainedEngineNotes` → pend per session) and must not be parked twice.
|
|
322
|
+
*/
|
|
323
|
+
readParkableUserInputs(): unknown[];
|
|
324
|
+
/**
|
|
325
|
+
* backlog #389 — the COMMIT half of {@link readParkableUserInputs}: the named records are now durable
|
|
326
|
+
* on a checkpoint, so their live frames leave the queues (identity-matched on the sidecar). Dropping
|
|
327
|
+
* them here is what keeps the terminal `#259` account honest — a migrated instruction is not "accepted
|
|
328
|
+
* then lost", and one this is never called for (a failed commit) still gets its loud undrained notice.
|
|
329
|
+
* Returns the per-lane drop counts so the caller can subtract them from an account already taken.
|
|
330
|
+
*/
|
|
331
|
+
dropParkedUserInputs(records: readonly unknown[]): {
|
|
332
|
+
steer: number;
|
|
333
|
+
followUp: number;
|
|
334
|
+
};
|
|
335
|
+
/**
|
|
336
|
+
* backlog #389 (CC 2.1.223 `control_request:"interrupt"`, `CC:1064478-1064513`) — an interrupt does
|
|
337
|
+
* NOT destroy accepted user input. CC's default keeps the queue and answers `still_queued`; only an
|
|
338
|
+
* explicit `cancel_queued` cancels, with a per-uuid `cancelled` receipt. This harness had the opposite
|
|
339
|
+
* polarity: both queues were emptied unconditionally and the contents handed back on a result object
|
|
340
|
+
* with ZERO readers in the repo — so the #259 "accepted then never consumed must be LOUD" account (run
|
|
341
|
+
* at `agent_end`) counted the emptied queues and found nothing to announce. An input accepted one
|
|
342
|
+
* millisecond before `interrupt()` therefore vanished behind its own `queued` receipt.
|
|
343
|
+
*
|
|
344
|
+
* The queues are now LEFT STANDING. The abort latch above stops any surviving boundary from draining
|
|
345
|
+
* (or silently splicing) them, and the terminal `agent_end` account — the one disclosure leg, unchanged
|
|
346
|
+
* — reports them through `onUndrainedUserInputs` + the `settled` frame, exactly as it does for a run
|
|
347
|
+
* that ended naturally with input still queued. Engine notes keep their own lossless recovery sweep,
|
|
348
|
+
* which must still run HERE: it is the one path `agent_end` cannot cover on its own (this sweep also
|
|
349
|
+
* serves a `waitForIdle` that never reaches `agent_end`).
|
|
350
|
+
*
|
|
351
|
+
* A sema `interrupt()` ends the RUN (CC's ends a turn inside a living session), so there is no state in
|
|
352
|
+
* which "still queued" means "will run next" — hence no `cancel_queued` opt-in: its two settings would
|
|
353
|
+
* be indistinguishable at the package face, which is the dead-knob shape this same ticket family flags
|
|
354
|
+
* elsewhere. The CC-shaped "survives the pause" seat is the DURABLE PARK, served by
|
|
355
|
+
* {@link readParkableUserInputs}.
|
|
356
|
+
*
|
|
357
|
+
* RECORDED, because the two halves must not be read as contradicting each other (adversarial r2
|
|
358
|
+
* MEDIUM-6): the #259 notice says a stranded input is "NOT redelivered", and that is a statement about
|
|
359
|
+
* the RUNNER's contract — a `TaskStream.steer` is never served to a later run, because the Runner
|
|
360
|
+
* builds one harness per run and prompts it exactly once. A host driving `AgentHarness` DIRECTLY (it is
|
|
361
|
+
* not on the package export surface) and re-prompting the same instance WILL see the leftovers served,
|
|
362
|
+
* both the queued ones (true before this ticket too) and the in-flight ones the next `prompt()`
|
|
363
|
+
* restores. Clearing them here instead is not available: the durable-park migration reads them AFTER
|
|
364
|
+
* this method returns, so an abort that empties the queues would take the park's payload with it.
|
|
365
|
+
*/
|
|
282
366
|
abort(): Promise<AbortResult>;
|
|
283
367
|
waitForIdle(): Promise<void>;
|
|
284
368
|
subscribe(listener: (event: AgentHarnessEvent<TSkill, TPromptTemplate>, signal?: AbortSignal) => Promise<void> | void): () => void;
|
|
@@ -35,6 +35,8 @@ function createUserMessage(text, images, provenance) {
|
|
|
35
35
|
}
|
|
36
36
|
const engineNotePayloads = new WeakMap();
|
|
37
37
|
const engineAuthoredInjections = new WeakSet();
|
|
38
|
+
const userInputParkRecords = new WeakMap();
|
|
39
|
+
const drainedFromLane = new WeakMap();
|
|
38
40
|
function isEngineAuthoredInjection(options) {
|
|
39
41
|
if (options === undefined)
|
|
40
42
|
return false;
|
|
@@ -160,6 +162,8 @@ export class AgentHarness {
|
|
|
160
162
|
session;
|
|
161
163
|
phase = "idle";
|
|
162
164
|
_stopAfterTurn = false;
|
|
165
|
+
aborting = false;
|
|
166
|
+
drainedPendingInjection = [];
|
|
163
167
|
runAbortController;
|
|
164
168
|
runPromise;
|
|
165
169
|
pendingSessionWrites = [];
|
|
@@ -170,7 +174,7 @@ export class AgentHarness {
|
|
|
170
174
|
onEngineNoteConsumed;
|
|
171
175
|
engineInjectionsHeld;
|
|
172
176
|
recoverUndrainedEngineNotes() {
|
|
173
|
-
this.sweepUndrainedEngineNotes([this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
177
|
+
this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
174
178
|
}
|
|
175
179
|
sweepUndrainedEngineNotes(queues) {
|
|
176
180
|
const undrained = [];
|
|
@@ -193,9 +197,10 @@ export class AgentHarness {
|
|
|
193
197
|
}
|
|
194
198
|
}
|
|
195
199
|
announceUndrainedUserInputs() {
|
|
200
|
+
const inFlight = (lane) => this.drainedPendingInjection.filter((m) => !engineAuthoredInjections.has(m) && drainedFromLane.get(m) === lane).length;
|
|
196
201
|
const counts = {
|
|
197
|
-
steer: this.steerQueue.filter((m) => !engineAuthoredInjections.has(m)).length,
|
|
198
|
-
followUp: this.followUpQueue.filter((m) => !engineAuthoredInjections.has(m)).length,
|
|
202
|
+
steer: this.steerQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("steer"),
|
|
203
|
+
followUp: this.followUpQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("followUp"),
|
|
199
204
|
};
|
|
200
205
|
if ((counts.steer > 0 || counts.followUp > 0) && this.onUndrainedUserInputs) {
|
|
201
206
|
try {
|
|
@@ -452,6 +457,8 @@ export class AgentHarness {
|
|
|
452
457
|
}
|
|
453
458
|
async drainQueuedMessages(queue, mode) {
|
|
454
459
|
let messages;
|
|
460
|
+
if (this.aborting)
|
|
461
|
+
return [];
|
|
455
462
|
if (this.engineInjectionsHeld?.() === true) {
|
|
456
463
|
messages = [];
|
|
457
464
|
const limit = mode === "all" ? Number.POSITIVE_INFINITY : 1;
|
|
@@ -474,6 +481,12 @@ export class AgentHarness {
|
|
|
474
481
|
if (messages.length === 0) {
|
|
475
482
|
return messages;
|
|
476
483
|
}
|
|
484
|
+
const lane = queue === this.steerQueue ? "steer" : queue === this.followUpQueue ? "followUp" : undefined;
|
|
485
|
+
if (lane !== undefined) {
|
|
486
|
+
for (const m of messages)
|
|
487
|
+
drainedFromLane.set(m, lane);
|
|
488
|
+
this.drainedPendingInjection.push(...messages);
|
|
489
|
+
}
|
|
477
490
|
try {
|
|
478
491
|
await this.emitQueueUpdate();
|
|
479
492
|
for (const m of messages) {
|
|
@@ -485,15 +498,28 @@ export class AgentHarness {
|
|
|
485
498
|
catch {
|
|
486
499
|
}
|
|
487
500
|
}
|
|
488
|
-
engineNotePayloads.delete(m);
|
|
489
501
|
}
|
|
490
502
|
return messages;
|
|
491
503
|
}
|
|
492
504
|
catch (error) {
|
|
493
505
|
queue.unshift(...messages);
|
|
506
|
+
this.unbookInFlight(messages);
|
|
494
507
|
throw normalizeHookError(error);
|
|
495
508
|
}
|
|
496
509
|
}
|
|
510
|
+
unbookInFlight(messages) {
|
|
511
|
+
if (messages.length === 0 || this.drainedPendingInjection.length === 0)
|
|
512
|
+
return;
|
|
513
|
+
const gone = new Set(messages);
|
|
514
|
+
this.drainedPendingInjection = this.drainedPendingInjection.filter((m) => !gone.has(m));
|
|
515
|
+
}
|
|
516
|
+
settleInjectedFrame(message) {
|
|
517
|
+
if (!this.drainedPendingInjection.includes(message))
|
|
518
|
+
return;
|
|
519
|
+
this.unbookInFlight([message]);
|
|
520
|
+
userInputParkRecords.delete(message);
|
|
521
|
+
engineNotePayloads.delete(message);
|
|
522
|
+
}
|
|
497
523
|
createLoopConfig(getTurnState, setTurnState) {
|
|
498
524
|
const turnState = getTurnState();
|
|
499
525
|
const self = this;
|
|
@@ -633,6 +659,7 @@ export class AgentHarness {
|
|
|
633
659
|
let entryId;
|
|
634
660
|
if (!isEmptyFailureAssistant(event.message)) {
|
|
635
661
|
entryId = await this.session.appendMessage(event.message);
|
|
662
|
+
this.settleInjectedFrame(event.message);
|
|
636
663
|
}
|
|
637
664
|
await this.emitAny(entryId !== undefined ? { ...event, entryId } : event, signal);
|
|
638
665
|
return;
|
|
@@ -656,7 +683,7 @@ export class AgentHarness {
|
|
|
656
683
|
}
|
|
657
684
|
if (event.type === "agent_end") {
|
|
658
685
|
await this.flushPendingSessionWrites();
|
|
659
|
-
this.sweepUndrainedEngineNotes([this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
686
|
+
this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
660
687
|
const undrainedUser = this.announceUndrainedUserInputs();
|
|
661
688
|
this.phase = "idle";
|
|
662
689
|
await this.emitAny(event, signal);
|
|
@@ -757,6 +784,19 @@ export class AgentHarness {
|
|
|
757
784
|
throw new AgentHarnessError("busy", "AgentHarness is busy");
|
|
758
785
|
}
|
|
759
786
|
this._stopAfterTurn = false;
|
|
787
|
+
this.aborting = false;
|
|
788
|
+
if (this.drainedPendingInjection.length > 0) {
|
|
789
|
+
const stranded = this.drainedPendingInjection;
|
|
790
|
+
this.drainedPendingInjection = [];
|
|
791
|
+
for (let i = stranded.length - 1; i >= 0; i--) {
|
|
792
|
+
const frame = stranded[i];
|
|
793
|
+
const lane = drainedFromLane.get(frame);
|
|
794
|
+
if (lane === "steer")
|
|
795
|
+
this.steerQueue.unshift(frame);
|
|
796
|
+
else if (lane === "followUp")
|
|
797
|
+
this.followUpQueue.unshift(frame);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
760
800
|
this.phase = "turn";
|
|
761
801
|
const finishRunPromise = this.startRunPromise();
|
|
762
802
|
try {
|
|
@@ -786,6 +826,8 @@ export class AgentHarness {
|
|
|
786
826
|
engineNotePayloads.set(m, options.enginePayload);
|
|
787
827
|
if (isEngineAuthoredInjection(options))
|
|
788
828
|
engineAuthoredInjections.add(m);
|
|
829
|
+
if (options?.parkRecord !== undefined)
|
|
830
|
+
userInputParkRecords.set(m, options.parkRecord);
|
|
789
831
|
queue.push(m);
|
|
790
832
|
await this.emitQueueUpdate();
|
|
791
833
|
}
|
|
@@ -938,12 +980,41 @@ export class AgentHarness {
|
|
|
938
980
|
throw normalizeHarnessError(error, "session");
|
|
939
981
|
}
|
|
940
982
|
}
|
|
983
|
+
readParkableUserInputs() {
|
|
984
|
+
const out = [];
|
|
985
|
+
for (const q of [this.drainedPendingInjection, this.steerQueue, this.followUpQueue]) {
|
|
986
|
+
for (const m of q) {
|
|
987
|
+
const record = userInputParkRecords.get(m);
|
|
988
|
+
if (record !== undefined)
|
|
989
|
+
out.push(record);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return out;
|
|
993
|
+
}
|
|
994
|
+
dropParkedUserInputs(records) {
|
|
995
|
+
const dropped = { steer: 0, followUp: 0 };
|
|
996
|
+
if (records.length === 0)
|
|
997
|
+
return dropped;
|
|
998
|
+
const migrated = new Set(records);
|
|
999
|
+
for (const q of [this.drainedPendingInjection, this.steerQueue, this.followUpQueue]) {
|
|
1000
|
+
const fixedLane = q === this.steerQueue ? "steer" : q === this.followUpQueue ? "followUp" : undefined;
|
|
1001
|
+
for (let i = 0; i < q.length; i++) {
|
|
1002
|
+
const frame = q[i];
|
|
1003
|
+
const record = userInputParkRecords.get(frame);
|
|
1004
|
+
if (record === undefined || !migrated.has(record))
|
|
1005
|
+
continue;
|
|
1006
|
+
const lane = fixedLane ?? drainedFromLane.get(frame);
|
|
1007
|
+
userInputParkRecords.delete(frame);
|
|
1008
|
+
q.splice(i, 1);
|
|
1009
|
+
i--;
|
|
1010
|
+
if (lane !== undefined)
|
|
1011
|
+
dropped[lane] += 1;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return dropped;
|
|
1015
|
+
}
|
|
941
1016
|
async abort() {
|
|
942
|
-
this.
|
|
943
|
-
const clearedSteer = [...this.steerQueue];
|
|
944
|
-
const clearedFollowUp = [...this.followUpQueue];
|
|
945
|
-
this.steerQueue = [];
|
|
946
|
-
this.followUpQueue = [];
|
|
1017
|
+
this.aborting = true;
|
|
947
1018
|
this.runAbortController?.abort();
|
|
948
1019
|
const errors = [];
|
|
949
1020
|
try {
|
|
@@ -958,8 +1029,13 @@ export class AgentHarness {
|
|
|
958
1029
|
catch (error) {
|
|
959
1030
|
errors.push(toError(error));
|
|
960
1031
|
}
|
|
1032
|
+
this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
|
|
1033
|
+
const userAuthored = (m) => !engineAuthoredInjections.has(m);
|
|
1034
|
+
const inFlightOf = (lane) => this.drainedPendingInjection.filter((m) => userAuthored(m) && drainedFromLane.get(m) === lane);
|
|
1035
|
+
const undrainedSteer = [...inFlightOf("steer"), ...this.steerQueue.filter(userAuthored)];
|
|
1036
|
+
const undrainedFollowUp = [...inFlightOf("followUp"), ...this.followUpQueue.filter(userAuthored)];
|
|
961
1037
|
try {
|
|
962
|
-
await this.emitOwn({ type: "abort",
|
|
1038
|
+
await this.emitOwn({ type: "abort", undrainedSteer, undrainedFollowUp });
|
|
963
1039
|
}
|
|
964
1040
|
catch (error) {
|
|
965
1041
|
errors.push(toError(error));
|
|
@@ -968,7 +1044,7 @@ export class AgentHarness {
|
|
|
968
1044
|
const cause = errors.length === 1 ? errors[0] : new AggregateError(errors, "Abort completed with errors");
|
|
969
1045
|
throw normalizeHarnessError(cause, "hook");
|
|
970
1046
|
}
|
|
971
|
-
return {
|
|
1047
|
+
return { undrainedSteer, undrainedFollowUp };
|
|
972
1048
|
}
|
|
973
1049
|
async waitForIdle() {
|
|
974
1050
|
await this.runPromise;
|
|
@@ -10,8 +10,10 @@ export declare const COMPACTION_SUMMARY_SUFFIX = "\n</summary>\n\nRecent message
|
|
|
10
10
|
export declare function bashExecutionToText(msg: BashExecutionMessage): string;
|
|
11
11
|
/** Build a persisted compaction summary message from the repository timestamp string.
|
|
12
12
|
* `elidedMessages` (RB-398-b①): the render-time count of messages this summary stands in for —
|
|
13
|
-
* drives the wrapper's scale disclosure; omitted ⇒ the wrapper stays byte-identical.
|
|
14
|
-
|
|
13
|
+
* drives the wrapper's scale disclosure; omitted ⇒ the wrapper stays byte-identical.
|
|
14
|
+
* `unsummarizedMessages` (PTL-shortfall accounting): how many of those the summary does NOT cover
|
|
15
|
+
* (input shed by a reactive prompt-too-long retry); omitted / 0 ⇒ no uncovered clause. */
|
|
16
|
+
export declare function createCompactionSummaryMessage(summary: string, tokensBefore: number, timestamp: string, elidedMessages?: number, unsummarizedMessages?: number): CompactionSummaryMessage;
|
|
15
17
|
/** Build a custom transcript message that can be shown and replayed into context. */
|
|
16
18
|
export declare function createCustomMessage(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details: unknown, timestamp: string): CustomMessage;
|
|
17
19
|
/** Prefix stamped on a salvaged block so readers (model, summaries, humans) see it is a normalization
|
|
@@ -36,13 +36,14 @@ export function bashExecutionToText(msg) {
|
|
|
36
36
|
}
|
|
37
37
|
return text;
|
|
38
38
|
}
|
|
39
|
-
export function createCompactionSummaryMessage(summary, tokensBefore, timestamp, elidedMessages) {
|
|
39
|
+
export function createCompactionSummaryMessage(summary, tokensBefore, timestamp, elidedMessages, unsummarizedMessages) {
|
|
40
40
|
return {
|
|
41
41
|
role: "compactionSummary",
|
|
42
42
|
summary,
|
|
43
43
|
tokensBefore,
|
|
44
44
|
timestamp: requireSessionTimestampMs(timestamp, "compaction summary timestamp"),
|
|
45
45
|
...(elidedMessages !== undefined ? { elidedMessages } : {}),
|
|
46
|
+
...(unsummarizedMessages !== undefined ? { unsummarizedMessages } : {}),
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
export function createCustomMessage(customType, content, display, details, timestamp) {
|
|
@@ -131,10 +132,14 @@ export function convertToLlm(messages) {
|
|
|
131
132
|
}
|
|
132
133
|
case "compactionSummary": {
|
|
133
134
|
const n = message.elidedMessages;
|
|
135
|
+
const uncovered = message.unsummarizedMessages;
|
|
136
|
+
const uncoveredClause = n !== undefined && n > 0 && uncovered !== undefined && uncovered > 0
|
|
137
|
+
? ` ${Math.min(uncovered, n)} of them could not be included in the summarization input and are NOT covered by this summary.`
|
|
138
|
+
: "";
|
|
134
139
|
const disclosure = n !== undefined && n > 0
|
|
135
140
|
? `\n\n(For scale: this summary stands in for ${n} earlier message${n === 1 ? "" : "s"}${message.tokensBefore > 0
|
|
136
141
|
? ` — approximately ${message.tokensBefore} tokens of context before compaction`
|
|
137
|
-
: ""}
|
|
142
|
+
: ""}.${uncoveredClause})`
|
|
138
143
|
: "";
|
|
139
144
|
return {
|
|
140
145
|
role: "user",
|
|
@@ -889,8 +889,12 @@ export interface SavePointEvent {
|
|
|
889
889
|
}
|
|
890
890
|
export interface AbortEvent {
|
|
891
891
|
type: "abort";
|
|
892
|
-
|
|
893
|
-
|
|
892
|
+
/** backlog #389 — the caller frames still QUEUED when the abort fired. They are no longer cleared by
|
|
893
|
+
* `abort()` (CC keeps its queue on interrupt): the terminal `agent_end` account announces them, so
|
|
894
|
+
* this list is the same set that reaches `onUndrainedUserInputs`, observed one step earlier. */
|
|
895
|
+
undrainedSteer: AgentMessage[];
|
|
896
|
+
/** backlog #389 — the follow-up half of {@link AbortEvent.undrainedSteer}. */
|
|
897
|
+
undrainedFollowUp: AgentMessage[];
|
|
894
898
|
}
|
|
895
899
|
export interface SettledEvent {
|
|
896
900
|
type: "settled";
|
|
@@ -1044,10 +1048,12 @@ export type AgentHarnessEventResultMap = {
|
|
|
1044
1048
|
abort: undefined;
|
|
1045
1049
|
settled: undefined;
|
|
1046
1050
|
};
|
|
1047
|
-
/**
|
|
1051
|
+
/** backlog #389 — what an abort found still QUEUED (it no longer removes caller frames; see
|
|
1052
|
+
* {@link AgentHarness.abort}). Engine-note frames are absent: their own sweep recovered them for
|
|
1053
|
+
* per-session redelivery before this snapshot was taken. */
|
|
1048
1054
|
export interface AbortResult {
|
|
1049
|
-
|
|
1050
|
-
|
|
1055
|
+
undrainedSteer: AgentMessage[];
|
|
1056
|
+
undrainedFollowUp: AgentMessage[];
|
|
1051
1057
|
}
|
|
1052
1058
|
/** Compaction data supplied by hooks or returned from compaction preparation. */
|
|
1053
1059
|
export interface CompactResult {
|
|
@@ -489,6 +489,13 @@ export interface CompactionSummaryMessage {
|
|
|
489
489
|
* (`convertToLlm`); absent (directly constructed messages, older render paths) ⇒ the wrapper is
|
|
490
490
|
* byte-identical to the historical constant. */
|
|
491
491
|
elidedMessages?: number;
|
|
492
|
+
/** PTL-shortfall accounting: how many of those {@link elidedMessages} the summary does NOT cover — messages a reactive
|
|
493
|
+
* prompt-too-long retry shed from the summarization input after the cut was already committed.
|
|
494
|
+
* Sourced from the compaction entry's own write-side accounting
|
|
495
|
+
* (`CompactionDetails.unsummarizedMessages`), same reason as its sibling. Absent (the lossless
|
|
496
|
+
* common case, directly constructed messages, pre-field entries) ⇒ no uncovered clause, wrapper
|
|
497
|
+
* byte-identical. */
|
|
498
|
+
unsummarizedMessages?: number;
|
|
492
499
|
/** Optional estimated context tokens after compaction. */
|
|
493
500
|
tokensAfter?: number;
|
|
494
501
|
/** Optional first retained entry id from the compaction range. */
|
|
@@ -183,6 +183,16 @@ export class StreamingImportValidator {
|
|
|
183
183
|
if (elided !== undefined && !(typeof elided === "number" && Number.isSafeInteger(elided) && elided >= 0)) {
|
|
184
184
|
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid elidedMessages count (non-negative integer)`);
|
|
185
185
|
}
|
|
186
|
+
const uncovered = e.details?.unsummarizedMessages;
|
|
187
|
+
if (uncovered !== undefined) {
|
|
188
|
+
if (!(typeof uncovered === "number" && Number.isSafeInteger(uncovered) && uncovered >= 0)) {
|
|
189
|
+
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid unsummarizedMessages count (non-negative integer)`);
|
|
190
|
+
}
|
|
191
|
+
if (uncovered > 0 && !(typeof elided === "number" && elided >= uncovered)) {
|
|
192
|
+
throw new SessionError("invalid_session", `compaction "${e.id}" carries unsummarizedMessages ${uncovered} without an elidedMessages count at least that ` +
|
|
193
|
+
`large (uncovered messages are a subset of the folded ones)`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
186
196
|
const refs = e.details?.persistedOutputRefs;
|
|
187
197
|
if (refs !== undefined) {
|
|
188
198
|
if (!Array.isArray(refs) || refs.length > PERSISTED_OUTPUT_REFS_MAX_ENTRIES) {
|