billion-context-pi-lean 0.1.69-lean.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +141 -81
  3. package/dist/abort.d.ts +1 -0
  4. package/dist/branch-guard.d.ts +8 -0
  5. package/dist/cache-tool.d.ts +12 -0
  6. package/dist/chunk-PPYOZRUP.js +18 -0
  7. package/dist/chunk-PPYOZRUP.js.map +1 -0
  8. package/dist/commands.d.ts +8 -0
  9. package/dist/compat.d.ts +23 -0
  10. package/dist/compress-model.d.ts +65 -0
  11. package/dist/compress-tool.d.ts +76 -0
  12. package/dist/config-dir.d.ts +32 -0
  13. package/dist/config-panel.d.ts +61 -0
  14. package/dist/config.d.ts +296 -0
  15. package/dist/contract.d.ts +51 -0
  16. package/dist/contract.js +9 -0
  17. package/dist/contract.js.map +1 -0
  18. package/dist/decompress-tool.d.ts +19 -0
  19. package/dist/degeneration.d.ts +98 -0
  20. package/dist/export.d.ts +20 -0
  21. package/dist/floor-stale.d.ts +31 -0
  22. package/dist/host.d.ts +15 -0
  23. package/dist/idle-compact.d.ts +39 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +23169 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/live-only-tail.d.ts +10 -0
  28. package/dist/log.d.ts +18 -0
  29. package/dist/messages.d.ts +85 -0
  30. package/dist/omp.d.ts +19 -0
  31. package/dist/overflow-selfheal.d.ts +132 -0
  32. package/dist/prompt-pack.d.ts +53 -0
  33. package/dist/proxy-detect.d.ts +21 -0
  34. package/dist/reasoning-drop.d.ts +68 -0
  35. package/dist/rule-tool.d.ts +8 -0
  36. package/dist/runtime.d.ts +199 -0
  37. package/dist/search-index.d.ts +20 -0
  38. package/dist/search-tool.d.ts +10 -0
  39. package/dist/sequence-match.d.ts +7 -0
  40. package/dist/session-log.d.ts +23 -0
  41. package/dist/state-rebuild.d.ts +30 -0
  42. package/dist/state.d.ts +44 -0
  43. package/dist/status-tool.d.ts +13 -0
  44. package/dist/strip-images.d.ts +18 -0
  45. package/dist/summary-sanitize.d.ts +8 -0
  46. package/dist/surface.d.ts +17 -0
  47. package/dist/system-passthrough.d.ts +4 -0
  48. package/dist/system-prompt.d.ts +20 -0
  49. package/dist/tag-tokens.d.ts +3 -0
  50. package/dist/throttle-retry.d.ts +52 -0
  51. package/dist/tokens.d.ts +61 -0
  52. package/dist/tool-guardrails.d.ts +33 -0
  53. package/dist/tool-pair-sanitizer.d.ts +8 -0
  54. package/dist/turn-boundary.d.ts +53 -0
  55. package/dist/user-config.d.ts +67 -0
  56. package/lean/index.ts +376 -0
  57. package/package.json +85 -36
  58. package/schema/bcp-block-v1.json +42 -0
  59. package/README.zh-CN.md +0 -79
  60. package/index.ts +0 -266
