@oh-my-pi/pi-coding-agent 14.6.2 → 14.6.4

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 (62) hide show
  1. package/CHANGELOG.md +95 -2
  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 +610 -100
  8. package/src/config/settings.ts +42 -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 +295 -40
  14. package/src/edit/renderer.ts +3 -3
  15. package/src/hindsight/backend.ts +205 -0
  16. package/src/hindsight/bank.ts +131 -0
  17. package/src/hindsight/client.ts +598 -0
  18. package/src/hindsight/config.ts +175 -0
  19. package/src/hindsight/content.ts +210 -0
  20. package/src/hindsight/index.ts +8 -0
  21. package/src/hindsight/mental-models.ts +382 -0
  22. package/src/hindsight/seeds.json +32 -0
  23. package/src/hindsight/state.ts +469 -0
  24. package/src/hindsight/transcript.ts +71 -0
  25. package/src/main.ts +7 -10
  26. package/src/memories/index.ts +1 -1
  27. package/src/memory-backend/index.ts +4 -0
  28. package/src/memory-backend/local-backend.ts +30 -0
  29. package/src/memory-backend/off-backend.ts +16 -0
  30. package/src/memory-backend/resolve.ts +24 -0
  31. package/src/memory-backend/types.ts +79 -0
  32. package/src/modes/components/settings-defs.ts +50 -451
  33. package/src/modes/components/settings-selector.ts +2 -2
  34. package/src/modes/components/status-line/presets.ts +1 -1
  35. package/src/modes/controllers/command-controller.ts +266 -6
  36. package/src/modes/controllers/event-controller.ts +12 -0
  37. package/src/modes/controllers/selector-controller.ts +3 -12
  38. package/src/modes/theme/theme.ts +4 -0
  39. package/src/prompts/tools/github.md +3 -0
  40. package/src/prompts/tools/hashline.md +21 -16
  41. package/src/prompts/tools/read.md +10 -6
  42. package/src/prompts/tools/recall.md +5 -0
  43. package/src/prompts/tools/reflect.md +5 -0
  44. package/src/prompts/tools/retain.md +5 -0
  45. package/src/prompts/tools/search.md +1 -1
  46. package/src/sdk.ts +21 -9
  47. package/src/session/agent-session.ts +118 -3
  48. package/src/slash-commands/builtin-registry.ts +12 -12
  49. package/src/task/executor.ts +3 -0
  50. package/src/task/index.ts +2 -0
  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 +68 -0
  57. package/src/tools/hindsight-reflect.ts +55 -0
  58. package/src/tools/hindsight-retain.ts +60 -0
  59. package/src/tools/index.ts +20 -0
  60. package/src/tools/path-utils.ts +55 -0
  61. package/src/tools/read.ts +1 -1
  62. package/src/tools/search.ts +45 -8
@@ -17,7 +17,16 @@ 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 {
21
+ diffMentalModelContent,
22
+ type HindsightApi,
23
+ type HindsightSessionState,
24
+ loadHindsightConfig,
25
+ reloadMentalModelsForSession,
26
+ resolveSeedsForScope,
27
+ summarizeMentalModel,
28
+ } from "../../hindsight";
29
+ import { resolveMemoryBackend } from "../../memory-backend";
21
30
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
31
  import { BorderedLoader } from "../../modes/components/bordered-loader";
23
32
  import { DynamicBorder } from "../../modes/components/dynamic-border";
