pi-blackhole 0.3.4 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +13 -16
  2. package/example-config.json +19 -27
  3. package/package.json +1 -1
  4. package/src/commands/memory.ts +1 -1
  5. package/src/commands/pi-vcc.ts +3 -9
  6. package/src/commands/vcc-recall.ts +16 -3
  7. package/src/core/brief.ts +18 -13
  8. package/src/core/build-sections.ts +1 -2
  9. package/src/core/content.ts +8 -5
  10. package/src/core/drill-down.ts +2 -9
  11. package/src/core/filter-noise.ts +7 -10
  12. package/src/core/format.ts +25 -25
  13. package/src/core/load-messages.ts +71 -2
  14. package/src/core/normalize.ts +10 -4
  15. package/src/core/recall-scope.ts +3 -3
  16. package/src/core/render-entries.ts +10 -2
  17. package/src/core/search-entries.ts +12 -7
  18. package/src/core/settings.ts +3 -34
  19. package/src/core/summarize.ts +33 -13
  20. package/src/core/tool-args.ts +4 -1
  21. package/src/core/unified-config.ts +27 -25
  22. package/src/extract/commits.ts +3 -2
  23. package/src/extract/files.ts +6 -4
  24. package/src/hooks/before-compact.ts +7 -3
  25. package/src/om/agents/dropper/agent.ts +2 -7
  26. package/src/om/agents/observer/agent.ts +11 -13
  27. package/src/om/agents/reflector/agent.ts +2 -7
  28. package/src/om/compaction-trigger.ts +54 -18
  29. package/src/om/configure-overlay.ts +5 -1
  30. package/src/om/consolidation.ts +35 -13
  31. package/src/om/cooldown.ts +15 -12
  32. package/src/om/debug-log.ts +79 -11
  33. package/src/om/key-matcher.ts +6 -32
  34. package/src/om/ledger/fold.ts +8 -0
  35. package/src/om/ledger/render-summary.ts +16 -15
  36. package/src/om/ledger/types.ts +1 -1
  37. package/src/om/pending.ts +38 -5
  38. package/src/om/provider-stream.ts +17 -0
  39. package/src/om/retryable-error.ts +26 -0
  40. package/src/om/reverse-recall.ts +9 -1
  41. package/src/om/runtime.ts +39 -18
  42. package/src/sections.ts +0 -3
  43. package/src/tools/recall.ts +11 -9
package/src/om/runtime.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - markConsolidationError sets 30s retry gate for failed runs.
11
11
  */
12
12
  import { type Config, type ConfiguredModel, DEFAULTS, loadConfig } from "./config.js";
13
- import { isCooldownActive, recordCooldown, expireCooldowns, modelKey } from "./cooldown.js";
13
+ import { isCooldownActive, getCooldownEntry, recordCooldown, expireCooldowns, modelKey } from "./cooldown.js";
14
14
 
15
15
  export type ResolveResult =
16
16
  | { ok: true; model: any; apiKey: string; headers?: Record<string, string>; cooldownApplied?: boolean }
