pi-observational-memory 2.4.2 → 3.0.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.
@@ -0,0 +1,77 @@
1
+ export const REFLECTOR_SYSTEM = `You are the reflection agent for a coding assistant.
2
+
3
+ These records are the ONLY information the assistant will have about past interactions once the raw conversation is compacted out of context. Anything you fail to preserve may be forgotten. Anything you distort may be remembered wrong. Take this seriously. Over-reflection is also memory distortion: it makes transient details look durable and crowds out the few facts future runs actually need.
4
+
5
+ Your task is different from the observer's: you are not recording events, you are distilling stable, long-lived facts and patterns from active observations into new reflections by calling record_reflections. Reflections are scarce, expensive durable orientation anchors, not a second observation layer.
6
+
7
+ You receive:
8
+ - Current reflections: durable facts already crystallized.
9
+ - Current observations: active timestamped evidence lines, each shown as "[id] YYYY-MM-DD HH:MM [relevance] content".
10
+
11
+ What to emit:
12
+ - Emit only new durable reflections not already present in current reflections.
13
+ - A good reflection captures meaning that should survive after individual observations are dropped from active compacted memory.
14
+ - High and critical observations deserve careful review, not automatic reflection. Many high observations are still active working evidence and should remain observations until completed, superseded, or generalized into a durable decision, invariant, or rationale.
15
+ - Ignore low observations unless a repeated pattern across many low observations is itself significant.
16
+ - Do not lightly reword existing reflections. Rewording creates a separate reflection, so only use different wording when the durable meaning is materially different, more specific, or corrects/refines an existing reflection.
17
+ - Do not emit update-style records or provenance metadata. Reflections are plain durable facts, not patches.
18
+ - It is fine to emit zero reflections when nothing new is stable enough; in that case do not call the tool and reply briefly.
19
+
20
+ Decision procedure:
21
+ 1. First reject observations that are transient, low-level, partial, routine, or only useful as current working state.
22
+ 2. From the remaining observations, identify only durable orientation facts: user preferences, constraints, corrections, decisions, invariants, completed outcomes, long-lived blockers, stable project goals, or rationale that future runs must know.
23
+ 3. Apply the future-agent utility test: would a future assistant need this fact automatically in compressed context to avoid a wrong decision, repeated work, or user-preference violation?
24
+ 4. If the candidate fails that future-agent utility test, leave it as an observation.
25
+ 5. If unsure, emit no reflection.
26
+
27
+ Abstraction gate:
28
+ - Do not turn each observation into a reflection. Observations are evidence; reflections are compressed durable conclusions.
29
+ - A reflection should usually do at least one of these: combine multiple observations into one durable pattern, preserve a user preference/constraint/correction/decision, record a completed outcome future runs must not redo, or capture durable rationale that explains why a decision was made.
30
+ - Single-observation reflections are allowed when the observation itself contains a durable user preference, constraint, correction, decision, invariant, completed outcome, or long-lived blocker.
31
+ - Do not copy or lightly paraphrase observation lines just because they are high or critical. If the reflection would say nearly the same thing as one observation with a few words removed, usually emit no reflection unless that observation contains a durable user assertion, durable decision, invariant, or completed outcome.
32
+ - Most transient task-log observations, tool status, one-off attempts, files inspected, commands run, failed attempts, partial implementation, and current working state should not become reflections. Let them remain observations until they are completed, superseded, repeated into a pattern, or captured by a higher-value reflection.
33
+ - Prefer fewer, higher-value reflections. It is better to emit zero reflections than to create one reflection per observation.
34
+
35
+ Focus on:
36
+ - User identity, role, preferences, constraints, and durable corrections.
37
+ - Project goals, architecture, technical decisions, and the rationale behind them.
38
+ - Recurring user behavior or preferences that will matter in future turns.
39
+ - Completed outcomes future runs must not redo.
40
+ - Durable blockers, invariants, and open decisions that should survive compaction.
41
+
42
+ Support ids:
43
+ - Every reflection must include supportingObservationIds from the current observations list.
44
+ - supportingObservationIds are a coverage/provenance set: include current observation ids whose durable meaning is preserved by the reflection and can later be treated as redundant active-memory detail.
45
+ - supportingObservationIds are not a checklist to cover every observation. They are evidence for a reflection that already passed the durable-value bar.
46
+ - Include additional observation ids only when the reflection preserves their durable meaning with equivalent fidelity.
47
+ - Leave observations unsupported when their details are still active working state, too specific to compress safely, or not yet durable enough.
48
+ - Do not include observations whose unique exact detail, current task state, user correction, user constraint, or concrete completion is not captured by the reflection.
49
+ - Never invent observation ids. Proposals with missing, empty, or invalid supportingObservationIds are rejected.
50
+
51
+ User assertions are authoritative. If the observation pool contains both "User stated they use Postgres" and a later "User asked which db they are on", the assertion answers the question — crystallize the assertion, never the question, as the durable fact.
52
+
53
+ Reflection content rules:
54
+ - Single line of plain prose. No markdown, no bullets, no code fences, no XML/HTML tags, no emojis.
55
+ - No timestamp, no priority marker, no bracketed tags, no "key: value" fields, no JSON.
56
+ - Lead with the fact or pattern; include the reason or mechanism when known so future readers can judge edge cases.
57
+ - Preserve user assertions exactly. Use the user's exact words when non-standard.
58
+ - Preserve named identifiers, paths, commands, package names, error codes, dates, decisions, constraints, and rationale when those details are part of the durable meaning.
59
+
60
+ Examples:
61
+ - BAD: User discussed databases.
62
+ - GOOD: User stated they use Postgres for the project database.
63
+ - BAD: User asked about database setup.
64
+ - GOOD: User stated they use Postgres for the project database.
65
+ - BAD: User ran npm test and it failed.
66
+ - GOOD: The test suite currently fails because auth middleware rejects expired JWT fixtures.
67
+ - BAD: User prefers React Query.
68
+ - BAD: User switched from SWR.
69
+ - GOOD: User chose React Query over SWR for server-state caching.
70
+ - BAD: completed: edited src/hooks/reflect-drop-trigger.ts.
71
+ - GOOD: completed: V3 reflect/drop coverage now uses raw progress watermarks, so same-turn reflection entries are no longer used as drop progress markers.
72
+ - BAD: npm test passed.
73
+ - GOOD: completed: V3 package namespace migration passed full tests and typecheck.
74
+ - BAD: Observation aaaaaaaaaaaa says the user likes short answers.
75
+ - GOOD: User prefers short answers without generic summaries.
76
+ - ZERO REFLECTIONS: The only new observations are files inspected, commands run, failed attempts, partial implementation, transient debugging, or current working state with no durable conclusion yet.
77
+ - ZERO REFLECTIONS: The only new observations are routine command outputs, transient debugging attempts, or partial work with no durable conclusion yet.`;
@@ -0,0 +1,63 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export interface ClipboardCommand {
4
+ command: string;
5
+ args: string[];
6
+ }
7
+
8
+ export type ClipboardCommandRunner = (command: ClipboardCommand, text: string) => Promise<boolean>;
9
+
10
+ export function getClipboardCommands(platform: NodeJS.Platform = process.platform): ClipboardCommand[] {
11
+ switch (platform) {
12
+ case "darwin":
13
+ return [{ command: "pbcopy", args: [] }];
14
+ case "win32":
15
+ return [{ command: "clip", args: [] }];
16
+ default:
17
+ return [
18
+ { command: "wl-copy", args: [] },
19
+ { command: "xclip", args: ["-selection", "clipboard"] },
20
+ { command: "xsel", args: ["--clipboard", "--input"] },
21
+ { command: "termux-clipboard-set", args: [] },
22
+ ];
23
+ }
24
+ }
25
+
26
+ export async function copyTextToClipboard(
27
+ text: string,
28
+ runner: ClipboardCommandRunner = runClipboardCommand,
29
+ commands: ClipboardCommand[] = getClipboardCommands(),
30
+ ): Promise<boolean> {
31
+ for (const command of commands) {
32
+ if (await runner(command, text)) return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ export function runClipboardCommand(command: ClipboardCommand, text: string): Promise<boolean> {
38
+ return new Promise((resolve) => {
39
+ let settled = false;
40
+ let timeout: ReturnType<typeof setTimeout> | undefined;
41
+
42
+ const finish = (ok: boolean) => {
43
+ if (settled) return;
44
+ settled = true;
45
+ if (timeout) clearTimeout(timeout);
46
+ resolve(ok);
47
+ };
48
+
49
+ const child = spawn(command.command, command.args, {
50
+ stdio: ["pipe", "ignore", "ignore"],
51
+ });
52
+
53
+ timeout = setTimeout(() => {
54
+ child.kill();
55
+ finish(false);
56
+ }, 2_000);
57
+
58
+ child.on("error", () => finish(false));
59
+ child.on("close", (code) => finish(code === 0));
60
+ child.stdin.on("error", () => undefined);
61
+ child.stdin.end(text, "utf8");
62
+ });
63
+ }
@@ -1,104 +1,105 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { SettingsManager } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { observationPoolMetrics } from "../agents/dropper/pool.js";
3
+ import type { Runtime } from "../runtime.js";
3
4
  import {
4
- getMemoryState,
5
- rawTokensSinceLastBound,
5
+ diffProjection,
6
+ foldLedger,
7
+ fullProjection,
6
8
  rawTokensSinceLastCompaction,
7
- } from "../branch.js";
8
- import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
9
- import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
10
- import type { Runtime } from "../runtime.js";
11
- import { estimateStringTokens } from "../tokens.js";
12
- import { reflectionContent, type MemoryReflection } from "../types.js";
9
+ rawTokensSinceObservationCoverage,
10
+ rawTokensSinceReflectionCoverage,
11
+ visibleProjection,
12
+ type Entry,
13
+ } from "../session-ledger/index.js";
13
14
 
14
- export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void {
15
- pi.registerCommand("om-status", {
16
- description: "Show observational memory status",
17
- handler: async (_args, ctx) => {
18
- runtime.ensureConfig(ctx.cwd);
19
- const entries = ctx.sessionManager.getBranch() as Parameters<typeof rawTokensSinceLastBound>[0];
20
- const sinceBound = rawTokensSinceLastBound(entries);
21
- const sinceCompaction = rawTokensSinceLastCompaction(entries);
15
+ function pct(current: number, total: number): number {
16
+ return total > 0 ? Math.min(100, Math.round((current / total) * 100)) : 0;
17
+ }
22
18
 
23
- const { reflections: committedRefs, committedObs, pendingObs } = getMemoryState(entries);
24
- const committedRefItems = committedRefs as MemoryReflection[];
25
- const committedObsTokens = estimateObservationPoolTokens(committedObs);
26
- const committedObsCount = committedObs.length;
27
- const committedRefsTokens = committedRefItems.reduce((s, r) => s + estimateStringTokens(reflectionContent(r)), 0);
28
- const committedRefsCount = committedRefItems.length;
19
+ function tokenSum(items: { tokenCount: number }[]): number {
20
+ return items.reduce((sum, item) => sum + item.tokenCount, 0);
21
+ }
29
22
 
30
- const pendingObsTokens = estimateObservationPoolTokens(pendingObs);
31
- const pendingObsCount = pendingObs.length;
23
+ function addedSuffix(count: number): string | undefined {
24
+ return count > 0 ? `+${count.toLocaleString()}` : undefined;
25
+ }
32
26
 
33
- const relevanceHistogram = countByRelevance([...committedObs, ...pendingObs]);
27
+ function removedSuffix(count: number): string | undefined {
28
+ return count > 0 ? `-${count.toLocaleString()}` : undefined;
29
+ }
34
30
 
35
- const keepRecentTokens = SettingsManager.create(ctx.cwd).getCompactionKeepRecentTokens();
31
+ function appendSuffixes(line: string, suffixes: (string | undefined)[]): string {
32
+ const rendered = suffixes.filter((suffix): suffix is string => suffix !== undefined);
33
+ return rendered.length > 0 ? `${line} ${rendered.join(" ")}` : line;
34
+ }
36
35
 
37
- const obsThreshold = runtime.config.observationThresholdTokens;
38
- const compThreshold = runtime.config.compactionThresholdTokens;
39
- const refThreshold = runtime.config.reflectionThresholdTokens;
40
- // Approximation: the real compaction gate excludes pending obs whose coversFromId falls inside
41
- // the new keep-recent tail (deferred to next cycle) and adds any sync-catch-up gap obs produced
42
- // at compaction entry. We over-count by the tail slice and can't predict the gap obs here.
43
- // Precise version would simulate the new firstKeptEntryId by walking back keepRecentTokens from
44
- // the branch tail and split pending into pre-tail vs tail-covering.
45
- const observationPoolTokens = estimateObservationPoolTokens([...committedObs, ...pendingObs]);
46
- const obsPct = Math.min(100, Math.round((sinceBound / obsThreshold) * 100));
47
- const compPct = Math.min(100, Math.round((sinceCompaction / compThreshold) * 100));
48
- const refPct = Math.min(100, Math.round((observationPoolTokens / refThreshold) * 100));
36
+ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void {
37
+ pi.registerCommand("om-status", {
38
+ description: "Show observational memory status",
39
+ handler: async (_args, ctx) => {
40
+ runtime.ensureConfig(ctx.cwd);
41
+ const entries = ctx.sessionManager.getBranch() as Entry[];
42
+ const folded = foldLedger(entries);
43
+ const visible = visibleProjection(entries);
44
+ const full = fullProjection(entries);
45
+ const drift = diffProjection(visible, full);
49
46
 
50
- const refLabel = committedRefsCount === 1 ? "entry" : "entries";
51
- const cObsLabel = committedObsCount === 1 ? "observation" : "observations";
52
- const pObsLabel = pendingObsCount === 1 ? "observation" : "observations";
47
+ const visibleObservationTokens = tokenSum(visible.observations);
48
+ const visibleReflectionTokens = tokenSum(visible.reflections);
49
+ const activeObservationPool = observationPoolMetrics(folded.activeObservations, runtime.config.observationsPoolTargetTokens);
50
+ const observationLine = appendSuffixes(
51
+ `Observations: ${folded.observations.length} recorded / ${folded.droppedObservationIds.size} dropped / ${folded.activeObservations.length} active / ${visible.observations.length} visible`,
52
+ [
53
+ addedSuffix(drift.observationsOnlyInFull.length),
54
+ removedSuffix(drift.droppedOnlyInFull.length),
55
+ ],
56
+ );
57
+ const reflectionLine = appendSuffixes(
58
+ `Reflections: ${folded.reflections.length} recorded / ${visible.reflections.length} visible`,
59
+ [addedSuffix(drift.reflectionsOnlyInFull.length)],
60
+ );
61
+ const obsProgress = rawTokensSinceObservationCoverage(entries);
62
+ const reflectionProgress = rawTokensSinceReflectionCoverage(entries);
63
+ const compactionProgress = rawTokensSinceLastCompaction(entries);
53
64
 
54
65
  const passiveLines = runtime.config.passive === true
55
66
  ? [
56
67
  "── Mode ──",
57
- "Passive: proactive observation and compaction triggers disabled; compaction hook remains active",
68
+ "Passive: automatic memory workers and auto-compaction disabled; manual/Pi compaction, commands, and recall remain active",
58
69
  "",
59
70
  ]
60
71
  : [];
61
72
 
62
- const activityLines = runtime.config.passive === true
63
- ? [
64
- "── Activity ──",
65
- `Observation trigger: passive (~${sinceBound.toLocaleString()} / ${obsThreshold.toLocaleString()} tokens, ${obsPct}%)`,
66
- " → proactive observation is disabled; manual/Pi compaction can still run sync catch-up observation",
67
- `Compaction trigger: passive (~${sinceCompaction.toLocaleString()} / ${compThreshold.toLocaleString()} tokens, ${compPct}%)`,
68
- " → proactive extension-triggered compaction is disabled; manual/Pi compaction still uses the custom hook",
69
- `Next reflection: ~${observationPoolTokens.toLocaleString()} / ${refThreshold.toLocaleString()} tokens (${refPct}%)`,
70
- ` → if observations exceed ${refThreshold.toLocaleString()} tokens when compaction runs, reflections are`,
71
- ` distilled from them and redundant observations are pruned away`,
72
- ]
73
- : [
74
- "── Activity ──",
75
- `Next observation: ~${sinceBound.toLocaleString()} / ${obsThreshold.toLocaleString()} tokens (${obsPct}%)`,
76
- ` → at ${obsThreshold.toLocaleString()} tokens, recent conversation is compressed into new observations`,
77
- `Next compaction: ~${sinceCompaction.toLocaleString()} / ${compThreshold.toLocaleString()} tokens (${compPct}%)`,
78
- ` → at ${compThreshold.toLocaleString()} tokens, raw history is replaced by the updated reflections and`,
79
- ` observations, keeping only the last ${keepRecentTokens.toLocaleString()} tokens of conversation verbatim`,
80
- `Next reflection: ~${observationPoolTokens.toLocaleString()} / ${refThreshold.toLocaleString()} tokens (${refPct}%)`,
81
- ` → if observations exceed ${refThreshold.toLocaleString()} tokens when compaction runs, reflections are`,
82
- ` distilled from them and redundant observations are pruned away`,
83
- ];
84
-
85
73
  const lines = [
86
74
  ...passiveLines,
87
75
  "── Memory ──",
88
- `Reflections: ~${committedRefsTokens.toLocaleString()} tokens (${committedRefsCount} ${refLabel}) — durable insights`,
89
- `Observations:`,
90
- ` committed ~${committedObsTokens.toLocaleString()} tokens (${committedObsCount} ${cObsLabel}) — folded into last compaction`,
91
- ` pending ~${pendingObsTokens.toLocaleString()} tokens (${pendingObsCount} ${pObsLabel}) — waiting for next compaction`,
92
- ` relevance ${formatRelevanceHistogram(relevanceHistogram)}`,
76
+ observationLine,
77
+ reflectionLine,
93
78
  "",
94
- ...activityLines,
79
+ "── Activity ──",
80
+ `Next observation: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
81
+ `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)}%)`,
83
+ `Visible observation pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
84
+ `Active observation pool: ~${activeObservationPool.observationTokens.toLocaleString()} / ${runtime.config.observationsPoolTargetTokens.toLocaleString()} target tokens (${pct(activeObservationPool.observationTokens, runtime.config.observationsPoolTargetTokens)}%)`,
85
+ `Reflection pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
95
86
  ];
96
87
 
97
- if (runtime.observerInFlight || runtime.compactInFlight) {
98
- lines.push("");
99
- lines.push("── In flight ──");
100
- if (runtime.observerInFlight) lines.push("Observer: running");
101
- if (runtime.compactInFlight) lines.push("Compaction: running");
88
+ if (runtime.consolidationInFlight || runtime.compactInFlight || runtime.compactHookInFlight) {
89
+ lines.push("", "── In flight ──");
90
+ if (runtime.consolidationInFlight) {
91
+ const phase = runtime.consolidationPhase ? ` (${runtime.consolidationPhase})` : "";
92
+ lines.push(`Consolidation: running${phase}`);
93
+ }
94
+ if (runtime.compactInFlight) lines.push("Auto-compaction: running");
95
+ if (runtime.compactHookInFlight) lines.push("Compaction hook: running");
96
+ }
97
+
98
+ if (runtime.lastObserverError || runtime.lastReflectorError || runtime.lastDropperError) {
99
+ lines.push("", "── Last error ──");
100
+ if (runtime.lastObserverError) lines.push(`Observer: ${runtime.lastObserverError}`);
101
+ if (runtime.lastReflectorError) lines.push(`Reflector: ${runtime.lastReflectorError}`);
102
+ if (runtime.lastDropperError) lines.push(`Dropper: ${runtime.lastDropperError}`);
102
103
  }
103
104
 
104
105
  ctx.ui.notify(lines.join("\n"), "info");
@@ -1,82 +1,74 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { getMemoryState } from "../branch.js";
3
- import { observationPoolTokens as estimateObservationPoolTokens } from "../compaction.js";
4
- import { countByRelevance, formatRelevanceHistogram } from "../relevance.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
2
  import type { Runtime } from "../runtime.js";
6
- import { estimateStringTokens } from "../tokens.js";
7
- import { reflectionContent, reflectionToPromptLine, type MemoryReflection, type ObservationRecord } from "../types.js";
3
+ import { copyTextToClipboard } from "../clipboard.js";
4
+ import {
5
+ fullProjection,
6
+ observationToSummaryLine,
7
+ reflectionToSummaryLine,
8
+ visibleProjection,
9
+ type Entry,
10
+ type Projection,
11
+ } from "../session-ledger/index.js";
8
12
 
9
- export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime): void {
10
- pi.registerCommand("om-view", {
11
- description: "Print observational memory details (reflections + observations)",
12
- handler: async (_args, ctx) => {
13
- runtime.ensureConfig(ctx.cwd);
14
- const entries = ctx.sessionManager.getBranch() as Parameters<typeof getMemoryState>[0];
15
- const { reflections: committedRefs, committedObs, pendingObs } = getMemoryState(entries);
16
- const committedRefItems = committedRefs as MemoryReflection[];
17
-
18
- const committedRefTokens = committedRefItems.reduce((s, r) => s + estimateStringTokens(reflectionContent(r)), 0);
19
- const committedRefCount = committedRefItems.length;
20
-
21
- const committedObsTokens = estimateObservationPoolTokens(committedObs);
22
- const committedObsCount = committedObs.length;
13
+ function firstArg(args: unknown): string | undefined {
14
+ if (Array.isArray(args)) return typeof args[0] === "string" ? args[0] : undefined;
15
+ if (typeof args === "string") return args.trim().split(/\s+/)[0];
16
+ if (args && typeof args === "object" && "mode" in args) {
17
+ const mode = (args as { mode?: unknown }).mode;
18
+ return typeof mode === "string" ? mode : undefined;
19
+ }
20
+ return undefined;
21
+ }
23
22
 
24
- const pendingObsTokens = estimateObservationPoolTokens(pendingObs);
25
- const pendingObsCount = pendingObs.length;
23
+ function renderList<T>(items: T[], render: (item: T) => string, empty: string): string {
24
+ return items.length > 0 ? items.map(render).join("\n") : empty;
25
+ }
26
26
 
27
- const totalObsCount = committedObsCount + pendingObsCount;
28
- const totalObsTokens = estimateObservationPoolTokens([...committedObs, ...pendingObs]);
29
- const totalTokens = committedRefTokens + totalObsTokens;
30
- const relevanceHistogram = countByRelevance([...committedObs, ...pendingObs]);
27
+ function renderContentOnlyProjection(projection: Projection, emptyScope: "visible" | "recorded"): string {
28
+ return [
29
+ "── Reflections ──",
30
+ renderList(projection.reflections, reflectionToSummaryLine, `No ${emptyScope} reflections.`),
31
+ "",
32
+ "── Observations ──",
33
+ renderList(projection.observations, observationToSummaryLine, `No ${emptyScope} observations.`),
34
+ ].join("\n");
35
+ }
31
36
 
32
- const plural = (n: number, singular: string, plural: string) => (n === 1 ? singular : plural);
33
- const renderObs = (r: ObservationRecord) =>
34
- `[${r.id}] ${r.timestamp} [${r.relevance}] ${r.content}`;
37
+ interface ViewCommandOptions {
38
+ copyToClipboard?: (text: string) => Promise<boolean>;
39
+ }
35
40
 
36
- const sections: string[] = [];
41
+ export function registerViewCommand(pi: ExtensionAPI, runtime: Runtime, options: ViewCommandOptions = {}): void {
42
+ const copyToClipboard = options.copyToClipboard ?? copyTextToClipboard;
37
43
 
38
- sections.push(
39
- `Memory: ${committedRefCount} ${plural(committedRefCount, "reflection", "reflections")} · ` +
40
- `${totalObsCount} ${plural(totalObsCount, "observation", "observations")} ` +
41
- `(${committedObsCount} committed, ${pendingObsCount} pending) · ` +
42
- `~${totalTokens.toLocaleString()} tokens · ` +
43
- `relevance ${formatRelevanceHistogram(relevanceHistogram)}`,
44
- );
45
- sections.push("");
44
+ pi.registerCommand("om-view", {
45
+ description: "Print and copy observational memory content (visible by default, full for recorded memory)",
46
+ handler: async (args, ctx) => {
47
+ runtime.ensureConfig(ctx.cwd);
48
+ const entries = ctx.sessionManager.getBranch() as Entry[];
49
+ const mode = firstArg(args);
46
50
 
47
- sections.push(
48
- `── Reflections (${committedRefCount} ${plural(committedRefCount, "entry", "entries")}, ~${committedRefTokens.toLocaleString()} tokens) ──`,
49
- );
50
- if (committedRefItems.length > 0) {
51
- sections.push(committedRefItems.map(reflectionToPromptLine).join("\n\n"));
52
- } else {
53
- sections.push("(none)");
54
- }
51
+ const notifyWithCopy = async (output: string) => {
52
+ const copied = await copyToClipboard(output).catch(() => false);
53
+ ctx.ui.notify(
54
+ copied
55
+ ? `${output}\n\nCopied /om-view output to clipboard.`
56
+ : `${output}\n\nWarning: failed to copy /om-view output to clipboard.`,
57
+ "info",
58
+ );
59
+ };
55
60
 
56
- sections.push("");
57
- sections.push(
58
- `── Observations — committed (${committedObsCount} ${plural(committedObsCount, "observation", "observations")}, ~${committedObsTokens.toLocaleString()} tokens) ──`,
59
- );
60
- if (committedObs.length > 0) {
61
- sections.push(committedObs.map(renderObs).join("\n"));
62
- } else {
63
- sections.push("(none)");
61
+ if (mode === "full") {
62
+ await notifyWithCopy(renderContentOnlyProjection(fullProjection(entries), "recorded"));
63
+ return;
64
64
  }
65
65
 
66
- sections.push("");
67
- sections.push(
68
- `── Observations — pending (${pendingObsCount} ${plural(pendingObsCount, "observation", "observations")}, ~${pendingObsTokens.toLocaleString()} tokens) ──`,
69
- );
70
- if (pendingObs.length > 0) {
71
- sections.push(pendingObs.map(renderObs).join("\n"));
72
- } else {
73
- sections.push("(none)");
66
+ if (mode && mode !== "visible") {
67
+ ctx.ui.notify("Usage: /om-view [full]", "info");
68
+ return;
74
69
  }
75
70
 
76
- sections.push("");
77
- sections.push("Tip: use /tree to browse the raw messages still live in the session.");
78
-
79
- ctx.ui.notify(sections.join("\n"), "info");
71
+ await notifyWithCopy(renderContentOnlyProjection(visibleProjection(entries), "visible"));
80
72
  },
81
73
  });
82
74
  }
package/src/config.ts CHANGED
@@ -1,28 +1,95 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
3
+ import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+
6
+ export interface ConfiguredModel {
7
+ provider: string;
8
+ id: string;
9
+ thinking?: ModelThinkingLevel;
10
+ }
4
11
 
5
12
  export interface Config {
6
- observationThresholdTokens: number;
7
- compactionThresholdTokens: number;
8
- reflectionThresholdTokens: number;
13
+ observeAfterTokens: number;
14
+ reflectAfterTokens: number;
15
+ compactAfterTokens: number;
16
+ observationsPoolMaxTokens: number;
17
+ observationsPoolTargetTokens: number;
18
+ agentMaxTurns: number;
19
+ model?: ConfiguredModel;
9
20
  passive: boolean;
10
- compactionModel?: { provider: string; id: string };
21
+ debugLog: boolean;
11
22
  }
12
23
 
13
24
  export const DEFAULTS: Config = {
14
- observationThresholdTokens: 1_000,
15
- compactionThresholdTokens: 50_000,
16
- reflectionThresholdTokens: 30_000,
25
+ observeAfterTokens: 10_000,
26
+ reflectAfterTokens: 20_000,
27
+ compactAfterTokens: 81_000,
28
+ observationsPoolMaxTokens: 20_000,
29
+ observationsPoolTargetTokens: 10_000,
30
+ agentMaxTurns: 16,
17
31
  passive: false,
32
+ debugLog: false,
18
33
  };
19
34
 
35
+ export const THINKING_LEVEL_VALUES: readonly ModelThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
36
+
20
37
  const SETTINGS_KEY = "observational-memory";
21
38
  const PASSIVE_ENV = "PI_OBSERVATIONAL_MEMORY_PASSIVE";
22
39
 
23
- function normalizeSettingsConfig(value: Partial<Config>): Partial<Config> {
24
- const normalized = { ...value };
25
- if ("passive" in normalized && typeof normalized.passive !== "boolean") delete normalized.passive;
40
+ function positiveIntegerOrUndefined(value: unknown): number | undefined {
41
+ return Number.isInteger(value) && typeof value === "number" && value > 0 ? value : undefined;
42
+ }
43
+
44
+ function validTargetOrUndefined(value: unknown, maxTokens: number): number | undefined {
45
+ const target = positiveIntegerOrUndefined(value);
46
+ return target !== undefined && target < maxTokens ? target : undefined;
47
+ }
48
+
49
+ function derivedObservationPoolTarget(maxTokens: number): number {
50
+ return Math.floor(maxTokens / 2);
51
+ }
52
+
53
+ function isThinkingLevel(value: unknown): value is ModelThinkingLevel {
54
+ return typeof value === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(value);
55
+ }
56
+
57
+ function isRecord(value: unknown): value is Record<string, unknown> {
58
+ return typeof value === "object" && value !== null;
59
+ }
60
+
61
+ function nonEmptyString(value: unknown): string | undefined {
62
+ return typeof value === "string" && value.length > 0 ? value : undefined;
63
+ }
64
+
65
+ function normalizeModel(value: unknown): ConfiguredModel | undefined {
66
+ if (!isRecord(value)) return undefined;
67
+ const provider = nonEmptyString(value.provider);
68
+ const id = nonEmptyString(value.id);
69
+ if (!provider || !id) return undefined;
70
+ const model: ConfiguredModel = { provider, id };
71
+ if (isThinkingLevel(value.thinking)) model.thinking = value.thinking;
72
+ return model;
73
+ }
74
+
75
+ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config> {
76
+ const normalized: Partial<Config> = {};
77
+ const numberKeys = [
78
+ "observeAfterTokens",
79
+ "reflectAfterTokens",
80
+ "compactAfterTokens",
81
+ "observationsPoolMaxTokens",
82
+ "observationsPoolTargetTokens",
83
+ "agentMaxTurns",
84
+ ] as const;
85
+ for (const key of numberKeys) {
86
+ const normalizedValue = positiveIntegerOrUndefined(value[key]);
87
+ if (normalizedValue !== undefined) normalized[key] = normalizedValue;
88
+ }
89
+ if (typeof value.passive === "boolean") normalized.passive = value.passive;
90
+ if (typeof value.debugLog === "boolean") normalized.debugLog = value.debugLog;
91
+ const model = normalizeModel(value.model);
92
+ if (model) normalized.model = model;
26
93
  return normalized;
27
94
  }
28
95
 
@@ -40,7 +107,7 @@ function readNamespacedConfig(path: string): Partial<Config> {
40
107
  try {
41
108
  const raw = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
42
109
  const nested = raw[SETTINGS_KEY];
43
- return nested && typeof nested === "object" ? normalizeSettingsConfig(nested as Partial<Config>) : {};
110
+ return isRecord(nested) ? normalizeSettingsConfig(nested) : {};
44
111
  } catch {
45
112
  return {};
46
113
  }
@@ -49,11 +116,23 @@ function readNamespacedConfig(path: string): Partial<Config> {
49
116
  export function loadConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): Config {
50
117
  const globalPath = join(getAgentDir(), "settings.json");
51
118
  const projectPath = join(cwd, ".pi", "settings.json");
119
+ const globalConfig = readNamespacedConfig(globalPath);
120
+ const projectConfig = readNamespacedConfig(projectPath);
121
+ const envConfig = readEnvConfig(env);
122
+ const merged = {
123
+ ...DEFAULTS,
124
+ observationsPoolTargetTokens: undefined,
125
+ ...globalConfig,
126
+ ...projectConfig,
127
+ ...envConfig,
128
+ };
129
+ const target = validTargetOrUndefined(
130
+ merged.observationsPoolTargetTokens,
131
+ merged.observationsPoolMaxTokens,
132
+ ) ?? derivedObservationPoolTarget(merged.observationsPoolMaxTokens);
52
133
 
53
134
  return {
54
- ...DEFAULTS,
55
- ...readNamespacedConfig(globalPath),
56
- ...readNamespacedConfig(projectPath),
57
- ...readEnvConfig(env),
135
+ ...merged,
136
+ observationsPoolTargetTokens: target,
58
137
  };
59
138
  }