@@ -570,11 +579,12 @@ export class CommandController {
570
579
  const argumentText = text.slice(7).trim();
571
580
  const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
572
581
  const agentDir = this.ctx.settings.getAgentDir();
582
+ const backend = resolveMemoryBackend(this.ctx.settings);
573
583
 
574
584
  if (action === "view") {
575
- const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
585
+ const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
576
586
  if (!payload) {
577
- this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
587
+ this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
578
588
  return;
579
589
  }
580
590
  this.ctx.chatContainer.addChild(new Spacer(1));
@@ -589,7 +599,7 @@ export class CommandController {
589
599
 
590
600
  if (action === "reset" || action === "clear") {
591
601
  try {
592
- await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
602
+ await backend.clear(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
593
603
  await this.ctx.session.refreshBaseSystemPrompt();
594
604
  this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
595
605
  } catch (error) {
@@ -600,7 +610,7 @@ export class CommandController {
600
610
 
601
611
  if (action === "enqueue" || action === "rebuild") {
602
612
  try {
603
- enqueueMemoryConsolidation(agentDir, this.ctx.sessionManager.getCwd());
613
+ await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
604
614
  this.ctx.showStatus("Memory consolidation enqueued.");
605
615
  } catch (error) {
606
616
  this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -608,7 +618,257 @@ export class CommandController {
608
618
  return;
609
619
  }
610
620
 
611
- this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
621
+ if (action === "mm") {
622
+ await this.#handleMentalModelsSubcommand(argumentText);
623
+ return;
624
+ }
625
+
626
+ this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild|mm ...>");
627
+ }
628
+
629
+ async #handleMentalModelsSubcommand(argumentText: string): Promise<void> {
630
+ // Parse: "mm <verb> [arg]"
631
+ const parts = argumentText.split(/\s+/).slice(1);
632
+ const verb = parts[0]?.toLowerCase() ?? "list";
633
+ const arg = parts[1];
634
+
635
+ const state = this.ctx.session.getHindsightSessionState();
636
+ const primary = state && !state.aliasOf ? state : undefined;
637
+ if (!primary) {
638
+ this.ctx.showError("Hindsight backend is not active for this session.");
639
+ return;
640
+ }
641
+ if (!primary.config.mentalModelsEnabled) {
642
+ this.ctx.showError("Mental models are disabled (hindsight.mentalModelsEnabled = false).");
643
+ return;
644
+ }
645
+
646
+ switch (verb) {
647
+ case "list":
648
+ await this.#mmList(primary);
649
+ return;
650
+ case "show":
651
+ if (!arg) return this.ctx.showError("Usage: /memory mm show <id>");
652
+ await this.#mmShow(primary, arg);
653
+ return;
654
+ case "refresh":
655
+ await this.#mmRefresh(primary, arg);
656
+ return;
657
+ case "history":
658
+ if (!arg) return this.ctx.showError("Usage: /memory mm history <id>");
659
+ await this.#mmHistory(primary, arg);
660
+ return;
661
+ case "seed":
662
+ await this.#mmSeed(primary);
663
+ return;
664
+ case "reload":
665
+ await this.#mmReload(primary);
666
+ return;
667
+ case "delete":
668
+ case "remove":
669
+ if (!arg) return this.ctx.showError("Usage: /memory mm delete <id>");
670
+ await this.#mmDelete(primary, arg);
671
+ return;
672
+ default:
673
+ this.ctx.showError("Usage: /memory mm <list|show|refresh|history|seed|reload|delete>");
674
+ }
675
+ }
676
+
677
+ async #mmList(state: HindsightSessionState): Promise<void> {
678
+ const client: HindsightApi = state.client;
679
+ try {
680
+ const response = await client.listMentalModels(state.bankId, { detail: "metadata" });
681
+ const items = response.items ?? [];
682
+ if (items.length === 0) {
683
+ this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
684
+ return;
685
+ }
686
+ const lines = items
687
+ .slice()
688
+ .sort((a, b) => a.id.localeCompare(b.id))
689
+ .map(summarizeMentalModel);
690
+ showMarkdownPanel(this.ctx, `Mental Models — ${state.bankId}`, lines.join("\n"));
691
+ } catch (error) {
692
+ this.ctx.showError(`mm list failed: ${error instanceof Error ? error.message : String(error)}`);
693
+ }
694
+ }
695
+
696
+ async #mmShow(state: HindsightSessionState, id: string): Promise<void> {
697
+ try {
698
+ const model = await state.client.getMentalModel(state.bankId, id, { detail: "content" });
699
+ if (!model) {
700
+ this.ctx.showError(`Mental model not found: ${id}`);
701
+ return;
702
+ }
703
+ const tags = model.tags && model.tags.length > 0 ? `\n_tags: ${model.tags.join(", ")}_` : "";
704
+ const refreshed = model.last_refreshed_at ? `\n_last refreshed: ${model.last_refreshed_at}_` : "";
705
+ const sourceQuery = model.source_query ? `\n\n**Source query:** ${model.source_query}` : "";
706
+ const content = (model.content ?? "_(empty — background reflect may still be running)_").trim();
707
+ showMarkdownPanel(
708
+ this.ctx,
709
+ model.name,
710
+ `**id:** \`${model.id}\`${tags}${refreshed}${sourceQuery}\n\n${content}`,
711
+ );
712
+ } catch (error) {
713
+ this.ctx.showError(`mm show failed: ${error instanceof Error ? error.message : String(error)}`);
714
+ }
715
+ }
716
+
717
+ async #mmRefresh(state: HindsightSessionState, id: string | undefined): Promise<void> {
718
+ try {
719
+ if (id) {
720
+ // Single-model refresh is explicit operator intent: bypass the
721
+ // auto-refresh filter so curated/manual models can still be
722
+ // refreshed on demand.
723
+ await state.client.refreshMentalModel(state.bankId, id);
724
+ this.ctx.showStatus(`Refresh queued for mental model ${id}.`);
725
+ } else {
726
+ // Bulk refresh: only touch models that opted into automatic
727
+ // refresh via `trigger.refresh_after_consolidation`. Curated
728
+ // models are reviewed before publishing and must not be
729
+ // silently regenerated by a bank-wide refresh sweep. Reading
730
+ // `detail: "content"` here is required because the trigger
731
+ // field is excluded from `detail: "metadata"`.
732
+ const list = await state.client.listMentalModels(state.bankId, { detail: "content" });
733
+ const items = list.items ?? [];
734
+ if (items.length === 0) {
735
+ this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
736
+ return;
737
+ }
738
+ const targets = items.filter(m => m.trigger?.refresh_after_consolidation === true);
739
+ const skipped = items.length - targets.length;
740
+ if (targets.length === 0) {
741
+ this.ctx.showStatus(
742
+ `No mental models opted into auto-refresh; ${skipped} curated model(s) left untouched. Pass an explicit id to refresh one of them.`,
743
+ );
744
+ return;
745
+ }
746
+ let queued = 0;
747
+ for (const item of targets) {
748
+ try {
749
+ await state.client.refreshMentalModel(state.bankId, item.id);
750
+ queued++;
751
+ } catch (error) {
752
+ this.ctx.showWarning(
753
+ `Refresh failed for ${item.id}: ${error instanceof Error ? error.message : String(error)}`,
754
+ );
755
+ }
756
+ }
757
+ const skippedSuffix = skipped > 0 ? `; skipped ${skipped} curated model(s)` : "";
758
+ this.ctx.showStatus(
759
+ `Refresh queued for ${queued}/${targets.length} auto-refresh model(s)${skippedSuffix}.`,
760
+ );
761
+ }
762
+ // Reload the cache after a brief grace so the new content (if the refresh
763
+ // completes synchronously on the server) flows into the system prompt.
764
+ await Bun.sleep(500);
765
+ await reloadMentalModelsForSession(state.session);
766
+ } catch (error) {
767
+ this.ctx.showError(`mm refresh failed: ${error instanceof Error ? error.message : String(error)}`);
768
+ }
769
+ }
770
+
771
+ async #mmHistory(state: HindsightSessionState, id: string): Promise<void> {
772
+ try {
773
+ const [model, history] = await Promise.all([
774
+ state.client.getMentalModel(state.bankId, id, { detail: "content" }),
775
+ state.client.getMentalModelHistory(state.bankId, id),
776
+ ]);
777
+ if (!model) {
778
+ this.ctx.showError(`Mental model not found: ${id}`);
779
+ return;
780
+ }
781
+ if (history.length === 0) {
782
+ this.ctx.showStatus(`No history recorded for ${id}.`);
783
+ return;
784
+ }
785
+ // History is most-recent first. Each entry stores the content BEFORE that
786
+ // change. To diff "what changed at entry N", compare entry N's
787
+ // previous_content (= state before that change) with entry N-1's
788
+ // previous_content (= state after that change, which was state before
789
+ // the next change). For the most recent change, compare against the
790
+ // model's CURRENT content.
791
+ const sections: string[] = [];
792
+ for (let i = 0; i < history.length; i++) {
793
+ const before = history[i].previous_content ?? "";
794
+ const after = i === 0 ? (model.content ?? "") : (history[i - 1].previous_content ?? "");
795
+ const diff = diffMentalModelContent(before, after);
796
+ sections.push(`### ${history[i].changed_at}\n\n\`\`\`diff\n${diff}\n\`\`\``);
797
+ }
798
+ showMarkdownPanel(this.ctx, `History — ${model.name}`, sections.join("\n\n"));
799
+ } catch (error) {
800
+ this.ctx.showError(`mm history failed: ${error instanceof Error ? error.message : String(error)}`);
801
+ }
802
+ }
803
+
804
+ async #mmSeed(state: HindsightSessionState): Promise<void> {
805
+ try {
806
+ const config = loadHindsightConfig(this.ctx.settings);
807
+ const seeds = resolveSeedsForScope(
808
+ {
809
+ bankId: state.bankId,
810
+ retainTags: state.retainTags,
811
+ recallTags: state.recallTags,
812
+ recallTagsMatch: state.recallTagsMatch,
813
+ },
814
+ config.scoping,
815
+ );
816
+ if (seeds.length === 0) {
817
+ this.ctx.showStatus(`No built-in seeds apply to scoping=${config.scoping}.`);
818
+ return;
819
+ }
820
+ const list = await state.client.listMentalModels(state.bankId, { detail: "metadata" });
821
+ const existing = new Set((list.items ?? []).map(m => m.id));
822
+ let created = 0;
823
+ let skipped = 0;
824
+ for (const seed of seeds) {
825
+ if (existing.has(seed.id)) {
826
+ skipped++;
827
+ continue;
828
+ }
829
+ try {
830
+ await state.client.createMentalModel(state.bankId, seed.name, seed.sourceQuery, {
831
+ id: seed.id,
832
+ tags: seed.tags.length > 0 ? seed.tags : undefined,
833
+ maxTokens: seed.maxTokens,
834
+ trigger: seed.trigger,
835
+ });
836
+ created++;
837
+ } catch (error) {
838
+ this.ctx.showWarning(
839
+ `Seed failed for ${seed.id}: ${error instanceof Error ? error.message : String(error)}`,
840
+ );
841
+ }
842
+ }
843
+ this.ctx.showStatus(`Seeded ${created} new mental model(s); ${skipped} already present.`);
844
+ } catch (error) {
845
+ this.ctx.showError(`mm seed failed: ${error instanceof Error ? error.message : String(error)}`);
846
+ }
847
+ }
848
+
849
+ async #mmReload(state: HindsightSessionState): Promise<void> {
850
+ const ok = await reloadMentalModelsForSession(state.session);
851
+ if (ok) {
852
+ this.ctx.showStatus("Mental-model cache reloaded.");
853
+ } else {
854
+ this.ctx.showError("Reload failed (Hindsight backend not active or mental models disabled).");
855
+ }
856
+ }
857
+
858
+ async #mmDelete(state: HindsightSessionState, id: string): Promise<void> {
859
+ try {
860
+ const removed = await state.client.deleteMentalModel(state.bankId, id);
861
+ if (!removed) {
862
+ this.ctx.showError(`Mental model not found: ${id}`);
863
+ return;
864
+ }
865
+ // Drop the cached snippet so the closing tag does not silently keep
866
+ // stale content in the system prompt until the next agent_end TTL.
867
+ await reloadMentalModelsForSession(state.session);
868
+ this.ctx.showStatus(`Deleted mental model ${id} from bank ${state.bankId}.`);
869
+ } catch (error) {
870
+ this.ctx.showError(`mm delete failed: ${error instanceof Error ? error.message : String(error)}`);
871
+ }
612
872
  }
