pi-blackhole 0.4.2 → 0.4.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 +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
|
@@ -7,9 +7,29 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { applyEnvOverrides, DECLARATIVE_ENV_OVERRIDES } from "./config-env.js";
|
|
11
|
+
import { getAgentDir as originalGetAgentDir } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
12
13
|
|
|
14
|
+
// ── getAgentDir with PI_CODING_AGENT_DIR override ───────────────────────────
|
|
15
|
+
|
|
16
|
+
let __lastAgentDirEnv: string | undefined;
|
|
17
|
+
let __cachedAgentDir: string | null = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the canonical pi-agent data directory.
|
|
21
|
+
* Respects the `PI_CODING_AGENT_DIR` environment variable when set.
|
|
22
|
+
* Memoized — only recomputes when the env var changes.
|
|
23
|
+
*/
|
|
24
|
+
export function getAgentDir(): string {
|
|
25
|
+
const current = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
26
|
+
if (current === __lastAgentDirEnv && __cachedAgentDir !== null)
|
|
27
|
+
return __cachedAgentDir;
|
|
28
|
+
__lastAgentDirEnv = current;
|
|
29
|
+
__cachedAgentDir = current || originalGetAgentDir();
|
|
30
|
+
return __cachedAgentDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
// ── Config path ──────────────────────────────────────────────────────────────
|
|
14
34
|
|
|
15
35
|
const CONFIG_DIR = "pi-blackhole";
|
|
@@ -20,166 +40,176 @@ let __testConfigDir: string | undefined;
|
|
|
20
40
|
|
|
21
41
|
/** Test-only: set to override config directory. Use in beforeEach/afterEach. */
|
|
22
42
|
export function __setTestConfigDir(dir: string | undefined): void {
|
|
23
|
-
|
|
43
|
+
__testConfigDir = dir;
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
export function configPath(): string {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
if (__testConfigDir) {
|
|
48
|
+
return join(__testConfigDir, CONFIG_DIR, CONFIG_FILE);
|
|
49
|
+
}
|
|
50
|
+
return join(getAgentDir(), CONFIG_DIR, CONFIG_FILE);
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
34
54
|
|
|
35
55
|
export interface OmModelConfig {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
provider: string;
|
|
57
|
+
id: string;
|
|
58
|
+
thinking?: ModelThinkingLevel;
|
|
59
|
+
/** Cooldown duration in hours after a retryable error (429/5xx/timeout).
|
|
60
|
+
* Defaults to 1 hour when omitted. */
|
|
61
|
+
cooldownHours?: number;
|
|
62
|
+
/** Context window override for this model. Inherits from Pi's model registry when unset. */
|
|
63
|
+
contextWindow?: number;
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
export interface UnifiedConfig {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
67
|
+
/** @deprecated Use compactionEngine instead. */
|
|
68
|
+
overrideDefaultCompaction?: boolean;
|
|
69
|
+
/** Write debug snapshots to /tmp/pi-blackhole-debug.json. */
|
|
70
|
+
debug: boolean;
|
|
71
|
+
|
|
72
|
+
// ── New config surface — compaction, engine, tail behavior ──
|
|
73
|
+
|
|
74
|
+
/** Unified compaction control: "auto" | "manual" | "off".
|
|
75
|
+
* "auto" — auto-trigger on compactAfterTokens threshold
|
|
76
|
+
* "manual" — only via /blackhole command
|
|
77
|
+
* "off" — never compact (disables auto + blocks /blackhole) */
|
|
78
|
+
compaction: "auto" | "manual" | "off";
|
|
79
|
+
|
|
80
|
+
/** Which engine handles compaction.
|
|
81
|
+
* "blackhole" — blackhole's compile() + OM injection
|
|
82
|
+
* "pi-default" — Pi's built-in summarization */
|
|
83
|
+
compactionEngine: "blackhole" | "pi-default";
|
|
84
|
+
|
|
85
|
+
/** Mid-run auto-compaction (turn_end trigger, fires while the agent is still
|
|
86
|
+
* executing tool loops — agent_end alone never fires during long runs).
|
|
87
|
+
* "resume" — compact at threshold and inject a resume message so the agent
|
|
88
|
+
* continues the task (default)
|
|
89
|
+
* "pause" — compact at threshold but stop; user continues manually
|
|
90
|
+
* "off" — only evaluate the threshold when the agent finishes a run */
|
|
91
|
+
midRunCompaction: "resume" | "pause" | "off";
|
|
92
|
+
|
|
93
|
+
/** How much recent transcript to keep visible after compaction.
|
|
94
|
+
* "pi-default" — use Pi's firstKeptEntryId (respects Pi's keepRecentTokens)
|
|
95
|
+
* "minimal" — keep only last user message (current agressive pi-vcc behavior)
|
|
96
|
+
* ONLY applies when compactionEngine: "blackhole" */
|
|
97
|
+
tailBehavior: "pi-default" | "minimal";
|
|
98
|
+
|
|
99
|
+
/** Token threshold for observer runs. */
|
|
100
|
+
observeAfterTokens: number;
|
|
101
|
+
/** Token threshold for reflector and dropper. */
|
|
102
|
+
reflectAfterTokens: number;
|
|
103
|
+
/** Token threshold for proactive auto-compaction. */
|
|
104
|
+
compactAfterTokens: number;
|
|
105
|
+
/** Observation pool token pressure for full fold. */
|
|
106
|
+
observationsPoolMaxTokens: number;
|
|
107
|
+
/** Treat every compaction as a full-fold boundary so early reflections/drops
|
|
108
|
+
* survive the first compaction in a fresh session. Default true. */
|
|
109
|
+
fullFoldAlways: boolean;
|
|
110
|
+
/** Target token budget for the observation pool (dropper aims here).
|
|
111
|
+
* Optional; defaults to half of observationsPoolMaxTokens when unset.
|
|
112
|
+
* Must be less than observationsPoolMaxTokens.
|
|
113
|
+
*
|
|
114
|
+
* NOTE: Ported from upstream as forward-compat (no-op in our pool algorithm).
|
|
115
|
+
* Upstream renamed budgetTokens→targetTokens (52b5844) and uses this
|
|
116
|
+
* for their tokensOverTarget / avgTokensPerObservation drop calculation.
|
|
117
|
+
* We keep our ratio-based urgency algorithm; this knob exists so future
|
|
118
|
+
* lockstep iterations don't diverge on the config shape. */
|
|
119
|
+
observationsPoolTargetTokens: number;
|
|
120
|
+
/** Max prompt tokens for reflector model input (rolling window cap). */
|
|
121
|
+
reflectorInputMaxTokens: number;
|
|
122
|
+
/** Max prompt tokens for dropper model input (rolling window cap). */
|
|
123
|
+
dropperInputMaxTokens: number;
|
|
124
|
+
/** Pressure threshold for dropper. When active observation pool tokens exceed
|
|
125
|
+
* this fraction of reflectorInputMaxTokens, the dropper runs even without new
|
|
126
|
+
* observations/reflections (to keep the pool pruned).
|
|
127
|
+
* Default 0.70 (70%). Must be in range (0, 1]. */
|
|
128
|
+
dropperPressureThreshold: number;
|
|
129
|
+
/** Minimum observation-pool fullness (fraction of observationsPoolMaxTokens)
|
|
130
|
+
* before the dropper may run. Prevents churn on a nearly empty pool.
|
|
131
|
+
* Default 0.10 (10%). Must be in range (0, 1]. */
|
|
132
|
+
dropperPoolFullnessThreshold: number;
|
|
133
|
+
/** Max source entries tokens sent to observer per chunk. */
|
|
134
|
+
observerChunkMaxTokens: number;
|
|
135
|
+
/** Max preamble tokens (CURRENT REFLECTIONS / OBSERVATIONS) in the observer prompt.
|
|
136
|
+
* Default 0 means auto-compute from observerChunkMaxTokens (30%). Only applied in
|
|
137
|
+
* noAutoCompact mode where accumulated batch history can grow unbounded.
|
|
138
|
+
* Set to an explicit value to override the auto-computed budget. */
|
|
139
|
+
observerPreambleMaxTokens: number;
|
|
140
|
+
/** Shared turn cap for background memory agents. */
|
|
141
|
+
agentMaxTurns: number;
|
|
142
|
+
|
|
143
|
+
/** Base model override for all memory workers. */
|
|
144
|
+
model?: OmModelConfig;
|
|
145
|
+
/** Model override for observer (most frequent worker). */
|
|
146
|
+
observerModel?: OmModelConfig;
|
|
147
|
+
/** Model override for reflector (synthesizes durable facts). */
|
|
148
|
+
reflectorModel?: OmModelConfig;
|
|
149
|
+
/** Model override for dropper (prunes observations). */
|
|
150
|
+
dropperModel?: OmModelConfig;
|
|
151
|
+
|
|
152
|
+
/** Fallback models for observer, tried in order after primary model fails. */
|
|
153
|
+
observerFallbackModels?: OmModelConfig[];
|
|
154
|
+
/** Fallback models for reflector, tried in order after primary model fails. */
|
|
155
|
+
reflectorFallbackModels?: OmModelConfig[];
|
|
156
|
+
/** Fallback models for dropper, tried in order after primary model fails. */
|
|
157
|
+
dropperFallbackModels?: OmModelConfig[];
|
|
158
|
+
|
|
159
|
+
/** When false, skip session model fallback when all OM model candidates are exhausted.
|
|
160
|
+
* Default true for backward compatibility. */
|
|
161
|
+
sessionFallback?: boolean;
|
|
162
|
+
|
|
163
|
+
/** @deprecated Use compaction instead. */
|
|
164
|
+
noAutoCompact?: boolean;
|
|
165
|
+
/** @deprecated Use compaction + memory instead. */
|
|
166
|
+
passive?: boolean;
|
|
167
|
+
/** Enables observational memory (workers + content injection). Set to false for pi-vcc only. */
|
|
168
|
+
memory: boolean;
|
|
169
|
+
/** Writes debug JSONL to agent directory. */
|
|
170
|
+
debugLog: boolean;
|
|
148
171
|
}
|
|
149
172
|
|
|
150
173
|
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
151
174
|
|
|
152
175
|
export const DEFAULTS: UnifiedConfig = {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
debug: false,
|
|
177
|
+
sessionFallback: true,
|
|
178
|
+
|
|
179
|
+
// New config surface
|
|
180
|
+
compaction: "auto",
|
|
181
|
+
compactionEngine: "blackhole",
|
|
182
|
+
tailBehavior: "minimal",
|
|
183
|
+
midRunCompaction: "off",
|
|
184
|
+
|
|
185
|
+
observeAfterTokens: 15_000,
|
|
186
|
+
reflectAfterTokens: 25_000,
|
|
187
|
+
compactAfterTokens: 81_000,
|
|
188
|
+
observationsPoolMaxTokens: 20_000,
|
|
189
|
+
fullFoldAlways: true,
|
|
190
|
+
observationsPoolTargetTokens: 10_000,
|
|
191
|
+
reflectorInputMaxTokens: 80_000,
|
|
192
|
+
dropperInputMaxTokens: 80_000,
|
|
193
|
+
dropperPressureThreshold: 0.7,
|
|
194
|
+
dropperPoolFullnessThreshold: 0.1,
|
|
195
|
+
observerChunkMaxTokens: 40_000,
|
|
196
|
+
observerPreambleMaxTokens: 0,
|
|
197
|
+
agentMaxTurns: 16,
|
|
198
|
+
|
|
199
|
+
memory: true,
|
|
200
|
+
debugLog: false,
|
|
178
201
|
};
|
|
179
202
|
|
|
180
203
|
// ── Parsing helpers ──────────────────────────────────────────────────────────
|
|
181
204
|
|
|
182
|
-
const THINKING_LEVELS: readonly string[] = [
|
|
205
|
+
const THINKING_LEVELS: readonly string[] = [
|
|
206
|
+
"off",
|
|
207
|
+
"minimal",
|
|
208
|
+
"low",
|
|
209
|
+
"medium",
|
|
210
|
+
"high",
|
|
211
|
+
"xhigh",
|
|
212
|
+
];
|
|
183
213
|
|
|
184
214
|
// String enums for new config surface
|
|
185
215
|
const COMPACTION_VALUES = ["auto", "manual", "off"] as const;
|
|
@@ -188,113 +218,159 @@ const TAIL_BEHAVIOR_VALUES = ["pi-default", "minimal"] as const;
|
|
|
188
218
|
const MID_RUN_COMPACTION_VALUES = ["resume", "pause", "off"] as const;
|
|
189
219
|
|
|
190
220
|
function isCompaction(v: unknown): v is "auto" | "manual" | "off" {
|
|
191
|
-
|
|
221
|
+
return (
|
|
222
|
+
typeof v === "string" &&
|
|
223
|
+
(COMPACTION_VALUES as readonly string[]).includes(v)
|
|
224
|
+
);
|
|
192
225
|
}
|
|
193
226
|
function isCompactionEngine(v: unknown): v is "blackhole" | "pi-default" {
|
|
194
|
-
|
|
227
|
+
return (
|
|
228
|
+
typeof v === "string" &&
|
|
229
|
+
(COMPACTION_ENGINE_VALUES as readonly string[]).includes(v)
|
|
230
|
+
);
|
|
195
231
|
}
|
|
196
232
|
function isTailBehavior(v: unknown): v is "pi-default" | "minimal" {
|
|
197
|
-
|
|
233
|
+
return (
|
|
234
|
+
typeof v === "string" &&
|
|
235
|
+
(TAIL_BEHAVIOR_VALUES as readonly string[]).includes(v)
|
|
236
|
+
);
|
|
198
237
|
}
|
|
199
238
|
function isMidRunCompaction(v: unknown): v is "resume" | "pause" | "off" {
|
|
200
|
-
|
|
239
|
+
return (
|
|
240
|
+
typeof v === "string" &&
|
|
241
|
+
(MID_RUN_COMPACTION_VALUES as readonly string[]).includes(v)
|
|
242
|
+
);
|
|
201
243
|
}
|
|
202
244
|
|
|
203
245
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
204
|
-
|
|
246
|
+
return typeof v === "object" && v !== null;
|
|
205
247
|
}
|
|
206
248
|
|
|
207
249
|
function nonEmptyString(v: unknown): string | undefined {
|
|
208
|
-
|
|
250
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
209
251
|
}
|
|
210
252
|
|
|
211
253
|
function isThinkingLevel(v: unknown): v is ModelThinkingLevel {
|
|
212
|
-
|
|
254
|
+
return typeof v === "string" && THINKING_LEVELS.includes(v);
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
function positiveInt(v: unknown): number | undefined {
|
|
216
|
-
|
|
258
|
+
return Number.isInteger(v) && typeof v === "number" && v > 0 ? v : undefined;
|
|
217
259
|
}
|
|
218
260
|
|
|
219
261
|
/** Like positiveInt but allows 0. Used for cooldownHours where 0 means "disabled". */
|
|
220
262
|
function nonNegativeInt(v: unknown): number | undefined {
|
|
221
|
-
|
|
263
|
+
return Number.isInteger(v) && typeof v === "number" && v >= 0 ? v : undefined;
|
|
222
264
|
}
|
|
223
265
|
|
|
224
266
|
function parseModel(v: unknown): OmModelConfig | undefined {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
267
|
+
if (!isRecord(v)) return undefined;
|
|
268
|
+
const provider = nonEmptyString(v.provider);
|
|
269
|
+
const id = nonEmptyString(v.id);
|
|
270
|
+
if (!provider || !id) return undefined;
|
|
271
|
+
const model: OmModelConfig = { provider, id };
|
|
272
|
+
if (isThinkingLevel(v.thinking)) model.thinking = v.thinking;
|
|
273
|
+
const cooldown = nonNegativeInt(v.cooldownHours);
|
|
274
|
+
if (cooldown !== undefined) model.cooldownHours = cooldown;
|
|
275
|
+
const ctxWindow = positiveInt(v.contextWindow);
|
|
276
|
+
if (ctxWindow !== undefined) model.contextWindow = ctxWindow;
|
|
277
|
+
return model;
|
|
236
278
|
}
|
|
237
279
|
|
|
238
280
|
function parseModelArray(v: unknown): OmModelConfig[] | undefined {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
if (!Array.isArray(v)) return undefined;
|
|
282
|
+
const parsed = v
|
|
283
|
+
.map(parseModel)
|
|
284
|
+
.filter((m): m is OmModelConfig => m !== undefined);
|
|
285
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
242
286
|
}
|
|
243
287
|
|
|
244
288
|
function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
289
|
+
const c: Partial<UnifiedConfig> = {};
|
|
290
|
+
|
|
291
|
+
// String enums — compaction surface
|
|
292
|
+
if (isCompaction(raw.compaction)) c.compaction = raw.compaction;
|
|
293
|
+
if (isCompactionEngine(raw.compactionEngine))
|
|
294
|
+
c.compactionEngine = raw.compactionEngine;
|
|
295
|
+
if (isTailBehavior(raw.tailBehavior)) c.tailBehavior = raw.tailBehavior;
|
|
296
|
+
if (isMidRunCompaction(raw.midRunCompaction))
|
|
297
|
+
c.midRunCompaction = raw.midRunCompaction;
|
|
298
|
+
|
|
299
|
+
// Booleans — pi-vcc
|
|
300
|
+
if (typeof raw.overrideDefaultCompaction === "boolean")
|
|
301
|
+
c.overrideDefaultCompaction = raw.overrideDefaultCompaction;
|
|
302
|
+
if (typeof raw.debug === "boolean") c.debug = raw.debug;
|
|
303
|
+
|
|
304
|
+
// Booleans — om
|
|
305
|
+
if (typeof raw.sessionFallback === "boolean")
|
|
306
|
+
c.sessionFallback = raw.sessionFallback;
|
|
307
|
+
if (typeof raw.noAutoCompact === "boolean")
|
|
308
|
+
c.noAutoCompact = raw.noAutoCompact;
|
|
309
|
+
if (typeof raw.passive === "boolean") c.passive = raw.passive;
|
|
310
|
+
if (typeof raw.memory === "boolean") c.memory = raw.memory;
|
|
311
|
+
if (typeof raw.fullFoldAlways === "boolean")
|
|
312
|
+
c.fullFoldAlways = raw.fullFoldAlways;
|
|
313
|
+
if (typeof raw.debugLog === "boolean") c.debugLog = raw.debugLog;
|
|
314
|
+
|
|
315
|
+
// Numeric fields — use nonNegativeInt for observerPreambleMaxTokens (0 = auto)
|
|
316
|
+
const numKeys = [
|
|
317
|
+
"observeAfterTokens",
|
|
318
|
+
"reflectAfterTokens",
|
|
319
|
+
"compactAfterTokens",
|
|
320
|
+
"observationsPoolMaxTokens",
|
|
321
|
+
"observationsPoolTargetTokens",
|
|
322
|
+
"reflectorInputMaxTokens",
|
|
323
|
+
"dropperInputMaxTokens",
|
|
324
|
+
"observerChunkMaxTokens",
|
|
325
|
+
"observerPreambleMaxTokens",
|
|
326
|
+
"agentMaxTurns",
|
|
327
|
+
] as const;
|
|
328
|
+
|
|
329
|
+
// dropperPressureThreshold: fractional, must be in (0, 1]
|
|
330
|
+
if (
|
|
331
|
+
typeof raw.dropperPressureThreshold === "number" &&
|
|
332
|
+
Number.isFinite(raw.dropperPressureThreshold) &&
|
|
333
|
+
raw.dropperPressureThreshold > 0 &&
|
|
334
|
+
raw.dropperPressureThreshold <= 1
|
|
335
|
+
) {
|
|
336
|
+
c.dropperPressureThreshold = raw.dropperPressureThreshold;
|
|
337
|
+
}
|
|
338
|
+
// dropperPoolFullnessThreshold: fractional, must be in (0, 1]
|
|
339
|
+
if (
|
|
340
|
+
typeof raw.dropperPoolFullnessThreshold === "number" &&
|
|
341
|
+
Number.isFinite(raw.dropperPoolFullnessThreshold) &&
|
|
342
|
+
raw.dropperPoolFullnessThreshold > 0 &&
|
|
343
|
+
raw.dropperPoolFullnessThreshold <= 1
|
|
344
|
+
) {
|
|
345
|
+
c.dropperPoolFullnessThreshold = raw.dropperPoolFullnessThreshold;
|
|
346
|
+
}
|
|
347
|
+
for (const k of numKeys) {
|
|
348
|
+
// observerPreambleMaxTokens accepts 0 (auto-compute); everything else must be > 0
|
|
349
|
+
const validator =
|
|
350
|
+
k === "observerPreambleMaxTokens" ? nonNegativeInt : positiveInt;
|
|
351
|
+
const v = validator(raw[k]);
|
|
352
|
+
if (v !== undefined) (c as Record<string, unknown>)[k] = v;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Models
|
|
356
|
+
const model = parseModel(raw.model);
|
|
357
|
+
if (model) c.model = model;
|
|
358
|
+
const obsModel = parseModel(raw.observerModel);
|
|
359
|
+
if (obsModel) c.observerModel = obsModel;
|
|
360
|
+
const refModel = parseModel(raw.reflectorModel);
|
|
361
|
+
if (refModel) c.reflectorModel = refModel;
|
|
362
|
+
const dropModel = parseModel(raw.dropperModel);
|
|
363
|
+
if (dropModel) c.dropperModel = dropModel;
|
|
364
|
+
|
|
365
|
+
// Fallback model arrays
|
|
366
|
+
const obsFallback = parseModelArray(raw.observerFallbackModels);
|
|
367
|
+
if (obsFallback) c.observerFallbackModels = obsFallback;
|
|
368
|
+
const refFallback = parseModelArray(raw.reflectorFallbackModels);
|
|
369
|
+
if (refFallback) c.reflectorFallbackModels = refFallback;
|
|
370
|
+
const dropFallback = parseModelArray(raw.dropperFallbackModels);
|
|
371
|
+
if (dropFallback) c.dropperFallbackModels = dropFallback;
|
|
372
|
+
|
|
373
|
+
return c;
|
|
298
374
|
}
|
|
299
375
|
|
|
300
376
|
// ── Migration ────────────────────────────────────────────────────────────────
|
|
@@ -306,48 +382,54 @@ function parseConfig(raw: Record<string, unknown>): Partial<UnifiedConfig> {
|
|
|
306
382
|
* Idempotent — safe to call repeatedly.
|
|
307
383
|
*/
|
|
308
384
|
function migrateOldKnobs(parsed: Record<string, unknown>): void {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
385
|
+
// Only run if new keys are absent AND old keys are present
|
|
386
|
+
if (
|
|
387
|
+
parsed.compaction !== undefined ||
|
|
388
|
+
parsed.compactionEngine !== undefined
|
|
389
|
+
) {
|
|
390
|
+
return; // new keys already set — no migration
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// passive → compaction: "off" + memory: false
|
|
394
|
+
if (parsed.passive === true) {
|
|
395
|
+
parsed.compaction = "off";
|
|
396
|
+
parsed.memory = false;
|
|
397
|
+
}
|
|
398
|
+
// noAutoCompact → compaction: "manual"
|
|
399
|
+
else if (parsed.noAutoCompact === true) {
|
|
400
|
+
parsed.compaction = "manual";
|
|
401
|
+
}
|
|
402
|
+
// overrideDefaultCompaction → compactionEngine + tailBehavior
|
|
403
|
+
if (parsed.overrideDefaultCompaction === true) {
|
|
404
|
+
parsed.compactionEngine = "blackhole";
|
|
405
|
+
// Preserve aggressive cut for existing users
|
|
406
|
+
if (parsed.tailBehavior === undefined) {
|
|
407
|
+
parsed.tailBehavior = "minimal";
|
|
408
|
+
}
|
|
409
|
+
} else if (parsed.overrideDefaultCompaction === false) {
|
|
410
|
+
parsed.compactionEngine = "pi-default";
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Remove old keys so migration runs only once
|
|
414
|
+
delete parsed.passive;
|
|
415
|
+
delete parsed.noAutoCompact;
|
|
416
|
+
delete parsed.overrideDefaultCompaction;
|
|
338
417
|
}
|
|
339
418
|
|
|
340
419
|
// ── Load and save ────────────────────────────────────────────────────────────
|
|
341
420
|
|
|
342
|
-
function readJson(path: string): {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
421
|
+
function readJson(path: string): {
|
|
422
|
+
data: Record<string, unknown> | null;
|
|
423
|
+
error: string | null;
|
|
424
|
+
} {
|
|
425
|
+
if (!existsSync(path)) return { data: null, error: null };
|
|
426
|
+
try {
|
|
427
|
+
return { data: JSON.parse(readFileSync(path, "utf-8")), error: null };
|
|
428
|
+
} catch (e) {
|
|
429
|
+
const msg = `blackhole: config file at ${path} has invalid JSON: ${(e as Error).message}. Using defaults.`;
|
|
430
|
+
console.warn(msg);
|
|
431
|
+
return { data: null, error: msg };
|
|
432
|
+
}
|
|
351
433
|
}
|
|
352
434
|
|
|
353
435
|
/** Optional warning callback invoked when the primary config file has invalid JSON.
|
|
@@ -361,144 +443,171 @@ type WarnFn = (message: string) => void;
|
|
|
361
443
|
* Falls back to legacy sources if the unified file doesn't exist.
|
|
362
444
|
*/
|
|
363
445
|
export function loadUnifiedConfig(cwd: string, onWarn?: WarnFn): UnifiedConfig {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
merged.observationsPoolTargetTokens >= merged.observationsPoolMaxTokens
|
|
476
|
-
) {
|
|
477
|
-
merged.observationsPoolTargetTokens = Math.floor(merged.observationsPoolMaxTokens / 2);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return merged;
|
|
446
|
+
const path = configPath();
|
|
447
|
+
let raw: Record<string, unknown> | null;
|
|
448
|
+
let primaryError: string | null = null;
|
|
449
|
+
const result = readJson(path);
|
|
450
|
+
raw = result.data;
|
|
451
|
+
primaryError = result.error;
|
|
452
|
+
if (primaryError && onWarn) onWarn(primaryError);
|
|
453
|
+
|
|
454
|
+
// Fallback to legacy sources if unified file doesn't exist
|
|
455
|
+
if (!raw) {
|
|
456
|
+
// Try legacy pi-vcc config
|
|
457
|
+
const piVccPath = join(getAgentDir(), "pi-vcc-config.json");
|
|
458
|
+
const piVccResult = readJson(piVccPath);
|
|
459
|
+
const piVccRaw = piVccResult.data;
|
|
460
|
+
if (piVccResult.error && onWarn) onWarn(piVccResult.error);
|
|
461
|
+
|
|
462
|
+
// Try legacy om config from settings.json
|
|
463
|
+
const settingsPath = join(getAgentDir(), "settings.json");
|
|
464
|
+
const settingsResult = readJson(settingsPath);
|
|
465
|
+
const settingsRaw = settingsResult.data;
|
|
466
|
+
if (settingsResult.error && onWarn) onWarn(settingsResult.error);
|
|
467
|
+
const omRaw =
|
|
468
|
+
settingsRaw?.["pi-blackhole"] ?? settingsRaw?.["observational-memory"];
|
|
469
|
+
const projectSettingsPath = join(cwd, ".pi", "settings.json");
|
|
470
|
+
const projectResult = readJson(projectSettingsPath);
|
|
471
|
+
const projectRaw = projectResult.data;
|
|
472
|
+
if (projectResult.error && onWarn) onWarn(projectResult.error);
|
|
473
|
+
const projectOmRaw =
|
|
474
|
+
projectRaw?.["pi-blackhole"] ?? projectRaw?.["observational-memory"];
|
|
475
|
+
|
|
476
|
+
// Merge legacy sources
|
|
477
|
+
const merged: Record<string, unknown> = {};
|
|
478
|
+
if (piVccRaw && isRecord(piVccRaw)) Object.assign(merged, piVccRaw);
|
|
479
|
+
if (omRaw && isRecord(omRaw)) Object.assign(merged, omRaw);
|
|
480
|
+
if (projectOmRaw && isRecord(projectOmRaw))
|
|
481
|
+
Object.assign(merged, projectOmRaw);
|
|
482
|
+
raw = merged;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Project-local override: <cwd>/.pi/pi-blackhole-config.json
|
|
486
|
+
const projectConfigPath = join(cwd, ".pi", CONFIG_FILE);
|
|
487
|
+
const projectResult = readJson(projectConfigPath);
|
|
488
|
+
const projectRaw = projectResult.data;
|
|
489
|
+
if (projectResult.error && onWarn) onWarn(projectResult.error);
|
|
490
|
+
if (projectRaw && isRecord(projectRaw)) {
|
|
491
|
+
raw = { ...raw, ...projectRaw };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const parsed = parseConfig(raw);
|
|
495
|
+
|
|
496
|
+
// ── Migration: old → new knobs ──
|
|
497
|
+
migrateOldKnobs(parsed);
|
|
498
|
+
|
|
499
|
+
// Env override — legacy passive env vars
|
|
500
|
+
const envPassive =
|
|
501
|
+
process.env.PI_BLACKHOLE_PASSIVE ??
|
|
502
|
+
process.env.PI_VCC_OM_PASSIVE ??
|
|
503
|
+
process.env.PI_OBSERVATIONAL_MEMORY_PASSIVE;
|
|
504
|
+
if (envPassive !== undefined) {
|
|
505
|
+
const v = envPassive.trim().toLowerCase();
|
|
506
|
+
if (["1", "true", "yes", "on"].includes(v)) {
|
|
507
|
+
parsed.compaction = "off";
|
|
508
|
+
parsed.memory = false;
|
|
509
|
+
} else if (["0", "false", "no", "off"].includes(v)) {
|
|
510
|
+
// Falsy env override: undo passive migration when config relied on legacy key
|
|
511
|
+
if (raw?.passive === true) {
|
|
512
|
+
delete parsed.compaction;
|
|
513
|
+
delete parsed.memory;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Merge defaults then override
|
|
519
|
+
const merged = { ...DEFAULTS, ...parsed };
|
|
520
|
+
|
|
521
|
+
// ── Env override — compaction ──
|
|
522
|
+
const envCompaction = process.env.PI_BLACKHOLE_COMPACTION;
|
|
523
|
+
if (envCompaction !== undefined) {
|
|
524
|
+
const trimmed = envCompaction.trim().toLowerCase();
|
|
525
|
+
if (isCompaction(trimmed)) {
|
|
526
|
+
merged.compaction = trimmed as "auto" | "manual" | "off";
|
|
527
|
+
} else {
|
|
528
|
+
console.warn(
|
|
529
|
+
`blackhole: invalid PI_BLACKHOLE_COMPACTION value "${envCompaction}"; ignoring`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Env override — compaction engine ──
|
|
535
|
+
const envCompactionEngine = process.env.PI_BLACKHOLE_COMPACTION_ENGINE;
|
|
536
|
+
if (envCompactionEngine !== undefined) {
|
|
537
|
+
const trimmed = envCompactionEngine.trim().toLowerCase();
|
|
538
|
+
if (isCompactionEngine(trimmed)) {
|
|
539
|
+
merged.compactionEngine = trimmed as "blackhole" | "pi-default";
|
|
540
|
+
} else {
|
|
541
|
+
console.warn(
|
|
542
|
+
`blackhole: invalid PI_BLACKHOLE_COMPACTION_ENGINE value "${envCompactionEngine}"; ignoring`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Declarative PI_BLACKHOLE_* overrides ──
|
|
548
|
+
// Same env map as the ConfigManager modal path, so env overrides apply
|
|
549
|
+
// to the RUNTIME config, not just the modal. Overrides any file value.
|
|
550
|
+
const withEnv = applyEnvOverrides(
|
|
551
|
+
merged,
|
|
552
|
+
DECLARATIVE_ENV_OVERRIDES,
|
|
553
|
+
DEFAULTS as unknown as Record<string, unknown>,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
return withEnv;
|
|
481
557
|
}
|
|
482
558
|
|
|
483
559
|
/**
|
|
484
560
|
* Write settings back to disk. Preserves unknown keys.
|
|
485
561
|
*/
|
|
486
562
|
export function saveUnifiedConfig(settings: Partial<UnifiedConfig>): boolean {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
563
|
+
try {
|
|
564
|
+
const path = configPath();
|
|
565
|
+
const dir = dirname(path);
|
|
566
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
567
|
+
const existingResult = readJson(path);
|
|
568
|
+
const existing = existingResult.data ?? {};
|
|
569
|
+
if (existingResult.error) {
|
|
570
|
+
console.warn("blackhole: overwriting corrupt config file at " + path);
|
|
571
|
+
}
|
|
572
|
+
const next = { ...existing, ...settings };
|
|
573
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
574
|
+
return true;
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Write settings back to disk for a specific scope.
|
|
582
|
+
*
|
|
583
|
+
* - global: writes to `<agentDir>/pi-blackhole/pi-blackhole-config.json`
|
|
584
|
+
* - project: writes to `<cwd>/.pi/pi-blackhole-config.json`
|
|
585
|
+
*
|
|
586
|
+
* Preserves unknown keys in the target file.
|
|
587
|
+
*/
|
|
588
|
+
export function saveUnifiedConfigScoped(
|
|
589
|
+
settings: Partial<UnifiedConfig>,
|
|
590
|
+
scope: "global" | "project",
|
|
591
|
+
cwd: string,
|
|
592
|
+
): boolean {
|
|
593
|
+
try {
|
|
594
|
+
const dir =
|
|
595
|
+
scope === "project"
|
|
596
|
+
? join(cwd, ".pi")
|
|
597
|
+
: join(getAgentDir(), "pi-blackhole");
|
|
598
|
+
const path = join(dir, CONFIG_FILE);
|
|
599
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
600
|
+
const existingResult = readJson(path);
|
|
601
|
+
const existing = existingResult.data ?? {};
|
|
602
|
+
if (existingResult.error) {
|
|
603
|
+
console.warn("blackhole: overwriting corrupt config file at " + path);
|
|
604
|
+
}
|
|
605
|
+
const next = { ...existing, ...settings };
|
|
606
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
607
|
+
return true;
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
502
611
|
}
|
|
503
612
|
|
|
504
613
|
/**
|
|
@@ -510,21 +619,19 @@ export function saveUnifiedConfig(settings: Partial<UnifiedConfig>): boolean {
|
|
|
510
619
|
* read-only filesystems where the config is managed externally (e.g., Nix).
|
|
511
620
|
*/
|
|
512
621
|
export function scaffoldConfig(): void {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
622
|
+
try {
|
|
623
|
+
const path = configPath();
|
|
624
|
+
const dir = dirname(path);
|
|
625
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
626
|
+
|
|
627
|
+
if (!existsSync(path)) {
|
|
628
|
+
writeFileSync(path, `${JSON.stringify(DEFAULTS, null, 2)}\n`);
|
|
629
|
+
}
|
|
630
|
+
} catch (e) {
|
|
631
|
+
console.error("blackhole: config scaffold failed", e);
|
|
632
|
+
}
|
|
524
633
|
}
|
|
525
634
|
|
|
526
|
-
|
|
527
|
-
|
|
528
635
|
// ── Migration detection ───────────────────────────────────────────────────────
|
|
529
636
|
|
|
530
637
|
/**
|
|
@@ -533,15 +640,46 @@ export function scaffoldConfig(): void {
|
|
|
533
640
|
* Used to prompt users to save their config with the new keys.
|
|
534
641
|
*/
|
|
535
642
|
export function configFileNeedsMigration(): boolean {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
643
|
+
try {
|
|
644
|
+
const path = configPath();
|
|
645
|
+
if (!existsSync(path)) return false;
|
|
646
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as Record<
|
|
647
|
+
string,
|
|
648
|
+
unknown
|
|
649
|
+
>;
|
|
650
|
+
if (
|
|
651
|
+
raw.compaction !== undefined ||
|
|
652
|
+
raw.compactionEngine !== undefined ||
|
|
653
|
+
raw.tailBehavior !== undefined
|
|
654
|
+
) {
|
|
655
|
+
return false; // already has new keys
|
|
656
|
+
}
|
|
657
|
+
return (
|
|
658
|
+
raw.passive !== undefined ||
|
|
659
|
+
raw.noAutoCompact !== undefined ||
|
|
660
|
+
raw.overrideDefaultCompaction !== undefined
|
|
661
|
+
);
|
|
662
|
+
} catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Check whether the OM pipeline is in manual mode.
|
|
669
|
+
*
|
|
670
|
+
* In manual mode, observations/reflections/dropped are saved to the
|
|
671
|
+
* per-session pending file instead of being appended to the branch.
|
|
672
|
+
* On `/blackhole`, pending entries are flushed to the branch and
|
|
673
|
+
* the pending file is cleared.
|
|
674
|
+
*
|
|
675
|
+
* Handles both the new `compaction: "manual"` key and the legacy
|
|
676
|
+
* `noAutoCompact: true` key for backward compatibility.
|
|
677
|
+
*
|
|
678
|
+
* @param config — The current runtime config (may include legacy noAutoCompact)
|
|
679
|
+
*/
|
|
680
|
+
export function isManualMode(config: {
|
|
681
|
+
compaction?: string;
|
|
682
|
+
noAutoCompact?: boolean;
|
|
683
|
+
}): boolean {
|
|
684
|
+
return config.compaction === "manual" || config.noAutoCompact === true;
|
|
547
685
|
}
|