pi-observational-memory 3.0.1 → 3.0.3

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/README.md CHANGED
@@ -208,6 +208,8 @@ A typical config:
208
208
  "observeAfterTokens": 10000,
209
209
  "reflectAfterTokens": 20000,
210
210
  "compactAfterTokens": 81000,
211
+ "compactAfterTokensMode": "calibrated",
212
+ "compactAfterTokensRatio": 0.68,
211
213
  "observationsPoolMaxTokens": 20000,
212
214
  "observationsPoolTargetTokens": 10000,
213
215
  "agentMaxTurns": 16,
@@ -224,13 +226,52 @@ A typical config:
224
226
 
225
227
  Most users can start with the defaults and tune only if they have a specific reason.
226
228
 
229
+ ### Scaling compaction to the model's context window
230
+
231
+ By default `compactAfterTokensMode` is `"calibrated"`, so the proactive
232
+ compaction trigger fires at the fixed `compactAfterTokens` value (81,000 by
233
+ default). That is backwards-compatible and works well for typical ~128K–200K
234
+ context models.
235
+
236
+ On a large-context model (e.g. 1M tokens) the calibrated default preempts
237
+ compaction at ~81K, wasting most of the window. Switch to `"ratio"` mode to let
238
+ the trigger scale with the active model's `contextWindow`:
239
+
240
+ ```json
241
+ {
242
+ "observational-memory": {
243
+ "compactAfterTokens": 81000,
244
+ "compactAfterTokensMode": "ratio",
245
+ "compactAfterTokensRatio": 0.5
246
+ }
247
+ }
248
+ ```
249
+
250
+ In ratio mode the effective threshold is
251
+ `floor(model.contextWindow * compactAfterTokensRatio)` (clamped to a minimum of
252
+ 1). With the example above, a 1,000,000-token window compacts at ~500,000 raw
253
+ tokens; a 200,000-token window compacts at ~100,000.
254
+
255
+ `compactAfterTokensRatio` is user-tunable precisely because **context window ≠
256
+ attention**. Some models advertise a large window but degrade at long range; set
257
+ a lower ratio (e.g. `0.4`) to compact earlier on those, or a higher ratio
258
+ (e.g. `0.7`) on models that stay sharp. The default ratio is `0.68`.
259
+
260
+ `compactAfterTokens` is always retained as the fallback: in `"calibrated"`
261
+ mode it is the threshold directly, and in `"ratio"` mode it is used whenever
262
+ the active model's `contextWindow` is unavailable (undefined, 0, or negative),
263
+ so compaction still triggers safely. `/om:status` shows the resolved threshold
264
+ on the `Next compaction` line regardless of mode.
265
+
227
266
  ### Defaults
228
267
 
229
268
  | Setting | Default | Meaning |
230
269
  | --------------------------- | ------------- | ------------------------------------------------------------------------------------------------- |
231
270
  | `observeAfterTokens` | `10000` | Raw/source token threshold for observation runs. |
232
271
  | `reflectAfterTokens` | `20000` | Raw/source token threshold for reflection runs; successful reflection creates dropper opportunities. |
233
- | `compactAfterTokens` | `81000` | Raw/source token threshold for proactive auto-compaction. |
272
+ | `compactAfterTokens` | `81000` | Raw/source token threshold for proactive auto-compaction (used directly in `"calibrated"` mode, and as the fallback in `"ratio"` mode). |
273
+ | `compactAfterTokensMode` | `"calibrated"`| `"calibrated"` uses `compactAfterTokens` directly (default, backwards-compatible). `"ratio"` scales the threshold by the active model's `contextWindow`. |
274
+ | `compactAfterTokensRatio` | `0.68` | In `"ratio"` mode, the threshold is `floor(contextWindow * ratio)`. Tunable because large windows do not always mean strong long-range attention. Must be in `(0, 1)`. |
234
275
  | `observationsPoolMaxTokens` | `20000` | Observation-token budget used for compaction full-fold pressure. |
235
276
  | `observationsPoolTargetTokens` | half of max | Active observation target used by post-reflection dropper maintenance. |
236
277
  | `agentMaxTurns` | `16` | Shared turn cap for background memory-agent loops. |
