@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.2

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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. package/src/utils/prompt-format.ts +0 -170
@@ -73,7 +73,7 @@ export class AgentStorage {
73
73
  * AuthCredentialStore handles auth_credentials and cache tables.
74
74
  */
75
75
  #initializeSchema(): void {
76
- this.#db.exec(`
76
+ this.#db.run(`
77
77
  PRAGMA journal_mode=WAL;
78
78
  PRAGMA synchronous=NORMAL;
79
79
  PRAGMA busy_timeout=5000;
@@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
92
92
  const hasValue = settingsInfo.some(column => column.name === "value");
93
93
 
94
94
  if (!hasSettingsTable) {
95
- this.#db.exec(`
95
+ this.#db.run(`
96
96
  CREATE TABLE settings (
97
97
  key TEXT PRIMARY KEY,
98
98
  value TEXT NOT NULL,
@@ -117,8 +117,8 @@ CREATE TABLE settings (
117
117
  }
118
118
 
119
119
  const migrate = this.#db.transaction((settings: Record<string, unknown> | null) => {
120
- this.#db.exec("DROP TABLE settings");
121
- this.#db.exec(`
120
+ this.#db.run("DROP TABLE settings");
121
+ this.#db.run(`
122
122
  CREATE TABLE settings (
123
123
  key TEXT PRIMARY KEY,
124
124
  value TEXT NOT NULL,
@@ -169,34 +169,34 @@ CREATE TABLE settings (
169
169
 
170
170
  #migrateSchemaV4ToV5(): void {
171
171
  const migrate = this.#db.transaction(() => {
172
- this.#db.exec("ALTER TABLE settings RENAME TO settings_legacy");
173
- this.#db.exec(`
172
+ this.#db.run("ALTER TABLE settings RENAME TO settings_legacy");
173
+ this.#db.run(`
174
174
  CREATE TABLE settings (
175
175
  key TEXT PRIMARY KEY,
176
176
  value TEXT NOT NULL,
177
177
  updated_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
178
178
  );
179
179
  `);
180
- this.#db.exec(`
180
+ this.#db.run(`
181
181
  INSERT INTO settings (key, value, updated_at)
182
182
  SELECT key, value, updated_at
183
183
  FROM settings_legacy
184
184
  `);
185
- this.#db.exec("DROP TABLE settings_legacy");
185
+ this.#db.run("DROP TABLE settings_legacy");
186
186
 
187
- this.#db.exec("ALTER TABLE model_usage RENAME TO model_usage_legacy");
188
- this.#db.exec(`
187
+ this.#db.run("ALTER TABLE model_usage RENAME TO model_usage_legacy");
188
+ this.#db.run(`
189
189
  CREATE TABLE model_usage (
190
190
  model_key TEXT PRIMARY KEY,
191
191
  last_used_at INTEGER NOT NULL DEFAULT (${SQLITE_NOW_EPOCH})
192
192
  );
193
193
  `);
194
- this.#db.exec(`
194
+ this.#db.run(`
195
195
  INSERT INTO model_usage (model_key, last_used_at)
196
196
  SELECT model_key, last_used_at
197
197
  FROM model_usage_legacy
198
198
  `);
199
- this.#db.exec("DROP TABLE model_usage_legacy");
199
+ this.#db.run("DROP TABLE model_usage_legacy");
200
200
  });
201
201
  migrate();
202
202
  }
@@ -7,7 +7,7 @@
7
7
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
8
  import type { Model } from "@oh-my-pi/pi-ai";
9
9
  import { completeSimple } from "@oh-my-pi/pi-ai";
10
- import { renderPromptTemplate } from "../../config/prompt-templates";
10
+ import { prompt } from "@oh-my-pi/pi-utils";
11
11
  import branchSummaryPrompt from "../../prompts/compaction/branch-summary.md" with { type: "text" };
12
12
  import branchSummaryPreamble from "../../prompts/compaction/branch-summary-preamble.md" with { type: "text" };
13
13
  import {
@@ -244,9 +244,9 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
244
244
  // Summary Generation
245
245
  // ============================================================================
246
246
 
247
- const BRANCH_SUMMARY_PREAMBLE = renderPromptTemplate(branchSummaryPreamble);
247
+ const BRANCH_SUMMARY_PREAMBLE = prompt.render(branchSummaryPreamble);
248
248
 
249
- const BRANCH_SUMMARY_PROMPT = renderPromptTemplate(branchSummaryPrompt);
249
+ const BRANCH_SUMMARY_PROMPT = prompt.render(branchSummaryPrompt);
250
250
 
251
251
  /**
252
252
  * Generate a summary of abandoned branch entries.
@@ -26,8 +26,7 @@ import {
26
26
  getOpenAIResponsesHistoryPayload,
27
27
  normalizeResponsesToolCallId,
28
28
  } from "@oh-my-pi/pi-ai/utils";
29
- import { logger } from "@oh-my-pi/pi-utils";
30
- import { renderPromptTemplate } from "../../config/prompt-templates";
29
+ import { logger, prompt } from "@oh-my-pi/pi-utils";
31
30
  import compactionShortSummaryPrompt from "../../prompts/compaction/compaction-short-summary.md" with { type: "text" };
32
31
  import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
33
32
  import compactionTurnPrefixPrompt from "../../prompts/compaction/compaction-turn-prefix.md" with { type: "text" };
@@ -474,11 +473,11 @@ export function findCutPoint(
474
473
  // Summarization
475
474
  // ============================================================================
476
475
 
477
- const SUMMARIZATION_PROMPT = renderPromptTemplate(compactionSummaryPrompt);
476
+ const SUMMARIZATION_PROMPT = prompt.render(compactionSummaryPrompt);
478
477
 
479
- const UPDATE_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionUpdateSummaryPrompt);
478
+ const UPDATE_SUMMARIZATION_PROMPT = prompt.render(compactionUpdateSummaryPrompt);
480
479
 
481
- const SHORT_SUMMARY_PROMPT = renderPromptTemplate(compactionShortSummaryPrompt);
480
+ const SHORT_SUMMARY_PROMPT = prompt.render(compactionShortSummaryPrompt);
482
481
 
483
482
  function formatAdditionalContext(context: string[] | undefined): string {
484
483
  if (!context || context.length === 0) return "";
@@ -1201,7 +1200,7 @@ export function prepareCompaction(
1201
1200
  // Main compaction function
1202
1201
  // ============================================================================
1203
1202
 
1204
- const TURN_PREFIX_SUMMARIZATION_PROMPT = renderPromptTemplate(compactionTurnPrefixPrompt);
1203
+ const TURN_PREFIX_SUMMARIZATION_PROMPT = prompt.render(compactionTurnPrefixPrompt);
1205
1204
 
1206
1205
  /**
1207
1206
  * Generate summaries for compaction using prepared data.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
5
5
  import type { Message } from "@oh-my-pi/pi-ai";
6
- import { renderPromptTemplate } from "../../config/prompt-templates";
6
+ import { prompt } from "@oh-my-pi/pi-utils";
7
7
  import fileOperationsTemplate from "../../prompts/system/file-operations.md" with { type: "text" };
8
8
  import summarizationSystemPrompt from "../../prompts/system/summarization-system.md" with { type: "text" };
9
9
 
@@ -86,7 +86,7 @@ function stripFileOperationTags(summary: string): string {
86
86
  }
87
87
  export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
88
88
  if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
89
- return renderPromptTemplate(fileOperationsTemplate, {
89
+ return prompt.render(fileOperationsTemplate, {
90
90
  readFiles: truncateFileList(readFiles),
91
91
  modifiedFiles: truncateFileList(modifiedFiles),
92
92
  });
@@ -181,4 +181,4 @@ export function serializeConversation(messages: Message[]): string {
181
181
  // Summarization System Prompt
182
182
  // ============================================================================
183
183
 
184
- export const SUMMARIZATION_SYSTEM_PROMPT = renderPromptTemplate(summarizationSystemPrompt);
184
+ export const SUMMARIZATION_SYSTEM_PROMPT = prompt.render(summarizationSystemPrompt);
@@ -19,12 +19,47 @@ type HistoryRow = {
19
19
 
20
20
  const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
21
21
 
22
+ class AsyncDrain<T> {
23
+ #queue?: T[];
24
+ #promise = Promise.resolve();
25
+
26
+ constructor(readonly delayMs: number = 0) {}
27
+
28
+ push(value: T, hnd: (values: T[]) => Promise<void> | void): Promise<void> {
29
+ let queue = this.#queue;
30
+ if (!queue) {
31
+ this.#queue = queue = [];
32
+ this.#promise = new Promise((resolve, reject) => {
33
+ const exec = () => {
34
+ try {
35
+ if (this.#queue === queue) {
36
+ this.#queue = undefined;
37
+ }
38
+ resolve(hnd(queue!));
39
+ } catch (error) {
40
+ reject(error);
41
+ }
42
+ };
43
+
44
+ if (this.delayMs > 0) {
45
+ setTimeout(exec, this.delayMs);
46
+ } else {
47
+ queueMicrotask(exec);
48
+ }
49
+ });
50
+ }
51
+ queue.push(value);
52
+ return this.#promise;
53
+ }
54
+ }
55
+
22
56
  export class HistoryStorage {
23
57
  #db: Database;
24
58
  static #instance?: HistoryStorage;
59
+ #drain = new AsyncDrain<Pick<HistoryEntry, "prompt" | "cwd">>(100);
25
60
 
26
61
  // Prepared statements
27
- #insertStmt: Statement;
62
+ #insertRowStmt: Statement;
28
63
  #recentStmt: Statement;
29
64
  #searchStmt: Statement;
30
65
  #lastPromptStmt: Statement;
@@ -39,7 +74,7 @@ export class HistoryStorage {
39
74
 
40
75
  const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
41
76
 
42
- this.#db.exec(`
77
+ this.#db.run(`
43
78
  PRAGMA journal_mode=WAL;
44
79
  PRAGMA synchronous=NORMAL;
45
80
  PRAGMA busy_timeout=5000;
@@ -71,7 +106,6 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
71
106
  }
72
107
  }
73
108
 
74
- this.#insertStmt = this.#db.prepare("INSERT INTO history (prompt, cwd) VALUES (?, ?)");
75
109
  this.#recentStmt = this.#db.prepare(
76
110
  "SELECT id, prompt, created_at, cwd FROM history ORDER BY created_at DESC, id DESC LIMIT ?",
77
111
  );
@@ -80,6 +114,8 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
80
114
  );
81
115
  this.#lastPromptStmt = this.#db.prepare("SELECT prompt FROM history ORDER BY id DESC LIMIT 1");
82
116
 
117
+ this.#insertRowStmt = this.#db.prepare("INSERT INTO history (prompt, cwd) VALUES (?, ?)");
118
+
83
119
  const last = this.#lastPromptStmt.get() as { prompt?: string } | undefined;
84
120
  this.#lastPromptCache = last?.prompt ?? null;
85
121
  }