613
873
 
614
874
  async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
@@ -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;
@@ -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,
@@ -451,13 +448,7 @@ export class SelectorController {
451
448
  projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
452
449
  marketplacesCacheDir: getMarketplacesCacheDir(),
453
450
  pluginsCacheDir: getPluginsCacheDir(),
454
- clearPluginRootsCache: (extraPaths?: readonly string[]) => {
455
- const home = os.homedir();
456
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
457
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
458
- for (const p of extraPaths ?? []) invalidateFsCache(p);
459
- clearClaudePluginRootsCache();
460
- },
451
+ clearPluginRootsCache: clearPluginRootsAndCaches,
461
452
  });
462
453
 
463
454
  const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
@@ -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,10 @@ 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.
82
+ - `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
78
83
  </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
@@ -61,6 +61,7 @@ import {
61
61
  } from "./extensibility/extensions";
62
62
  import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./extensibility/skills";
63
63
  import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
64
+ import type { HindsightSessionState } from "./hindsight/state";
64
65
  import {
65
66
  AgentProtocolHandler,
66
67
  ArtifactProtocolHandler,
@@ -82,7 +83,8 @@ import {
82
83
  selectDiscoverableMCPToolNamesByServer,
83
84
  summarizeDiscoverableMCPTools,
84
85
  } from "./mcp/discoverable-tool-metadata";
85
- import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
86
+ import { getMemoryRoot } from "./memories";
87
+ import { resolveMemoryBackend } from "./memory-backend";
86
88
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
87
89
  import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
88
90
  import {
@@ -215,6 +217,8 @@ export interface CreateAgentSessionOptions {
215
217
  requireYieldTool?: boolean;
216
218
  /** Task recursion depth (for subagent sessions). Default: 0 */
217
219
  taskDepth?: number;
220
+ /** Parent Hindsight state to alias for subagent memory tools. */
221
+ parentHindsightSessionState?: HindsightSessionState;
218
222
  /** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
219
223
  agentId?: string;
220
224
  /** Display name for the agent in IRC. Default: "main" or "sub". */
@@ -967,6 +971,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
967
971
  trackEvalExecution: (execution, abortController) =>
968
972
  session ? session.trackEvalExecution(execution, abortController) : execution,
969
973
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
974
+ getHindsightSessionState: () => session?.getHindsightSessionState(),
970
975
  getAgentId: () => resolvedAgentId,
971
976
  getToolByName: name => session?.getToolByName(name),
972
977
  agentRegistry,
@@ -1334,7 +1339,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1334
1339
  const promptTools = buildSystemPromptToolMetadata(tools, {
1335
1340
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1336
1341
  });
1337
- const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1342
+ const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
1343
+ agentDir,
1344
+ settings,
1345
+ session,
1346
+ );
1338
1347
 
1339
1348
  // Build combined append prompt: memory instructions + MCP server instructions
1340
1349
  const serverInstructions = mcpManager?.getServerInstructions();
@@ -1747,13 +1756,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1747
1756
  }
1748
1757
 
1749
1758
  logger.time("startMemoryStartupTask", () =>
1750
- startMemoryStartupTask({
1751
- session,
1752
- settings,
1753
- modelRegistry,
1754
- agentDir,
1755
- taskDepth,
1756
- }),
1759
+ Promise.resolve(
1760
+ resolveMemoryBackend(settings).start({
1761
+ session,
1762
+ settings,
1763
+ modelRegistry,
1764
+ agentDir,
1765
+ taskDepth,
1766
+ parentHindsightSessionState: options.parentHindsightSessionState,
1767
+ }),
1768
+ ),
1757
1769
  );
1758
1770
 
1759
1771
  // Wire MCP manager callbacks to session for reactive tool updates.