@@ -0,0 +1,296 @@
1
+ import { type Config, type Prompts } from "acp-kernel";
2
+ import type { CompressReasoningConfig } from "./reasoning-drop.js";
3
+ import type { DegenerationGuardConfig } from "./degeneration.js";
4
+ import type { ThrottleRetryConfig } from "./throttle-retry.js";
5
+ import type { IdleCompactConfig } from "./idle-compact.js";
6
+ import type { PiPromptSections } from "./system-prompt.js";
7
+ import type { NudgeSectionsConfig, ToolPromptsConfig } from "./surface.js";
8
+ /** Compression tuning fields, shared by all three levels (global, provider,
9
+ * model). Percentage fields accept a ratio (0.75) or percent string ("75%").
10
+ * Resolution is per-field, deepest-wins (model > provider > global); an
11
+ * undefined field at a deeper level does NOT clear a shallower value. */
12
+ export interface CompressSettings {
13
+ /** Context usage percentage that triggers forced compression nudges
14
+ * (bypasses growth-gate + cadence). Accepts a ratio (0.75) or percent
15
+ * string ("75%"). Default: 0.75. Maps to kernel nudge.maxContextLimitPct. */
16
+ maxContextLimit?: number | string;
17
+ /** Context usage percentage that triggers emergency truncation of large
18
+ * tool outputs. Accepts a ratio (0.95) or percent string ("95%").
19
+ * Default: 0.95. Must be >= maxContextLimit. Maps to kernel
20
+ * nudge.emergencyThresholdPct + truncate.threshold. */
21
+ emergencyThresholdPercent?: number | string;
22
+ /** Token growth threshold for soft compression nudges. Default: 50000.
23
+ * Maps to kernel nudge.growthFloor + nudge.growthCap. */
24
+ nudgeGrowthTokens?: number;
25
+ /** Minimum reclaimable tokens for a pressure-band nudge (kernel #198).
26
+ * Default: max(5000, round(limit×0.01)). Explicit 0 restores the legacy
27
+ * any-pending behavior — useful for tiny windows (e.g. e2e scenarios with
28
+ * modelContextLimit 1500) where a fixed 5000-token floor exceeds the whole
29
+ * window and would suppress every nudge. Maps to kernel
30
+ * nudge.minPressureBenefitTokens. */
31
+ minPressureBenefitTokens?: number;
32
+ /** [#336] Drop oversized reasoning (thinking) from historical `compress`
33
+ * tool calls — see CompressReasoningConfig in src/reasoning-drop.ts.
34
+ * Merged field-wise (drop, threshold) across the three levels. */
35
+ reasoning?: CompressReasoningConfig;
36
+ /** Active prompt pack name (see CONFIGURATION.md “Prompt packs”). Base
37
+ * level; override per provider/model via `providers`. "default" or unset =
38
+ * built-in defaults. Resolved per request against the live model, so
39
+ * switching models mid-session switches the pack. Packs ship text-level
40
+ * overrides only; a pack's `toolPrompts` follow the base selection (tool
41
+ * definitions freeze at extension load, before the model is known). */
42
+ promptPack?: string;
43
+ /** Opt-in wire-level strip of historical image payloads (issue #321, kernel
44
+ * #215). Default: false (images ride along verbatim, current behavior).
45
+ * When true, every message older than `stripImagesKeepRecent` has its image
46
+ * parts dropped from the outbound provider body (image-only messages
47
+ * collapse to a "[image]" text placeholder). Host-side policy only — the
48
+ * strip primitive lives in acp-kernel's wire layer. */
49
+ stripImages?: boolean;
50
+ /** How many of the MOST RECENT messages keep their image payloads when
51
+ * `stripImages` is enabled. Default: 5. Ignored when stripImages is off. */
52
+ stripImagesKeepRecent?: number;
53
+ }
54
+ /** Per-provider compression overrides. Carries the same tuning fields as the
55
+ * global level, plus an optional per-model map keyed by model id. */
56
+ export interface ProviderCompress extends CompressSettings {
57
+ /** Per-model overrides within this provider, keyed by model id
58
+ * (e.g. "claude-sonnet-4-5"). */
59
+ models?: Record<string, CompressSettings>;
60
+ }
61
+ /** Compression tuning. Top-level fields are global defaults; `providers`
62
+ * optionally narrows them per Pi provider name and per model id. The active
63
+ * entry is resolved live each turn from the current model
64
+ * (`ctx.model.provider` / `ctx.model.id`). */
65
+ export interface CompressConfig extends CompressSettings {
66
+ /** Per-provider (and per-model) overrides, keyed by Pi provider name
67
+ * (e.g. "anthropic", "openai", "zhipu") — the same name used in
68
+ * models.json and `pi --provider`. */
69
+ providers?: Record<string, ProviderCompress>;
70
+ }
71
+ /** Generic tool-call repetition guard. Detects the same tool being called
72
+ * repeatedly with byte-identical arguments — a sequence-level attractor that
73
+ * token-level penalties cannot break (greedy small models loop on e.g.
74
+ * acp_status or bash polls forever, each round adding protected tokens that
75
+ * the compression protection band then refuses to reclaim). Consecutive
76
+ * identical calls accumulate per session: at `warn` a strong warning is
77
+ * appended to that call's toolResult; at `abort` the call is refused (blocked,
78
+ * error toolResult) and the turn aborted so the attractor breaks. Any change
79
+ * to the arguments (or a switch to a different tool) resets the counter.
80
+ * Accepts a boolean shorthand (`false` disables) or an object. Default:
81
+ * enabled, warn=3, abort=5. See issue #308. */
82
+ export interface RepetitionGuardConfig {
83
+ /** Enable/disable the guard. Default: true. */
84
+ enabled?: boolean;
85
+ /** Consecutive identical calls before a strong warning is appended to the
86
+ * matching toolResult. Default: 3. */
87
+ warn?: number;
88
+ /** Consecutive identical calls before the call is refused and the turn
89
+ * aborted. Default: 5. Must be greater than `warn`; clamped up
90
+ * automatically if not. */
91
+ abort?: number;
92
+ }
93
+ /**
94
+ * Adapter configuration. Maps onto acp-kernel's `Config` plus Pi-specific knobs
95
+ * (live model context window, protected tools, state persistence).
96
+ */
97
+ export interface AdapterConfig {
98
+ /** Master switch. Default: true. Set `enabled: false` in acp.json (or
99
+ * programmatically) to turn the whole adapter off — no tools, no system
100
+ * prompt, no context transform — for models too small to handle ACP, where
101
+ * Pi's native context management should run instead. Checked at extension
102
+ * load; requires a Pi restart to take effect. */
103
+ enabled?: boolean;
104
+ /** When omitted, the adapter reads `ctx.model.contextWindow` live each turn.
105
+ * Set explicitly for tests/headless runs. */
106
+ modelContextLimit?: number;
107
+ /** Tool-name patterns (glob suffix allowed) whose EVERY call+result pair is
108
+ * hard-excluded from compression — matching refs render as BLOCKED in every
109
+ * view. Default: none. Intended for low-frequency high-value tools whose
110
+ * outputs are independent content (e.g. skill loads); do NOT use for chatty
111
+ * tools — protecting every instance grows context unboundedly (#639
112
+ * rationale). Settable via acp.json since #499. */
113
+ protectedTools?: string[];
114
+ /** Tool-name patterns (glob suffix allowed) whose LATEST call+result pair is
115
+ * hard-excluded from compression; older pairs remain compressible. Default:
116
+ * none. Intended for cumulative-snapshot tools where each call supersedes
117
+ * the last. Settable via acp.json since #499. */
118
+ protectedLatestTools?: string[];
119
+ /** Tool-name patterns (glob suffix allowed) EXCLUDED from the soft-protected
120
+ * recent zone: matching tool results inside the recent window fold
121
+ * immediately instead of aging out first (kernel >= 0.0.92). Default:
122
+ * unset → kernel built-in ["decompress", "search_context", "read",
123
+ * "bash"]. The built-in keeps read/bash compressible — the largest
124
+ * reclaimable mass — but that also folds freshly-read files in batch-read
125
+ * workflows (#1198-style fold→re-read loop, bili #1277). Recommended
126
+ * remedy: ["decompress", "search_context", "bash"] (remove only read).
127
+ * ⚠ Unlike the two protection keys, an EMPTY ARRAY IS VALID — it excludes
128
+ * nothing and gives every tool recent-zone protection (escape hatch); do
129
+ * not re-include decompress/search_context or just-restored blocks get
130
+ * pinned in the recent zone and become unreclaimable. */
131
+ neverPreserveRecentTools?: string[];
132
+ /** The positive counterpart of neverPreserveRecentTools: tool-name
133
+ * patterns REMOVED from the effective recent-zone exclusion list (kernel
134
+ * >= 0.0.93) — `(neverPreserveRecentTools ?? built-in) minus
135
+ * preserveRecentTools`. The one-entry #1198/#1277 batch-read fold→re-read
136
+ * remedy: ["read"] protects fresh read results without restating (or
137
+ * freezing a stale copy of) the built-in list. Default: unset → no
138
+ * subtraction. Unlike neverPreserveRecentTools an EMPTY ARRAY IS
139
+ * INVALID here (pure no-op — use neverPreserveRecentTools: [] for
140
+ * protect-everything instead). */
141
+ preserveRecentTools?: string[];
142
+ preserveRecentMessages?: number;
143
+ /** Enable debug-level events in the ACP log file (default ~/.pi/acp.log).
144
+ * Always-on events (session/turn/compress lifecycle, all errors and
145
+ * warnings) are written regardless; `debug` only adds verbose diagnostics.
146
+ * Default: false (or env ACP_DEBUG=1/true). */
147
+ debug?: boolean;
148
+ /** Default timeout in seconds injected into the bash tool when the model
149
+ * omits `timeout`. Pi has NO built-in default, so without this a command
150
+ * that the model forgets to time out can hang for thousands of seconds.
151
+ * Default: 60 (catches hangs quickly). On timeout the model is guided to
152
+ * re-run with a larger `timeout`. Set to 0 to disable (restore Pi's
153
+ * unbounded behavior). */
154
+ toolBashDefaultTimeout?: number;
155
+ /** Hard byte cap applied to tool result text via the `tool_result` hook.
156
+ * Default: 50000 (~50KB) — aligned with Pi's own bash/read/grep cap so
157
+ * every tool path lands under one ceiling; the net still catches runaway
158
+ * output from tools Pi does not cap (MCP/custom). Set higher for large
159
+ * MCP outputs, lower (e.g. 8192) for a tighter context budget, or 0 to
160
+ * disable. When capped, oversized text is head-truncated with a notice
161
+ * telling the model how to see the full output (bash: read
162
+ * BashToolDetails.fullOutputPath). */
163
+ toolOutputMaxBytes?: number;
164
+ /** Compression tuning. */
165
+ compress?: CompressConfig;
166
+ /** Provider token-throttle (Bedrock "Too many tokens, please wait before
167
+ * trying again.") auto-retry. Accepts a boolean shorthand (`false`
168
+ * disables) or a ThrottleRetryConfig object. Default: enabled, 10 retries,
169
+ * 60s exponential base capped at 300s per kick. */
170
+ throttleRetry?: boolean | ThrottleRetryConfig;
171
+ /** Lean: idle compaction check (src/idle-compact.ts). After `idleMinutes`
172
+ * (default 29) of idle time, one turn shows the main model the compress
173
+ * nudge so it can compress while the prefix is still cached.
174
+ * `true` or `{ enabled: true, idleMinutes: 29 }`. Default: off. */
175
+ idleCompact?: boolean | IdleCompactConfig;
176
+ /** Cap on the output-headroom reservation as a fraction of the context
177
+ * window: reserved = min(model.maxTokens, pct * window). Accepts a ratio
178
+ * (0.25) or percent string ("25%"). Default: 0.25. Set 0 to disable the
179
+ * reservation entirely; >= 1 restores the legacy full-capability
180
+ * reservation (issue #207). */
181
+ outputHeadroomMaxPct?: number | string;
182
+ /** Generic tool-call repetition guard (see RepetitionGuardConfig). Accepts a
183
+ * boolean shorthand (`false` disables) or an object. Default: enabled,
184
+ * warn=3, abort=5. Stops greedy small models looping on byte-identical
185
+ * tool calls (issue #308). */
186
+ repetitionGuard?: boolean | RepetitionGuardConfig;
187
+ /** Character-level degenerate-repeat guard (see DegenerationGuardConfig).
188
+ * Collapses long single-codepoint runs (e.g. 4655×「【」) in assistant
189
+ * text/thinking of the outgoing view and injects a one-shot recovery notice
190
+ * after a degenerated turn — breaking the abort loop where pi replays the
191
+ * degenerated thinking back to the provider on every request (issue #351).
192
+ * Distinct from `repetitionGuard`, which is tool-call level. Accepts a
193
+ * boolean shorthand (`false` disables) or an object. Default: enabled,
194
+ * minRun=200. */
195
+ degenerationGuard?: boolean | DegenerationGuardConfig;
196
+ /** Host multi-session turn-boundary policy (#364). Accepts a boolean
197
+ * shorthand (`true` → count host-injected custom_message entries as turn
198
+ * boundaries) or a HostSessionConfig object. Default: off — pi-native
199
+ * behavior where only genuine user-role messages start a turn, so existing
200
+ * single-session users' nudge cadence is unchanged. Enable for inline
201
+ * multi-session hosts (Prime RLM & co.) whose injected agent messages must
202
+ * delimit real turns. See docs/host-adapter.md. */
203
+ hostSession?: boolean | HostSessionConfig;
204
+ /** Persistent-rules feature gate (#526): opt-in `rules: true` in acp.json.
205
+ * Gates the human `/acp-rule` command (list + record + remove + clear — kernel
206
+ * `listRules`/`addRule`/`removeRule`/`clearRules` against the session `.acp.json` sidecar state) and
207
+ * the model-side `acp_rule` tool (#490). Custom limits via
208
+ * coreOverrides.rules ({ maxRules?, maxRuleChars? }). */
209
+ rules?: boolean;
210
+ /** Override acp-kernel's load-bearing compression prompt rules (the 4
211
+ * Prompts fields). Each set field replaces the kernel default verbatim.
212
+ * Requires acknowledgePromptsRisk: true — without it, overrides are dropped
213
+ * (defaults used) and a warning is logged. Set via ~/.pi/acp.json. */
214
+ prompts?: Partial<Prompts>;
215
+ /** Must be true for `prompts` overrides to take effect. Acknowledges that
216
+ * replacing the kernel's tuned compression rules may reduce summary quality
217
+ * (lost paths/signatures/decisions → worse retrieval). */
218
+ acknowledgePromptsRisk?: boolean;
219
+ /** Override structural sections of the ACP system prompt (ACP TAGS, TOOLS,
220
+ * WHEN TO COMPRESS, ...). Tri-state per section: string = replace, null =
221
+ * remove, omitted = default. Not risk-gated — these are documentation
222
+ * sections, not compression rules. Set via acp.json. */
223
+ promptSections?: Partial<PiPromptSections>;
224
+ /** Override guidance-class nudge texts (efficiencyNote, emergencyHeader,
225
+ * t2Guidance, t3Guidance). Same tri-state semantics. Not risk-gated. */
226
+ nudgeSections?: NudgeSectionsConfig;
227
+ /** Override the four ACP tool definitions' LLM-facing text (description,
228
+ * paramDescriptions, promptSnippet, promptGuidelines). Read synchronously
229
+ * at extension load — tool defs are frozen at registration time. */
230
+ toolPrompts?: ToolPromptsConfig;
231
+ /** Dedicated summary model (#241 port). `session` reuses the active model
232
+ * and the captured prompt prefix; `provider/id` or a bare id resolves via
233
+ * Pi's model registry. Omitted = the main model writes summaries. Managed
234
+ * by `/acp compact`. */
235
+ compressionModelId?: string;
236
+ coreOverrides?: Partial<Config>;
237
+ /** Host-only (never read from acp.json): the embedding extension replaces
238
+ * the ACP system prompt with its own text. The before_agent_start handler
239
+ * still runs its side effects (pack stamp, summary rules), and config keys
240
+ * that only shape the replaced prompt (promptSections) are
241
+ * reported once as ignored instead of silently dropped. */
242
+ externalSystemPrompt?: boolean;
243
+ }
244
+ export declare const DEFAULT_TOOL_BASH_TIMEOUT = 60;
245
+ export declare const DEFAULT_TOOL_OUTPUT_MAX_BYTES = 50000;
246
+ /** Defaults for the generic tool-call repetition guard (issue #308). */
247
+ export declare const REPETITION_GUARD_DEFAULTS: {
248
+ readonly warn: 3;
249
+ readonly abort: 5;
250
+ };
251
+ /** Resolve the repetition-guard configuration from the adapter, handling the
252
+ * boolean shorthand (`false` disables) and clamping `abort` to stay strictly
253
+ * above `warn` (a guard that aborts at or before its warn threshold would be
254
+ * meaningless). Non-numeric / out-of-range values fall back to defaults. */
255
+ export declare function resolveRepetitionGuard(adapter: AdapterConfig): {
256
+ enabled: boolean;
257
+ warn: number;
258
+ abort: number;
259
+ };
260
+ /** Host multi-session turn-boundary policy (#364). See TurnBoundaryPolicy in
261
+ * src/turn-boundary.ts for the semantics this resolves. */
262
+ export interface HostSessionConfig {
263
+ /** Count host-injected custom_message entries (agent_message) as turn
264
+ * boundaries. Default: false (pi-native behavior). */
265
+ countCustomMessages?: boolean;
266
+ /** #578: optional customType allowlist refining countCustomMessages — when
267
+ * set (with countCustomMessages:true), only injected entries whose
268
+ * customType is listed start a turn, keeping metadata injections
269
+ * (harness_digest, ipython_state) out of nudge/retry accounting. Ignored
270
+ * unless countCustomMessages:true; malformed values warn and fall back to
271
+ * "all injected types count". */
272
+ customMessageTypes?: string[];
273
+ }
274
+ export interface ResolvedHostSession {
275
+ countCustomMessages: boolean;
276
+ /** Present only when countCustomMessages is on and a valid allowlist was
277
+ * configured (#578). */
278
+ customMessageTypes?: readonly string[];
279
+ }
280
+ /** Resolve the host-session turn-boundary policy from the adapter, handling
281
+ * the boolean shorthand (`true` enables countCustomMessages). Invalid values
282
+ * fall back to the pi-native default (off) with a logged warning — they never
283
+ * fail the session. */
284
+ export declare function resolveHostSession(adapter: AdapterConfig): ResolvedHostSession;
285
+ /** Per-field deepest-wins merge of the three compression levels (global →
286
+ * provider → model). An undefined field at a deeper level does NOT clear a
287
+ * value set at a shallower level — only a defined value overrides. */
288
+ export declare function mergeCompress(global?: CompressSettings, provider?: CompressSettings, model?: CompressSettings): CompressSettings;
289
+ /** Resolve the effective compression settings for the active model: global →
290
+ * provider (matched by Pi provider name) → model (matched by model id).
291
+ * Returns a CompressSettings whose fields are undefined when nothing is set
292
+ * at any level. The Pi adapter keys providers by name (ctx.model.provider),
293
+ * not URL — it never sees the upstream URL the way the proxy does. */
294
+ export declare function resolveCompress(compress: CompressConfig | undefined, provider: string | undefined, modelId: string | undefined): CompressSettings;
295
+ export declare function resolveConfig(adapter: AdapterConfig, liveContextLimit: number, provider?: string, modelId?: string): Config;
296
+ export declare function parsePercent(v: number | string): number;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Machine-readable contract for the `<sessionFile>.acp.json` sidecar (#368).
3
+ *
4
+ * Downstream tools (e.g. pi-billion-memory) glob the sidecar directly instead
5
+ * of running inside pi. To keep that stable across releases:
6
+ *
7
+ * - `schemaVersion` is written on every save. A MISSING version means v1
8
+ * (pre-contract files). An unknown NEWER version means "fields may have
9
+ * changed" — skip it, log once, never rewrite the file.
10
+ * - `producer` identifies the writer (`billion-context-pi@x.y.z`).
11
+ * - The file is replaced atomically (tmp + rename), so a reader either sees
12
+ * the previous or the next complete file, never a torn write.
13
+ * - Blocks are stored verbatim as the kernel's CompressionBlock; unknown
14
+ * extra fields are additive and must be ignored by readers.
15
+ */
16
+ export declare const SIDECAR_SCHEMA_VERSION = 1;
17
+ /**
18
+ * One compressed block, as persisted in the sidecar's `blocks[]`. Mirrors the
19
+ * kernel's CompressionBlock (acp-kernel 0.0.81) — inlined rather than aliased
20
+ * so this subpath carries no type dependency on acp-kernel (dev-only, bundled
21
+ * at build time). Fields the kernel adds later are additive; tests assert the
22
+ * kernel shape still covers this one.
23
+ */
24
+ export interface BcpBlockV1 {
25
+ blockId: string;
26
+ runId: string;
27
+ tier: 1 | 2 | 3;
28
+ topic?: string;
29
+ summary: string;
30
+ directMessageIds: string[];
31
+ effectiveMessageIds: string[];
32
+ directBlockIds: string[];
33
+ compressedTokens: number;
34
+ createdAt: number;
35
+ survivedCount: number;
36
+ generation: "young" | "old";
37
+ active: boolean;
38
+ expanded?: boolean;
39
+ durationMs?: number;
40
+ compressCallId?: string;
41
+ startRef?: string;
42
+ endRef?: string;
43
+ }
44
+ /** Top-level shape of the sidecar (state fields beyond the contract allowed). */
45
+ export interface BcpSidecarV1 {
46
+ schemaVersion: number;
47
+ producer: string;
48
+ blocks: BcpBlockV1[];
49
+ [extra: string]: unknown;
50
+ }
51
+ export declare function sidecarProducer(): string;
@@ -0,0 +1,9 @@
1
+ import {
2
+ SIDECAR_SCHEMA_VERSION,
3
+ sidecarProducer
4
+ } from "./chunk-PPYOZRUP.js";
5
+ export {
6
+ SIDECAR_SCHEMA_VERSION,
7
+ sidecarProducer
8
+ };
9
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,19 @@
1
+ import { Type } from "typebox";
2
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
3
+ import type { AcpRuntime } from "./runtime.js";
4
+ import { type ToolPromptOverrides } from "./surface.js";
5
+ declare const DecompressParams: Type.TObject<{
6
+ blockId: Type.TString;
7
+ full: Type.TOptional<Type.TBoolean>;
8
+ toFile: Type.TOptional<Type.TString>;
9
+ inline: Type.TOptional<Type.TBoolean>;
10
+ }>;
11
+ export declare function makeDecompressTool(runtime: AcpRuntime, overrides?: ToolPromptOverrides): ToolDefinition<typeof DecompressParams>;
12
+ /** Generate a unique auto file path for a block. Uses a timestamp so repeated
13
+ * decompressions of the same block never overwrite each other. */
14
+ /** Lean: filename stem for a message ref. Raw entry refs can carry
15
+ * characters Windows rejects in filenames (e.g. `4d9e965e#toolu_x|fc_toolu_x`);
16
+ * keep safe refs readable and give unsafe ones a sanitized prefix plus a
17
+ * short hash so distinct refs never collide. */
18
+ export declare function messageFileStem(ref: string): string;
19
+ export {};
@@ -0,0 +1,98 @@
1
+ import type { SessionMessageEntry } from "@earendil-works/pi-coding-agent";
2
+ type AgentMessage = SessionMessageEntry["message"];
3
+ /** [#351] Character-level degenerate-repeat guard. Models occasionally
4
+ * degenerate into long single-codepoint runs (observed in the wild: a
5
+ * 5968-char thinking block ending in 4655 consecutive 「【」, escalating over
6
+ * turns until the turn aborts). This is distinct from `repetitionGuard`
7
+ * (issue #308), which breaks byte-identical TOOL-CALL loops; here the
8
+ * attractor is inside the generated text/thinking itself.
9
+ *
10
+ * Why the adapter must act: pi replays prior assistant thinking back to the
11
+ * provider on every subsequent request (openai-completions sends it as
12
+ * `reasoning_content`, or as plain text when `requiresThinkingAsText`), and
13
+ * an aborted turn's partial message persists in the session log. A
14
+ * degenerated tail therefore rides along every later prompt, where the model
15
+ * sees its own previous output ending in thousands of repeated characters —
16
+ * a continuation bias that re-triggers the same degeneration, aborting the
17
+ * next turn too. The session dies in an abort loop with no recovery path.
18
+ *
19
+ * The pass below collapses runs >= minRun in assistant text/thinking of the
20
+ * OUTGOING view (persisted history is never modified) and appends a one-shot
21
+ * recovery notice while the degenerated message is the most recent assistant
22
+ * turn. Position-based self-limiting: a fresh model turn makes the old
23
+ * message non-last, so the notice stops appearing without any persistent
24
+ * state (and cannot accumulate — #223 lesson). */
25
+ export interface DegenerationGuardConfig {
26
+ /** Master switch. Default: true. `false` disables the pass entirely
27
+ * (kill-switch). */
28
+ enabled?: boolean;
29
+ /** Minimum length of a single-codepoint run (counted in codepoints) before
30
+ * it is treated as degeneration. Legitimate single-char runs in coding
31
+ * sessions (markdown hrules, dotted leaders, comment banners) stay well
32
+ * under this; observed pre-degeneration drift maxed at ~60 before the
33
+ * catastrophic 4655×「【」 run. Default: 200. Values below 8 are raised to
34
+ * 8: the collapse marker must stay shorter than any detectable run, and a
35
+ * sub-8 threshold would start matching ordinary dotted leaders/hrules. */
36
+ minRun?: number;
37
+ }
38
+ export declare const DEFAULT_DEGENERATION_GUARD: Required<DegenerationGuardConfig>;
39
+ /** Resolve the guard config, handling the boolean shorthand (`false`
40
+ * disables). Invalid minRun values (< 2 or non-numeric) fall back to the
41
+ * default with a logged warning — they never fail the session. Valid values
42
+ * below MIN_VALID_MIN_RUN are raised to it. */
43
+ export declare function resolveDegenerationGuard(cfg?: boolean | DegenerationGuardConfig): Required<DegenerationGuardConfig>;
44
+ /** One maximal run of a repeated codepoint. */
45
+ export interface DegenerateRun {
46
+ /** The repeated codepoint (string of length 1 or 2 — surrogate pairs kept whole). */
47
+ char: string;
48
+ /** Run length in codepoints. */
49
+ count: number;
50
+ /** UTF-16 index of the run start in the original string. */
51
+ index: number;
52
+ }
53
+ /** Find maximal runs of a single repeated codepoint with length >= minRun.
54
+ * Codepoint-safe: surrogate pairs count as one unit, so a run of astral
55
+ * characters is detected like any other. Returns [] for empty input or
56
+ * minRun < 2 (a "run" shorter than 2 carries no signal). */
57
+ export declare function findDegenerateRuns(text: string, minRun: number): DegenerateRun[];
58
+ /** Collapse every degenerate run in `text` into a short marker that names the
59
+ * character and its original count. Pure, idempotent, fail-safe. Returns the
60
+ * input unchanged when there is nothing to collapse. */
61
+ export declare function collapseDegenerateRuns(text: string, minRun: number): string;
62
+ /** Evidence of one rewritten message: which blocks changed and what was collapsed. */
63
+ export interface CollapseEvidence {
64
+ /** Index of the rewritten message in the input array. */
65
+ msgIndex: number;
66
+ /** Block kinds rewritten ("content" for string content, else "text"/"thinking"). */
67
+ blocks: string[];
68
+ /** Runs collapsed across those blocks. */
69
+ runs: DegenerateRun[];
70
+ }
71
+ /** Request-time pass: collapse degenerate single-codepoint runs in ASSISTANT
72
+ * messages' text/thinking blocks (string content included). Every position is
73
+ * scanned — including the current turn's partial assistant message, which is
74
+ * exactly the one pi will replay back to the provider on the next request.
75
+ * Tool-call arguments are never touched (rewriting them would desync the
76
+ * model's view from the call that actually executed). Pure: returns the same
77
+ * array reference when nothing changed; idempotent; fail-safe (any error
78
+ * returns the input unchanged). Persisted history is never modified. */
79
+ export declare function collapseAssistantDegeneration(messages: AgentMessage[], cfg?: boolean | DegenerationGuardConfig): {
80
+ messages: AgentMessage[];
81
+ evidence: CollapseEvidence[];
82
+ };
83
+ /** Scan backward for the LAST assistant message and collect its degenerate
84
+ * runs. Call it with the PERSISTED originals in session order (not the
85
+ * outgoing view): thinking-only aborted turns never reach the outgoing view
86
+ * (projectMessage drops them), yet they are still "the previous turn".
87
+ * Returns null when absent or clean. Gates the recovery notice: because the
88
+ * notice fires only while the degenerated message is the most recent
89
+ * assistant turn, it self-limits — once the model produces a new turn the
90
+ * old message is no longer last and the notice disappears on the next
91
+ * rebuild (no persistent state needed). */
92
+ export declare function lastAssistantRuns(messages: AgentMessage[], minRun: number): DegenerateRun[] | null;
93
+ /** One-shot recovery notice appended while a degenerated assistant message is
94
+ * the most recent turn: tells the model the repeated segment carries no
95
+ * information and was truncated, and to resume from the last valid step
96
+ * instead of continuing the attractor. */
97
+ export declare function degenerationNotice(runs: DegenerateRun[]): AgentMessage;
98
+ export {};
@@ -0,0 +1,20 @@
1
+ export interface ExportOptions {
2
+ output?: string;
3
+ full?: boolean;
4
+ }
5
+ export interface SessionSummary {
6
+ id: string;
7
+ title?: string;
8
+ label?: string;
9
+ savedAt?: number;
10
+ contextTokens?: number;
11
+ blocks: number;
12
+ }
13
+ export declare function listSessions(sessionDir: string): Promise<SessionSummary[]>;
14
+ export declare function exportSession(selector: string | undefined, opts: ExportOptions, sessionDir: string): Promise<string>;
15
+ export declare function parseExportArgs(args: string): {
16
+ selector?: string;
17
+ full: boolean;
18
+ output?: string;
19
+ error?: string;
20
+ };
@@ -0,0 +1,31 @@
1
+ import type { CompressionBlock } from "acp-kernel";
2
+ type UsageLike = {
3
+ totalTokens?: number;
4
+ input?: number;
5
+ output?: number;
6
+ cacheRead?: number;
7
+ cacheWrite?: number;
8
+ } | null | undefined;
9
+ type AnchorEntry = {
10
+ type: string;
11
+ message?: {
12
+ role?: string;
13
+ stopReason?: string;
14
+ usage?: UsageLike;
15
+ toolName?: string;
16
+ toolCallId?: string;
17
+ isError?: boolean;
18
+ content?: unknown;
19
+ };
20
+ };
21
+ /** True when the last valid assistant usage anchor comes strictly BEFORE the
22
+ * last successful compress toolResult — the host's provider-usage number
23
+ * still reflects the pre-compression request. Failed/no-op compresses don't
24
+ * count (nothing was reclaimed). */
25
+ export declare function usageAnchorPredatesCompression(entries: AnchorEntry[]): boolean;
26
+ export interface AnchorStaleness {
27
+ predates: boolean;
28
+ netReclaimed: number;
29
+ }
30
+ export declare function compressionAnchorStaleness(entries: AnchorEntry[], blocks: readonly CompressionBlock[], countTokens: (text: string) => number): AnchorStaleness;
31
+ export {};
package/dist/host.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ /** Which session-entry source API the host exposes (same precedence as readContextEntries). */
3
+ export type EntrySourceKind = "buildContextEntries" | "getBranch";
4
+ export declare function entrySourceOf(sm: ExtensionContext["sessionManager"]): EntrySourceKind | null;
5
+ /**
6
+ * Whether the host declared itself a Pi-compatible fork via environment.
7
+ *
8
+ * The SessionManager shape alone cannot distinguish e.g. Prime from OMP — both are
9
+ * Pi forks exposing `getBranch()` but not `buildContextEntries()` — so an
10
+ * unsupported shape requires an explicit declaration before the adapter runs.
11
+ * Read at call time so hosts can set it per-launch. See docs/host-adapter.md.
12
+ */
13
+ export declare function isDeclaredForkHost(): boolean;
14
+ /** Unsupported host: not Pi-shaped and not declared as a fork. OMP stays blocked by default. */
15
+ export declare function isUnsupportedHost(sm: ExtensionContext["sessionManager"]): boolean;
@@ -0,0 +1,39 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { NudgeDecision } from "acp-kernel";
3
+ import type { AcpRuntime } from "./runtime.js";
4
+ /**
5
+ * Idle compaction check (Lean). After the session has been idle for
6
+ * `idleMinutes` (default 29, just inside a 30-minute prompt cache), start one
7
+ * turn that shows the main model the regular compress nudge and lets it decide
8
+ * whether to compress. If the cache is still warm, that turn reads the long
9
+ * prefix from cache, and the request the user sends on return carries the
10
+ * shorter context instead of rewriting the long one uncached.
11
+ *
12
+ * Timing follows pi-keepwarm: the anchor is the start of the last provider
13
+ * request of a run. Any run counts (user input, sub-agent wake, throttle kick)
14
+ * except the idle check itself, so one idle period gets at most one check.
15
+ */
16
+ export declare const IDLE_CHECK_CUSTOM_TYPE = "acp-idle-check";
17
+ export declare const DEFAULT_IDLE_MINUTES = 29;
18
+ /** Timer woke this much later than planned (machine slept): the cache is likely cold, skip. */
19
+ export declare const IDLE_LATE_MS = 60000;
20
+ export interface IdleCompactConfig {
21
+ enabled?: boolean;
22
+ /** Minutes of idle time before the check. Default 29. */
23
+ idleMinutes?: number;
24
+ }
25
+ export interface ResolvedIdleCompact {
26
+ enabled: boolean;
27
+ idleMinutes: number;
28
+ }
29
+ export declare function resolveIdleCompact(cfg: boolean | IdleCompactConfig | undefined): ResolvedIdleCompact;
30
+ export declare function idleCheckText(awayMinutes: number): string;
31
+ export interface IdleCompactHandle {
32
+ /** Record the last context event's compressible mass (context hook, every event). */
33
+ noteNudge(sid: string, nudge: NudgeDecision | undefined): void;
34
+ /** True once per idle check: the context hook appends the nudge even below its usual thresholds. */
35
+ takeForcedNudge(sid: string): boolean;
36
+ /** Re-plan the pending timer after the config changed (settings panel). */
37
+ rearm(): void;
38
+ }
39
+ export declare function wireIdleCompact(pi: ExtensionAPI, runtime: AcpRuntime): IdleCompactHandle;
@@ -0,0 +1,10 @@
1
+ import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
+ import type { NudgeDecision } from "acp-kernel";
3
+ import { type AdapterConfig } from "./config.js";
4
+ export { createRuntime } from "./runtime.js";
5
+ export type { AcpRuntime, SessionRef } from "./runtime.js";
6
+ export { deriveChildState } from "./state.js";
7
+ export declare function createAcpExtension(adapter?: AdapterConfig): ExtensionFactory;
8
+ declare const _default: ExtensionFactory;
9
+ export default _default;
10
+ export declare function formatNudgeRecord(nudge: NudgeDecision, emergency: boolean): string;