@@ -94,13 +94,17 @@ export class Runtime {
94
94
  * Tries the candidate list in order:
95
95
  * 1. Primary stage model → 2. Stage fallbacks → 3. Base config.model → 4. Session model.
96
96
  *
97
+ * Session model fallback can be disabled via config.sessionFallback: false.
98
+ * When disabled, returns { ok: false } instead of using the session model,
99
+ * allowing the stage to be skipped entirely when all configured OM models fail.
100
+ *
97
101
  * Skips models that are currently in a cooldown window.
98
102
  * On retryable error (after the agent runs), the model that failed is cooled down
99
103
  * and the next candidate is tried. The caller must call `recordRetryableError`
100
104
  * after the API attempt to mark the failed model.
101
105
  *
102
106
  * Returns `ok: true` with the resolved model, or `ok: false` with a reason
103
- * if all candidates (including session model) are exhausted or unavailable.
107
+ * if all candidates (including session model, if enabled) are exhausted or unavailable.
104
108
  */
105
109
  async resolveModel(ctx: ResolveCtx): Promise<ResolveResult> {
106
110
  const candidates = this.buildCandidateList(ctx.stageModel, ctx.stageFallbacks);
@@ -123,8 +127,10 @@ export class Runtime {
123
127
 
124
128
  if (isCooldownActive(candidate)) {
125
129
  if (ctx.hasUI && ctx.ui) {
130
+ const entry = getCooldownEntry(candidate);
131
+ const reason = entry ? `: ${entry.reason}` : "";
126
132
  ctx.ui.notify(
127
- `Observational memory: ${stageName} skipping ${key} (cooldown active)`,
133
+ `Observational memory: ${stageName} skipping ${key} (cooldown${reason})`,
128
134
  "info",
129
135
  );
130
136
  }
@@ -162,25 +168,40 @@ export class Runtime {
162
168
  };
163
169
  }
164
170
 
165
- // Fall back to session model
166
- const sessionModel = ctx.model;
167
- if (!sessionModel) {
168
- return { ok: false, reason: `no model available for ${stageName} (all candidates exhausted, no session model)` };
171
+ // Fall back to session model (if enabled)
172
+ if (this.config.sessionFallback !== false) {
173
+ const sessionModel = ctx.model;
174
+ if (!sessionModel) {
175
+ return { ok: false, reason: `no model available for ${stageName} (all candidates exhausted, no session model)` };
176
+ }
177
+
178
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(sessionModel);
179
+ if (!auth.ok || !auth.apiKey) {
180
+ const provider = (sessionModel as { provider?: string }).provider ?? "unknown";
181
+ return { ok: false, reason: `no API key for session model provider "${provider}"` };
182
+ }
183
+
184
+ return {
185
+ ok: true,
186
+ model: sessionModel,
187
+ apiKey: auth.apiKey as string,
188
+ headers: auth.headers as Record<string, string> | undefined,
189
+ cooldownApplied: false,
190
+ };
169
191
  }
170
192
 
171
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(sessionModel);
172
- if (!auth.ok || !auth.apiKey) {
173
- const provider = (sessionModel as { provider?: string }).provider ?? "unknown";
174
- return { ok: false, reason: `no API key for session model provider "${provider}"` };
193
+ // All configured candidates exhausted and session fallback disabled —
194
+ // skip the stage entirely. Info-level to match cooldown-disabled pattern.
195
+ // Set resolveFailureNotified so the consolidation layer doesn't duplicate.
196
+ if (ctx.hasUI && ctx.ui) {
197
+ ctx.ui.notify(
198
+ `Observational memory: ${stageName} skipped — all candidates failed (sessionFallback disabled, won't use main model)`,
199
+ "info",
200
+ );
175
201
  }
202
+ this.resolveFailureNotified = true;
176
203
 
177
- return {
178
- ok: true,
179
- model: sessionModel,
180
- apiKey: auth.apiKey as string,
181
- headers: auth.headers as Record<string, string> | undefined,
182
- cooldownApplied: false,
183
- };
204
+ return { ok: false, reason: `no model available for ${stageName} (all candidates exhausted, sessionFallback disabled)` };
184
205
  }
185
206
 
186
207
  /**
package/src/sections.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * Upstream: https://github.com/sting8k/pi-vcc (src/sections.ts)
5
5
  * Unmodified.
6
6
  */
7
- import type { TranscriptEntry } from "./core/brief";
8
7
 
9
8
  export interface SectionData {
10
9
  sessionGoal: string[];
@@ -13,6 +12,4 @@ export interface SectionData {
13
12
  commits: string[];
14
13
  userPreferences: string[];
15
14
  briefTranscript: string;
16
- /** Structured transcript entries (verbose object format) */
17
- transcriptEntries: TranscriptEntry[];
18
15
  }
@@ -138,7 +138,11 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
138
138
 
139
139
  // Merge expanded entries into full result set BEFORE pagination
140
140
  // so pagination counts and positioning stay consistent
141
+ let appendedExpandCount = 0;
141
142
  if (expandedFullEntries) {
143
+ // Track expand-only entries that don't match the query so the header can distinguish them
144
+ const existingIndices = new Set(allResults.map((r) => r.index));
145
+ appendedExpandCount = expandedFullEntries.filter((fe) => !existingIndices.has(fe.index)).length;
142
146
  allResults = mergeExpandedIntoSearchResults(allResults, expandedFullEntries);
143
147
  }
144
148
 
@@ -148,9 +152,10 @@ async function vccRecall(params: { query?: string; expand?: number[]; page?: num
148
152
  const pageResults: SearchHit[] = allResults.slice(start, start + PAGE_SIZE);
149
153
  const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
150
154
  const scopeSuffix = scope === "all" ? " (scope: all)" : "";
155
+ const matchCount = allResults.length - appendedExpandCount;
151
156
  const header = totalPages > 1
152
- ? `Page ${page}/${totalPages} (${allResults.length} total matches${scopeSuffix})`
153
- : `${allResults.length} matches${scopeSuffix}`;
157
+ ? `Page ${page}/${totalPages} (${matchCount} matches${appendedExpandCount > 0 ? ` + ${appendedExpandCount} expanded` : ""}${scopeSuffix})`
158
+ : `${matchCount} matches${appendedExpandCount > 0 ? ` (+ ${appendedExpandCount} expanded)` : ""}${scopeSuffix}`;
154
159
  const footer = page < totalPages
155
160
  ? `\n--- Use page:${page + 1}${scope === "all" ? " with scope:'all'" : ""} for more results ---`
156
161
  : "";
@@ -233,13 +238,10 @@ export function registerRecallTool(pi: ExtensionAPI): void {
233
238
  "Search session history and earlier lines omitted, file write/edit content by text/regex. " +
234
239
  "Expand entries (#N), drill-down file content (#N:path) with paging, or aggregate touched files (mode:touched).",
235
240
  promptSnippet:
236
- "recall: Search session history + file write/edit content by text/regex. #N expand, #N:path drill-down with offset/limit paging, mode:file/transcript/touched.",
241
+ "recall: Search session history + file write/edit content by text/regex. #N expand, #N:path drill-down with optional :offset:limit or :full, mode:file/touched.",
237
242
  promptGuidelines: [
238
- "Use recall — search is literal text/regex matching, NOT semantic. If no results, try different terms or a regex pattern. Set scope:'all' to search the full session.",
239
- "Use recall — mode:file to search only write/edit file content, mode:transcript for conversation-only, mode:touched for aggregate view of all files written/edited across the session.",
240
- "Use recall — drill-down supports paging: #42:auth.ts shows first 30 lines, #42:auth.ts:30 shows next 30, #42:auth.ts:full shows everything. Note: edit diffs are not indexed for text search — drill-down reads them from raw JSONL.",
241
- "Use recall — when a drill-down path matches multiple files, options are listed. Narrow with a more specific path substring.",
242
- "Use recall — hex observation/reflection ids (12-char hex) link memory evidence to session entries with cross-references for navigation.",
243
+ "Use recall — literal text/regex search across session history and file write/edit content. #N expands an entry; #N:path with optional :offset:limit or :full drills down into file content; 12-char hex ids recover observation/reflection sources. mode:file for file-content-only, mode:touched for aggregated files-by-path. scope:'all' to search the full session. If no results, try fewer terms or a regex pattern.",
244
+ "Use recall — when a drill-down path matches multiple files, options are listed. Narrow with a more specific path substring. Only full-file writes are indexed for text search (edit diffs are not).",
243
245
  ],
244
246
  parameters: Type.Object({
245
247
  query: Type.Optional(
@@ -255,7 +257,7 @@ export function registerRecallTool(pi: ExtensionAPI): void {
255
257
  StringEnum(["lineage", "all"] as const, { description: "Search scope. lineage = active lineage (default), all = entire session." }),
256
258
  ),
257
259
  mode: Type.Optional(
258
- StringEnum(["hybrid", "file", "transcript", "touched"] as const, { description: "What content to search. hybrid (default) = transcript + file indicators. file = write/edit file content only. transcript = conversation only. touched = aggregate files grouped by path (not per-entry search)." }),
260
+ StringEnum(["hybrid", "file", "touched"] as const, { description: "What content to search. hybrid (default) = all session content. file = file content only. touched = files-by-path summary with entry indices." }),
259
261
  ),
260
262
  }),
261
263
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {