dsh-context-fold 0.0.0-stage → 0.1.1

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.d.ts ADDED
@@ -0,0 +1,663 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Session, SessionEvent, SessionSeq } from "@deepseek-ai/dsh-session";
3
+ import { AssistantMessage, ContentBlock, Message } from "@deepseek-ai/dsh-llm";
4
+ import { Context } from "@deepseek-ai/cordis";
5
+ import { SessionMessageProjectionContext } from "@deepseek-ai/dsh-session/surface";
6
+ import { SpillStore } from "@deepseek-ai/dsh-spill";
7
+ /** Resolved, validated fold budgets. */
8
+ interface ContextFoldConfig {
9
+ /** Skip results smaller than this many characters. Never below 300 (design §3.4: negative return below that). */
10
+ minFoldChars: number;
11
+ /** Only fold when the candidate batch is worth the one cache invalidation. */
12
+ minBatchChars: number;
13
+ /** Never fold the newest N steps. */
14
+ keepRecentSteps: number;
15
+ /** Age condition that lets a single long turn fold without a turn boundary. */
16
+ staleSteps: number;
17
+ /** Split a very long turn into contiguous step ranges of this size. */
18
+ batchSpanSteps: number;
19
+ /** Fold reasoning blocks on historical tool-call turns (phase 2). */
20
+ foldReasoning: boolean;
21
+ /**
22
+ * Also fold reasoning on plain (non-tool-call) turns.
23
+ *
24
+ * Off by default because `dsh-llm-deepseek` drops plain-turn reasoning, so on
25
+ * that route folding it saves nothing. Turn it on for a route whose serializer
26
+ * re-sends plain-turn reasoning unconditionally (e.g. `dsh-router-core`), where
27
+ * it is a real saving.
28
+ */
29
+ foldPlainTurnReasoning: boolean;
30
+ /** Master switch; `false` means the plugin does nothing at all. */
31
+ enabled: boolean;
32
+ /** Compute and report candidates without mutating anything. */
33
+ dryRun: boolean;
34
+ }
35
+ /** Low-friction defaults from design §6.9. */
36
+ declare const DEFAULT_CONFIG: Readonly<ContextFoldConfig>;
37
+ /** Config schema shown in the settings UI. */
38
+ declare const Config: z<Schemastery.ObjectS<NoInfer<{
39
+ minFoldChars: z<number, number, "defined">;
40
+ minBatchChars: z<number, number, "defined">;
41
+ keepRecentSteps: z<number, number, "defined">;
42
+ staleSteps: z<number, number, "defined">;
43
+ batchSpanSteps: z<number, number, "defined">;
44
+ foldReasoning: z<boolean, boolean, "defined">;
45
+ foldPlainTurnReasoning: z<boolean, boolean, "defined">;
46
+ enabled: z<boolean, boolean, "defined">;
47
+ dryRun: z<boolean, boolean, "defined">;
48
+ }>>, Schemastery.ObjectT<NoInfer<{
49
+ minFoldChars: z<number, number, "defined">;
50
+ minBatchChars: z<number, number, "defined">;
51
+ keepRecentSteps: z<number, number, "defined">;
52
+ staleSteps: z<number, number, "defined">;
53
+ batchSpanSteps: z<number, number, "defined">;
54
+ foldReasoning: z<boolean, boolean, "defined">;
55
+ foldPlainTurnReasoning: z<boolean, boolean, "defined">;
56
+ enabled: z<boolean, boolean, "defined">;
57
+ dryRun: z<boolean, boolean, "defined">;
58
+ }>>, "plain">;
59
+ /**
60
+ * Resolve and validate a partly-specified configuration.
61
+ *
62
+ * @param config - raw plugin configuration.
63
+ * @returns a detached, frozen configuration.
64
+ * @throws when a budget is not a safe integer or `minFoldChars` is below 300.
65
+ */
66
+ declare function resolveConfig(config?: Partial<ContextFoldConfig>): ContextFoldConfig;
67
+ /** One candidate result as the rules see it. */
68
+ interface FoldFacts {
69
+ /** Tool name from the paired `tool/call`. */
70
+ toolName: string;
71
+ /** The result's model-issued call id. */
72
+ callId: string;
73
+ /** Current model-visible content of the result. */
74
+ content: readonly ContentBlock[];
75
+ /** Raw `arguments` JSON of the paired `tool/call`, when that event is still present. */
76
+ args?: string;
77
+ /** Whole-result character count under {@link codePointLength}. */
78
+ chars: number;
79
+ }
80
+ /** Parsed spill notice: where the full text already lives. */
81
+ interface SpillNotice {
82
+ locator: string;
83
+ bytes: number | undefined;
84
+ }
85
+ /**
86
+ * Count Unicode code points without splitting surrogate pairs, matching the
87
+ * pruner's measurement so sizes are comparable across plugins.
88
+ *
89
+ * @param text - text to measure.
90
+ * @returns the code-point count.
91
+ */
92
+ declare function codePointLength(text: string): number;
93
+ /**
94
+ * Model-visible text of one result, including image/file placeholders.
95
+ *
96
+ * @param content - ordered result blocks.
97
+ * @returns concatenated text with a stable placeholder for non-text blocks.
98
+ */
99
+ declare function textOf(content: readonly ContentBlock[]): string;
100
+ /**
101
+ * Whether a result can be folded without losing anything structural.
102
+ *
103
+ * Image and unknown blocks are never folded: their placeholder text is not the
104
+ * content, and phase 1 must not trade an attachment for a sentence about it.
105
+ *
106
+ * @param content - ordered result blocks.
107
+ * @returns true when every block is text.
108
+ */
109
+ declare function isFoldableContent(content: readonly ContentBlock[]): boolean;
110
+ /**
111
+ * Find the spill notice an already-bounded result carries, so folding reuses
112
+ * that locator instead of spilling the same text twice (design §6.5).
113
+ *
114
+ * @param text - result text.
115
+ * @returns the parsed notice, or undefined when the result was never spilled.
116
+ */
117
+ declare function parseSpillNotice(text: string): SpillNotice | undefined;
118
+ /**
119
+ * Produce the one-line conclusion for a result.
120
+ *
121
+ * The rule is chosen by tool family, mirroring design §6.3, and falls back to
122
+ * the first line plus a size for anything unlisted. A conclusion is always
123
+ * shorter than the result it replaces; callers still compare sizes before
124
+ * folding, because a pathological result can defeat any rule.
125
+ *
126
+ * @param facts - candidate facts.
127
+ * @returns the conclusion body (without the fold header), or undefined when the
128
+ * result carries no foldable text.
129
+ */
130
+ declare function conclude(facts: FoldFacts): string | undefined;
131
+ /** One model-visible `tool/result` node at evaluation time. */
132
+ interface NodeSnapshot {
133
+ /** Current surface sequence number. */
134
+ seq: SessionSeq;
135
+ /** Turn the result belongs to. */
136
+ turn: number;
137
+ /** Step the result belongs to. */
138
+ step: number;
139
+ /** Tool name, resolved from the paired `tool/call` when that event is still reachable. */
140
+ toolName: string;
141
+ /** Model-issued call id. */
142
+ callId: string;
143
+ /** Raw `arguments` JSON of the paired `tool/call`, when available. */
144
+ args?: string;
145
+ /** Current model-visible content. */
146
+ content: readonly ContentBlock[];
147
+ /** Whether the tool invocation failed. */
148
+ isError: boolean;
149
+ }
150
+ /** One searchable later event used by reference detection (design §6.2). */
151
+ interface MentionRecord {
152
+ /** Event sequence number. */
153
+ seq: number;
154
+ /** Text searched for a candidate's call id. */
155
+ text: string;
156
+ }
157
+ /** The complete input to one evaluation. */
158
+ interface PlanInput {
159
+ /** Current surface `tool/result` nodes, in surface order. */
160
+ nodes: readonly NodeSnapshot[];
161
+ /** Later events whose text can cite a call id: tool-call arguments and message text. */
162
+ mentions: readonly MentionRecord[];
163
+ /** Turn being evaluated (the open turn). */
164
+ currentTurn: number;
165
+ /** Step being evaluated: the step about to run, or the closing step. */
166
+ currentStep: number;
167
+ /** Resolved configuration. */
168
+ config: ContextFoldConfig;
169
+ }
170
+ /** One planned fold, independent of any spill locator. */
171
+ interface FoldPlan {
172
+ /** Surface seq of the node to rewrite. */
173
+ seq: SessionSeq;
174
+ /** Detached facts of the node. */
175
+ facts: FoldFacts;
176
+ /** The node's turn. */
177
+ turn: number;
178
+ /** The node's step. */
179
+ step: number;
180
+ /** Conclusion body that will be rendered inline. */
181
+ conclusion: string;
182
+ /** Full current text to persist before the rewrite. */
183
+ original: string;
184
+ /** Whether this node carries the batch roster. */
185
+ head: boolean;
186
+ /** The head's call id, present on non-head members. */
187
+ headCallId?: string;
188
+ }
189
+ /** One batch of folds sharing a turn and a contiguous step range. */
190
+ interface FoldBatch {
191
+ /** Stable batch key (`turn:stepFrom-stepTo`), used only for reporting. */
192
+ key: string;
193
+ /** The batch head's call id. */
194
+ callId: string;
195
+ /** Members in fold order (largest first). */
196
+ members: readonly FoldPlan[];
197
+ /** Original character count of the batch. */
198
+ chars: number;
199
+ }
200
+ /** Why one node did not fold, for the dry-run report. */
201
+ interface SkippedNode {
202
+ /** Surface seq. */
203
+ seq: SessionSeq;
204
+ /** Tool name. */
205
+ toolName: string;
206
+ /** Character count. */
207
+ chars: number;
208
+ /** Machine-readable reason. */
209
+ reason: 'recent' | 'alive' | 'referenced' | 'error' | 'folded' | 'small' | 'unfoldable' | 'no-conclusion';
210
+ }
211
+ /** The complete result of one evaluation. */
212
+ interface FoldDecision {
213
+ /** Batches to fold, in fold order. */
214
+ batches: readonly FoldBatch[];
215
+ /** Nodes considered and rejected, for diagnostics and the dry-run report. */
216
+ skipped: readonly SkippedNode[];
217
+ /** Total character count of every candidate that qualified, before batching. */
218
+ candidateChars: number;
219
+ }
220
+ /**
221
+ * Decide what to fold for one snapshot.
222
+ *
223
+ * Candidate rules, in the order they are checked (design §6.1): error results
224
+ * never fold, recently-used results never fold, already-folded nodes never
225
+ * fold, nodes still cited by a later event never fold, and a node folds only
226
+ * when it is at least `minFoldChars` and a conclusion exists that is smaller
227
+ * than the result.
228
+ *
229
+ * @param input - one detached snapshot plus configuration.
230
+ * @returns the decision; an empty `batches` array means "do nothing".
231
+ */
232
+ declare function planFolds(input: PlanInput): FoldDecision;
233
+ /** Fixed prefix identifying a folded node. A `startsWith` test is the idempotency check. */
234
+ declare const FOLD_MARKER = "[context-fold]";
235
+ /**
236
+ * Whether already-folded content is being inspected.
237
+ *
238
+ * @param text - current model-visible text of a node.
239
+ * @returns true when the node is a fold this plugin produced.
240
+ */
241
+ declare function isFoldedText(text: string): boolean;
242
+ /** Marker opening a folded reasoning block. Must be distinctive: `startsWith` is the idempotency test. */
243
+ declare const REASONING_MARKER = "[context-fold] [reasoning]";
244
+ /** Durable record type carrying one reasoning-fold decision. */
245
+ declare const REASONING_EVENT = "plugin:context-fold/reasoning";
246
+ /** One reasoning fold recorded by a decision record. */
247
+ interface ReasoningTarget {
248
+ /** Surface seq of the assistant node whose reasoning is folded. */
249
+ seq: SessionSeq;
250
+ /** Content indexes of the folded reasoning blocks, in content order. */
251
+ indexes: readonly number[];
252
+ /** Original code-point length of each folded block, for the report. */
253
+ chars: readonly number[];
254
+ /** Spill locator per folded block, parallel to `indexes`. Absent when spilling failed. */
255
+ locators?: readonly (string | undefined)[];
256
+ }
257
+ /** Data of one `plugin:context-fold/reasoning` decision record. */
258
+ interface ReasoningFoldData {
259
+ /** Targets, in surface order. */
260
+ targets: readonly ReasoningTarget[];
261
+ }
262
+ /**
263
+ * Declare the durable decision record.
264
+ *
265
+ * This uses {@link PluginRecordMap}, **not** `SessionEventMap`. That distinction
266
+ * is load-bearing and was initially got wrong here:
267
+ *
268
+ * A `SessionEventMap` augmentation only adds a *TypeScript* type. The record
269
+ * written by `session.append()` carries an envelope of exactly
270
+ * `{type, seq, time, data}` with **no `ignorable` marker**, and the persistence
271
+ * read path refuses any type outside `KNOWN_SESSION_EVENT_TYPES` unless
272
+ * `ignorable === true`:
273
+ *
274
+ * > "contains event type … unknown to this harness and not marked ignorable;
275
+ * > refusing to interpret the log"
276
+ * > — `dsh-session-persistence` `assertEventsSupported`
277
+ *
278
+ * So a plain `append` of a plugin-invented type makes the session **unreopenable**,
279
+ * which would lose the whole conversation. `appendPluginRecord` is the host's
280
+ * sanctioned alternative: it stamps `ignorable: true`, and a reader that does not
281
+ * know the type retains and skips it.
282
+ *
283
+ * `dsh-compaction-image-offload` is *not* a precedent for the other approach,
284
+ * because `image/offload` is a harness-core member of
285
+ * `KNOWN_SESSION_EVENT_TYPES`; a downstream plugin cannot join that set.
286
+ */
287
+ declare module '@deepseek-ai/dsh-session/types' {
288
+ interface PluginRecordMap {
289
+ /**
290
+ * Fold historical reasoning blocks on tool-call turns.
291
+ *
292
+ * Targets name current `assistant/message` surface nodes and the content
293
+ * indexes of the reasoning blocks the decision covers. Message identity,
294
+ * block count, and block types are preserved; the durable assistant event
295
+ * keeps the full original text.
296
+ */
297
+ 'plugin:context-fold/reasoning': ReasoningFoldData;
298
+ }
299
+ }
300
+ /**
301
+ * Whether a reasoning block is already folded.
302
+ *
303
+ * @param text - reasoning text.
304
+ * @returns true for a block this plugin produced.
305
+ */
306
+ declare function isFoldedReasoning(text: string): boolean;
307
+ /**
308
+ * Whether a message is a tool-call turn, i.e. a turn that requested tool work.
309
+ *
310
+ * This is a **policy** filter, not a token-accounting claim. Reasoning is re-sent
311
+ * differently per route:
312
+ *
313
+ * - `dsh-llm-deepseek`'s `serializeAssistant` emits `reasoning_content` only when
314
+ * `toolCalls.length > 0` ("it is ignored on plain turns, so we drop it there"),
315
+ * so a plain turn's reasoning is free on that route.
316
+ * - `dsh-router-core`'s `wireMessages` — the route this profile actually runs —
317
+ * does **not** gate it: `...reasoning.length > 0 ? { reasoning_content } : {}`
318
+ * is unconditional, so plain-turn reasoning IS re-sent and does cost tokens.
319
+ *
320
+ * Restricting to tool-call turns therefore leaves real bytes on the table on the
321
+ * router route. It stays the default only because the conservative shape matches
322
+ * phase 1's "completed stage" intent and the measured tool-call share dominates;
323
+ * `foldPlainTurnReasoning` widens it for routes that replay plain-turn reasoning.
324
+ *
325
+ * @param content - assistant content blocks.
326
+ * @returns true when the message carries at least one tool call.
327
+ */
328
+ declare function isToolCallTurn(content: readonly ContentBlock[]): boolean;
329
+ /**
330
+ * Build the replacement text for one reasoning block.
331
+ *
332
+ * The head is preserved verbatim, so no information is invented and the
333
+ * conclusion-like opening of the thought stays visible. The character count and
334
+ * the full-length note make the omission visible, which is the difference
335
+ * between a cautious model and a fluent one.
336
+ *
337
+ * When a `locator` is supplied the folded text inlines it, so the omission is
338
+ * **recoverable**: `read`/`grep` on that path returns the original reasoning.
339
+ * Without it the note is a pointer to nothing — it tells the model detail is
340
+ * missing while offering no way to fetch it — which is why the fold path always
341
+ * tries to spill first (design §12.7).
342
+ *
343
+ * @param text - original reasoning text.
344
+ * @param locator - spill path holding the full original, when one was written.
345
+ * @returns folded text, or undefined when folding would not shorten it.
346
+ */
347
+ declare function foldReasoningText(text: string, locator?: string): string | undefined;
348
+ /** One candidate assistant node for reasoning folding. */
349
+ interface ReasoningCandidate {
350
+ /** Current surface seq. */
351
+ seq: SessionSeq;
352
+ /** Turn the message belongs to. */
353
+ turn: number;
354
+ /** Step the message belongs to. */
355
+ step: number;
356
+ /** Current model-visible content. */
357
+ content: readonly ContentBlock[];
358
+ }
359
+ /** One planned reasoning fold. */
360
+ interface ReasoningPlan {
361
+ /** Surface seq to project. */
362
+ seq: SessionSeq;
363
+ /** Folded block indexes, in content order. */
364
+ indexes: readonly number[];
365
+ /** Original character count per folded block. */
366
+ chars: readonly number[];
367
+ /** Total characters removed. */
368
+ charsRemoved: number;
369
+ /**
370
+ * Spill locators parallel to `indexes`: where each block's full original lives.
371
+ *
372
+ * The record carries these so the projection can inline the same recovery path
373
+ * on replay; without them a restored session would show an omission note with
374
+ * no way to recover the text.
375
+ */
376
+ locators?: readonly (string | undefined)[];
377
+ }
378
+ /**
379
+ * Plan reasoning folds for one snapshot.
380
+ *
381
+ * A node qualifies when it is a tool-call turn (where reasoning is re-sent most,
382
+ * and where the measurable win is), when it is old enough by the same age rules
383
+ * phase 1 uses, and when at least one reasoning block is large enough to shrink.
384
+ *
385
+ * The plan itself does not spill: writing to the store is side-effectful, so it
386
+ * happens in `recordReasoningFolds`, which fills in the locators and re-derives
387
+ * the conclusion text with them.
388
+ *
389
+ * @param candidates - current assistant nodes, in surface order.
390
+ * @param currentTurn - turn being evaluated.
391
+ * @param currentStep - step being evaluated.
392
+ * @param config - resolved configuration.
393
+ * @returns plans in surface order.
394
+ */
395
+ declare function planReasoningFolds(candidates: readonly ReasoningCandidate[], currentTurn: number, currentStep: number, config: ContextFoldConfig): ReasoningPlan[];
396
+ /**
397
+ * Project one assistant message with selected reasoning blocks folded.
398
+ *
399
+ * Message identity and every other block are preserved verbatim, which is what
400
+ * `replayedAssistant` and `foreignAssistant` both require: the block count and
401
+ * block types are unchanged, so even a signed route would still validate.
402
+ *
403
+ * @param message - message to project.
404
+ * @param indexes - reasoning block indexes to fold, strictly increasing.
405
+ * @returns an immutable projected copy.
406
+ * @throws when an index is out of range or does not name a reasoning block.
407
+ */
408
+ declare function projectReasoning(message: AssistantMessage, indexes: readonly number[], locators?: readonly (string | undefined)[]): AssistantMessage;
409
+ /**
410
+ * Collect the assistant nodes a decision event may target.
411
+ *
412
+ * @param session - session to read.
413
+ * @returns candidates in surface order.
414
+ */
415
+ declare function reasoningCandidates(session: Session): ReasoningCandidate[];
416
+ /**
417
+ * Append one decision record covering the planned folds.
418
+ *
419
+ * The record is self-describing: it stores the exact content indexes it covers,
420
+ * so replay never has to re-derive *which* blocks were chosen from a count. The
421
+ * record is durable and pure — replaying the log through the registered
422
+ * projection reproduces exactly the same request content.
423
+ *
424
+ * @param session - session to extend.
425
+ * @param plans - folds to record.
426
+ * @param spill - optional spill store; when supplied, every folded block's full
427
+ * text is written first and its locator is recorded so the projection can
428
+ * inline a real recovery path.
429
+ * @returns the record's sequence number.
430
+ */
431
+ declare function recordReasoningFolds(session: Session, plans: readonly ReasoningPlan[], spill?: ReasoningSpill): Promise<SessionSeq>;
432
+ /**
433
+ * The slice of the spill store this module needs.
434
+ *
435
+ * Typed against the host's own `SpillStore` rather than a hand-written shape:
436
+ * its ids are branded (`SessionId`, `ToolCallId`), so a local structural type
437
+ * would either fail to accept the real store or force a cast.
438
+ */
439
+ type ReasoningSpill = {
440
+ saveText(input: SpillStore['saveText'] extends ((input: infer I) => Promise<unknown>) ? I : never): Promise<{
441
+ locator: string;
442
+ }>;
443
+ };
444
+ /**
445
+ * The projection definition registered with `ctx.sessions`.
446
+ *
447
+ * It is deliberately **tolerant of stale targets** rather than strict: a
448
+ * decision is durable, but the surface it referred to is not frozen — a
449
+ * `compaction` replacement can remove a node the decision named. Throwing there
450
+ * would make every later `deriveMessages()` fail and brick the session, which
451
+ * violates the design's "a session never errors" anti-goal. A target whose node
452
+ * is no longer current, or whose reasoning blocks are already folded, is
453
+ * therefore skipped: the fold it described is simply no longer applicable.
454
+ *
455
+ * Each target's `chars` array is only a length hint — how many reasoning blocks
456
+ * that decision intended to fold. The projection folds that many still-unfolded
457
+ * reasoning blocks, which keeps replay correct even after a later decision has
458
+ * already folded a sibling block.
459
+ */
460
+ /**
461
+ * The projection definition registered with `ctx.sessions`.
462
+ *
463
+ * **Why a cast is needed, and why it is correct:** `SessionMessageProjection<T>`
464
+ * constrains `T extends keyof SessionEventMap`, so the host's *types* do not
465
+ * admit a `plugin:` record type. The *runtime* does: `SurfaceManager` resolves a
466
+ * projection with `projections.find(item => item.type === event.type)` **before**
467
+ * it looks at `surfaceOp`, so any committed event type can drive one. The cast is
468
+ * therefore only widening a compile-time parameter that the implementation
469
+ * already accepts.
470
+ *
471
+ * A projection must **not** be registered for a surface event type
472
+ * (`assistant/message`, `user/message`, `tool/result`): `planSurfaceEvent`
473
+ * returns a `project` plan *instead of* an append/replace plan, and
474
+ * `applySurfacePlan` only pushes nodes for `append`, so such a registration would
475
+ * silently drop every message of that type off the surface. A non-surface
476
+ * plugin-owned record is the only correct carrier.
477
+ *
478
+ * It is deliberately **tolerant of stale targets** rather than strict: a
479
+ * decision is durable, but the surface it referred to is not frozen — a
480
+ * `compaction` replacement can remove a node the decision named. Throwing there
481
+ * would make every later derivation fail and brick the session, which violates
482
+ * the design's "a session never errors" anti-goal.
483
+ */
484
+ declare const reasoningProjection: {
485
+ readonly type: "plugin:context-fold/reasoning";
486
+ readonly project: (event: SessionEvent, context: SessionMessageProjectionContext) => ReadonlyMap<SessionSeq, Message>;
487
+ };
488
+ /** One landed fold, for the measurement record. */
489
+ interface FoldedNodeRecord {
490
+ /** Seq of the replacement node. */
491
+ replacementSeq: number;
492
+ /** Seq of the replaced node. */
493
+ originalSeq: number;
494
+ /** Tool call id. */
495
+ callId: string;
496
+ /** Tool name. */
497
+ toolName: string;
498
+ /** Characters before. */
499
+ charsBefore: number;
500
+ /** Characters after. */
501
+ charsAfter: number;
502
+ /** Where the full original lives. */
503
+ locator: string;
504
+ /** Bytes written to the spill store by this fold (0 when an existing spill was reused). */
505
+ spillBytes: number;
506
+ }
507
+ /** One failed fold; earlier folds of the pass stay durable. */
508
+ interface FoldFailure {
509
+ /** Seq of the node that could not be folded. */
510
+ seq: number;
511
+ /** Tool name, when known. */
512
+ toolName: string;
513
+ /** Human-readable cause. */
514
+ reason: string;
515
+ }
516
+ /** Result of one fold pass. */
517
+ interface FoldPassResult {
518
+ /** The decision this pass acted on. */
519
+ decision: FoldDecision;
520
+ /** Landed folds, in fold order. */
521
+ folded: readonly FoldedNodeRecord[];
522
+ /** Nodes that failed, in fold order. */
523
+ failed: readonly FoldFailure[];
524
+ /** Characters removed from the surface by this pass. */
525
+ charsRemoved: number;
526
+ }
527
+ /**
528
+ * Build the detached evaluation input for one moment in the session.
529
+ *
530
+ * Surface seqs are always sourced fresh from `session.surface.nodes`, never
531
+ * cached across evaluations (design §6.7), because a replacement splices the new
532
+ * seq over the old one and the old seq leaves the surface.
533
+ *
534
+ * @param session - session to read.
535
+ * @returns a snapshot that does not reference live session objects.
536
+ */
537
+ declare function snapshotInput(session: Session): {
538
+ nodes: NodeSnapshot[];
539
+ mentions: MentionRecord[];
540
+ };
541
+ /**
542
+ * Plan one evaluation without touching the session.
543
+ *
544
+ * @param session - session to read.
545
+ * @param currentTurn - the turn being evaluated.
546
+ * @param currentStep - the step being evaluated.
547
+ * @param config - resolved configuration.
548
+ * @returns the decision.
549
+ */
550
+ declare function planFor(session: Session, currentTurn: number, currentStep: number, config: ContextFoldConfig): FoldDecision;
551
+ /** One fold found in the log. */
552
+ interface FoldRecord {
553
+ /** Seq of the replacement node. */
554
+ seq: number;
555
+ /** Turn the folded result belonged to. */
556
+ turn: number;
557
+ /** Step the folded result belonged to. */
558
+ step: number;
559
+ /** Tool name parsed from the marker line. */
560
+ toolName: string;
561
+ /** Original character count parsed from the marker line. */
562
+ charsBefore: number;
563
+ /** Current character count of the folded node. */
564
+ charsAfter: number;
565
+ /** Sequential calls later found that cite this fold's call id. */
566
+ reReads: number;
567
+ /** Whether this node is a batch head. */
568
+ head: boolean;
569
+ }
570
+ /** Aggregate facts about one session's folds. */
571
+ interface FoldReport {
572
+ /** Every fold currently on the surface, in surface order. */
573
+ folds: readonly FoldRecord[];
574
+ /** Characters removed across the surface. */
575
+ charsRemoved: number;
576
+ /** Total sequential references to folded content. */
577
+ reReads: number;
578
+ /** How many of the folds were cited at least once. */
579
+ rereadNodes: number;
580
+ /** Reasoning folds recorded by decision events (phase 2). */
581
+ reasoningFolds: readonly ReasoningFoldRecord[];
582
+ }
583
+ /** One reasoning fold found in the log. */
584
+ interface ReasoningFoldRecord {
585
+ /** Message surface seq the decision named. */
586
+ seq: number;
587
+ /** Reasoning blocks the decision covered. */
588
+ blocks: number;
589
+ /** Original characters in those blocks. */
590
+ charsBefore: number;
591
+ /** Characters the projected text currently occupies. */
592
+ charsAfter: number;
593
+ }
594
+ /**
595
+ * Derive a report from one session's current log.
596
+ *
597
+ * @param session - session to inspect.
598
+ * @returns the fold report.
599
+ */
600
+ declare function reportOf(session: Session): FoldReport;
601
+ /**
602
+ * Render the `/context-fold` report.
603
+ *
604
+ * @param report - derived report.
605
+ * @param dryRun - whether the plugin is configured to mutate nothing.
606
+ * @returns markdown text for the command result.
607
+ */
608
+ declare function renderReport(report: FoldReport, dryRun: boolean): string;
609
+ /** Cordis plugin identity. */
610
+ declare const name = "context-fold";
611
+ /** The plugin needs a token meter; without a spill store it no-ops with a warning (design §6.5). */
612
+ declare const inject: string[];
613
+ /**
614
+ * Mount the fold policy.
615
+ *
616
+ * @param ctx - plugin context.
617
+ * @param rawConfig - configuration from the settings row.
618
+ */
619
+ declare function apply(ctx: Context, rawConfig?: Partial<ContextFoldConfig>): void;
620
+ /**
621
+ * One-line summary of a decision for the log during `dryRun`.
622
+ *
623
+ * @param prefix - leading phrase.
624
+ * @param decision - the decision to describe.
625
+ * @param config - resolved configuration.
626
+ * @returns a single log line.
627
+ */
628
+ declare function describeDecision(prefix: string, decision: {
629
+ batches: readonly {
630
+ members: readonly unknown[];
631
+ }[];
632
+ candidateChars: number;
633
+ skipped: readonly {
634
+ reason: string;
635
+ }[];
636
+ }, config: ContextFoldConfig): string;
637
+ declare const _default: {
638
+ name: string;
639
+ inject: string[];
640
+ Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
641
+ minFoldChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
642
+ minBatchChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
643
+ keepRecentSteps: import("@deepseek-ai/schemastery").default<number, number, "defined">;
644
+ staleSteps: import("@deepseek-ai/schemastery").default<number, number, "defined">;
645
+ batchSpanSteps: import("@deepseek-ai/schemastery").default<number, number, "defined">;
646
+ foldReasoning: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
647
+ foldPlainTurnReasoning: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
648
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
649
+ dryRun: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
650
+ }>>, Schemastery.ObjectT<NoInfer<{
651
+ minFoldChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
652
+ minBatchChars: import("@deepseek-ai/schemastery").default<number, number, "defined">;
653
+ keepRecentSteps: import("@deepseek-ai/schemastery").default<number, number, "defined">;
654
+ staleSteps: import("@deepseek-ai/schemastery").default<number, number, "defined">;
655
+ batchSpanSteps: import("@deepseek-ai/schemastery").default<number, number, "defined">;
656
+ foldReasoning: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
657
+ foldPlainTurnReasoning: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
658
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
659
+ dryRun: import("@deepseek-ai/schemastery").default<boolean, boolean, "defined">;
660
+ }>>, "plain">;
661
+ apply: typeof apply;
662
+ };
663
+ export { Config, type ContextFoldConfig, DEFAULT_CONFIG, FOLD_MARKER, type FoldBatch, type FoldDecision, type FoldFacts, type FoldFailure, type FoldPassResult, type FoldPlan, type FoldRecord, type FoldReport, type FoldedNodeRecord, type NodeSnapshot, REASONING_EVENT, REASONING_MARKER, type ReasoningCandidate, type ReasoningFoldData, type ReasoningPlan, type ReasoningTarget, type SkippedNode, apply, codePointLength, conclude, _default as default, describeDecision, foldReasoningText, inject, isFoldableContent, isFoldedReasoning, isFoldedText, isToolCallTurn, name, parseSpillNotice, planFolds, planFor, planReasoningFolds, projectReasoning, reasoningCandidates, reasoningProjection, recordReasoningFolds, renderReport, reportOf, resolveConfig, snapshotInput, textOf };