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
package/src/om/pending.ts
CHANGED
|
@@ -1,70 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pending OM state persistence.
|
|
3
3
|
*
|
|
4
|
-
* When `
|
|
4
|
+
* When `compaction: "manual"` is enabled (or legacy `noAutoCompact`), observations, reflections, and dropper
|
|
5
5
|
* results are saved to disk instead of being appended to the conversation.
|
|
6
6
|
* Each new pipeline run replaces the previous result (latest subsumes earlier
|
|
7
7
|
* since every run processes all entries since the last actual branch append).
|
|
8
8
|
*
|
|
9
|
-
* On manual `/
|
|
9
|
+
* On manual `/blackhole` trigger, pending entries are flushed to the branch
|
|
10
10
|
* and the file is cleared.
|
|
11
11
|
*
|
|
12
12
|
* Per-session files: each session gets its own <sessionId>-pending.json
|
|
13
13
|
* under ~/.pi/agent/pi-blackhole/. This eliminates race conditions from
|
|
14
14
|
* concurrent pi sessions writing to a shared file.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
renameSync,
|
|
22
|
+
unlinkSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
17
25
|
import { join } from "node:path";
|
|
18
26
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
19
27
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
20
28
|
|
|
21
29
|
export interface PendingObservation {
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
coversUpToId: string;
|
|
31
|
+
data: unknown;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export interface PendingReflection {
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
coversUpToId: string;
|
|
36
|
+
data: unknown;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface PendingDropped {
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
coversUpToId: string;
|
|
41
|
+
data: unknown;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
export interface PendingOMState {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
/** Latest observation run (replaced each time, not accumulated). */
|
|
46
|
+
observation?: PendingObservation;
|
|
47
|
+
/** Latest reflection run (replaced each time, not accumulated). */
|
|
48
|
+
reflection?: PendingReflection;
|
|
49
|
+
/** Latest dropper run (replaced each time, not accumulated). */
|
|
50
|
+
dropped?: PendingDropped;
|
|
51
|
+
/**
|
|
52
|
+
* All observation batches accumulated across manual mode pipeline runs.
|
|
53
|
+
* Each batch preserves per-run coverage (coversUpToId) matching the normal
|
|
54
|
+
* branch-marker pattern. Used for LLM context and /blackhole flush.
|
|
55
|
+
*/
|
|
56
|
+
observationBatches?: PendingObservation[];
|
|
57
|
+
/**
|
|
58
|
+
* All reflection batches accumulated across manual mode pipeline runs.
|
|
59
|
+
* Preserves per-run coverage for LLM context and /blackhole flush.
|
|
60
|
+
*/
|
|
61
|
+
reflectionBatches?: PendingReflection[];
|
|
62
|
+
/**
|
|
63
|
+
* All dropper batches accumulated across manual mode pipeline runs.
|
|
64
|
+
* Each batch preserves which observations were dropped in that run.
|
|
65
|
+
* Without accumulation, earlier drops are lost when the next dropper
|
|
66
|
+
* run overwrites pending.dropped, causing them to be "un-dropped" on
|
|
67
|
+
* /blackhole flush.
|
|
68
|
+
*/
|
|
69
|
+
droppedBatches?: PendingDropped[];
|
|
70
|
+
/** Pipeline progress cursors — persist across restarts and fork recovery. */
|
|
71
|
+
cursors?: {
|
|
72
|
+
observer?: { entryId: string; state: string };
|
|
73
|
+
reflector?: { entryId: string; state: string };
|
|
74
|
+
dropper?: { entryId: string; state: string };
|
|
75
|
+
};
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
// ── Persistence ─────────────────────────────────────────────────────────────
|
|
@@ -75,31 +83,37 @@ const STALE_SUFFIX = "-pending.stale.json";
|
|
|
75
83
|
|
|
76
84
|
/** Build the path for a given session's pending file. */
|
|
77
85
|
function pendingPath(sessionId: string): string {
|
|
78
|
-
|
|
86
|
+
return join(getAgentDir(), PENDING_DIR, `${sessionId}${PENDING_SUFFIX}`);
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/** Build the path for a given session's stale pending file (backup of previous write). */
|
|
82
90
|
function stalePath(sessionId: string): string {
|
|
83
|
-
|
|
91
|
+
return join(getAgentDir(), PENDING_DIR, `${sessionId}${STALE_SUFFIX}`);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
/** Ensure the pending directory exists. */
|
|
87
95
|
function ensureDir(): void {
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const dir = join(getAgentDir(), PENDING_DIR);
|
|
97
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
function defaultState(): PendingOMState {
|
|
93
|
-
|
|
101
|
+
return {};
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
function isEmptyState(s: PendingOMState): boolean {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
const hasCursors =
|
|
106
|
+
s.cursors &&
|
|
107
|
+
(s.cursors.observer || s.cursors.reflector || s.cursors.dropper);
|
|
108
|
+
if (hasCursors) return false;
|
|
109
|
+
return (
|
|
110
|
+
!s.observation &&
|
|
111
|
+
!s.reflection &&
|
|
112
|
+
!s.dropped &&
|
|
113
|
+
(!s.observationBatches || s.observationBatches.length === 0) &&
|
|
114
|
+
(!s.reflectionBatches || s.reflectionBatches.length === 0) &&
|
|
115
|
+
(!s.droppedBatches || s.droppedBatches.length === 0)
|
|
116
|
+
);
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
// ── Per-session file read/write ─────────────────────────────────────────────
|
|
@@ -109,16 +123,16 @@ function isEmptyState(s: PendingOMState): boolean {
|
|
|
109
123
|
* Returns default (empty) state if file doesn't exist or is corrupt.
|
|
110
124
|
*/
|
|
111
125
|
function readSessionState(sessionId: string): PendingOMState {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
const path = pendingPath(sessionId);
|
|
127
|
+
if (!existsSync(path)) return defaultState();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
131
|
+
if (isPendingOMState(raw)) return sanitizePendingState(raw);
|
|
132
|
+
return defaultState();
|
|
133
|
+
} catch {
|
|
134
|
+
return defaultState();
|
|
135
|
+
}
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
/**
|
|
@@ -127,49 +141,70 @@ function readSessionState(sessionId: string): PendingOMState {
|
|
|
127
141
|
* Preserves the previous state as <sessionId>-pending.stale.json as backup.
|
|
128
142
|
*/
|
|
129
143
|
function writeSessionState(sessionId: string, state: PendingOMState): void {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
144
|
+
const path = pendingPath(sessionId);
|
|
145
|
+
if (isEmptyState(state)) {
|
|
146
|
+
// Clear: remove both main and stale
|
|
147
|
+
try {
|
|
148
|
+
if (existsSync(path)) unlinkSync(path);
|
|
149
|
+
} catch {
|
|
150
|
+
/* best-effort */
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const stale = stalePath(sessionId);
|
|
154
|
+
if (existsSync(stale)) unlinkSync(stale);
|
|
155
|
+
} catch {
|
|
156
|
+
/* best-effort */
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ensureDir();
|
|
162
|
+
|
|
163
|
+
// Before writing new state, rename current file to stale as backup
|
|
164
|
+
try {
|
|
165
|
+
if (existsSync(path)) {
|
|
166
|
+
renameSync(path, stalePath(sessionId));
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
/* best-effort — stale backup is optional */
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
174
|
+
} catch {
|
|
175
|
+
// Best-effort: pending state loss means consolidation may re-process
|
|
176
|
+
// the same data on the next run — safe (idempotent by design).
|
|
177
|
+
// Prevents process crash on read-only filesystems.
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
|
|
161
181
|
/** Validate that unknown value is shape-compatible with PendingOMState. */
|
|
162
182
|
function isPendingOMState(value: unknown): value is PendingOMState {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
if (!value || typeof value !== "object") return false;
|
|
184
|
+
const v = value as Record<string, unknown>;
|
|
185
|
+
const hasObs = !!(
|
|
186
|
+
v.observation &&
|
|
187
|
+
typeof v.observation === "object" &&
|
|
188
|
+
typeof (v.observation as any).coversUpToId === "string"
|
|
189
|
+
);
|
|
190
|
+
const hasRef = !!(
|
|
191
|
+
v.reflection &&
|
|
192
|
+
typeof v.reflection === "object" &&
|
|
193
|
+
typeof (v.reflection as any).coversUpToId === "string"
|
|
194
|
+
);
|
|
195
|
+
const hasDrop = !!(
|
|
196
|
+
v.dropped &&
|
|
197
|
+
typeof v.dropped === "object" &&
|
|
198
|
+
typeof (v.dropped as any).coversUpToId === "string"
|
|
199
|
+
);
|
|
200
|
+
// Also accept states with only batch arrays (no singular fields)
|
|
201
|
+
const hasBatches =
|
|
202
|
+
Array.isArray(v.observationBatches) ||
|
|
203
|
+
Array.isArray(v.reflectionBatches) ||
|
|
204
|
+
Array.isArray(v.droppedBatches);
|
|
205
|
+
// Accept cursor-only states (no observations/reflections yet, but persisted)
|
|
206
|
+
const hasCursors = !!(v.cursors && typeof v.cursors === "object");
|
|
207
|
+
return hasObs || hasRef || hasDrop || hasBatches || hasCursors;
|
|
173
208
|
}
|
|
174
209
|
|
|
175
210
|
/**
|
|
@@ -177,32 +212,41 @@ function isPendingOMState(value: unknown): value is PendingOMState {
|
|
|
177
212
|
* Returns a clean copy; does not mutate the input.
|
|
178
213
|
*/
|
|
179
214
|
function sanitizePendingState(raw: PendingOMState): PendingOMState {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
const sanitized: PendingOMState = {
|
|
216
|
+
...raw,
|
|
217
|
+
observation: raw.observation ?? undefined,
|
|
218
|
+
reflection: raw.reflection ?? undefined,
|
|
219
|
+
dropped: raw.dropped ?? undefined,
|
|
220
|
+
};
|
|
221
|
+
// Filter batch arrays to only include valid entries with the required shape
|
|
222
|
+
if (Array.isArray(raw.observationBatches)) {
|
|
223
|
+
sanitized.observationBatches = raw.observationBatches.filter(
|
|
224
|
+
(b): b is PendingObservation =>
|
|
225
|
+
!!b &&
|
|
226
|
+
typeof b === "object" &&
|
|
227
|
+
typeof (b as any).coversUpToId === "string" &&
|
|
228
|
+
(b as any).data !== undefined,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(raw.reflectionBatches)) {
|
|
232
|
+
sanitized.reflectionBatches = raw.reflectionBatches.filter(
|
|
233
|
+
(b): b is PendingReflection =>
|
|
234
|
+
!!b &&
|
|
235
|
+
typeof b === "object" &&
|
|
236
|
+
typeof (b as any).coversUpToId === "string" &&
|
|
237
|
+
(b as any).data !== undefined,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(raw.droppedBatches)) {
|
|
241
|
+
sanitized.droppedBatches = raw.droppedBatches.filter(
|
|
242
|
+
(b): b is PendingDropped =>
|
|
243
|
+
!!b &&
|
|
244
|
+
typeof b === "object" &&
|
|
245
|
+
typeof (b as any).coversUpToId === "string" &&
|
|
246
|
+
(b as any).data !== undefined,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return sanitized;
|
|
206
250
|
}
|
|
207
251
|
|
|
208
252
|
// ── API ─────────────────────────────────────────────────────────────────────
|
|
@@ -212,25 +256,31 @@ function sanitizePendingState(raw: PendingOMState): PendingOMState {
|
|
|
212
256
|
* Each new run covers all entries since last branch append, so the latest
|
|
213
257
|
* result fully subsumes any previous one.
|
|
214
258
|
*/
|
|
215
|
-
export function savePendingObservation(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
259
|
+
export function savePendingObservation(
|
|
260
|
+
sessionId: string,
|
|
261
|
+
entry: PendingObservation,
|
|
262
|
+
): void {
|
|
263
|
+
const state = readSessionState(sessionId);
|
|
264
|
+
state.observation = entry;
|
|
265
|
+
// Append to accumulated batches for LLM context and /blackhole flush.
|
|
266
|
+
// Each batch preserves per-run coverage (coversUpToId) matching the
|
|
267
|
+
// normal branch-marker pattern.
|
|
268
|
+
state.observationBatches = [...(state.observationBatches ?? []), entry];
|
|
269
|
+
writeSessionState(sessionId, state);
|
|
223
270
|
}
|
|
224
271
|
|
|
225
272
|
/**
|
|
226
273
|
* Save (replace) the latest reflection result for a session.
|
|
227
274
|
*/
|
|
228
|
-
export function savePendingReflection(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
export function savePendingReflection(
|
|
276
|
+
sessionId: string,
|
|
277
|
+
entry: PendingReflection,
|
|
278
|
+
): void {
|
|
279
|
+
const state = readSessionState(sessionId);
|
|
280
|
+
state.reflection = entry;
|
|
281
|
+
// Append to accumulated batches for LLM context and /blackhole flush.
|
|
282
|
+
state.reflectionBatches = [...(state.reflectionBatches ?? []), entry];
|
|
283
|
+
writeSessionState(sessionId, state);
|
|
234
284
|
}
|
|
235
285
|
|
|
236
286
|
/**
|
|
@@ -238,58 +288,72 @@ export function savePendingReflection(sessionId: string, entry: PendingReflectio
|
|
|
238
288
|
* append to droppedBatches so no drops are lost across cycles
|
|
239
289
|
* before /blackhole flush.
|
|
240
290
|
*/
|
|
241
|
-
export function savePendingDropped(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
291
|
+
export function savePendingDropped(
|
|
292
|
+
sessionId: string,
|
|
293
|
+
entry: PendingDropped,
|
|
294
|
+
): void {
|
|
295
|
+
const state = readSessionState(sessionId);
|
|
296
|
+
state.dropped = entry;
|
|
297
|
+
state.droppedBatches = [...(state.droppedBatches ?? []), entry];
|
|
298
|
+
writeSessionState(sessionId, state);
|
|
246
299
|
}
|
|
247
300
|
|
|
248
301
|
/**
|
|
249
302
|
* Check whether a coversUpToId matches the already-pending observation
|
|
250
303
|
* for the given session. Returns true if the chunk was already processed.
|
|
251
304
|
*/
|
|
252
|
-
export function isObservationChunkPending(
|
|
253
|
-
|
|
254
|
-
|
|
305
|
+
export function isObservationChunkPending(
|
|
306
|
+
sessionId: string,
|
|
307
|
+
coversUpToId: string,
|
|
308
|
+
): boolean {
|
|
309
|
+
const s = readSessionState(sessionId);
|
|
310
|
+
return s.observation?.coversUpToId === coversUpToId;
|
|
255
311
|
}
|
|
256
312
|
|
|
257
313
|
/**
|
|
258
314
|
* Check whether a coversUpToId matches the already-pending reflection
|
|
259
315
|
* for the given session.
|
|
260
316
|
*/
|
|
261
|
-
export function isReflectionChunkPending(
|
|
262
|
-
|
|
263
|
-
|
|
317
|
+
export function isReflectionChunkPending(
|
|
318
|
+
sessionId: string,
|
|
319
|
+
coversUpToId: string,
|
|
320
|
+
): boolean {
|
|
321
|
+
const s = readSessionState(sessionId);
|
|
322
|
+
return s.reflection?.coversUpToId === coversUpToId;
|
|
264
323
|
}
|
|
265
324
|
|
|
266
325
|
/** Read the pending OM state for a specific session. */
|
|
267
326
|
export function readPendingState(sessionId: string): PendingOMState {
|
|
268
|
-
|
|
327
|
+
return readSessionState(sessionId);
|
|
269
328
|
}
|
|
270
329
|
|
|
271
330
|
/** Clear the pending OM state for a specific session after flushing to branch. */
|
|
272
331
|
export function clearPendingState(sessionId: string): void {
|
|
273
|
-
|
|
332
|
+
writeSessionState(sessionId, defaultState());
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
/** Check whether there is any pending OM state for a specific session. */
|
|
277
336
|
export function hasPendingData(sessionId: string): boolean {
|
|
278
|
-
|
|
337
|
+
return !isEmptyState(readSessionState(sessionId));
|
|
279
338
|
}
|
|
280
339
|
|
|
281
340
|
/** Read cursors from pending state for a session. */
|
|
282
|
-
export function readPendingCursors(
|
|
283
|
-
|
|
284
|
-
|
|
341
|
+
export function readPendingCursors(
|
|
342
|
+
sessionId: string,
|
|
343
|
+
): PendingOMState["cursors"] {
|
|
344
|
+
const state = readSessionState(sessionId);
|
|
345
|
+
return state.cursors;
|
|
285
346
|
}
|
|
286
347
|
|
|
287
348
|
/** Write cursors to pending state for a session (replaces existing cursors).
|
|
288
349
|
* Uses assignment (not merge) so deletions from validateCursors persist. */
|
|
289
|
-
export function writePendingCursors(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
350
|
+
export function writePendingCursors(
|
|
351
|
+
sessionId: string,
|
|
352
|
+
cursors: PendingOMState["cursors"],
|
|
353
|
+
): void {
|
|
354
|
+
const state = readSessionState(sessionId);
|
|
355
|
+
state.cursors = { ...cursors };
|
|
356
|
+
writeSessionState(sessionId, state);
|
|
293
357
|
}
|
|
294
358
|
|
|
295
359
|
/**
|
|
@@ -297,22 +361,22 @@ export function writePendingCursors(sessionId: string, cursors: PendingOMState["
|
|
|
297
361
|
* for *-pending.json files.
|
|
298
362
|
*/
|
|
299
363
|
export function listPendingSessions(): string[] {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
364
|
+
const dir = join(getAgentDir(), PENDING_DIR);
|
|
365
|
+
if (!existsSync(dir)) return [];
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const files = readdirSync(dir);
|
|
369
|
+
const sessions: string[] = [];
|
|
370
|
+
for (const file of files) {
|
|
371
|
+
if (!file.endsWith(PENDING_SUFFIX)) continue;
|
|
372
|
+
const sessionId = file.slice(0, -PENDING_SUFFIX.length);
|
|
373
|
+
if (!sessionId) continue;
|
|
374
|
+
// Verify the file actually has non-empty data
|
|
375
|
+
const state = readSessionState(sessionId);
|
|
376
|
+
if (!isEmptyState(state)) sessions.push(sessionId);
|
|
377
|
+
}
|
|
378
|
+
return sessions;
|
|
379
|
+
} catch {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
318
382
|
}
|
|
@@ -6,12 +6,57 @@
|
|
|
6
6
|
* bridge logic so all OM agents (observer, reflector, dropper) use the same
|
|
7
7
|
* custom-provider resolution instead of each duplicating the 15-line function.
|
|
8
8
|
*/
|
|
9
|
+
interface RegisteredProviderConfig {
|
|
10
|
+
api?: string;
|
|
11
|
+
streamSimple?: Function;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProviderRegistry {
|
|
15
|
+
getRegisteredProviderIds?: () => readonly string[];
|
|
16
|
+
getRegisteredProviderConfig?: (
|
|
17
|
+
providerId: string,
|
|
18
|
+
) => RegisteredProviderConfig | undefined;
|
|
19
|
+
registeredProviders?: Map<string, RegisteredProviderConfig>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function captureRegisteredProviderStreams(
|
|
23
|
+
registry: ProviderRegistry,
|
|
24
|
+
providerStreams: Map<string, Function>,
|
|
25
|
+
): void {
|
|
26
|
+
if (
|
|
27
|
+
registry.getRegisteredProviderIds &&
|
|
28
|
+
registry.getRegisteredProviderConfig
|
|
29
|
+
) {
|
|
30
|
+
for (const providerId of registry.getRegisteredProviderIds()) {
|
|
31
|
+
const config = registry.getRegisteredProviderConfig(providerId);
|
|
32
|
+
if (
|
|
33
|
+
config?.streamSimple &&
|
|
34
|
+
config.api &&
|
|
35
|
+
!providerStreams.has(config.api)
|
|
36
|
+
) {
|
|
37
|
+
providerStreams.set(config.api, config.streamSimple);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
registry.registeredProviders?.forEach((config) => {
|
|
44
|
+
if (config.streamSimple && config.api && !providerStreams.has(config.api)) {
|
|
45
|
+
providerStreams.set(config.api, config.streamSimple);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
9
50
|
export function createBridgeStreamFn(streamSimple: any) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
51
|
+
const PROVIDER_STREAMS_KEY = Symbol.for("pi-blackhole:provider-streams");
|
|
52
|
+
return (model: any, ctx: any, opts: any) => {
|
|
53
|
+
const providerStreams: Map<string, Function> | undefined = (
|
|
54
|
+
globalThis as any
|
|
55
|
+
)[PROVIDER_STREAMS_KEY];
|
|
56
|
+
if (!providerStreams) return streamSimple(model, ctx, opts);
|
|
57
|
+
const customFn = model?.api ? providerStreams.get(model.api) : undefined;
|
|
58
|
+
return customFn
|
|
59
|
+
? customFn(model, ctx, opts)
|
|
60
|
+
: streamSimple(model, ctx, opts);
|
|
61
|
+
};
|
|
17
62
|
}
|
|
@@ -18,23 +18,19 @@ export function isRetryableError(error: unknown): boolean {
|
|
|
18
18
|
return RETRYABLE_ERROR_RE.test(message);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Re-export Pi's context-overflow detection so blackhole callers use one
|
|
23
|
-
* authoritative source instead of maintaining their own provider-specific patterns.
|
|
24
|
-
* @see @earendil-works/pi-ai/dist/utils/overflow.d.ts
|
|
25
|
-
*/
|
|
26
|
-
export { isContextOverflow } from "@earendil-works/pi-ai";
|
|
27
|
-
|
|
28
21
|
/** Detect Pi's "extension ctx is stale" error from session replacement/reload.
|
|
29
22
|
* These are not model errors and must not be recorded as cooldowns. */
|
|
30
23
|
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
let message: string;
|
|
25
|
+
if (error instanceof Error) {
|
|
26
|
+
message = error.message;
|
|
27
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
28
|
+
message = String((error as { message: unknown }).message);
|
|
29
|
+
} else {
|
|
30
|
+
message = String(error || "");
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
message.includes("extension ctx is stale") ||
|
|
34
|
+
message.includes("ctx is stale")
|
|
35
|
+
);
|
|
40
36
|
}
|