@stackstackstack/dsh-compaction-basic 0.1.5

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/lib/index.js ADDED
@@ -0,0 +1,962 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { CompactionEngine, CompactionId, ManualCompactionError, compactCheckpointSource, toolPairingBalancedAfter, toolPairingBalancedBefore } from "@stackstackstack/dsh-compaction";
3
+ import { BlockAssembler, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, assertNever, contentHasImage, createUserMessage, deepFreeze, errorChain } from "@stackstackstack/dsh-llm";
4
+ import { randomUUID } from "node:crypto";
5
+ import { isDeepStrictEqual } from "node:util";
6
+ //#region lib/types/config.js
7
+ /**
8
+ * Load-time validation and routed-model policy resolution for compaction-basic.
9
+ *
10
+ * @module @stackstackstack/dsh-compaction-basic/config
11
+ */
12
+ /** Default request-pressure fraction for every routed model. */
13
+ const DEFAULT_THRESHOLD_RATIO = .8;
14
+ /** Default verbatim-tail fraction for every routed model. */
15
+ const DEFAULT_RETAIN_RATIO = .16;
16
+ /** Fields shared by top-level defaults and exact-target overrides. */
17
+ const POLICY_CONFIG_KEYS = [
18
+ "thresholdRatio",
19
+ "retainRatio",
20
+ "retainTokens",
21
+ "summarizationProvider",
22
+ "summarizationModel",
23
+ "maxTokens",
24
+ "compactionRetries",
25
+ "maxOverflowRetries"
26
+ ];
27
+ /** Complete public top-level configuration key set. */
28
+ const BASIC_COMPACT_CONFIG_KEYS = new Set([
29
+ ...POLICY_CONFIG_KEYS,
30
+ "modelPolicies",
31
+ "auto"
32
+ ]);
33
+ /** Complete exact-target override key set. */
34
+ const MODEL_POLICY_KEYS = new Set([
35
+ "provider",
36
+ "model",
37
+ ...POLICY_CONFIG_KEYS
38
+ ]);
39
+ /** Target-specific pressure configuration failure eligible for warning suppression. */
40
+ var TargetPressureConfigError = class extends Error {
41
+ targetKey;
42
+ /**
43
+ * @param targetKey - exact provider/model route used as the warning key.
44
+ * @param message - actionable configuration failure detail.
45
+ */
46
+ constructor(targetKey, message) {
47
+ super(message);
48
+ this.targetKey = targetKey;
49
+ }
50
+ };
51
+ /**
52
+ * Resolve and validate service defaults plus exact-target partial overrides.
53
+ * @param config - untrusted plugin configuration after Loader normalization.
54
+ * @returns detached immutable defaults and validated exact-target overrides.
55
+ */
56
+ function resolveConfig(config = {}) {
57
+ validateKeys(config, BASIC_COMPACT_CONFIG_KEYS, "BasicCompactionConfig");
58
+ validatePolicy(config, "BasicCompactionConfig");
59
+ if (config.auto !== void 0 && typeof config.auto !== "boolean") throw new Error("BasicCompactionConfig: auto must be a boolean");
60
+ const thresholdRatio = config.thresholdRatio ?? DEFAULT_THRESHOLD_RATIO;
61
+ const retention = resolveRetention(config, { retainRatio: DEFAULT_RETAIN_RATIO });
62
+ validateRatioRetention(thresholdRatio, retention, "BasicCompactionConfig");
63
+ const modelPolicies = resolveModelPolicies(config.modelPolicies);
64
+ for (const [index, policy] of modelPolicies.entries()) validateRatioRetention(policy.thresholdRatio ?? thresholdRatio, resolveRetention(policy, retention), `BasicCompactionConfig: modelPolicies[${index}]`);
65
+ return deepFreeze({
66
+ thresholdRatio,
67
+ ...retention,
68
+ summarizationProvider: config.summarizationProvider ?? "",
69
+ summarizationModel: config.summarizationModel ?? "",
70
+ maxTokens: config.maxTokens ?? 8192,
71
+ compactionRetries: config.compactionRetries ?? 1,
72
+ maxOverflowRetries: config.maxOverflowRetries ?? 1,
73
+ modelPolicies,
74
+ auto: config.auto ?? true
75
+ });
76
+ }
77
+ /**
78
+ * Merge the exact provider/model override over the validated default policy.
79
+ * @param config - validated service defaults and override table.
80
+ * @param target - exact durable provider/model route to match.
81
+ * @returns detached immutable policy before model-capacity scaling.
82
+ */
83
+ function resolveTargetPolicy(config, target) {
84
+ const override = config.modelPolicies.find((policy) => policy.provider === target.provider && policy.model === target.model);
85
+ const inheritedRetention = config.retainTokens === void 0 ? { retainRatio: config.retainRatio } : { retainTokens: config.retainTokens };
86
+ return deepFreeze({
87
+ target: {
88
+ provider: target.provider,
89
+ model: target.model
90
+ },
91
+ thresholdRatio: override?.thresholdRatio ?? config.thresholdRatio,
92
+ ...resolveRetention(override ?? {}, inheritedRetention),
93
+ summarizationProvider: override?.summarizationProvider ?? config.summarizationProvider,
94
+ summarizationModel: override?.summarizationModel ?? config.summarizationModel,
95
+ maxTokens: override?.maxTokens ?? config.maxTokens,
96
+ compactionRetries: override?.compactionRetries ?? config.compactionRetries,
97
+ maxOverflowRetries: override?.maxOverflowRetries ?? config.maxOverflowRetries
98
+ });
99
+ }
100
+ /**
101
+ * Scale one routed policy into concrete token budgets for its model capacity.
102
+ * @param policy - merged policy for the exact routed target.
103
+ * @param contextWindow - positive adapter-owned capacity for that target.
104
+ * @returns detached immutable pressure and retention budgets.
105
+ */
106
+ function resolveCompactSpec(policy, contextWindow) {
107
+ const targetKey = `${policy.target.provider}/${policy.target.model}`;
108
+ if (!Number.isInteger(contextWindow) || contextWindow <= 0) throw new TargetPressureConfigError(targetKey, `BasicCompactionConfig: contextWindow (${contextWindow}) must be a positive integer`);
109
+ const thresholdTokens = Math.floor(contextWindow * policy.thresholdRatio);
110
+ const retainTokens = policy.retainTokens === void 0 ? Math.floor(contextWindow * policy.retainRatio) : policy.retainTokens;
111
+ if (retainTokens >= thresholdTokens) throw new TargetPressureConfigError(targetKey, `BasicCompactionConfig: ${policy.target.provider}/${policy.target.model} retainTokens (${retainTokens}) must be less than threshold tokens ${thresholdTokens}`);
112
+ return deepFreeze({
113
+ target: { ...policy.target },
114
+ contextWindow,
115
+ thresholdRatio: policy.thresholdRatio,
116
+ thresholdTokens,
117
+ retainTokens,
118
+ summarizationProvider: policy.summarizationProvider,
119
+ summarizationModel: policy.summarizationModel,
120
+ maxTokens: policy.maxTokens,
121
+ compactionRetries: policy.compactionRetries,
122
+ maxOverflowRetries: policy.maxOverflowRetries
123
+ });
124
+ }
125
+ /** Choose an explicit retention form or inherit the already-resolved fallback. */
126
+ function resolveRetention(config, fallback) {
127
+ if (config.retainTokens !== void 0) return { retainTokens: config.retainTokens };
128
+ if (config.retainRatio !== void 0) return { retainRatio: config.retainRatio };
129
+ return fallback;
130
+ }
131
+ /** Reject a capacity-independent retention conflict at plugin load. */
132
+ function validateRatioRetention(thresholdRatio, retention, name) {
133
+ if (retention.retainRatio !== void 0 && retention.retainRatio >= thresholdRatio) throw new Error(`${name}: retainRatio (${retention.retainRatio}) must be less than the resolved thresholdRatio (${thresholdRatio})`);
134
+ }
135
+ /** Validate, detach, and reject duplicate exact-target policies. */
136
+ function resolveModelPolicies(configured) {
137
+ if (configured === void 0) return [];
138
+ if (!Array.isArray(configured)) throw new Error("BasicCompactionConfig: modelPolicies must be an array");
139
+ const seen = /* @__PURE__ */ new Set();
140
+ return configured.map((source, index) => {
141
+ assertModelPolicy(source, `BasicCompactionConfig: modelPolicies[${index}]`);
142
+ const key = `${source.provider}\u0000${source.model}`;
143
+ if (seen.has(key)) throw new Error(`BasicCompactionConfig: duplicate model policy for ${source.provider}/${source.model}`);
144
+ seen.add(key);
145
+ return { ...source };
146
+ });
147
+ }
148
+ /** Validate one untrusted exact-target override and narrow its public type. */
149
+ function assertModelPolicy(source, name) {
150
+ if (!isUnknownRecord(source)) throw new Error(`${name} must be an object`);
151
+ validateKeys(source, MODEL_POLICY_KEYS, name);
152
+ assertNonEmptyString(`${name}.provider`, source.provider);
153
+ assertNonEmptyString(`${name}.model`, source.model);
154
+ validatePolicy(source, name);
155
+ }
156
+ /** Validate the fields common to defaults and exact-target partial overrides. */
157
+ function validatePolicy(config, name) {
158
+ const thresholdRatio = config.thresholdRatio;
159
+ const retainRatio = config.retainRatio;
160
+ const retainTokens = config.retainTokens;
161
+ const maxTokens = config.maxTokens;
162
+ const compactionRetries = config.compactionRetries;
163
+ const maxOverflowRetries = config.maxOverflowRetries;
164
+ if (thresholdRatio !== void 0) assertRatio(`${name}.thresholdRatio`, thresholdRatio);
165
+ if (retainRatio !== void 0) assertRatio(`${name}.retainRatio`, retainRatio);
166
+ if (retainTokens !== void 0) assertNonNegativeInteger(`${name}.retainTokens`, retainTokens);
167
+ if (retainRatio !== void 0 && retainTokens !== void 0) throw new Error(`${name}: retainRatio and retainTokens are mutually exclusive`);
168
+ if (maxTokens !== void 0) assertPositiveInteger(`${name}.maxTokens`, maxTokens);
169
+ if (compactionRetries !== void 0) assertNonNegativeInteger(`${name}.compactionRetries`, compactionRetries);
170
+ if (maxOverflowRetries !== void 0) assertNonNegativeInteger(`${name}.maxOverflowRetries`, maxOverflowRetries);
171
+ validateSummarizationPair(config, name);
172
+ }
173
+ /** Require one scope to omit, clear, or replace the summarization target as a pair. */
174
+ function validateSummarizationPair(config, name) {
175
+ const provider = config.summarizationProvider;
176
+ const model = config.summarizationModel;
177
+ if (provider !== void 0 && typeof provider !== "string") throw new Error(`${name}.summarizationProvider must be a string`);
178
+ if (model !== void 0 && typeof model !== "string") throw new Error(`${name}.summarizationModel must be a string`);
179
+ if (provider === void 0 && model === void 0) return;
180
+ if (provider === void 0 || model === void 0 || provider.length === 0 !== (model.length === 0)) throw new Error(`${name}: summarizationProvider and summarizationModel must be set together as an empty or non-empty pair`);
181
+ }
182
+ /** Reject stale or misspelled keys before defaults can hide them. */
183
+ function validateKeys(config, keys, name) {
184
+ for (const key of Object.keys(config)) if (!keys.has(key)) throw new Error(`${name}: unknown key "${key}"`);
185
+ }
186
+ function isUnknownRecord(value) {
187
+ return typeof value === "object" && value !== null && !Array.isArray(value);
188
+ }
189
+ function assertNonEmptyString(name, value) {
190
+ if (typeof value !== "string" || value.length === 0) throw new Error(`${name} must be a non-empty string`);
191
+ }
192
+ function assertPositiveInteger(name, value) {
193
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${name} (${String(value)}) must be a positive integer`);
194
+ }
195
+ function assertNonNegativeInteger(name, value) {
196
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`${name} (${String(value)}) must be a non-negative integer`);
197
+ }
198
+ function assertRatio(name, value) {
199
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) throw new Error(`${name} (${String(value)}) must be a number in (0, 1]`);
200
+ }
201
+ //#endregion
202
+ //#region lib/types/summarizer.js
203
+ /**
204
+ * Default one-shot summarization and durable checkpoint framing.
205
+ *
206
+ * @module @stackstackstack/dsh-compaction-basic/summarizer
207
+ */
208
+ /** Tags wrapping the structured summary inside the landed checkpoint node. */
209
+ const SUMMARY_OPEN_TAG = "<compacted-summary>";
210
+ const SUMMARY_CLOSE_TAG = "</compacted-summary>";
211
+ /**
212
+ * The summarization directive, delivered as the FINAL user message after the
213
+ * replayed conversation rather than as a distinct summarizer system prompt.
214
+ * Keeping the conversation's own system prompt, tools, and message prefix in
215
+ * front of it makes the auxiliary call a genuine prefix of the last routed
216
+ * request, so the provider's KV cache is reused instead of invalidated.
217
+ */
218
+ const COMPACTION_INSTRUCTION = [
219
+ "You are now acting as a compaction engine for this AI coding assistant. Condense the conversation ABOVE into a structured checkpoint that lets another model resume the work with no loss of essential context.",
220
+ "",
221
+ "Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write \"(none)\" for an empty section — never drop a section.",
222
+ "",
223
+ "## Primary Request and Intent",
224
+ "- [the user's original and evolving goals; quote verbatim where the exact wording matters]",
225
+ "",
226
+ "## Key Technical Concepts",
227
+ "- [technologies, frameworks, patterns, and conventions in play]",
228
+ "",
229
+ "## Files and Code",
230
+ "- [exact path: why it matters, key changes or snippets]",
231
+ "",
232
+ "## Errors and Fixes",
233
+ "- [error: how it was resolved, plus any related user feedback]",
234
+ "",
235
+ "## Pending Jobs",
236
+ "- [explicitly requested work not yet completed]",
237
+ "",
238
+ "## Current Work",
239
+ "- [precisely what was in progress at this checkpoint]",
240
+ "",
241
+ "## Next Step",
242
+ "- [the single next action, directly in line with the most recent request, or \"(none)\"]",
243
+ "",
244
+ "## Critical Context",
245
+ "- [decisions and their rationale, constraints, user preferences, open questions, data needed to continue]",
246
+ "",
247
+ "Rules:",
248
+ "- Write concise English engineering prose. Preserve exact file paths, commands, error strings, identifiers, numeric values, function signatures, and syntax fragments.",
249
+ "- Capture user feedback and explicit instructions faithfully, especially corrections.",
250
+ "- Do NOT mention this summarization request or that the context was compacted.",
251
+ "- Output only the checkpoint text: do not call any tool or take any other action.",
252
+ `- If the conversation already contains a ${SUMMARY_OPEN_TAG} block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge newer information into a single consolidated summary under the same structure.`
253
+ ].join("\n");
254
+ /** Framing that makes the replacement user message established context. */
255
+ const CHECKPOINT_PREAMBLE = "This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.";
256
+ /**
257
+ * Run the default cache-reusing `ctx.llm.stream()` summarization call: replay
258
+ * the conversation prefix, then append the compaction instruction as the final
259
+ * user message so the provider's warm prefix cache is reused.
260
+ * @param ctx - context providing the LLM service.
261
+ * @param config - resolved backend configuration.
262
+ * @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
263
+ * @param agent - supplies routed-model history, fallback model, and session id.
264
+ * @param signal - optional cancellation forwarded to the adapter.
265
+ * @returns safe text-only summary blocks and the exact call envelope and output.
266
+ */
267
+ async function summarizeWithLlm(ctx, config, input, agent, signal) {
268
+ const latest = agent.session.requestHeader()?.config;
269
+ const configured = config.summarizationProvider.length === 0 ? void 0 : {
270
+ provider: config.summarizationProvider,
271
+ model: config.summarizationModel
272
+ };
273
+ const agentTarget = agent.options.provider !== void 0 && agent.options.provider.length > 0 && agent.options.model !== void 0 && agent.options.model.length > 0 ? {
274
+ provider: agent.options.provider,
275
+ model: agent.options.model
276
+ } : void 0;
277
+ const target = configured ?? latest ?? agentTarget;
278
+ if (target === void 0) throw new Error("no provider/model available for summarization: set both BasicCompactionConfig summarization fields, route one request, or set both AgentOptions fields");
279
+ const assembler = new BlockAssembler();
280
+ const messages = [...input.messages, createUserMessage({
281
+ content: [{
282
+ type: "text",
283
+ text: COMPACTION_INSTRUCTION
284
+ }],
285
+ source: {
286
+ kind: "plugin",
287
+ plugin: "dsh-compaction-basic"
288
+ }
289
+ })];
290
+ const options = {
291
+ provider: target.provider,
292
+ model: target.model,
293
+ messages,
294
+ ...input.system === void 0 ? {} : { system: input.system },
295
+ ...input.tools === void 0 ? {} : { tools: [...input.tools] },
296
+ maxTokens: config.maxTokens,
297
+ sessionId: agent.session.id,
298
+ purpose: "compaction",
299
+ ...signal === void 0 ? {} : { signal }
300
+ };
301
+ for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk);
302
+ const error = finishError(assembler.finish);
303
+ if (error !== void 0) throw error;
304
+ const rawOutput = assembler.blocks();
305
+ const summary = summaryText(rawOutput);
306
+ if (!summary.some((block) => block.text.trim().length > 0)) throw new Error("summarization produced no text summary content");
307
+ return {
308
+ summary,
309
+ rawOutput,
310
+ llmStreamCall: true,
311
+ provider: options.provider,
312
+ model: options.model,
313
+ maxTokens: config.maxTokens,
314
+ ...assembler.usage === void 0 ? {} : { usage: assembler.usage }
315
+ };
316
+ }
317
+ /**
318
+ * Wrap raw summary blocks in the durable checkpoint framing.
319
+ * @param summary - safe text-only model output.
320
+ * @returns content for the synthesized replacement user message.
321
+ */
322
+ function frameSummary(summary) {
323
+ return [
324
+ {
325
+ type: "text",
326
+ text: `${CHECKPOINT_PREAMBLE}\n\n${SUMMARY_OPEN_TAG}`
327
+ },
328
+ ...summary,
329
+ {
330
+ type: "text",
331
+ text: SUMMARY_CLOSE_TAG
332
+ }
333
+ ];
334
+ }
335
+ /** Map a terminal summarization finish to its fail-closed error. */
336
+ function finishError(finish) {
337
+ switch (finish.kind) {
338
+ case "error":
339
+ case "aborted": {
340
+ const error = new Error(finish.failure.message);
341
+ error.code = finish.failure.code;
342
+ return error;
343
+ }
344
+ case "max-tokens": {
345
+ const error = /* @__PURE__ */ new Error("summarization truncated at the token cap (incomplete checkpoint)");
346
+ error.code = "MAX_TOKENS";
347
+ return error;
348
+ }
349
+ default: return;
350
+ }
351
+ }
352
+ /** Reject visual output and keep only text before synthesizing a user message. */
353
+ function summaryText(blocks) {
354
+ if (contentHasImage(blocks)) throw new LlmError("compaction summary cannot contain image output", "UNSUPPORTED_CONTENT");
355
+ return blocks.filter((block) => block.type === "text");
356
+ }
357
+ //#endregion
358
+ //#region lib/types/region.js
359
+ /**
360
+ * Surface retention selection and the shared log-recorded compaction
361
+ * transaction for automatic open-turn and manual idle-session compaction.
362
+ *
363
+ * @module @stackstackstack/dsh-compaction-basic/region
364
+ */
365
+ /**
366
+ * Rejects a summary whose replacement boundaries are no longer the ones it was
367
+ * built from, distinguished from summarizer and shrink failures so a manual
368
+ * caller can report the two causes differently.
369
+ */
370
+ var SurfaceChangedError = class extends Error {};
371
+ /**
372
+ * Resolve the next head-anchored range while retaining a priced recent tail
373
+ * and never splitting an assistant tool-call/result pair.
374
+ * @param session - session supplying authoritative current surface positions.
375
+ * @param measurement - unified pressure and surface measurement from the conversation meter.
376
+ * @param retainTokens - minimum recent tail budget retained verbatim.
377
+ * @returns the inclusive positional seq range to compact, or `null`.
378
+ */
379
+ function selectCompactableRange(session, measurement, retainTokens) {
380
+ const pricedNodes = measurement.nodes;
381
+ if (pricedNodes.length === 0) return null;
382
+ const surfaceNodes = session.surface.nodes;
383
+ if (surfaceNodes.length !== pricedNodes.length || surfaceNodes.some((seq, index) => seq !== pricedNodes[index]?.seq)) throw new Error("compaction: token-meter surface does not match the current session surface");
384
+ let accumulated = 0;
385
+ let keepFromIdx = pricedNodes.length;
386
+ for (let index = pricedNodes.length - 1; index >= 0; index -= 1) {
387
+ accumulated += pricedNodes[index].tokens;
388
+ keepFromIdx = index;
389
+ if (accumulated >= retainTokens) break;
390
+ }
391
+ if (keepFromIdx === 0) return null;
392
+ while (keepFromIdx > 0) {
393
+ if (toolPairingBalancedBefore(session, surfaceNodes[keepFromIdx])) break;
394
+ keepFromIdx -= 1;
395
+ }
396
+ if (keepFromIdx === 0) return null;
397
+ return {
398
+ start: surfaceNodes[0],
399
+ end: surfaceNodes[keepFromIdx - 1]
400
+ };
401
+ }
402
+ /**
403
+ * Run the single compaction transaction over one selected positional span.
404
+ * Selection and validation are read-only. Idle/log validation and
405
+ * `compaction/start` are synchronously adjacent, so the durable opening marker is
406
+ * the compaction lock before summarization yields. Every later failure makes
407
+ * exactly one `compaction/end` attempt; a failed close deliberately leaves the
408
+ * unmatched start detectable.
409
+ * @param dependencies - conversation meter and dynamically dispatched summarizer hook.
410
+ * @param session - session whose surface is mutated.
411
+ * @param start - inclusive first surface-node seq.
412
+ * @param end - inclusive last surface-node seq.
413
+ * @param agent - agent used by the summarizer.
414
+ * @param options - bracket owner, stability rule, and optional durability checkpoint.
415
+ * @param signal - optional summarization cancellation signal.
416
+ * @returns the successful durable compaction result.
417
+ */
418
+ async function compactSurfaceRegion(dependencies, session, start, end, agent, options, signal) {
419
+ if (options.owner === null) signal?.throwIfAborted();
420
+ const selection = validateSurfaceRegion(session, start, end);
421
+ const entryState = inspectCompactionEntryState(session.events);
422
+ assertCompactionInactive(entryState.unmatchedCompactionStart, entryState.latestEndSeedSeq, "compaction");
423
+ let owner;
424
+ if (options.owner === null) {
425
+ if (entryState.openTurn !== null) throw new ManualCompactionError("busy", "manual compaction: the session already has an open turn");
426
+ owner = null;
427
+ } else {
428
+ if (entryState.openTurn === null) throw new Error("compactRegion: no open turn — automatic compaction events must be enclosed in a turn");
429
+ owner = entryState.openTurn;
430
+ }
431
+ const compactionId = CompactionId(randomUUID());
432
+ const lifecycle = {
433
+ compactionId,
434
+ ...options.sourceCommandId === void 0 ? {} : { sourceCommandId: options.sourceCommandId },
435
+ turn: owner
436
+ };
437
+ const startEvent = session.append("compaction/start", lifecycle);
438
+ const assertStable = options.stability === "whole-surface" ? assertWholeSurfaceUnchanged : assertSelectedSpanStable;
439
+ let failure;
440
+ let flushFailure;
441
+ let result;
442
+ let closed = false;
443
+ let closing = false;
444
+ let stage = "summary";
445
+ try {
446
+ const summarized = await summarizeCompaction(dependencies, prepareCompaction(dependencies, session, selection), agent, compactionId, options.sourceCommandId, signal);
447
+ if (options.owner === null) signal?.throwIfAborted();
448
+ assertStable(dependencies, session, summarized);
449
+ stage = "commit";
450
+ const pending = commitCompactionBody(session, startEvent, summarized);
451
+ closing = true;
452
+ const endEvent = session.append("compaction/end", lifecycle);
453
+ closed = true;
454
+ result = completeCompaction(pending, endEvent);
455
+ } catch (error) {
456
+ failure = {
457
+ error,
458
+ stage: closing ? "commit" : stage
459
+ };
460
+ if (!closing) {
461
+ closing = true;
462
+ try {
463
+ session.append("compaction/end", {
464
+ ...lifecycle,
465
+ error: errorChain(error)
466
+ });
467
+ closed = true;
468
+ } catch (closeError) {
469
+ failure = {
470
+ error: closeError,
471
+ stage: "commit"
472
+ };
473
+ }
474
+ }
475
+ }
476
+ if (closed && options.flush !== void 0) try {
477
+ await options.flush();
478
+ } catch (error) {
479
+ flushFailure = error;
480
+ }
481
+ if (options.owner === null) signal?.throwIfAborted();
482
+ if (failure !== void 0) {
483
+ if (options.owner === null) throwManualFailure(failure);
484
+ throw failure.error;
485
+ }
486
+ if (flushFailure !== void 0) throw new ManualCompactionError("persistence", "manual compaction durability checkpoint failed", { cause: flushFailure });
487
+ /* v8 ignore next -- every path without a result records and throws a failure above. */
488
+ if (result === void 0) throw new Error("compaction committed without a result");
489
+ return result;
490
+ }
491
+ /** Classify one closed manual attempt without weakening cancellation precedence. */
492
+ function throwManualFailure(failure) {
493
+ if (failure.stage === "commit") throw new ManualCompactionError("commit", "manual compaction did not commit cleanly", { cause: failure.error });
494
+ if (failure.error instanceof SurfaceChangedError) throw new ManualCompactionError("changed", "the compacted history changed during manual compaction", { cause: failure.error });
495
+ throw new ManualCompactionError("summary", "manual compaction could not produce a smaller summary", { cause: failure.error });
496
+ }
497
+ /**
498
+ * Reject a durable unmatched compaction marker unless a later constructor-seed
499
+ * boundary proves that its owner belongs to an earlier session lifecycle.
500
+ * @param unmatchedCompactionStart - latest unmatched opening marker, if any.
501
+ * @param latestEndSeedSeq - newest constructor-seed boundary, if any.
502
+ * @param stage - operation label included in the busy diagnostic.
503
+ */
504
+ function assertCompactionInactive(unmatchedCompactionStart, latestEndSeedSeq, stage) {
505
+ if (unmatchedCompactionStart === void 0 || latestEndSeedSeq !== void 0 && latestEndSeedSeq > unmatchedCompactionStart.seq) return;
506
+ throw new ManualCompactionError("busy", `${stage}: compaction already in progress; the session compaction lock is already active`);
507
+ }
508
+ /**
509
+ * Recheck the durable compaction lock after an asynchronous policy decision.
510
+ * @param session - session whose latest marker state is inspected.
511
+ * @param stage - operation label included in the busy diagnostic.
512
+ */
513
+ function assertNoActiveCompaction(session, stage) {
514
+ const entryState = inspectCompactionEntryState(session.events);
515
+ assertCompactionInactive(entryState.unmatchedCompactionStart, entryState.latestEndSeedSeq, stage);
516
+ }
517
+ /** Validate one requested surface-position span before asynchronous work begins. */
518
+ function validateSurfaceRegion(session, start, end) {
519
+ const nodes = session.surface.nodes;
520
+ const startIdx = nodes.indexOf(start);
521
+ const endIdx = nodes.indexOf(end);
522
+ if (startIdx === -1) throw new Error(`compactRegion: start seq ${start} not found in surface`);
523
+ if (endIdx === -1) throw new Error(`compactRegion: end seq ${end} not found in surface`);
524
+ if (startIdx > endIdx) throw new Error(`compactRegion: start seq ${start} (position ${startIdx}) is after end seq ${end} (position ${endIdx}) on the surface`);
525
+ if (!toolPairingBalancedBefore(session, nodes[startIdx])) throw new Error(`compactRegion: start seq ${start} is not a balanced boundary (would split a step's tool-call/result pair)`);
526
+ if (!toolPairingBalancedAfter(session, nodes[endIdx])) throw new Error(`compactRegion: end seq ${end} is not a balanced boundary (would split a step, or the step is still open)`);
527
+ return {
528
+ start,
529
+ end,
530
+ startIdx,
531
+ endIdx,
532
+ shadowedSeqs: nodes.slice(startIdx, endIdx + 1)
533
+ };
534
+ }
535
+ /** Snapshot pricing and replay input for a validated surface range. */
536
+ function prepareCompaction(dependencies, session, selection) {
537
+ const measurement = dependencies.meter.measure(session);
538
+ const selectedNodes = measurement.nodes.slice(selection.startIdx, selection.endIdx + 1);
539
+ if (selectedNodes.length !== selection.shadowedSeqs.length || selectedNodes.some((node, index) => node.seq !== selection.shadowedSeqs[index])) throw new SurfaceChangedError("compaction: selected surface changed before summarization began");
540
+ return {
541
+ ...selection,
542
+ measurement,
543
+ selectedNodes,
544
+ shadowedTokenCount: selectedNodes.reduce((total, node) => total + node.tokens, 0),
545
+ input: buildSummarizationInput(session, selection.shadowedSeqs)
546
+ };
547
+ }
548
+ /** Run the summarizer and frame its replacement checkpoint. */
549
+ async function summarizeCompaction(dependencies, prepared, agent, compactionId, sourceCommandId, signal) {
550
+ const summaryResult = await dependencies.summarize(prepared.input, agent, signal);
551
+ const checkpointMessage = createUserMessage({
552
+ content: frameSummary(summaryResult.summary),
553
+ source: compactCheckpointSource(compactionId, sourceCommandId)
554
+ });
555
+ const framedSummaryTokenCount = dependencies.meter.estimateMessage(checkpointMessage);
556
+ if (framedSummaryTokenCount >= prepared.shadowedTokenCount) throw new Error(`summary is not smaller than the shadowed content (${framedSummaryTokenCount} estimated framed tokens >= ${prepared.shadowedTokenCount})`);
557
+ return {
558
+ ...prepared,
559
+ ...summaryResult,
560
+ checkpointMessage
561
+ };
562
+ }
563
+ /** Reject a summary prepared against any earlier surface generation. */
564
+ function assertWholeSurfaceUnchanged(dependencies, session, prepared) {
565
+ if (!isDeepStrictEqual(dependencies.meter.measure(session).nodes, prepared.measurement.nodes)) throw new SurfaceChangedError("compaction: session surface changed during summarization");
566
+ }
567
+ /**
568
+ * Require only that the selected span remain the same present, contiguous,
569
+ * equally priced, balanced replacement target. Nodes added outside it remain
570
+ * visible and do not invalidate the summary.
571
+ */
572
+ function assertSelectedSpanStable(dependencies, session, prepared) {
573
+ let current;
574
+ try {
575
+ current = validateSurfaceRegion(session, prepared.start, prepared.end);
576
+ } catch (error) {
577
+ throw new SurfaceChangedError("compaction: the selected span is no longer a valid replacement target", { cause: error });
578
+ }
579
+ if (!isDeepStrictEqual([...current.shadowedSeqs], [...prepared.shadowedSeqs])) throw new SurfaceChangedError("compaction: the selected span changed during summarization");
580
+ if (!isDeepStrictEqual(dependencies.meter.measure(session).nodes.slice(current.startIdx, current.endIdx + 1), prepared.selectedNodes)) throw new SurfaceChangedError("compaction: the selected span was rewritten during summarization");
581
+ }
582
+ /** Append one completed summary record and replacement body without yielding. */
583
+ function commitCompactionBody(session, startEvent, summarized) {
584
+ const { start, end, shadowedSeqs, shadowedTokenCount, summary, provider, model, maxTokens, usage, checkpointMessage } = summarized;
585
+ const callProvenance = summarized.llmStreamCall === true ? {
586
+ rawOutput: summarized.rawOutput,
587
+ llmStreamCall: true
588
+ } : summarized.rawOutput === void 0 ? {} : { rawOutput: summarized.rawOutput };
589
+ const summaryEvent = session.append("compaction/summary", {
590
+ compactionId: startEvent.data.compactionId,
591
+ ...startEvent.data.sourceCommandId === void 0 ? {} : { sourceCommandId: startEvent.data.sourceCommandId },
592
+ summary,
593
+ ...callProvenance,
594
+ shadowedRange: {
595
+ start,
596
+ end
597
+ },
598
+ shadowedSeqs: [...shadowedSeqs],
599
+ shadowedTokenCount,
600
+ provider,
601
+ model,
602
+ ...maxTokens === void 0 ? {} : { maxTokens },
603
+ ...usage === void 0 ? {} : { usage }
604
+ });
605
+ session.append("user/message", checkpointMessage, {
606
+ surfaceOp: {
607
+ op: "replace",
608
+ start,
609
+ end
610
+ },
611
+ sourceEventSeqs: [
612
+ startEvent.seq,
613
+ summaryEvent.seq,
614
+ ...shadowedSeqs
615
+ ]
616
+ });
617
+ return {
618
+ compactionId: startEvent.data.compactionId,
619
+ ...startEvent.data.sourceCommandId === void 0 ? {} : { sourceCommandId: startEvent.data.sourceCommandId },
620
+ startSeq: startEvent.seq,
621
+ summarySeq: summaryEvent.seq,
622
+ summary,
623
+ shadowedRange: {
624
+ start,
625
+ end
626
+ },
627
+ shadowedSeqs: [...shadowedSeqs],
628
+ shadowedTokenCount
629
+ };
630
+ }
631
+ /** Attach the successfully appended close event to a pending result. */
632
+ function completeCompaction(pending, endEvent) {
633
+ return {
634
+ ...pending,
635
+ endSeq: endEvent.seq
636
+ };
637
+ }
638
+ /**
639
+ * Reconstruct the last routed request's cacheable prefix for the shadowed
640
+ * region: its system prompt and tool schemas, then the region's own derived
641
+ * messages in surface order. The summarizer appends only the compaction
642
+ * instruction after this, so the call is a genuine prefix of the conversation
643
+ * and reuses the provider's KV cache.
644
+ * @param session - session supplying the request header and per-node projection.
645
+ * @param shadowedSeqs - the surface-node seqs, in order, being compacted.
646
+ * @returns the replayed conversation prefix to condense.
647
+ */
648
+ function buildSummarizationInput(session, shadowedSeqs) {
649
+ const header = session.requestHeader();
650
+ const events = session.events;
651
+ const regionMessages = shadowedSeqs.map((seq) => session.deriveEventMessage(events[seq])).filter((message) => message !== null);
652
+ return {
653
+ ...header?.system === void 0 ? {} : { system: header.system },
654
+ ...header?.tools === void 0 ? {} : { tools: header.tools },
655
+ messages: regionMessages
656
+ };
657
+ }
658
+ /** Inspect open-turn, unmatched-compaction, and latest seed-boundary state independently. */
659
+ function inspectCompactionEntryState(events) {
660
+ let openTurn = null;
661
+ let openTurnStateKnown = false;
662
+ let unmatchedCompactionStart;
663
+ let compactionEntryStateKnown = false;
664
+ let latestEndSeedSeq;
665
+ for (let index = events.length - 1; index >= 0; index -= 1) {
666
+ const event = events[index];
667
+ if (latestEndSeedSeq === void 0 && event.type === "session/end-seed") latestEndSeedSeq = event.seq;
668
+ if (!compactionEntryStateKnown) {
669
+ if (event.type === "compaction/start") {
670
+ unmatchedCompactionStart = event;
671
+ compactionEntryStateKnown = true;
672
+ } else if (event.type === "compaction/end") compactionEntryStateKnown = true;
673
+ }
674
+ if (!openTurnStateKnown) {
675
+ if (event.type === "turn/start") {
676
+ openTurn = event.data.turn;
677
+ openTurnStateKnown = true;
678
+ } else if (event.type === "turn/end") openTurnStateKnown = true;
679
+ }
680
+ if (openTurnStateKnown && compactionEntryStateKnown && latestEndSeedSeq !== void 0) break;
681
+ }
682
+ return {
683
+ openTurn,
684
+ unmatchedCompactionStart,
685
+ latestEndSeedSeq
686
+ };
687
+ }
688
+ //#endregion
689
+ //#region lib/types/index.js
690
+ /**
691
+ * Basic replay-aware compaction backend.
692
+ *
693
+ * @module @stackstackstack/dsh-compaction-basic
694
+ */
695
+ /** Resolve the exact provider/model durably routed for the latest request. */
696
+ function routedTarget(session) {
697
+ const config = session.requestHeader()?.config;
698
+ if (config === void 0 || config.provider.length === 0 || config.model.length === 0) return;
699
+ return {
700
+ provider: config.provider,
701
+ model: config.model
702
+ };
703
+ }
704
+ /** Resolve the conversation target used to select an optional policy override. */
705
+ function conversationTarget(agent) {
706
+ const routed = routedTarget(agent.session);
707
+ if (routed !== void 0) return routed;
708
+ if (agent.options.provider === void 0 || agent.options.provider.length === 0 || agent.options.model === void 0 || agent.options.model.length === 0) return void 0;
709
+ return {
710
+ provider: agent.options.provider,
711
+ model: agent.options.model
712
+ };
713
+ }
714
+ const thresholdRatioSchema = z.number();
715
+ const retainRatioSchema = z.number();
716
+ const retainTokensSchema = z.number().step(1).min(0);
717
+ const summarizationProviderSchema = z.string();
718
+ const summarizationModelSchema = z.string();
719
+ const maxTokensSchema = z.number().step(1).min(1);
720
+ const compactionRetriesSchema = z.number().step(1).min(0);
721
+ const maxOverflowRetriesSchema = z.number().step(1).min(0);
722
+ const modelPolicy = z.object({
723
+ provider: z.string().required(),
724
+ model: z.string().required(),
725
+ thresholdRatio: thresholdRatioSchema,
726
+ retainRatio: retainRatioSchema,
727
+ retainTokens: retainTokensSchema,
728
+ summarizationProvider: summarizationProviderSchema,
729
+ summarizationModel: summarizationModelSchema,
730
+ maxTokens: maxTokensSchema,
731
+ compactionRetries: compactionRetriesSchema,
732
+ maxOverflowRetries: maxOverflowRetriesSchema
733
+ });
734
+ /**
735
+ * Dependency-light compaction backend using `ctx.tokenMeter` for pressure,
736
+ * retention, cited source events, and summary-convergence pricing.
737
+ *
738
+ * `summarize()` is the sole subclass customization hook; the replay and durable
739
+ * mutation strategy stays fixed so every pricing decision uses the singleton
740
+ * token meter.
741
+ */
742
+ var BasicCompactionEngine = class extends CompactionEngine {
743
+ static inject = [
744
+ "llm",
745
+ "tokenMeter",
746
+ "sessions"
747
+ ];
748
+ static Config = z.object({
749
+ thresholdRatio: thresholdRatioSchema,
750
+ retainRatio: retainRatioSchema,
751
+ retainTokens: retainTokensSchema,
752
+ summarizationProvider: summarizationProviderSchema,
753
+ summarizationModel: summarizationModelSchema,
754
+ maxTokens: maxTokensSchema,
755
+ compactionRetries: compactionRetriesSchema,
756
+ maxOverflowRetries: maxOverflowRetriesSchema,
757
+ modelPolicies: z.array(modelPolicy),
758
+ auto: z.boolean()
759
+ });
760
+ /** Resolved and validated compaction configuration. */
761
+ config;
762
+ warnedPressureConfigTargets = /* @__PURE__ */ new Set();
763
+ overflowRetries = /* @__PURE__ */ new WeakMap();
764
+ overflowAgents = /* @__PURE__ */ new WeakMap();
765
+ constructor(ctx, config = {}) {
766
+ super(ctx);
767
+ this.config = resolveConfig(config);
768
+ if (this.config.auto) this._registerAutomaticCompaction();
769
+ }
770
+ /**
771
+ * Register automatic between-step pressure and model-request overflow
772
+ * recovery. `compactIfNeeded` stays dynamically dispatched so subclass
773
+ * overrides are honored at event time.
774
+ */
775
+ _registerAutomaticCompaction() {
776
+ const { ctx } = this;
777
+ const logResult = (result, trigger) => {
778
+ ctx.logger.info(`compaction (${trigger}): shadowed ${result.shadowedSeqs.length} surface nodes (seqs ${result.shadowedRange.start}-${result.shadowedRange.end}, ~${result.shadowedTokenCount} tokens)`);
779
+ };
780
+ ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
781
+ if (!signal.aborted) try {
782
+ const result = await this.compactIfNeeded(agent, "pressure", signal);
783
+ if (result !== null) logResult(result, "step pressure");
784
+ } catch (error) {
785
+ if (error instanceof TargetPressureConfigError) {
786
+ if (this.warnedPressureConfigTargets.has(error.targetKey)) return next();
787
+ this.warnedPressureConfigTargets.add(error.targetKey);
788
+ }
789
+ const message = error instanceof Error ? error.message : String(error);
790
+ ctx.logger.warn(`step compaction failed: ${message}; continuing the turn`);
791
+ }
792
+ return next();
793
+ });
794
+ ctx.on("agent/status", ({ agent, status }) => {
795
+ if (status === "idle") this.overflowRetries.delete(agent);
796
+ });
797
+ ctx.on("session/event", (session, event) => {
798
+ if (event.type !== "assistant/message") return;
799
+ const agent = this.overflowAgents.get(session);
800
+ if (agent !== void 0) this.overflowRetries.delete(agent);
801
+ });
802
+ ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
803
+ if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE || signal.aborted) return next();
804
+ this.overflowAgents.set(agent.session, agent);
805
+ const target = routedTarget(agent.session);
806
+ if (target === void 0) return next();
807
+ const policy = resolveTargetPolicy(this.config, target);
808
+ const retries = this.overflowRetries.get(agent) ?? 0;
809
+ if (retries >= policy.maxOverflowRetries) return next();
810
+ const generation = agent.session.surface.replaceGeneration;
811
+ let result;
812
+ try {
813
+ result = await this.compactIfNeeded(agent, "context-overflow", signal);
814
+ } catch (recoveryError) {
815
+ const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
816
+ if (!signal.aborted && agent.session.surface.replaceGeneration > generation) {
817
+ ctx.logger.warn(`context-overflow compaction failed after durable surface progress: ${message}; retrying from the replacement surface`);
818
+ this.overflowRetries.set(agent, retries + 1);
819
+ return { kind: "retry" };
820
+ }
821
+ ctx.logger.warn(`context-overflow compaction failed: ${message}; ${signal.aborted ? "cancellation prevents retry" : "preserving the original request error"}`);
822
+ return next();
823
+ }
824
+ if (signal.aborted || agent.session.surface.replaceGeneration <= generation) return next();
825
+ if (result !== null) logResult(result, "context overflow recovery");
826
+ this.overflowRetries.set(agent, retries + 1);
827
+ return { kind: "retry" };
828
+ });
829
+ }
830
+ /**
831
+ * Summarize the replayed conversation region through a direct one-shot
832
+ * `ctx.llm.stream()` call whose prefix reuses the conversation's own system
833
+ * prompt, tools, and messages so the provider's KV cache is not invalidated.
834
+ * Override this sole hook for a template or remote summarizer.
835
+ * @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
836
+ * @param agent - supplies routed-model history, fallback model, and session id.
837
+ * @param signal - optional cancellation forwarded to the adapter.
838
+ * @returns safe text summary blocks and the exact auxiliary call envelope and output.
839
+ */
840
+ async summarize(input, agent, signal) {
841
+ const target = conversationTarget(agent);
842
+ const config = target === void 0 ? this.config : resolveTargetPolicy(this.config, target);
843
+ return summarizeWithLlm(this.ctx, config, input, agent, signal);
844
+ }
845
+ /**
846
+ * Compact for replayed step-boundary pressure or one provider-confirmed context
847
+ * overflow. Both triggers price the latest durable routed request envelope;
848
+ * overflow bypasses the normal threshold and retained-tail policy so it can
849
+ * force one useful balanced reduction.
850
+ * @param agent - agent whose latest durable routed request is measured.
851
+ * @param trigger - normal step-boundary pressure or context-overflow recovery.
852
+ * @param signal - live turn cancellation signal forwarded to summarization.
853
+ * @returns the latest summary compaction result, or `null` when no summary ran.
854
+ */
855
+ async compactIfNeeded(agent, trigger, signal) {
856
+ const target = routedTarget(agent.session);
857
+ if (target === void 0) return null;
858
+ const policy = resolveTargetPolicy(this.config, target);
859
+ const meter = this.ctx.tokenMeter;
860
+ let measurement = meter.measure(agent.session);
861
+ switch (trigger) {
862
+ case "context-overflow": break;
863
+ case "pressure": break;
864
+ /* v8 ignore next -- closed-union exhaustiveness guard */
865
+ default: assertNever(trigger, "compaction trigger");
866
+ }
867
+ const prune = this.ctx.get("toolResultPruner");
868
+ if (trigger === "context-overflow") {
869
+ if (prune !== void 0) {
870
+ prune.pruneSession(agent.session);
871
+ measurement = meter.measure(agent.session);
872
+ }
873
+ const range = selectCompactableRange(agent.session, measurement, 0);
874
+ if (range === null) return null;
875
+ return this.compactRegion(range.start, range.end, agent, signal);
876
+ }
877
+ const context = (await this.ctx.llm.resolveModelInfo(target.provider, target.model, signal)).context;
878
+ assertNoActiveCompaction(agent.session, "automatic pressure compaction");
879
+ const targetKey = `${target.provider}/${target.model}`;
880
+ if (context === void 0) throw new TargetPressureConfigError(targetKey, `compaction-basic: no context capacity for ${targetKey}; configure contextWindow on that adapter model`);
881
+ const spec = resolveCompactSpec(policy, context.contextWindow);
882
+ if (measurement.totalTokens < spec.thresholdTokens) return null;
883
+ if (prune !== void 0) {
884
+ prune.pruneSession(agent.session);
885
+ measurement = meter.measure(agent.session);
886
+ }
887
+ if (measurement.totalTokens < spec.thresholdTokens) return null;
888
+ let result = null;
889
+ for (let attempt = 0; attempt <= spec.compactionRetries; attempt += 1) {
890
+ const range = selectCompactableRange(agent.session, measurement, spec.retainTokens);
891
+ if (range === null) {
892
+ /* v8 ignore else -- concrete replacement preserves a compactable checkpoint; subclass hooks cannot mutate it. */
893
+ if (result === null) return null;
894
+ /* v8 ignore next -- paired with the defensive post-success branch above. */
895
+ break;
896
+ }
897
+ result = await this.compactRegion(range.start, range.end, agent, signal);
898
+ measurement = meter.measure(agent.session);
899
+ if (measurement.totalTokens < spec.thresholdTokens) return result;
900
+ }
901
+ throw new Error(`compaction still above threshold after ${spec.compactionRetries + 1} compaction attempts (${measurement.totalTokens} estimated tokens >= threshold ${spec.thresholdTokens})`);
902
+ }
903
+ /**
904
+ * Compact one inclusive positional range from the agent-owned surface using
905
+ * the effective token meter for all retention and shrink pricing.
906
+ * @param start - inclusive first surface-node seq.
907
+ * @param end - inclusive last surface-node seq.
908
+ * @param agent - owner of the target session, used by the summarizer.
909
+ * @param signal - optional summarization cancellation signal.
910
+ * @returns the successful durable compaction result.
911
+ */
912
+ async compactRegion(start, end, agent, signal) {
913
+ return compactSurfaceRegion(this.regionDependencies(), agent.session, start, end, agent, {
914
+ owner: "current-turn",
915
+ stability: "whole-surface"
916
+ }, signal);
917
+ }
918
+ /**
919
+ * Force one useful idle-session compaction below the pressure threshold, and
920
+ * resolve only after its standalone marker pair is durably checkpointed.
921
+ * @param agent - idle agent whose next-turn admission this call reserves.
922
+ * @param signal - cancellation scoped to this compaction request.
923
+ * @param sourceCommandId - initiating command identity for presentation correlation.
924
+ * @returns the committed result, or `null` when no safe useful range exists.
925
+ */
926
+ compactNow(agent, signal, sourceCommandId) {
927
+ signal.throwIfAborted();
928
+ try {
929
+ return agent.runMaintenance(async (agentSignal) => {
930
+ const operationSignal = AbortSignal.any([agentSignal, signal]);
931
+ try {
932
+ operationSignal.throwIfAborted();
933
+ const range = selectCompactableRange(agent.session, this.ctx.tokenMeter.measure(agent.session), 0);
934
+ if (range === null) return null;
935
+ return await compactSurfaceRegion(this.regionDependencies(), agent.session, range.start, range.end, agent, {
936
+ owner: null,
937
+ stability: "selected-span",
938
+ ...sourceCommandId === void 0 ? {} : { sourceCommandId },
939
+ flush: async () => {
940
+ await this.ctx.sessions.flush(agent.session);
941
+ }
942
+ }, operationSignal);
943
+ } catch (error) {
944
+ if (agentSignal.aborted && operationSignal.reason === agentSignal.reason) throw new ManualCompactionError("cancelled", "manual compaction was cancelled", { cause: error });
945
+ operationSignal.throwIfAborted();
946
+ throw error;
947
+ }
948
+ });
949
+ } catch (error) {
950
+ throw new ManualCompactionError("busy", "manual compaction requires an idle agent with no waking queued work", { cause: error });
951
+ }
952
+ }
953
+ /** Bind the effective token meter and dynamically dispatched summarizer hook. */
954
+ regionDependencies() {
955
+ return {
956
+ meter: this.ctx.tokenMeter,
957
+ summarize: (input, owner, abort) => this.summarize(input, owner, abort)
958
+ };
959
+ }
960
+ };
961
+ //#endregion
962
+ export { BasicCompactionEngine, BasicCompactionEngine as default };