@@ -263,12 +304,12 @@ For details and tuning guidance, see [`docs/configuration.md`](docs/configuratio
263
304
 
264
305
  | Surface | What it does |
265
306
  | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
266
- | `/om-status` | Shows memory counts, plain `+N` / `-N` visible/full drift suffixes, progress clocks, visible and active observation pool pressure, passive/in-flight state, and last worker errors. |
267
- | `/om-view` | Shows current visible memory and attempts to copy the rendered memory text to the clipboard. |
268
- | `/om-view full` | Shows the full current memory state for the branch and attempts to copy the rendered memory text to the clipboard. |
307
+ | `/om:status` | Shows memory counts, plain `+N` / `-N` visible/full drift suffixes, progress clocks, visible and active observation pool pressure, passive/in-flight state, and last worker errors. |
308
+ | `/om:view` | Shows current visible memory and attempts to copy the rendered memory text to the clipboard. |
309
+ | `/om:view full` | Shows the full current memory state for the branch and attempts to copy the rendered memory text to the clipboard. |
269
310
  | `recall` agent tool | Recovers source evidence for a 12-character observation/reflection id on the current branch. It is not semantic search or a transcript browser. |
270
311
 
271
- `/om-view` copies only the rendered memory content. The success/failure line shown in Pi is not included in the clipboard text. If clipboard support is unavailable, the command still prints the memory view and shows a warning. Before the first V3 compaction, visible memory can be empty because nothing has been folded into `om.folded` details; use `/om-view full` to inspect recorded branch memory.
312
+ `/om:view` copies only the rendered memory content. The success/failure line shown in Pi is not included in the clipboard text. If clipboard support is unavailable, the command still prints the memory view and shows a warning. Before the first V3 compaction, visible memory can be empty because nothing has been folded into `om.folded` details; use `/om:view full` to inspect recorded branch memory.
272
313
 
273
314
  ---
274
315
 
@@ -310,7 +351,7 @@ Current behavior:
310
351
  * **Fast compaction.** `session_before_compact` does not call a model or wait for background workers. It renders the current prepared memory state.
311
352
  * **Background memory work.** Observation and reflection work run from `turn_end` when their token clocks are due; dropper work runs only after successful reflection and prunes the folded active observation ledger toward `observationsPoolTargetTokens`.
312
353
  * **Source-backed recall.** Observations and reflections can be traced back through the `recall` tool.
313
- * **Visible/full views.** `/om-view` shows visible memory and `/om-view full` shows the full current memory state. Use `/om-status` for visible-vs-full drift and for the separate visible observation pool vs active observation pool.
354
+ * **Visible/full views.** `/om:view` shows visible memory and `/om:view full` shows the full current memory state. Use `/om:status` for visible-vs-full drift and for the separate visible observation pool vs active observation pool.
314
355
  * **No V2 compatibility layer.** Old V2 settings and memory entries are ignored rather than migrated.
315
356
 
316
357
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-observational-memory",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Observational memory extension for pi — cache-friendly tiered compaction with observations and reflections.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { observationPoolMetrics } from "../agents/dropper/pool.js";
3
+ import { resolveCompactAfterTokens } from "../config.js";
3
4
  import type { Runtime } from "../runtime.js";
4
5
  import {
5
6
  diffProjection,
@@ -13,7 +14,7 @@ import {
13
14
  } from "../session-ledger/index.js";
14
15
 
15
16
  function pct(current: number, total: number): number {
16
- return total > 0 ? Math.min(100, Math.round((current / total) * 100)) : 0;
17
+ return total > 0 ? Math.round((current / total) * 100) : 0;
17
18
  }
18
19
 
19
20
  function tokenSum(items: { tokenCount: number }[]): number {
@@ -34,7 +35,7 @@ function appendSuffixes(line: string, suffixes: (string | undefined)[]): string
34
35
  }
35
36
 
36
37
  export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void {
37
- pi.registerCommand("om-status", {
38
+ pi.registerCommand("om:status", {
38
39
  description: "Show observational memory status",
39
40
  handler: async (_args, ctx) => {
40
41
  runtime.ensureConfig(ctx.cwd);
@@ -61,6 +62,8 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
61
62
  const obsProgress = rawTokensSinceObservationCoverage(entries);
62
63
  const reflectionProgress = rawTokensSinceReflectionCoverage(entries);
63
64
  const compactionProgress = rawTokensSinceLastCompaction(entries);
65
+ const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
66
+ const compactThreshold = resolveCompactAfterTokens(runtime.config, contextWindow);
64
67
 
65
68
  const passiveLines = runtime.config.passive === true
66
69
  ? [
@@ -79,7 +82,7 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
79
82
  "── Activity ──",
80
83
  `Next observation: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
81
84
  `Next reflection: ~${reflectionProgress.toLocaleString()} / ${runtime.config.reflectAfterTokens.toLocaleString()} tokens (${pct(reflectionProgress, runtime.config.reflectAfterTokens)}%)`,
82
- `Next compaction: ~${compactionProgress.toLocaleString()} / ${runtime.config.compactAfterTokens.toLocaleString()} tokens (${pct(compactionProgress, runtime.config.compactAfterTokens)}%)`,
85
+ `Next compaction: ~${compactionProgress.toLocaleString()} / ${compactThreshold.toLocaleString()} tokens (${pct(compactionProgress, compactThreshold)}%)`,
83
86
  `Visible observation pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
84
87
  `Active observation pool: ~${activeObservationPool.observationTokens.toLocaleString()} / ${runtime.config.observationsPoolTargetTokens.toLocaleString()} target tokens (${pct(activeObservationPool.observationTokens, runtime.config.observationsPoolTargetTokens)}%)`,
85
88
  `Reflection pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
@@ -41,7 +41,7 @@ interface ViewCommandOptions {
41
41
  export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime, options: ViewCommandOptions = {}): void {
42
42
  const copyToClipboard = options.copyToClipboard ?? copyTextToClipboard;
43
43
 
44
- pi.registerCommand("om-view", {
44
+ pi.registerCommand("om:view", {
45
45
  description: "Print and copy observational memory content (visible by default, full for recorded memory)",
46
46
  handler: async (args, ctx) => {
47
47
  runtime.ensureConfig(ctx.cwd);
@@ -52,8 +52,8 @@ export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime, options:
52
52
  const copied = await copyToClipboard(output).catch(() => false);
53
53
  ctx.ui.notify(
54
54
  copied
55
- ? `${output}\n\nCopied /om-view output to clipboard.`
56
- : `${output}\n\nWarning: failed to copy /om-view output to clipboard.`,
55
+ ? `${output}\n\nCopied /om:view output to clipboard.`
56
+ : `${output}\n\nWarning: failed to copy /om:view output to clipboard.`,
57
57
  "info",
58
58
  );
59
59
  };
@@ -64,7 +64,7 @@ export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime, options:
64
64
  }
65
65
 
66
66
  if (mode && mode !== "visible") {
67
- ctx.ui.notify("Usage: /om-view [full]", "info");
67
+ ctx.ui.notify("Usage: /om:view [full]", "info");
68
68
  return;
69
69
  }
70
70
 
package/src/config.ts CHANGED
@@ -9,10 +9,33 @@ export interface ConfiguredModel {
9
9
  thinking?: ModelThinkingLevel;
10
10
  }
11
11
 
12
+ /**
13
+ * How `compactAfterTokens` is interpreted.
14
+ *
15
+ * - `"calibrated"` (default): use the static `compactAfterTokens` value directly.
16
+ * Backwards-compatible with all existing V3 configs.
17
+ *
18
+ * - `"ratio"`: compute the effective threshold as
19
+ * `floor(model.contextWindow * compactAfterTokensRatio)`. This auto-scales the
20
+ * proactive compaction trigger to the active model's context window, so a 1M
21
+ * context model is not preempted at the same 81K threshold as a 128K model.
22
+ *
23
+ * Some models advertise a large context window but lose attention at long
24
+ * range; users can lower `compactAfterTokensRatio` to compact earlier on such
25
+ * models without giving up the window on models that stay sharp.
26
+ *
27
+ * When the active model's `contextWindow` is unavailable (undefined, 0, or
28
+ * negative), ratio mode falls back to the calibrated `compactAfterTokens`
29
+ * value so compaction still triggers safely.
30
+ */
31
+ export type CompactAfterTokensMode = "calibrated" | "ratio";
32
+
12
33
  export interface Config {
13
34
  observeAfterTokens: number;
14
35
  reflectAfterTokens: number;
15
36
  compactAfterTokens: number;
37
+ compactAfterTokensMode: CompactAfterTokensMode;
38
+ compactAfterTokensRatio: number;
16
39
  observationsPoolMaxTokens: number;
17
40
  observationsPoolTargetTokens: number;
18
41
  agentMaxTurns: number;
@@ -25,6 +48,8 @@ export const DEFAULTS: Config = {
25
48
  observeAfterTokens: 10_000,
26
49
  reflectAfterTokens: 20_000,
27
50
  compactAfterTokens: 81_000,
51
+ compactAfterTokensMode: "calibrated",
52
+ compactAfterTokensRatio: 0.68,
28
53
  observationsPoolMaxTokens: 20_000,
29
54
  observationsPoolTargetTokens: 10_000,
30
55
  agentMaxTurns: 16,
@@ -32,6 +57,25 @@ export const DEFAULTS: Config = {
32
57
  debugLog: false,
33
58
  };
34
59
 
60
+ export const COMPACT_AFTER_TOKENS_MODE_VALUES: readonly CompactAfterTokensMode[] = ["calibrated", "ratio"] as const;
61
+
62
+ /**
63
+ * Resolve the effective proactive-compaction token threshold for the given
64
+ * config and active model context window.
65
+ *
66
+ * In `"calibrated"` mode this is always `config.compactAfterTokens`.
67
+ *
68
+ * In `"ratio"` mode this is `floor(contextWindow * compactAfterTokensRatio)`
69
+ * (clamped to a minimum of 1) when `contextWindow` is a positive number, and
70
+ * falls back to `config.compactAfterTokens` otherwise.
71
+ */
72
+ export function resolveCompactAfterTokens(config: Config, contextWindow: number | undefined): number {
73
+ if (config.compactAfterTokensMode === "ratio" && typeof contextWindow === "number" && contextWindow > 0) {
74
+ return Math.max(1, Math.floor(contextWindow * config.compactAfterTokensRatio));
75
+ }
76
+ return config.compactAfterTokens;
77
+ }
78
+
35
79
  export const THINKING_LEVEL_VALUES: readonly ModelThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
36
80
 
37
81
  const SETTINGS_KEY = "observational-memory";
@@ -54,6 +98,19 @@ function isThinkingLevel(value: unknown): value is ModelThinkingLevel {
54
98
  return typeof value === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(value);
55
99
  }
56
100
 
101
+ function isCompactAfterTokensMode(value: unknown): value is CompactAfterTokensMode {
102
+ return typeof value === "string" && (COMPACT_AFTER_TOKENS_MODE_VALUES as readonly string[]).includes(value);
103
+ }
104
+
105
+ /**
106
+ * A valid ratio is a finite number strictly between 0 and 1.
107
+ * 0 would never trigger; >= 1 would compact at/after the full window with no
108
+ * room left for the response.
109
+ */
110
+ function validRatioOrUndefined(value: unknown): number | undefined {
111
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value < 1 ? value : undefined;
112
+ }
113
+
57
114
  function isRecord(value: unknown): value is Record<string, unknown> {
58
115
  return typeof value === "object" && value !== null;
59
116
  }
@@ -86,6 +143,11 @@ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config
86
143
  const normalizedValue = positiveIntegerOrUndefined(value[key]);
87
144
  if (normalizedValue !== undefined) normalized[key] = normalizedValue;
88
145
  }
146
+ if (isCompactAfterTokensMode(value.compactAfterTokensMode)) {
147
+ normalized.compactAfterTokensMode = value.compactAfterTokensMode;
148
+ }
149
+ const ratio = validRatioOrUndefined(value.compactAfterTokensRatio);
150
+ if (ratio !== undefined) normalized.compactAfterTokensRatio = ratio;
89
151
  if (typeof value.passive === "boolean") normalized.passive = value.passive;
90
152
  if (typeof value.debugLog === "boolean") normalized.debugLog = value.debugLog;
91
153
  const model = normalizeModel(value.model);
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { resolveCompactAfterTokens } from "../config.js";
2
3
  import { rawTokensSinceLastCompaction, type Entry } from "../session-ledger/index.js";
3
4
  import type { Runtime } from "../runtime.js";
4
5
 
@@ -33,7 +34,12 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
33
34
 
34
35
  const entries = ctx.sessionManager.getBranch() as Entry[];
35
36
  const tokens = rawTokensSinceLastCompaction(entries);
36
- if (tokens < runtime.config.compactAfterTokens) return;
37
+ // Resolve the proactive-compaction threshold from the active model's context
38
+ // window when ratio mode is configured. ctx.model is the current session model
39
+ // (Model<any> | undefined per ExtensionContext).
40
+ const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
41
+ const threshold = resolveCompactAfterTokens(runtime.config, contextWindow);
42
+ if (tokens < threshold) return;
37
43
 
38
44
  // Capture ctx properties synchronously — the setTimeout + async work below
39
45
  // may outlive the extension ctx (stale after session replacement/reload).
@@ -58,7 +64,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
58
64
  }
59
65
  const currentEntries = ctx.sessionManager.getBranch() as Entry[];
60
66
  const currentTokens = rawTokensSinceLastCompaction(currentEntries);
61
- if (currentTokens < runtime.config.compactAfterTokens) {
67
+ if (currentTokens < threshold) {
62
68
  runtime.compactInFlight = false;
63
69
  if (hasUI) ui?.notify(
64
70
  "Observational memory: compaction skipped — another compaction already ran before deferred compaction",
@@ -453,7 +453,7 @@ export const recallObservationTool = defineTool({
453
453
  parameters: Type.Object({
454
454
  id: Type.String({
455
455
  pattern: "^[a-f0-9]{12}$",
456
- description: "12-character lowercase hex observation or reflection id shown in compacted memory, /om-view, or a previous recall result. Must be a specific id; this tool does not search by topic.",
456
+ description: "12-character lowercase hex observation or reflection id shown in compacted memory, /om:view, or a previous recall result. Must be a specific id; this tool does not search by topic.",
457
457
  }),
458
458
  }),
459
459
  renderCall(args) {