@@ -91,19 +127,26 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
91
127
  return HistoryStorage.#instance;
92
128
  }
93
129
 
94
- add(prompt: string, cwd?: string): void {
95
- const trimmed = prompt.trim();
96
- if (!trimmed) return;
97
- if (this.#lastPromptCache === trimmed) return;
98
-
99
- this.#lastPromptCache = trimmed;
130
+ /** @internal Reset the singleton — test-only. */
131
+ static resetInstance(): void {
132
+ HistoryStorage.#instance = undefined;
133
+ }
100
134
 
101
- setImmediate(() => {
102
- try {
103
- this.#insertStmt.run(trimmed, cwd ?? null);
104
- } catch (error) {
105
- logger.error("HistoryStorage add failed", { error: String(error) });
135
+ #insertBatch(rows: Array<Pick<HistoryEntry, "prompt" | "cwd">>): void {
136
+ this.#db.transaction((rows: Array<Pick<HistoryEntry, "prompt" | "cwd">>) => {
137
+ for (const row of rows) {
138
+ this.#insertRowStmt.run(row.prompt, row.cwd ?? null);
106
139
  }
140
+ })(rows);
141
+ }
142
+
143
+ add(prompt: string, cwd?: string): Promise<void> {
144
+ const trimmed = prompt.trim();
145
+ if (!trimmed) return Promise.resolve();
146
+ if (this.#lastPromptCache === trimmed) return Promise.resolve();
147
+ this.#lastPromptCache = trimmed;
148
+ return this.#drain.push({ prompt: trimmed, cwd: cwd ?? undefined }, rows => {
149
+ this.#insertBatch(rows);
107
150
  });
108
151
  }
109
152
 
@@ -150,11 +193,11 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
150
193
 
151
194
  #migrateHistorySchema(): void {
152
195
  const migrate = this.#db.transaction(() => {
153
- this.#db.exec("ALTER TABLE history RENAME TO history_legacy");
154
- this.#db.exec("DROP INDEX IF EXISTS idx_history_created_at");
155
- this.#db.exec("DROP TRIGGER IF EXISTS history_ai");
156
- this.#db.exec("DROP TABLE IF EXISTS history_fts");
157
- this.#db.exec(`
196
+ this.#db.run("ALTER TABLE history RENAME TO history_legacy");
197
+ this.#db.run("DROP INDEX IF EXISTS idx_history_created_at");
198
+ this.#db.run("DROP TRIGGER IF EXISTS history_ai");
199
+ this.#db.run("DROP TABLE IF EXISTS history_fts");
200
+ this.#db.run(`
158
201
  CREATE TABLE history (
159
202
  id INTEGER PRIMARY KEY AUTOINCREMENT,
160
203
  prompt TEXT NOT NULL,
@@ -14,7 +14,7 @@ import type {
14
14
  TextContent,
15
15
  ToolResultMessage,
16
16
  } from "@oh-my-pi/pi-ai";
17
- import { renderPromptTemplate } from "../config/prompt-templates";
17
+ import { prompt } from "@oh-my-pi/pi-utils";
18
18
  import branchSummaryContextPrompt from "../prompts/compaction/branch-summary-context.md" with { type: "text" };
19
19
  import compactionSummaryContextPrompt from "../prompts/compaction/compaction-summary-context.md" with { type: "text" };
20
20
  import type { OutputMeta } from "../tools/output-meta";
@@ -310,7 +310,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
310
310
  content: [
311
311
  {
312
312
  type: "text" as const,
313
- text: renderPromptTemplate(BRANCH_SUMMARY_TEMPLATE, { summary: m.summary }),
313
+ text: prompt.render(BRANCH_SUMMARY_TEMPLATE, { summary: m.summary }),
314
314
  },
315
315
  ],
316
316
  attribution: "agent",
@@ -322,7 +322,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
322
322
  content: [
323
323
  {
324
324
  type: "text" as const,
325
- text: renderPromptTemplate(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
325
+ text: prompt.render(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
326
326
  },
327
327
  ],
328
328
  attribution: "agent",
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Plain-text / markdown session formatting (same shape as /dump clipboard export).
3
+ */
4
+ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
+ import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
6
+ import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
7
+ import {
8
+ type BashExecutionMessage,
9
+ type BranchSummaryMessage,
10
+ bashExecutionToText,
11
+ type CompactionSummaryMessage,
12
+ type CustomMessage,
13
+ type FileMentionMessage,
14
+ type HookMessage,
15
+ type PythonExecutionMessage,
16
+ pythonExecutionToText,
17
+ } from "./messages";
18
+
19
+ /** Minimal tool shape for dump output (matches AgentTool fields used by formatSessionDumpText). */
20
+ export interface SessionDumpToolInfo {
21
+ name: string;
22
+ description: string;
23
+ parameters: unknown;
24
+ }
25
+
26
+ export interface FormatSessionDumpTextOptions {
27
+ messages: readonly AgentMessage[];
28
+ systemPrompt?: string | null;
29
+ model?: Model | null;
30
+ thinkingLevel?: ThinkingLevel | string | null;
31
+ tools?: readonly SessionDumpToolInfo[];
32
+ }
33
+
34
+ function stripTypeBoxFields(obj: unknown): unknown {
35
+ if (Array.isArray(obj)) {
36
+ return obj.map(stripTypeBoxFields);
37
+ }
38
+ if (obj && typeof obj === "object") {
39
+ const result: Record<string, unknown> = {};
40
+ for (const [k, v] of Object.entries(obj)) {
41
+ if (!k.startsWith("TypeBox.")) {
42
+ result[k] = stripTypeBoxFields(v);
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ return obj;
48
+ }
49
+
50
+ /** Serialize an object as XML parameter elements, one per key. */
51
+ function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
52
+ const parts: string[] = [];
53
+ for (const [key, value] of Object.entries(args)) {
54
+ if (key === INTENT_FIELD) continue;
55
+ const text = typeof value === "string" ? value : JSON.stringify(value);
56
+ parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
57
+ }
58
+ return parts.join("\n");
59
+ }
60
+
61
+ /**
62
+ * Format messages and session metadata as markdown/plain text (same as AgentSession.formatSessionAsText / /dump).
63
+ */
64
+ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string {
65
+ const lines: string[] = [];
66
+
67
+ const systemPrompt = options.systemPrompt;
68
+ if (systemPrompt) {
69
+ lines.push("## System Prompt\n");
70
+ lines.push(systemPrompt);
71
+ lines.push("\n");
72
+ }
73
+
74
+ const model = options.model;
75
+ const thinkingLevel = options.thinkingLevel;
76
+ lines.push("## Configuration\n");
77
+ lines.push(`Model: ${model ? `${model.provider}/${model.id}` : "(not selected)"}`);
78
+ lines.push(`Thinking Level: ${thinkingLevel ?? ""}`);
79
+ lines.push("\n");
80
+
81
+ const tools = options.tools ?? [];
82
+ if (tools.length > 0) {
83
+ lines.push("## Available Tools\n");
84
+ for (const tool of tools) {
85
+ lines.push(`<tool name="${tool.name}">`);
86
+ lines.push(tool.description);
87
+ const parametersClean = stripTypeBoxFields(tool.parameters);
88
+ lines.push(`\nParameters:\n${formatArgsAsXml(parametersClean as Record<string, unknown>)}`);
89
+ lines.push("<" + "/tool>\n");
90
+ }
91
+ lines.push("\n");
92
+ }
93
+
94
+ for (const msg of options.messages) {
95
+ if (msg.role === "user" || msg.role === "developer") {
96
+ lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
97
+ if (typeof msg.content === "string") {
98
+ lines.push(msg.content);
99
+ } else {
100
+ for (const c of msg.content) {
101
+ if (c.type === "text") {
102
+ lines.push(c.text);
103
+ } else if (c.type === "image") {
104
+ lines.push("[Image]");
105
+ }
106
+ }
107
+ }
108
+ lines.push("\n");
109
+ } else if (msg.role === "assistant") {
110
+ const assistantMsg = msg as AssistantMessage;
111
+ lines.push("## Assistant\n");
112
+
113
+ for (const c of assistantMsg.content) {
114
+ if (c.type === "text") {
115
+ lines.push(c.text);
116
+ } else if (c.type === "thinking") {
117
+ lines.push("<thinking>");
118
+ lines.push(c.thinking);
119
+ lines.push("</thinking>\n");
120
+ } else if (c.type === "toolCall") {
121
+ lines.push(`<invoke name="${c.name}">`);
122
+ if (c.arguments && typeof c.arguments === "object") {
123
+ lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
124
+ }
125
+ lines.push("<" + "/invoke>\n");
126
+ }
127
+ }
128
+ lines.push("");
129
+ } else if (msg.role === "toolResult") {
130
+ lines.push(`### Tool Result: ${msg.toolName}`);
131
+ if (msg.isError) {
132
+ lines.push("(error)");
133
+ }
134
+ for (const c of msg.content) {
135
+ if (c.type === "text") {
136
+ lines.push("```");
137
+ lines.push(c.text);
138
+ lines.push("```");
139
+ } else if (c.type === "image") {
140
+ lines.push("[Image output]");
141
+ }
142
+ }
143
+ lines.push("");
144
+ } else if (msg.role === "bashExecution") {
145
+ const bashMsg = msg as BashExecutionMessage;
146
+ if (!bashMsg.excludeFromContext) {
147
+ lines.push("## Bash Execution\n");
148
+ lines.push(bashExecutionToText(bashMsg));
149
+ lines.push("\n");
150
+ }
151
+ } else if (msg.role === "pythonExecution") {
152
+ const pythonMsg = msg as PythonExecutionMessage;
153
+ if (!pythonMsg.excludeFromContext) {
154
+ lines.push("## Python Execution\n");
155
+ lines.push(pythonExecutionToText(pythonMsg));
156
+ lines.push("\n");
157
+ }
158
+ } else if (msg.role === "custom" || msg.role === "hookMessage") {
159
+ const customMsg = msg as CustomMessage | HookMessage;
160
+ lines.push(`## ${customMsg.customType}\n`);
161
+ if (typeof customMsg.content === "string") {
162
+ lines.push(customMsg.content);
163
+ } else {
164
+ for (const c of customMsg.content) {
165
+ if (c.type === "text") {
166
+ lines.push(c.text);
167
+ } else if (c.type === "image") {
168
+ lines.push("[Image]");
169
+ }
170
+ }
171
+ }
172
+ lines.push("\n");
173
+ } else if (msg.role === "branchSummary") {
174
+ const branchMsg = msg as BranchSummaryMessage;
175
+ lines.push("## Branch Summary\n");
176
+ lines.push(`(from branch: ${branchMsg.fromId})\n`);
177
+ lines.push(branchMsg.summary);
178
+ lines.push("\n");
179
+ } else if (msg.role === "compactionSummary") {
180
+ const compactMsg = msg as CompactionSummaryMessage;
181
+ lines.push("## Compaction Summary\n");
182
+ lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
183
+ lines.push(compactMsg.summary);
184
+ lines.push("\n");
185
+ } else if (msg.role === "fileMention") {
186
+ const fileMsg = msg as FileMentionMessage;
187
+ lines.push("## File Mention\n");
188
+ for (const file of fileMsg.files) {
189
+ lines.push(`<file path="${file.path}">`);
190
+ if (file.content) {
191
+ lines.push(file.content);
192
+ }
193
+ if (file.image) {
194
+ lines.push("[Image attached]");
195
+ }
196
+ lines.push("</file>\n");
197
+ }
198
+ lines.push("\n");
199
+ }
200
+ }
201
+
202
+ return lines.join("\n").trim();
203
+ }
@@ -1,7 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as fsp from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { isEnoent, toError } from "@oh-my-pi/pi-utils";
4
+ import { isEnoent, peekFile, toError } from "@oh-my-pi/pi-utils";
5
+
6
+ const utf8Decoder = new TextDecoder("utf-8");
5
7
 
6
8
  export interface SessionStorageStat {
7
9
  size: number;
@@ -163,7 +165,7 @@ export class FileSessionStorage implements SessionStorage {
163
165
  }
164
166
 
165
167
  async readTextPrefix(path: string, maxBytes: number): Promise<string> {
166
- return Bun.file(path).slice(0, maxBytes).text();
168
+ return peekFile(path, maxBytes, header => utf8Decoder.decode(header));
167
169
  }
168
170
 
169
171
  async writeText(path: string, content: string): Promise<void> {
@@ -747,6 +747,6 @@ export function formatHeadTruncationNotice(
747
747
  const totalFileLines = options.totalFileLines ?? truncation.totalLines;
748
748
  const endLineDisplay = startLineDisplay + (truncation.outputLines ?? truncation.totalLines) - 1;
749
749
  const nextOffset = endLineDisplay + 1;
750
- const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
750
+ const notice = `[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use sel=L${nextOffset} to continue]`;
751
751
  return `\n\n${notice}`;
752
752
  }