billion-context-dsh 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lru.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A size-capped Map that evicts least-recently-used entries once the cap is
3
+ * reached (issue #113). Recency is refreshed by both get and set. Backs the
4
+ * engine's per-session caches so idle sessions can be dropped and later
5
+ * rebuilt from the durable session log instead of accumulating forever.
6
+ * @module billion-context-dsh/lru
7
+ */
8
+ /** Default cap for the engine's per-session caches (kernel states, nudge dedup). */
9
+ export declare const DEFAULT_SESSION_CACHE_LIMIT = 512;
10
+ export declare class LruMap<K, V> extends Map<K, V> {
11
+ private readonly maxEntries;
12
+ constructor(maxEntries: number);
13
+ get(key: K): V | undefined;
14
+ set(key: K, value: V): this;
15
+ }
@@ -20,6 +20,20 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
20
20
  * the actual `text` blocks, so a top-level-only walk would drop every tool
21
21
  * result from the projection (and with it the seq's ref assignment, breaking
22
22
  * compress boundary resolution). Nested arrays are flattened depth-first.
23
+ *
24
+ * Non-text blocks that the provider still bills for render as a deterministic
25
+ * one-line placeholder instead of vanishing (issue #117). An `image`/`file`
26
+ * block used to contribute nothing, which silently made a picture-only user
27
+ * message — or a tool result carrying a screenshot — invisible to the engine:
28
+ * no ref (so no compress boundary), `hasPlainRef` false (so the range solver
29
+ * shrank past it and swallowed neighbours), invisible to the kernel's
30
+ * recent/last-user protection (so the last real user turn could be compressed
31
+ * away), priced at zero tokens, and absent from search/decompress output. The
32
+ * host itself projects non-text references to deterministic handle text for
33
+ * files ("request assembly projects every occurrence to deterministic handle
34
+ * text", dsh-llm/lib/types/types.d.ts), and this is the same idea one layer
35
+ * down. Only durable attachment metadata is used, so the placeholder is stable
36
+ * across turns (cache prefix, summary text, search hits).
23
37
  */
24
38
  export declare function extractText(content: unknown): string;
25
39
  /**
@@ -50,6 +64,17 @@ export declare function buildToolCallIndex(events: readonly SessionEvent[]): Rea
50
64
  * untagged (`toolName: ''`), never "text".
51
65
  * Non-surface events project to nothing.
52
66
  */
67
+ /**
68
+ * B1 summary source framing. A compaction summary is MODEL-WRITTEN text, not
69
+ * user words — injected as a user/message with the same standing as real input,
70
+ * which let obligation sentences inside summaries read as user directives and
71
+ * the model's own guesses read as user commitments. The frame says both things
72
+ * up front. It is applied at creation (src/region.ts writes the framed blocks
73
+ * to BOTH durable writes) and again at projection (below) as an idempotent
74
+ * safety net for legacy blocks written before the feature.
75
+ */
76
+ export declare const SUMMARY_FRAME_PREFIX = "[Model-written summary \u2014 not user words; re-verify any obligations before relying on them]";
77
+ export declare function withSummaryFramePrefix(text: string): string;
53
78
  export declare function projectEvent(event: SessionEvent, toolNames?: ReadonlyMap<string, string>): CoreMessage[];
54
79
  /** Project a session's message events into CoreMessage[] in log order. */
55
80
  export declare function eventsToCoreMessages(events: readonly SessionEvent[], toolNames?: ReadonlyMap<string, string>): CoreMessage[];
@@ -65,3 +90,78 @@ export declare function surfaceEventsOf(session: Session): SessionEvent[];
65
90
  export declare function allLogMessages(session: import('@deepseek-ai/dsh-session').Session): CoreMessage[];
66
91
  /** Extract the model-facing text of any surface message event. */
67
92
  export declare function extractEventText(event: SessionEvent): string;
93
+ /** Count image/file blocks reachable from a content payload (same walk as extractText). */
94
+ export declare function countAttachmentBlocks(content: unknown): {
95
+ images: number;
96
+ files: number;
97
+ };
98
+ /**
99
+ * Attachments carried by one surface event, walked exactly like
100
+ * `extractEventText`. Used in two places: the compressible-range rows mark
101
+ * media-bearing spans, and the callers that already own a token meter price
102
+ * those spans with the provider-anchored media price instead of the text-only
103
+ * estimate (issue #117).
104
+ */
105
+ export declare function attachmentsOfEvent(event: SessionEvent): {
106
+ images: number;
107
+ files: number;
108
+ };
109
+ /**
110
+ * The image/file blocks themselves (not just their counts), in document order.
111
+ * The compressible-range rows price these with the fixed-heuristic media price
112
+ * when the meter reports no routed surcharge, so a media-bearing span is never
113
+ * shown as free (issue #117).
114
+ */
115
+ export declare function mediaBlocksOfEvent(event: SessionEvent): readonly unknown[];
116
+ /**
117
+ * Whether a surface user message is a compaction checkpoint node (already
118
+ * compressed). Defined here (not in region.ts) so the classifier below and
119
+ * region.ts share ONE implementation.
120
+ */
121
+ export declare function isCheckpointNode(event: SessionEvent): boolean;
122
+ /**
123
+ * Injection/authoring classification of one surface event — the ONE shared
124
+ * classifier for range scanning and the protected-tail scan (never ad-hoc
125
+ * predicates that drift apart).
126
+ *
127
+ * - `real` — genuine conversation content (user turns without an injected
128
+ * source, assistant prose/tool-calls, tool results, sub-agent relay rows).
129
+ * This is the only class that may win "last real user message" protection
130
+ * (minus relay rows, see `isRealUserTurn`).
131
+ * - `metadata` — the engine's own ephemeral rows: nudge echoes and
132
+ * compress-pair replacement stubs. Their content is derived from
133
+ * already-visible messages, so folding them into an adjacent real segment
134
+ * is zero-loss — this preserves main's behavior for engine-authored rows.
135
+ * - `checkpoint` — compaction summary nodes (`plugin: 'compact'`).
136
+ * Distillation is an explicit act; never folded into any segment.
137
+ * - `instruction` — host-authored policy/instructions: AGENTS.md injections
138
+ * (both host shapes), skill catalogs, and ANY unknown `kind:'plugin'` row.
139
+ * Folding these is unsafe (the model would lose live policy text, and the
140
+ * host re-injects the current AGENTS.md copy when it disappears — the
141
+ * compress → re-inject loop this PR fixes). Unknown plugin names fall here
142
+ * deliberately: a future host injection must never silently become
143
+ * compressible content.
144
+ */
145
+ export type SurfaceEventClass = 'real' | 'metadata' | 'checkpoint' | 'instruction';
146
+ /** Plugin names the engine itself authors — safe to fold into real segments. */
147
+ export declare const METADATA_PLUGINS: ReadonlySet<string>;
148
+ /**
149
+ * True for AGENTS.md instruction rows in BOTH host shapes: the hook shape
150
+ * (`kind:'agent-instructions'`, form 'instructions') and the baseline shape
151
+ * (`kind:'plugin'` + plugin 'agent-instructions'). Shared by the newest-row
152
+ * scan and the range scanner so protection and folding always agree on what
153
+ * counts as an AGENTS.md row.
154
+ */
155
+ export declare function isAgentInstructionsRow(event: SessionEvent): boolean;
156
+ export declare function classifySurfaceEvent(event: SessionEvent): SurfaceEventClass;
157
+ /**
158
+ * Whether an event is a real user turn — the protected-tail criterion. An
159
+ * injected row (AGENTS.md, skill catalog, nudge echo, tool notice) is real
160
+ * *content* at most but is never the user speaking: the latest real user
161
+ * message must keep its protection window even when an injected row lands
162
+ * after it. The scan this replaces protected "the last non-checkpoint
163
+ * user/message", which on live sessions is frequently an AGENTS.md injection
164
+ * row (the host appends it in the same enter batch) — the actual last user
165
+ * message was left compressible while synthetic output sat safe.
166
+ */
167
+ export declare function isRealUserTurn(event: SessionEvent): boolean;
package/dist/nudge.d.ts CHANGED
@@ -5,12 +5,14 @@
5
5
  * targets ranges by surface seq rather than by <acp> tags).
6
6
  * @module billion-context-dsh/nudge
7
7
  */
8
- import { type CompressionCore, type CoreMessage, type NudgeDecision } from 'acp-kernel';
8
+ import { type CompressionCore, type CompressionState, type ContextBreakdown, type CoreMessage, type NudgeDecision } from 'acp-kernel';
9
9
  import { type UserMessage } from '@deepseek-ai/dsh-llm';
10
10
  import type { Agent } from '@deepseek-ai/dsh-agent';
11
11
  import { AcpStateStore } from './state.ts';
12
+ import { type KernelRangeView, type MediaPriceOf } from './region.ts';
12
13
  import { type KernelConfigInput } from './config.ts';
13
14
  import { type ResolvedPrompts } from './prompts.ts';
15
+ export declare function stripNudgeGuidance(text: string): string;
14
16
  /** Kernel inputs the nudge path shares with the compress tool. */
15
17
  export interface NudgeEnvironment extends KernelConfigInput {
16
18
  readonly kernel: CompressionCore;
@@ -38,21 +40,65 @@ export interface NudgeOutcome {
38
40
  export declare function resolveTokenCount(agent: Agent, coreMessages: CoreMessage[]): number;
39
41
  /**
40
42
  * Render the compressible-range table as seq refs for the model.
41
- * Computed directly from the surface (not the kernel's ref map, which can
42
- * drift and hide large tool results) — see buildCompressibleSeqRanges.
43
- * UPSTREAM: this self-computation is a labeled workaround for kernel
44
- * ref-map drift after surface replacements (AGENTS.md rule 11) — drop it and
45
- * use kernel compressibleRanges once the drift is fixed upstream.
43
+ *
44
+ * The spans are the kernel's own (`compressibleRanges`, translated to surface
45
+ * seqs) with the host guards applied on top — see buildCompressibleSeqRanges.
46
+ * This function used to self-compute them from the surface as a labeled
47
+ * `UPSTREAM:` workaround for kernel ref-map drift; that drift is fixed upstream
48
+ * (acp-kernel #207) and the workaround is gone (rule 11).
49
+ */
50
+ export declare function rangeTable(session: import('@deepseek-ai/dsh-session').Session, kernelView: KernelRangeView, prompts?: ResolvedPrompts, mediaPriceOf?: MediaPriceOf): string;
51
+ /**
52
+ * Compute a SURFACE-ONLY context breakdown for display, aligned with
53
+ * `acp_status` (kernel `buildStatusReport`/`renderOverview`).
54
+ *
55
+ * The kernel's own `computeContextBreakdown` (which the nudge text renders)
56
+ * walks the message array it is fed — and `buildNudge` feeds it the FULL log
57
+ * (`allLogMessages`, needed so T2/T3 distillation can anchor every block). So
58
+ * a session with compressed blocks reports HISTORICAL totals there: every
59
+ * original tool/text message already absorbed into a block is counted again,
60
+ * e.g. `85.2K tool` for ~8.5K of live tool context. `acp_status` instead feeds
61
+ * `buildStatusReport` the VISIBLE surface + active-block summaries, so its
62
+ * breakdown reads the true current context. This function reproduces that
63
+ * visible-surface reality for the nudge line so the two tools agree.
64
+ *
65
+ * Classification replicates kernel `computeContextBreakdown` (tool-call/
66
+ * tool-result → tool, `system` role → system, `` code `` fence in text →
67
+ * code, else text) EXCEPT summaries: kernel detects summaries by a
68
+ * `[Compressed conversation section]` text prefix, which never matches a DSH
69
+ * checkpoint node (our summary is the plain summary + `compactCheckpointSource`
70
+ * source marker). We instead count active-block summaries directly from kernel
71
+ * state (same source `buildStatusReport` uses), and the caller must exclude
72
+ * checkpoint summary nodes from `messages` (they are not in any block's
73
+ * `effectiveMessageIds` and would double-count — mirror of `/acp` status's
74
+ * `isCheckpointNode` exclusion).
75
+ */
76
+ export declare function computeSurfaceBreakdown(state: CompressionState, messages: readonly CoreMessage[], total: number, growth: number): ContextBreakdown;
77
+ /**
78
+ * Max emergency nudge injections within a single user turn. Bounds the
79
+ * positive-feedback loop where an unrelieved ≥emergency-threshold pressure
80
+ * re-injects a durable emergency nudge on every pre-step forever (issue #108).
81
+ * Mirrors billion-context-pi commit 414acd1 (cap emergency nudge injections per
82
+ * user turn). Normal-pressure nudges remain limited to one per turn regardless.
46
83
  */
47
- export declare function rangeTable(session: import('@deepseek-ai/dsh-session').Session, prompts?: ResolvedPrompts): string;
84
+ export declare const EMERGENCY_NUDGE_MAX_PER_TURN = 3;
48
85
  /**
49
86
  * Decide and build one nudge message for the agent's next pre-step. Returns
50
- * null when the kernel recommends no nudge or one was already injected for the
51
- * current turn (emergency nudges always bypass the dedup). Also advances the
52
- * in-memory kernel state (ref assignment) so the compress tool can resolve
53
- * seq → mNNNNN refs.
87
+ * null when the kernel recommends no nudge or the per-turn budget is spent:
88
+ * normal-pressure nudges fire at most once per user turn, and emergency nudges
89
+ * are capped at {@link EMERGENCY_NUDGE_MAX_PER_TURN} per user turn so an
90
+ * unrelieved ≥threshold pressure cannot re-inject a durable nudge on every
91
+ * pre-step forever (issue #108). Also advances the in-memory kernel state (ref
92
+ * assignment) so the compress tool can resolve seq → mNNNNN refs.
93
+ *
94
+ * `onEmergencyCapHit` (optional) fires when the kernel still wants an
95
+ * emergency nudge but the per-turn budget is spent — the host uses it to log
96
+ * why the model stops receiving nudges (issue #108 review).
54
97
  */
55
- export declare function buildNudge(agent: Agent, env: NudgeEnvironment, lastNudgeTurn: Map<string, number>): NudgeOutcome | null;
98
+ export declare function buildNudge(agent: Agent, env: NudgeEnvironment, lastNudgeTurn: Map<string, number>, emergencyNudges: Map<string, {
99
+ turn: number;
100
+ count: number;
101
+ }>, onEmergencyCapHit?: () => void): NudgeOutcome | null;
56
102
  /**
57
103
  * Render the nudge message text. DEFAULT (no `config.prompts.nudge` override)
58
104
  * calls the kernel's own `renderNudgeText` — EFFICIENCY_NOTE/EMERGENCY_HEADER,
@@ -66,4 +112,4 @@ export declare function buildNudge(agent: Agent, env: NudgeEnvironment, lastNudg
66
112
  * When a host overrides any `prompts.nudge` slot, the template path is used so
67
113
  * `config.prompts` keeps full control (custom copy wins over kernel defaults).
68
114
  */
69
- export declare function buildNudgeText(nudge: NudgeDecision, emergency: boolean, session: import('@deepseek-ai/dsh-session').Session, prompts?: ResolvedPrompts): string;
115
+ export declare function buildNudgeText(nudge: NudgeDecision, emergency: boolean, session: import('@deepseek-ai/dsh-session').Session, kernelView: KernelRangeView, prompts?: ResolvedPrompts, mediaPriceOf?: MediaPriceOf): string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Named presets for the nudge thresholds — how eagerly the model is asked to
3
+ * compress, in one word instead of three hand-tuned percentages (issue #105).
4
+ *
5
+ * A preset is a bundle of the three first-class nudge-threshold knobs
6
+ * (`nudgeMinContextLimitPct` / `nudgeMaxContextLimitPct` /
7
+ * `nudgeEmergencyThresholdPct`). It does NOT touch any other knob: `modelContextLimit`,
8
+ * `autoNudge`, prompts, and the `coreOverrides` escape hatch all stay exactly as
9
+ * configured. The individual thresholds remain fully available and win over the
10
+ * preset when both are set — precedence is explicit value > preset > engine
11
+ * default (applied in `resolveAcpConfig`, src/index.ts), so a partial override
12
+ * on top of a preset is honored.
13
+ *
14
+ * The five tiers form a monotonic spectrum from least to most aggressive
15
+ * compression. `balanced` reproduces the current out-of-the-box engine defaults
16
+ * exactly (min 0.45 = kernel default, max 0.70, emergency 0.85), so choosing it
17
+ * changes nothing relative to today's behavior.
18
+ *
19
+ * Presets are set at composition time (`config: { preset: 'efficient' }`).
20
+ * Runtime hot-reload of the underlying keys rides on issue #75 Phase 1
21
+ * (`settings.yaml` + `/acp config`); surfacing the `preset` alias through that
22
+ * same channel is the small follow-up once Phase 1 lands. The two knobs named in
23
+ * the original request that are NOT first-class engine knobs today — `growthRatio`
24
+ * (exists in acp-kernel as `nudge.growthRatio`, reachable via `coreOverrides`) and
25
+ * `protectedLastMessages` (≈ kernel `preserveRecentMessages`) — are deliberately
26
+ * out of scope here; adopting them as named knobs is an owner decision, not a
27
+ * preset detail.
28
+ * @module billion-context-dsh/presets
29
+ */
30
+ /** The five preset tier names. */
31
+ export type PresetName = 'preserve' | 'relaxed' | 'balanced' | 'efficient' | 'aggressive';
32
+ /** The preset tiers ordered least → most aggressive (for help text / display). */
33
+ export declare const PRESET_NAMES: readonly PresetName[];
34
+ /** One preset tier: a human label plus the three nudge-threshold values it sets. */
35
+ export interface NudgePreset {
36
+ /** Plain-language one-liner describing the tier's trade-off. */
37
+ readonly label: string;
38
+ /** Nudge window lower bound (usage fraction; the threshold gate floor). */
39
+ readonly nudgeMinContextLimitPct: number;
40
+ /** Over-limit guarantee line — above this the nudge fires regardless of growth. */
41
+ readonly nudgeMaxContextLimitPct: number;
42
+ /** Emergency nudge threshold (bypasses the per-turn dedup). */
43
+ readonly nudgeEmergencyThresholdPct: number;
44
+ }
45
+ /**
46
+ * The five tiers. Every row satisfies the kernel invariant
47
+ * `min ≤ max ≤ emergency` (the kernel only WARNS on the reverse — it never rejects
48
+ * the config, so `resolveAcpConfig` rejects an inverted merged triple itself), and
49
+ * all three values move monotonically toward "compress sooner" as you go down
50
+ * the list. Values are fractions of the context window, not token counts.
51
+ */
52
+ export declare const PRESETS: Readonly<Record<PresetName, NudgePreset>>;
53
+ /** Type guard: true when `value` is one of the five preset names. */
54
+ export declare function isPresetName(value: unknown): value is PresetName;
55
+ /**
56
+ * Resolve a preset name to its tier. Throws on an unknown name so a typo in the
57
+ * composition config fails engine construction loudly (the same fail-fast
58
+ * contract as prompt-template validation) rather than silently falling back to
59
+ * the engine defaults.
60
+ */
61
+ export declare function resolvePreset(name: string): NudgePreset;
package/dist/prompts.d.ts CHANGED
@@ -22,9 +22,9 @@ export type PromptOverride<T> = {
22
22
  [K in keyof T]?: PromptInput;
23
23
  };
24
24
  export interface NudgePrompts {
25
- /** 普通档首句。占位符:{pct} {philosophy} */
25
+ /** 普通档首句。占位符:{pct}(`{philosophy}` 仍被校验器接受,但 B6 会把渲染出的哲学段摘掉——它只能经系统提示到达模型) */
26
26
  normal: string;
27
- /** 紧急档首句。占位符:{pct} {philosophy} */
27
+ /** 紧急档首句。占位符:{pct}(`{philosophy}` 同上) */
28
28
  emergency: string;
29
29
  /** 指导行(HOW_TO_COMPRESS_RULES)。无占位符 */
30
30
  guidance: string;
package/dist/region.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { Session, SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session';
14
14
  import { type ContentBlock } from '@deepseek-ai/dsh-llm';
15
+ import { type AcpBlockLedgerPayload } from './block-ledger.ts';
15
16
  /** One durable ACP block as rebuilt from the session log. */
16
17
  export interface AcpBlockLedgerEntry {
17
18
  /** The compaction transaction id (stable block identity). */
@@ -34,6 +35,8 @@ export interface AcpBlockLedgerEntry {
34
35
  /** The kernel block's raw direct/effective message ids at creation (recorded since the tier feature; absent for legacy). */
35
36
  readonly directMessageIds?: readonly string[];
36
37
  readonly effectiveMessageIds?: readonly string[];
38
+ /** B3: acceptance readings that were already green before compression (absent when the compress call carried none). */
39
+ readonly verifiedReadings?: readonly string[];
37
40
  /** Unix epoch ms of the compaction/summary event. */
38
41
  readonly createdAt: number;
39
42
  }
@@ -117,33 +120,32 @@ export interface CompactionTransactionInput {
117
120
  /** The kernel block's direct/effective message ids (raw CoreMessage ids) — recorded for faithful rehydration. */
118
121
  readonly directMessageIds?: readonly string[];
119
122
  readonly effectiveMessageIds?: readonly string[];
123
+ /** B3:压缩前已绿的验收读数(结构化,压缩后仍可读)。 */
124
+ readonly verifiedReadings?: readonly string[];
120
125
  }
126
+ type CompactionSummaryData = SessionEventMap['compaction/summary'];
121
127
  /**
122
- * ACP tier extension fields carried on `compaction/summary` events. The
123
- * upstream dsh-compaction event type does not know them, so reads and writes
124
- * go through this precise intersection (never `any`).
128
+ * Read a `compaction/summary` event's data. The six ACP tier/lineage fields are
129
+ * no longer top-level members (issue #141): post-fix writers carry them in the
130
+ * admitted optional `rawOutput` member (decode via {@link decodeAcpBlockLedger}),
131
+ * while logs written by pre-fix engines still carry them as top-level members —
132
+ * so the returned type also intersects with {@link AcpBlockLedgerPayload}, letting
133
+ * readers fall back to the legacy shape. Never `any`.
125
134
  */
126
- export interface AcpCompactionSummaryFields {
127
- /** Compression tier (1/2/3) — 1 = message range, 2 = distills tier-1, 3 = distills tier-2. */
128
- readonly tier?: 1 | 2 | 3;
129
- /** Short block label (kernel `CompressionBlock.topic`) — the acp_status block title. */
130
- readonly topic?: string;
131
- /** The acp-kernel block id (`bN`) created for this transaction. */
132
- readonly kernelBlockId?: string;
133
- /** Durable compaction ids of the blocks distilled into this one. */
134
- readonly parentBlockIds?: readonly string[];
135
- /**
136
- * The kernel block's direct message ids (raw CoreMessage ids) at creation —
137
- * recorded so a restarted engine rehydrates the SAME coverage (a tier-2
138
- * block's coverage is its parents' originals, not the checkpoint node).
139
- */
140
- readonly directMessageIds?: readonly string[];
141
- /** The kernel block's effective message ids (raw CoreMessage ids) at creation. */
142
- readonly effectiveMessageIds?: readonly string[];
143
- }
144
- type CompactionSummaryData = SessionEventMap['compaction/summary'];
145
- /** Read a `compaction/summary` event's data including the ACP tier extension fields. */
146
- export declare function readCompactionSummary(event: SessionEvent): CompactionSummaryData & AcpCompactionSummaryFields;
135
+ export declare function readCompactionSummary(event: SessionEvent): CompactionSummaryData & AcpBlockLedgerPayload;
136
+ /**
137
+ * B3: read the structured verified readings a compress call recorded for this
138
+ * block (acceptance checks that were already green before the range was
139
+ * shadowed — e.g. "t0-fastpath 8/8"). Post-fix writers carry them inside the
140
+ * admitted `rawOutput` member (AcpBlockLedgerPayload); legacy writers put them
141
+ * top-level. Absent in either shape → empty array; never throws.
142
+ */
143
+ export declare function verifiedReadingsOf(event: SessionEvent): string[];
144
+ /**
145
+ * B1:给摘要块数组的第一个文本块加标源前缀(幂等——已带前缀不重复加)。
146
+ * 只动文本块,工具/图片块原样保留。
147
+ */
148
+ export declare function prefixSummaryBlocks(blocks: readonly ContentBlock[]): ContentBlock[];
147
149
  /**
148
150
  * Run one durable compression transaction. Throws on invalid state; on success
149
151
  * the four events are in the log and the surface has one summary node.
@@ -162,7 +164,30 @@ export interface SeqCompressibleRange {
162
164
  readonly tokens: number;
163
165
  /** Share of messages that are tool messages (tool-call or tool-result), 0-100 — kernel `toolPct` parity. */
164
166
  readonly toolPct: number;
167
+ /** Image blocks reachable inside the span (directly or through a tool result). */
168
+ readonly images: number;
169
+ /** File blocks reachable inside the span. */
170
+ readonly files: number;
165
171
  }
172
+ /**
173
+ * Per-seq provider-anchored price for non-text blocks (see `mediaPriceViaMeter`
174
+ * in host-tokens.ts). A callback, not a map, so a media-free session never pays
175
+ * for a meter measurement: the range walk only asks about seqs it already knows
176
+ * carry an image/file block.
177
+ */
178
+ export type MediaPriceOf = (seq: number) => number;
179
+ /**
180
+ * Durable model-free prune: append `compaction/prune` as the shadow price,
181
+ * then replace the given surface seqs with a user message. dsh-session 0.1.5+
182
+ * allows only user/message (and system/message) replacements to cite source
183
+ * events — assistant/message FORBIDS `sourceEventSeqs` because it embeds its
184
+ * own provider stream — so there is no invisible replacement node anymore:
185
+ * every hidden span becomes a user message. Callers with meaningful text pass
186
+ * it (compress call/result hiding keeps the tool outcome visible to the
187
+ * model); callers without get the fixed prune note. The originals remain in
188
+ * the append-only log.
189
+ */
190
+ export declare const PRUNE_NOTE = "(removed by context management)";
166
191
  /**
167
192
  * Hide one successful `compress` tool's call/result pair after its tool/result
168
193
  * has been logged. The durable compaction summary is inserted BEFORE the
@@ -208,18 +233,92 @@ export declare function openToolCallIds(session: Session): Set<string>;
208
233
  */
209
234
  export declare function deferCompressPairHide(session: Session, callId: string, resultSeq: number, onError?: (error: unknown) => void): void;
210
235
  /**
211
- * Compute compressible spans directly from the surface — independent of the
212
- * kernel's ref map, which can drift after surface replacements in long
213
- * sessions and hide large tool results from the nudge range table. Skips the
214
- * recent protected tail, the last user message, and compaction checkpoints;
215
- * edges are then balanced through resolveSurfaceRange. Ranges are ordered
216
- * oldest-first (stable across turns — matches the kernel's `oldest first`).
217
- * UPSTREAM: this self-computation is a labeled workaround for kernel
218
- * ref-map drift after surface replacements (AGENTS.md rule 11) — drop it and
219
- * use kernel compressibleRanges once the drift is fixed upstream.
236
+ * Newest AGENTS.md instruction row per scope (source file). The host
237
+ * re-injects a file's instructions when its CURRENT copy is absent from the
238
+ * surface (deepseek-harness packages/context/agent-instructions presence
239
+ * gate, index.ts:137/:163 — presence+identity, not payload diff), so
240
+ * compressing the newest row of a scope makes that file come straight back,
241
+ * while compressing a STALE copy of the same file is silent. Live-audited
242
+ * shape (session-f25e4fad): EVERY injection row — baseline and worktree —
243
+ * carries `source.changes[].scope` = `"<dir>\u0000<file>"` (root
244
+ * `.\u0000AGENTS.md`, worktree `worktrees/<name>\u0000AGENTS.md`), which is
245
+ * stable across config tweaks unlike `baselineIdentity`. Tail-scan the log,
246
+ * group by scope, keep the last seq of each group. O(events), mirrors
247
+ * indexWatermarkOf. Rows without `changes[]` (legacy shapes) are SKIPPED
248
+ * entirely: identity is what the host's presence gate needs in order to
249
+ * re-inject a file, so a scope-less row can never come back and must not be
250
+ * guarded (the earlier shape gave each its own group, which made every legacy
251
+ * row a permanent hard-reject — issue #71 review S3).
220
252
  */
221
- export declare function buildCompressibleSeqRanges(session: Session, opts?: {
253
+ export declare function newestInstructionSeqsOf(session: Session): Set<number>;
254
+ /**
255
+ * Surface seqs NO caller may compress: the CURRENT (newest) injected
256
+ * agent-instructions row of every scope, restricted to rows still visible on
257
+ * the surface (one definition of "current" — `newestInstructionSeqsOf`).
258
+ * `buildCompressibleSeqRanges` never OFFERS them, and both compress entry
259
+ * points (`handleCompress` in src/tools.ts, `/acp compress` in
260
+ * src/commands.ts) probe the RESOLVED span against this set and HARD-REJECT a
261
+ * covering range before the kernel applies it, so nothing durable lands and no
262
+ * phantom block can exist. This supersedes the earlier F7 draft (warn only):
263
+ * folding a current copy reclaims nothing — the host re-injects it — so there
264
+ * is no legitimate outcome to warn about. Deliberately NARROW (issue #71
265
+ * review F4): only CURRENT agent-instructions rows — the audited loop driver.
266
+ * Engine-authored metadata rows (nudge echo, compress-pair stub) stay
267
+ * foldable like main, and STALE copies of the same file stay compressible —
268
+ * removing them while the newest copy stays visible is the real cleanup.
269
+ */
270
+ export declare function guardedSurfaceSeqsOf(session: Session): Set<number>;
271
+ /**
272
+ * The kernel's own view of what can be compressed, as the engine hands it to
273
+ * the range table: the geometry (`nudge.compressibleRanges`) plus the ref map
274
+ * that turns a kernel ref back into a surface seq (`state.messageRefs`).
275
+ *
276
+ * Structural shapes only, so the engine passes the kernel's own objects
277
+ * straight through and tests can hand-build a view.
278
+ */
279
+ export interface KernelRangeView {
280
+ /** Kernel `recommendedRanges`/`compressibleRanges` entries (oldest first). */
281
+ readonly ranges: readonly {
282
+ readonly startRef: string;
283
+ readonly endRef: string;
284
+ }[];
285
+ /** Kernel ref map: `mNNNNN` → our message id (which IS the surface seq). */
286
+ readonly refs: {
287
+ readonly byRef: Readonly<Record<string, string>>;
288
+ };
289
+ }
290
+ /**
291
+ * Compressible spans in the DSH seq dialect, for the nudge range table.
292
+ *
293
+ * The GEOMETRY — which messages group into one compressible span — comes from
294
+ * the kernel's own ranges (design decision 7: the kernel owns the algorithm).
295
+ * The kernel splits a group when the next message is a user turn and the group
296
+ * already holds 3+ messages, and after any protected or already-compressed
297
+ * message, so a row reads as "roughly one stretch of work" rather than an
298
+ * arbitrary slice. This function only does the two jobs the kernel cannot:
299
+ *
300
+ * 1. Translate refs into surface seqs — DSH has no `<acp>` ref tags; seq is our
301
+ * ref (design decision 2).
302
+ * 2. Apply the host guards on top of the kernel's grouping: injected
303
+ * instruction rows split a span and the newest copy of every scope is never
304
+ * offered, checkpoints and the surface's system node are not compressible,
305
+ * and the recent tail plus the last REAL user turn stay protected (rule 16).
306
+ *
307
+ * History — why this used to compute the spans itself. A kernel range's edges
308
+ * were derived by counting refs, and a surface replacement breaks that
309
+ * arithmetic: the checkpoint node of a replace lands mid-array carrying a much
310
+ * higher ref, so ref order and array order diverge and the spans came back
311
+ * reversed (`end < start`) or lost large tool results entirely. The table was
312
+ * therefore self-computed from the surface, labeled `UPSTREAM:` and tracked as
313
+ * issue #38 (rule 11). The pinned kernel segments by ARRAY adjacency instead
314
+ * (upstream #207) and the drift is gone — measured on a session whose
315
+ * compressed span sits in the MIDDLE of the surface: every ref resolves to the
316
+ * right seq, no span crosses the shadowed hole, and the compressed span is
317
+ * excluded. Rules 3 and 11 are updated with it.
318
+ */
319
+ export declare function buildCompressibleSeqRanges(session: Session, kernelView: KernelRangeView, opts?: {
222
320
  preserveRecent?: number;
321
+ mediaPriceOf?: MediaPriceOf;
223
322
  }): SeqCompressibleRange[];
224
323
  /**
225
324
  * A compact human-readable description of the current surface for the model:
@@ -280,4 +379,56 @@ export declare function summarySeqOfKernelBlock(session: Session, kernelBlockId:
280
379
  * seqs. Cycle-safe (a block can never be its own ancestor).
281
380
  */
282
381
  export declare function expandShadowedSeqs(session: Session, blockId: string): number[];
382
+ /**
383
+ * Default decompress page size (#112): a block shadowing hundreds of
384
+ * messages used to be returned whole in ONE tool result — big enough to
385
+ * flood the context window or get silently trimmed by the host's
386
+ * tool-result pruner before the model ever saw the tail. One page per call
387
+ * keeps every recovery usable; `offset` walks the rest.
388
+ *
389
+ * A page is bounded by BOTH this message count and a rendered-character
390
+ * budget ({@link DEFAULT_DECOMPRESS_PAGE_CHARS}). Count alone was not enough:
391
+ * the host's `dsh-compaction-tool-result-pruner` (docs/dsh-porting-analysis.md)
392
+ * trims by CHARACTERS (thresholdChars 8192), so a wide page of long messages
393
+ * still crossed that line and had its middle dropped. The char bound keeps an
394
+ * ordinary page under the pruner threshold so it comes back intact; the
395
+ * message count doubles as a hard ceiling so a pathological `limit` can't
396
+ * re-open the whole-block flooding half of #112.
397
+ */
398
+ export declare const DEFAULT_DECOMPRESS_PAGE = 100;
399
+ /**
400
+ * Rendered-character budget per decompress page (#112). Kept below the host's
401
+ * tool-result pruner threshold (8192, docs/dsh-porting-analysis.md) with
402
+ * headroom for the block header, the `[seq N]` prefixes, and the continue hint,
403
+ * so a normal page survives intact instead of middle-trimmed. Deliberately NOT
404
+ * tied to acp-kernel's `config.truncate.threshold`: that knob truncates a single
405
+ * oversized tool output during compression, whereas the host pruner trims our
406
+ * whole decompress result — different mechanisms, different thresholds.
407
+ */
408
+ export declare const DEFAULT_DECOMPRESS_PAGE_CHARS = 7000;
409
+ export interface DecompressPage {
410
+ /** Requested offset floored to >= 0; reported as-is when it lands past the end. */
411
+ offset: number;
412
+ /** Limit actually applied (clamped to [1, DEFAULT_DECOMPRESS_PAGE]). */
413
+ limit: number;
414
+ /** Total shadowed messages in the block (tier-expanded). */
415
+ total: number;
416
+ /** This page's shadowed seqs, in expansion order. */
417
+ seqs: number[];
418
+ /** True when no further page follows this one. */
419
+ exhausted: boolean;
420
+ }
421
+ /**
422
+ * Slice a block's expanded shadowed-seq list into one page. A page holds at most
423
+ * `limit` messages AND at most `charBudget` rendered characters, where
424
+ * `renderLen(seq)` reports each message's on-the-wire length (0 when it carries
425
+ * no text). Seqs whose original carries no text still occupy a slot, so `offset`
426
+ * stays a stable continuation index across calls while the log is frozen.
427
+ * Out-of-range / negative / non-finite values clamp instead of failing (optional
428
+ * convenience params, not semantic boundaries); non-numeric input falls back to
429
+ * the default rather than leaking NaN into the result. The first message of the
430
+ * page is always included even if it alone exceeds the budget, so a walk always
431
+ * makes progress past a single giant message.
432
+ */
433
+ export declare function sliceDecompressPage(expanded: number[], offset: number, limit: number, charBudget: number, renderLen: (seq: number) => number): DecompressPage;
283
434
  export {};