@oh-my-pi/pi-coding-agent 14.6.1 → 14.6.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +21 -0
  3. package/package.json +23 -7
  4. package/src/cli/grievances-cli.ts +89 -4
  5. package/src/commands/grievances.ts +33 -7
  6. package/src/config/prompt-templates.ts +14 -7
  7. package/src/config/settings-schema.ts +595 -100
  8. package/src/config/settings.ts +46 -0
  9. package/src/discovery/helpers.ts +13 -6
  10. package/src/edit/index.ts +3 -3
  11. package/src/edit/line-hash.ts +73 -25
  12. package/src/edit/modes/hashline.lark +10 -3
  13. package/src/edit/modes/hashline.ts +104 -38
  14. package/src/edit/renderer.ts +3 -3
  15. package/src/hindsight/backend.ts +444 -0
  16. package/src/hindsight/bank.ts +131 -0
  17. package/src/hindsight/client.ts +445 -0
  18. package/src/hindsight/config.ts +165 -0
  19. package/src/hindsight/content.ts +205 -0
  20. package/src/hindsight/index.ts +6 -0
  21. package/src/hindsight/retain-queue.ts +166 -0
  22. package/src/hindsight/transcript.ts +71 -0
  23. package/src/main.ts +7 -10
  24. package/src/memories/index.ts +1 -1
  25. package/src/memory-backend/index.ts +4 -0
  26. package/src/memory-backend/local-backend.ts +30 -0
  27. package/src/memory-backend/off-backend.ts +16 -0
  28. package/src/memory-backend/resolve.ts +24 -0
  29. package/src/memory-backend/types.ts +69 -0
  30. package/src/modes/components/settings-defs.ts +50 -451
  31. package/src/modes/components/settings-selector.ts +4 -2
  32. package/src/modes/components/status-line/presets.ts +1 -1
  33. package/src/modes/components/status-line.ts +4 -1
  34. package/src/modes/controllers/command-controller.ts +6 -5
  35. package/src/modes/controllers/event-controller.ts +12 -0
  36. package/src/modes/controllers/mcp-command-controller.ts +23 -0
  37. package/src/modes/controllers/selector-controller.ts +10 -12
  38. package/src/modes/interactive-mode.ts +3 -2
  39. package/src/modes/theme/theme.ts +4 -0
  40. package/src/prompts/tools/github.md +3 -0
  41. package/src/prompts/tools/hashline.md +20 -16
  42. package/src/prompts/tools/read.md +10 -6
  43. package/src/prompts/tools/recall.md +5 -0
  44. package/src/prompts/tools/reflect.md +5 -0
  45. package/src/prompts/tools/retain.md +5 -0
  46. package/src/prompts/tools/search.md +1 -1
  47. package/src/sdk.ts +12 -9
  48. package/src/session/agent-session.ts +75 -3
  49. package/src/slash-commands/builtin-registry.ts +2 -12
  50. package/src/ssh/connection-manager.ts +1 -1
  51. package/src/tools/ast-edit.ts +14 -5
  52. package/src/tools/ast-grep.ts +12 -3
  53. package/src/tools/find.ts +47 -7
  54. package/src/tools/gh-renderer.ts +10 -1
  55. package/src/tools/gh.ts +233 -5
  56. package/src/tools/hindsight-recall.ts +70 -0
  57. package/src/tools/hindsight-reflect.ts +57 -0
  58. package/src/tools/hindsight-retain.ts +63 -0
  59. package/src/tools/index.ts +17 -0
  60. package/src/tools/output-meta.ts +1 -0
  61. package/src/tools/path-utils.ts +55 -0
  62. package/src/tools/read.ts +1 -1
  63. package/src/tools/search.ts +45 -8
@@ -17,7 +17,7 @@ import { clearClaudePluginRootsCache } from "../../discovery/helpers";
17
17
  import { getGatewayStatus } from "../../eval/py/gateway-coordinator";
18
18
  import { loadCustomShare } from "../../export/custom-share";
19
19
  import type { CompactOptions } from "../../extensibility/extensions/types";
20
- import { buildMemoryToolDeveloperInstructions, clearMemoryData, enqueueMemoryConsolidation } from "../../memories";
20
+ import { resolveMemoryBackend } from "../../memory-backend";
21
21
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
22
  import { BorderedLoader } from "../../modes/components/bordered-loader";
23
23
  import { DynamicBorder } from "../../modes/components/dynamic-border";
@@ -570,11 +570,12 @@ export class CommandController {
570
570
  const argumentText = text.slice(7).trim();
571
571
  const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
572
572
  const agentDir = this.ctx.settings.getAgentDir();
573
+ const backend = resolveMemoryBackend(this.ctx.settings);
573
574
 
574
575
  if (action === "view") {
575
- const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
576
+ const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings);
576
577
  if (!payload) {
577
- this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
578
+ this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
578
579
  return;
579
580
  }
580
581
  this.ctx.chatContainer.addChild(new Spacer(1));
@@ -589,7 +590,7 @@ export class CommandController {
589
590
 
590
591
  if (action === "reset" || action === "clear") {
591
592
  try {
592
- await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
593
+ await backend.clear(agentDir, this.ctx.sessionManager.getCwd());
593
594
  await this.ctx.session.refreshBaseSystemPrompt();
594
595
  this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
595
596
  } catch (error) {
@@ -600,7 +601,7 @@ export class CommandController {
600
601
 
601
602
  if (action === "enqueue" || action === "rebuild") {
602
603
  try {
603
- enqueueMemoryConsolidation(agentDir, this.ctx.sessionManager.getCwd());
604
+ await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd());
604
605
  this.ctx.showStatus("Memory consolidation enqueued.");
605
606
  } catch (error) {
606
607
  this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -53,6 +53,7 @@ export class EventController {
53
53
  todo_reminder: e => this.#handleTodoReminder(e),
54
54
  todo_auto_clear: e => this.#handleTodoAutoClear(e),
55
55
  irc_message: e => this.#handleIrcMessage(e),
56
+ notice: e => this.#handleNotice(e),
56
57
  } satisfies AgentSessionEventHandlers;
57
58
  }
58
59
 
@@ -223,6 +224,17 @@ export class EventController {
223
224
  this.ctx.ui.requestRender();
224
225
  }
225
226
 
227
+ async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
228
+ const message = event.source ? `${event.source}: ${event.message}` : event.message;
229
+ if (event.level === "error") {
230
+ this.ctx.showError(message);
231
+ } else if (event.level === "warning") {
232
+ this.ctx.showWarning(message);
233
+ } else {
234
+ this.ctx.showStatus(message);
235
+ }
236
+ }
237
+
226
238
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
227
239
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
228
240
  this.ctx.streamingMessage = event.message;
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Handles /mcp subcommands for managing MCP servers.
5
5
  */
6
+ import * as path from "node:path";
6
7
  import { Spacer, Text } from "@oh-my-pi/pi-tui";
7
8
  import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils";
8
9
  import type { SourceMeta } from "../../capability/types";
@@ -656,6 +657,28 @@ export class MCPCommandController {
656
657
  if (projectConfig.mcpServers?.[name]) {
657
658
  return { filePath: projectPath, scope: "project", config: projectConfig.mcpServers[name] };
658
659
  }
660
+
661
+ // Check standalone fallback files (mcp.json, .mcp.json) in the project root —
662
+ // these match the discovery paths used by the mcp-json provider. Reads run in
663
+ // parallel (mirroring user/project above) but precedence is preserved by the
664
+ // for-loop's iteration order: mcp.json wins over .mcp.json on a same-name hit.
665
+ const standalonePaths = [path.join(cwd, "mcp.json"), path.join(cwd, ".mcp.json")];
666
+ const fallbackConfigs = await Promise.all(
667
+ standalonePaths.map(async fallbackPath => {
668
+ try {
669
+ return await readMCPConfigFile(fallbackPath);
670
+ } catch {
671
+ // Malformed JSON in a standalone file — skip and continue lookup.
672
+ return null;
673
+ }
674
+ }),
675
+ );
676
+ for (const [index, fallbackConfig] of fallbackConfigs.entries()) {
677
+ const config = fallbackConfig?.mcpServers?.[name];
678
+ if (config) {
679
+ return { filePath: standalonePaths[index]!, scope: "project", config };
680
+ }
681
+ }
659
682
  return null;
660
683
  }
661
684
 
@@ -1,18 +1,15 @@
1
- import * as os from "node:os";
2
- import * as path from "node:path";
3
1
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
2
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
5
3
  import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
6
4
  import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
7
5
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
8
- import { getAgentDbPath, getConfigDirName, getProjectDir } from "@oh-my-pi/pi-utils";
9
- import { invalidate as invalidateFsCache } from "../../capability/fs";
6
+ import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
10
7
  import { getRoleInfo } from "../../config/model-registry";
11
8
  import { formatModelSelectorValue } from "../../config/model-resolver";
12
9
  import { settings } from "../../config/settings";
13
10
  import { DebugSelectorComponent } from "../../debug";
14
11
  import { disableProvider, enableProvider } from "../../discovery";
15
- import { clearClaudePluginRootsCache, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
12
+ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
16
13
  import {
17
14
  getInstalledPluginsRegistryPath,
18
15
  getMarketplacesCacheDir,
@@ -118,6 +115,7 @@ export class SelectorController {
118
115
  rightSegments: settings.get("statusLine.rightSegments"),
119
116
  separator: settings.get("statusLine.separator"),
120
117
  showHookStatus: settings.get("statusLine.showHookStatus"),
118
+ sessionAccent: settings.get("statusLine.sessionAccent"),
121
119
  ...previewSettings,
122
120
  });
123
121
  this.ctx.updateEditorTopBorder();
@@ -140,6 +138,7 @@ export class SelectorController {
140
138
  rightSegments: settings.get("statusLine.rightSegments"),
141
139
  separator: settings.get("statusLine.separator"),
142
140
  showHookStatus: settings.get("statusLine.showHookStatus"),
141
+ sessionAccent: settings.get("statusLine.sessionAccent"),
143
142
  });
144
143
  this.ctx.updateEditorTopBorder();
145
144
  this.ctx.ui.requestRender();
@@ -332,8 +331,12 @@ export class SelectorController {
332
331
  break;
333
332
  }
334
333
  case "statusLinePreset":
334
+ case "statusLine.preset":
335
335
  case "statusLineSeparator":
336
+ case "statusLine.separator":
336
337
  case "statusLineShowHooks":
338
+ case "statusLine.showHookStatus":
339
+ case "statusLine.sessionAccent":
337
340
  case "statusLineSegments":
338
341
  case "statusLineModelThinking":
339
342
  case "statusLinePathAbbreviate":
@@ -351,6 +354,7 @@ export class SelectorController {
351
354
  rightSegments: settings.get("statusLine.rightSegments"),
352
355
  separator: settings.get("statusLine.separator"),
353
356
  showHookStatus: settings.get("statusLine.showHookStatus"),
357
+ sessionAccent: settings.get("statusLine.sessionAccent"),
354
358
  segmentOptions: settings.get("statusLine.segmentOptions"),
355
359
  };
356
360
  this.ctx.statusLine.updateSettings(statusLineSettings);
@@ -444,13 +448,7 @@ export class SelectorController {
444
448
  projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
445
449
  marketplacesCacheDir: getMarketplacesCacheDir(),
446
450
  pluginsCacheDir: getPluginsCacheDir(),
447
- clearPluginRootsCache: (extraPaths?: readonly string[]) => {
448
- const home = os.homedir();
449
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
450
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
451
- for (const p of extraPaths ?? []) invalidateFsCache(p);
452
- clearClaudePluginRootsCache();
453
- },
451
+ clearPluginRootsCache: clearPluginRootsAndCaches,
454
452
  });
455
453
 
456
454
  const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
@@ -28,7 +28,7 @@ import {
28
28
  import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
29
29
  import chalk from "chalk";
30
30
  import { KeybindingsManager } from "../config/keybindings";
31
- import { type Settings, settings } from "../config/settings";
31
+ import { isSettingsInitialized, type Settings, settings } from "../config/settings";
32
32
  import type {
33
33
  ExtensionUIContext,
34
34
  ExtensionUIDialogOptions,
@@ -644,7 +644,8 @@ export class InteractiveMode implements InteractiveModeContext {
644
644
  } else if (this.isPythonMode) {
645
645
  this.editor.borderColor = theme.getPythonModeBorderColor();
646
646
  } else {
647
- const sessionName = this.sessionManager.getSessionName();
647
+ const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
648
+ const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
648
649
  const hex = sessionName ? getSessionAccentHex(sessionName) : undefined;
649
650
  const ansi = getSessionAccentAnsi(hex);
650
651
  if (ansi) {
@@ -186,6 +186,7 @@ export type SymbolKey =
186
186
  | "tab.context"
187
187
  | "tab.editing"
188
188
  | "tab.tools"
189
+ | "tab.memory"
189
190
  | "tab.tasks"
190
191
  | "tab.providers";
191
192
 
@@ -346,6 +347,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
346
347
  "tab.context": "📋",
347
348
  "tab.editing": "💻",
348
349
  "tab.tools": "🔧",
350
+ "tab.memory": "🧠",
349
351
  "tab.tasks": "📦",
350
352
  "tab.providers": "🌐",
351
353
  };
@@ -599,6 +601,7 @@ const NERD_SYMBOLS: SymbolMap = {
599
601
  "tab.context": "󰘸",
600
602
  "tab.editing": "",
601
603
  "tab.tools": "󰠭",
604
+ "tab.memory": "󰧑",
602
605
  "tab.tasks": "󰐱",
603
606
  "tab.providers": "󰖟",
604
607
  };
@@ -757,6 +760,7 @@ const ASCII_SYMBOLS: SymbolMap = {
757
760
  "tab.context": "[X]",
758
761
  "tab.editing": "[E]",
759
762
  "tab.tools": "[T]",
763
+ "tab.memory": "[Y]",
760
764
  "tab.tasks": "[K]",
761
765
  "tab.providers": "[P]",
762
766
  };
@@ -10,6 +10,9 @@ Pick the operation via `op`. Each op uses a subset of the parameters:
10
10
  - `pr_push` — Push a checked-out PR branch back to its source branch. Requires the branch to have been checked out via `op: pr_checkout` (carries push metadata). Optional `branch`; defaults to the current checked-out git branch. Optional `forceWithLease`.
11
11
  - `search_issues` — Search issues using normal GitHub issue search syntax. Required `query`. Optional `repo`, `limit`.
12
12
  - `search_prs` — Search pull requests using normal GitHub PR search syntax. Required `query`. Optional `repo`, `limit`.
13
+ - `search_code` — Search code with GitHub code search syntax. Required `query`. Optional `repo`, `limit`. Returns matching paths with surrounding fragments.
14
+ - `search_commits` — Search commits across GitHub. Required `query`. Optional `repo`, `limit`. Returns short SHA, author, and the first line of each commit message.
15
+ - `search_repos` — Search repositories across GitHub. Required `query`. Optional `limit` (use query qualifiers like `org:`, `language:` instead of `repo`).
13
16
  - `run_watch` — Watch a GitHub Actions workflow run. Optional `run` (id or URL). Omitting `run` watches all workflow runs for the current HEAD commit; `branch` falls back to the current branch. Optional `tail` (log lines per failed job). Streams snapshots, fast-fails on the first detected job failure (with a brief grace period to capture concurrent failures), then fetches tailed logs for the failed jobs. The full failed-job logs are saved as a session artifact for on-demand reads.
14
17
  </instruction>
15
18
 
@@ -8,15 +8,15 @@ This format is purely textual. The tool has NO awareness of language, indentatio
8
8
 
9
9
  <ops>
10
10
  @PATH header: subsequent ops apply to PATH
11
- < ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `|TEXT` lines
12
- + ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `|TEXT` lines
11
+ < ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
12
+ + ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `{{hsep}}TEXT` lines
13
13
  - A..B delete the line range (inclusive); `- A` for one line
14
- = A..B replace the range with payload `|TEXT` lines, or with one blank line if no payload follows
14
+ = A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows
15
15
  </ops>
16
16
 
17
17
  <rules>
18
- - Every line of inserted/replacement content **MUST** be emitted as a payload line starting with `|`.
19
- - `|` is syntax, not content. The inserted text begins after the first `|`; use a bare `|` to insert a blank line.
18
+ - Every line of inserted/replacement content **MUST** be emitted as a payload line starting with `{{hsep}}`.
19
+ - `{{hsep}}` is syntax, not content. The inserted text begins after the first `{{hsep}}`; use a bare `{{hsep}}` to insert a blank line.
20
20
  - `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
21
21
  - `= A..B` replaces the inclusive range with the following payload lines. `= A` (or `= A..B`) with no payload blanks the range to a single empty line.
22
22
  - `- A..B` deletes the inclusive range; omit `..B` for one line.
@@ -35,30 +35,34 @@ This format is purely textual. The tool has NO awareness of language, indentatio
35
35
  # Replace one line (preserve the leading tab from the original)
36
36
  @a.ts
37
37
  = {{hrefr 5}}
38
- | return clean.trim().toUpperCase();
38
+ {{hsep}} return clean.trim().toUpperCase();
39
39
 
40
40
  # Replace a contiguous range with multiple lines
41
41
  @a.ts
42
42
  = {{hrefr 3}}..{{hrefr 6}}
43
- |export function label(name: string): string {
44
- | const clean = (name || DEF).trim();
45
- | return clean.length === 0 ? DEF : clean.toUpperCase();
46
- |}
43
+ {{hsep}}export function label(name: string): string {
44
+ {{hsep}} const clean = (name || DEF).trim();
45
+ {{hsep}} return clean.length === 0 ? DEF : clean.toUpperCase();
46
+ {{hsep}}}
47
47
 
48
48
  # Insert BEFORE a line
49
49
  @a.ts
50
50
  < {{hrefr 5}}
51
- | const debug = false;
51
+ {{hsep}} const debug = false;
52
52
 
53
53
  # Insert AFTER a line
54
54
  @a.ts
55
55
  + {{hrefr 4}}
56
- | if (clean.length === 0) return DEF;
56
+ {{hsep}} if (clean.length === 0) return DEF;
57
+
58
+ # Append WITHIN a line
59
+ @a.ts
60
+ + {{hrefr 4}}{{hsep}} // first run
57
61
 
58
62
  # Append to end of file
59
63
  @a.ts
60
64
  + EOF
61
- |export const done = true;
65
+ {{hsep}}export const done = true;
62
66
 
63
67
  # Delete a single line
64
68
  @a.ts
@@ -70,9 +74,9 @@ This format is purely textual. The tool has NO awareness of language, indentatio
70
74
  </examples>
71
75
 
72
76
  <critical>
73
- - Always copy anchors exactly from tool output, but **NEVER** include line content after the `|` separator in the op line.
77
+ - Always copy anchors exactly from tool output, but **NEVER** include line content after the `{{hsep}}` separator in the op line.
74
78
  - Only emit changed lines. Do not restate unchanged context as payload.
75
- - Every inserted/replacement content line **MUST** start with `|`; raw content lines are invalid.
79
+ - Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
76
80
  - Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
77
- - To replace a block, use one `= A..B` op followed by all replacement `|TEXT` payload lines.
81
+ - To replace a block, use one `= A..B` op followed by all replacement `{{hsep}}TEXT` payload lines.
78
82
  </critical>
@@ -14,7 +14,7 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
14
14
 
15
15
  |`sel` value|Behavior|
16
16
  |---|---|
17
- |*(omitted)*|Read full file (up to {{DEFAULT_LIMIT}} lines)|
17
+ |_(omitted)_|Read full file (up to {{DEFAULT_LIMIT}} lines)|
18
18
  |`50`|Read from line 50 onward|
19
19
  |`50-200`|Read lines 50-200|
20
20
  |`50+150`|Read 150 lines starting at line 50|
@@ -22,21 +22,24 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
22
22
 
23
23
  # Filesystem
24
24
  - Reading a directory path returns a list of dirents.
25
- {{#if IS_HASHLINE_MODE}}
25
+ {{#if IS_HL_MODE}}
26
26
  - Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():`
27
- {{else}}
28
- {{#if IS_LINE_NUMBER_MODE}}
27
+ {{else}}
28
+ {{#if IS_LINE_NUMBER_MODE}}
29
29
  - Reading a file returns lines prefixed with line numbers: `41|def alpha():`
30
- {{/if}}
31
- {{/if}}
30
+ {{/if}}
31
+ {{/if}}
32
32
 
33
33
  # Inspection
34
+
34
35
  Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Can inspect images.
35
36
 
36
37
  # Directories & Archives
38
+
37
39
  Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents.
38
40
 
39
41
  # SQLite Databases
42
+
40
43
  For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
41
44
  - `file.db` — list tables with row counts
42
45
  - `file.db:table` — schema + sample rows
@@ -46,6 +49,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
46
49
  - `file.db?q=SELECT …` — read-only SELECT query
47
50
 
48
51
  # URLs
52
+
49
53
  Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
50
54
  </instruction>
51
55
 
@@ -0,0 +1,5 @@
1
+ Search long-term memory for relevant information. Returns raw matching entries ranked by relevance.
2
+
3
+ Use proactively — before answering questions about past conversations, user preferences, project decisions, or any topic where prior context would help accuracy. When in doubt, recall first.
4
+
5
+ Prefer `recall` when you need specific facts or entries. Use `reflect` instead when you need a synthesised answer across many memories.
@@ -0,0 +1,5 @@
1
+ Generate a synthesised answer by reasoning over long-term memory. Unlike `recall` (which returns raw entries), `reflect` blends relevant memories into a single coherent response.
2
+
3
+ Use for open-ended questions that span many stored facts: "What do you know about this user?", "Summarize project decisions.", "What are my preferences for X?"
4
+
5
+ Provide an optional `context` to focus the synthesis on a specific angle or sub-topic.
@@ -0,0 +1,5 @@
1
+ Store one or more facts in long-term memory for future sessions.
2
+
3
+ Use for durable, reusable knowledge: user preferences, project decisions, architectural choices, and anything that would improve future responses if recalled. Ephemeral task state does not belong here.
4
+
5
+ Each item must be specific and self-contained — include who, what, when, and why. Batch related facts in a single call; they are deduplicated and consolidated together.
@@ -7,7 +7,7 @@ Searches files using powerful regex matching.
7
7
  </instruction>
8
8
 
9
9
  <output>
10
- {{#if IS_HASHLINE_MODE}}
10
+ {{#if IS_HL_MODE}}
11
11
  - Text output is anchor-prefixed: `*5th|content` (match) or ` 9x}|content` (context, leading space). The 2-char suffix is a content fingerprint.
12
12
  {{else}}
13
13
  {{#if IS_LINE_NUMBER_MODE}}
package/src/sdk.ts CHANGED
@@ -82,7 +82,8 @@ import {
82
82
  selectDiscoverableMCPToolNamesByServer,
83
83
  summarizeDiscoverableMCPTools,
84
84
  } from "./mcp/discoverable-tool-metadata";
85
- import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
85
+ import { getMemoryRoot } from "./memories";
86
+ import { resolveMemoryBackend } from "./memory-backend";
86
87
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
87
88
  import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
88
89
  import {
@@ -1334,7 +1335,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1334
1335
  const promptTools = buildSystemPromptToolMetadata(tools, {
1335
1336
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1336
1337
  });
1337
- const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1338
+ const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(agentDir, settings);
1338
1339
 
1339
1340
  // Build combined append prompt: memory instructions + MCP server instructions
1340
1341
  const serverInstructions = mcpManager?.getServerInstructions();
@@ -1747,13 +1748,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1747
1748
  }
1748
1749
 
1749
1750
  logger.time("startMemoryStartupTask", () =>
1750
- startMemoryStartupTask({
1751
- session,
1752
- settings,
1753
- modelRegistry,
1754
- agentDir,
1755
- taskDepth,
1756
- }),
1751
+ Promise.resolve(
1752
+ resolveMemoryBackend(settings).start({
1753
+ session,
1754
+ settings,
1755
+ modelRegistry,
1756
+ agentDir,
1757
+ taskDepth,
1758
+ }),
1759
+ ),
1757
1760
  );
1758
1761
 
1759
1762
  // Wire MCP manager callbacks to session for reactive tool updates.
@@ -111,6 +111,7 @@ import {
111
111
  isMCPToolName,
112
112
  selectDiscoverableMCPToolNamesByServer,
113
113
  } from "../mcp/discoverable-tool-metadata";
114
+ import { resolveMemoryBackend } from "../memory-backend";
114
115
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
115
116
  import type { PlanModeState } from "../plan-mode/state";
116
117
  import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
@@ -192,7 +193,8 @@ export type AgentSessionEvent =
192
193
  | { type: "ttsr_triggered"; rules: Rule[] }
193
194
  | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
194
195
  | { type: "todo_auto_clear" }
195
- | { type: "irc_message"; message: CustomMessage };
196
+ | { type: "irc_message"; message: CustomMessage }
197
+ | { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string };
196
198
 
197
199
  /** Listener function for agent session events */
198
200
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
@@ -742,6 +744,19 @@ export class AgentSession {
742
744
  }
743
745
  }
744
746
 
747
+ /**
748
+ * Emit a UI-only notice to the session. Surfaces in interactive mode as a
749
+ * `showWarning` / `showError` / `showStatus` line; non-interactive modes
750
+ * receive the event through the normal subscribe stream.
751
+ *
752
+ * Notices are NOT added to agent state and never reach the LLM — use this
753
+ * for out-of-band conditions the user should see but the model shouldn't
754
+ * react to (e.g. background queue flush failures).
755
+ */
756
+ emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void {
757
+ this.#emit({ type: "notice", level, message, source });
758
+ }
759
+
745
760
  #queuedExtensionEvents: Promise<void> = Promise.resolve();
746
761
 
747
762
  #queueExtensionEvent(event: AgentSessionEvent): Promise<void> {
@@ -2289,6 +2304,23 @@ export class AgentSession {
2289
2304
  this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
2290
2305
  }
2291
2306
 
2307
+ async #buildSystemPromptForAgentStart(promptText: string): Promise<string> {
2308
+ const backend = resolveMemoryBackend(this.settings);
2309
+ if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
2310
+
2311
+ try {
2312
+ const injected = await backend.beforeAgentStartPrompt(this, promptText);
2313
+ if (!injected) return this.#baseSystemPrompt;
2314
+ return `${this.#baseSystemPrompt}\n\n${injected}`;
2315
+ } catch (err) {
2316
+ logger.debug("Memory backend beforeAgentStartPrompt failed", {
2317
+ backend: backend.id,
2318
+ error: String(err),
2319
+ });
2320
+ return this.#baseSystemPrompt;
2321
+ }
2322
+ }
2323
+
2292
2324
  /**
2293
2325
  * Compose a stable signature for the inputs that `rebuildSystemPrompt` reads.
2294
2326
  * Two calls producing identical signatures are guaranteed to produce identical
@@ -2908,12 +2940,14 @@ export class AgentSession {
2908
2940
  messages.push(...fileMentionMessages);
2909
2941
  }
2910
2942
 
2943
+ const beforeAgentStartSystemPrompt = await this.#buildSystemPromptForAgentStart(expandedText);
2944
+
2911
2945
  // Emit before_agent_start extension event
2912
2946
  if (this.#extensionRunner) {
2913
2947
  const result = await this.#extensionRunner.emitBeforeAgentStart(
2914
2948
  expandedText,
2915
2949
  options?.images,
2916
- this.#baseSystemPrompt,
2950
+ beforeAgentStartSystemPrompt,
2917
2951
  );
2918
2952
  if (result?.messages) {
2919
2953
  const promptAttribution: "user" | "agent" | undefined =
@@ -2934,8 +2968,10 @@ export class AgentSession {
2934
2968
  if (result?.systemPrompt !== undefined) {
2935
2969
  this.agent.setSystemPrompt(result.systemPrompt);
2936
2970
  } else {
2937
- this.agent.setSystemPrompt(this.#baseSystemPrompt);
2971
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
2938
2972
  }
2973
+ } else {
2974
+ this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
2939
2975
  }
2940
2976
 
2941
2977
  // Bail out if a newer abort/prompt cycle has started since we began setup
@@ -4118,6 +4154,11 @@ export class AgentSession {
4118
4154
  preserveData = result?.preserveData;
4119
4155
  }
4120
4156
 
4157
+ const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
4158
+ if (memoryBackendContext) {
4159
+ hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
4160
+ }
4161
+
4121
4162
  let summary: string;
4122
4163
  let shortSummary: string | undefined;
4123
4164
  let firstKeptEntryId: string;
@@ -4204,6 +4245,32 @@ export class AgentSession {
4204
4245
  }
4205
4246
  }
4206
4247
 
4248
+ /**
4249
+ * Ask the active memory backend for an extra-context block to splice into
4250
+ * the compaction summary prompt. Both the manual and auto compaction paths
4251
+ * funnel through this helper so the behaviour stays identical.
4252
+ *
4253
+ * Failures are swallowed: a memory backend going sideways MUST NOT block
4254
+ * compaction (which is itself the recovery path for context overflow).
4255
+ */
4256
+ async #collectMemoryBackendContext(preparation: {
4257
+ messagesToSummarize: AgentMessage[];
4258
+ turnPrefixMessages: AgentMessage[];
4259
+ }): Promise<string | undefined> {
4260
+ const backend = resolveMemoryBackend(this.settings);
4261
+ if (!backend.preCompactionContext) return undefined;
4262
+ const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
4263
+ try {
4264
+ return await backend.preCompactionContext(messages, this.settings);
4265
+ } catch (err) {
4266
+ logger.debug("Memory backend preCompactionContext failed", {
4267
+ backend: backend.id,
4268
+ error: String(err),
4269
+ });
4270
+ return undefined;
4271
+ }
4272
+ }
4273
+
4207
4274
  /**
4208
4275
  * Cancel in-progress context maintenance (manual compaction, auto-compaction, or auto-handoff).
4209
4276
  */
@@ -5190,6 +5257,11 @@ export class AgentSession {
5190
5257
  preserveData = result?.preserveData;
5191
5258
  }
5192
5259
 
5260
+ const memoryBackendContext = await this.#collectMemoryBackendContext(preparation);
5261
+ if (memoryBackendContext) {
5262
+ hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
5263
+ }
5264
+
5193
5265
  let summary: string;
5194
5266
  let shortSummary: string | undefined;
5195
5267
  let firstKeptEntryId: string;
@@ -1,13 +1,7 @@
1
- import * as os from "node:os";
2
- import * as path from "node:path";
3
-
4
1
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
5
- import { getConfigDirName } from "@oh-my-pi/pi-utils";
6
- import { invalidate as invalidateFsCache } from "../capability/fs";
7
2
  import type { SettingPath, SettingValue } from "../config/settings";
8
3
  import { settings } from "../config/settings";
9
4
  import {
10
- clearClaudePluginRootsCache,
11
5
  clearPluginRootsAndCaches,
12
6
  resolveActiveProjectRegistryPath,
13
7
  resolveOrDefaultProjectRegistryPath,
@@ -942,14 +936,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
942
936
  name: "reload-plugins",
943
937
  description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)",
944
938
  handle: async (_command, runtime) => {
945
- // Invalidate the fs content cache for all registry files so
939
+ // Invalidate registry fs caches and the plugin roots cache so
946
940
  // listClaudePluginRoots re-reads from disk on next access.
947
- const home = os.homedir();
948
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
949
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
950
941
  const projectPath = await resolveActiveProjectRegistryPath(runtime.ctx.sessionManager.getCwd());
951
- if (projectPath) invalidateFsCache(projectPath);
952
- clearClaudePluginRootsCache();
942
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
953
943
  await runtime.ctx.refreshSlashCommandState();
954
944
  runtime.ctx.showStatus("Plugins reloaded.");
955
945
  runtime.ctx.editor.setText("");
@@ -25,7 +25,7 @@ export interface SSHHostInfo {
25
25
  }
26
26
 
27
27
  const CONTROL_DIR = getSshControlDir();
28
- const CONTROL_PATH = path.join(CONTROL_DIR, "%h.sock");
28
+ const CONTROL_PATH = path.join(CONTROL_DIR, "%C.sock");
29
29
  const HOST_INFO_DIR = getRemoteHostDir();
30
30
  const HOST_INFO_VERSION = 2;
31
31