@oh-my-pi/pi-coding-agent 15.8.3 → 15.9.1

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 (172) hide show
  1. package/CHANGELOG.md +113 -31
  2. package/dist/types/capability/skill.d.ts +7 -0
  3. package/dist/types/cli/update-cli.d.ts +15 -1
  4. package/dist/types/config/append-only-context-mode.d.ts +8 -0
  5. package/dist/types/config/model-registry.d.ts +3 -0
  6. package/dist/types/config/models-config-schema.d.ts +15 -0
  7. package/dist/types/config/settings-schema.d.ts +38 -24
  8. package/dist/types/debug/protocol-probe.d.ts +38 -0
  9. package/dist/types/debug/terminal-info.d.ts +34 -0
  10. package/dist/types/exa/mcp-client.d.ts +2 -1
  11. package/dist/types/export/html/template.generated.d.ts +1 -1
  12. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  13. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  14. package/dist/types/extensibility/shared-events.d.ts +1 -1
  15. package/dist/types/index.d.ts +1 -0
  16. package/dist/types/mcp/json-rpc.d.ts +6 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +4 -0
  18. package/dist/types/mcp/transports/stdio.d.ts +16 -7
  19. package/dist/types/mnemopi/state.d.ts +2 -2
  20. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  21. package/dist/types/modes/components/assistant-message.d.ts +3 -2
  22. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
  23. package/dist/types/modes/components/hook-selector.d.ts +11 -0
  24. package/dist/types/modes/components/plugin-settings.d.ts +40 -8
  25. package/dist/types/modes/components/session-selector.d.ts +8 -3
  26. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  27. package/dist/types/modes/components/todo-reminder.d.ts +1 -1
  28. package/dist/types/modes/components/transcript-container.d.ts +36 -0
  29. package/dist/types/modes/interactive-mode.d.ts +2 -1
  30. package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
  31. package/dist/types/modes/theme/theme.d.ts +20 -1
  32. package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
  33. package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
  34. package/dist/types/session/agent-session.d.ts +5 -2
  35. package/dist/types/session/history-storage.d.ts +3 -4
  36. package/dist/types/session/indexed-session-storage.d.ts +59 -0
  37. package/dist/types/session/messages.d.ts +1 -0
  38. package/dist/types/session/redis-session-storage.d.ts +12 -85
  39. package/dist/types/session/session-manager.d.ts +21 -0
  40. package/dist/types/session/session-storage.d.ts +5 -7
  41. package/dist/types/session/sql-session-storage.d.ts +16 -85
  42. package/dist/types/slash-commands/types.d.ts +17 -4
  43. package/dist/types/task/executor.d.ts +9 -0
  44. package/dist/types/task/index.d.ts +3 -1
  45. package/dist/types/telemetry-export.d.ts +19 -0
  46. package/dist/types/tiny/compiled-runtime.d.ts +35 -0
  47. package/dist/types/tiny/text.d.ts +17 -0
  48. package/dist/types/tools/ask.d.ts +1 -0
  49. package/dist/types/tools/index.d.ts +4 -2
  50. package/dist/types/tools/path-utils.d.ts +1 -1
  51. package/dist/types/tools/search.d.ts +2 -2
  52. package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
  53. package/dist/types/utils/session-color.d.ts +7 -2
  54. package/dist/types/web/search/index.d.ts +1 -1
  55. package/dist/types/web/search/provider.d.ts +2 -2
  56. package/dist/types/web/search/providers/base.d.ts +14 -0
  57. package/dist/types/web/search/providers/exa.d.ts +9 -0
  58. package/dist/types/web/search/providers/perplexity.d.ts +2 -2
  59. package/dist/types/web/search/types.d.ts +66 -1
  60. package/package.json +15 -9
  61. package/scripts/build-binary.ts +12 -0
  62. package/src/capability/skill.ts +7 -0
  63. package/src/cli/args.ts +1 -1
  64. package/src/cli/session-picker.ts +1 -0
  65. package/src/cli/update-cli.ts +54 -2
  66. package/src/commands/completions.ts +1 -1
  67. package/src/config/append-only-context-mode.ts +37 -0
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +24 -57
  70. package/src/debug/index.ts +67 -1
  71. package/src/debug/protocol-probe.ts +267 -0
  72. package/src/debug/terminal-info.ts +127 -0
  73. package/src/exa/mcp-client.ts +11 -5
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +3 -3
  76. package/src/extensibility/custom-tools/types.ts +1 -1
  77. package/src/extensibility/extensions/types.ts +11 -0
  78. package/src/extensibility/shared-events.ts +1 -1
  79. package/src/extensibility/skills.ts +3 -3
  80. package/src/index.ts +1 -0
  81. package/src/internal-urls/docs-index.generated.ts +7 -7
  82. package/src/main.ts +16 -2
  83. package/src/mcp/json-rpc.ts +8 -0
  84. package/src/mcp/render.ts +3 -0
  85. package/src/mcp/tool-bridge.ts +10 -2
  86. package/src/mcp/transports/http.ts +33 -16
  87. package/src/mcp/transports/stdio.ts +37 -12
  88. package/src/mnemopi/state.ts +4 -4
  89. package/src/modes/acp/acp-agent.ts +168 -3
  90. package/src/modes/acp/acp-event-mapper.ts +7 -7
  91. package/src/modes/components/agent-dashboard.ts +103 -31
  92. package/src/modes/components/assistant-message.ts +3 -2
  93. package/src/modes/components/extensions/extension-dashboard.ts +56 -10
  94. package/src/modes/components/history-search.ts +128 -14
  95. package/src/modes/components/hook-selector.ts +149 -14
  96. package/src/modes/components/plugin-settings.ts +270 -36
  97. package/src/modes/components/session-selector.ts +81 -19
  98. package/src/modes/components/settings-selector.ts +1 -1
  99. package/src/modes/components/status-line/segments.ts +2 -1
  100. package/src/modes/components/status-line.ts +1 -1
  101. package/src/modes/components/tips.txt +6 -2
  102. package/src/modes/components/todo-reminder.ts +1 -1
  103. package/src/modes/components/tool-execution.ts +9 -4
  104. package/src/modes/components/transcript-container.ts +109 -0
  105. package/src/modes/controllers/command-controller.ts +4 -3
  106. package/src/modes/controllers/event-controller.ts +12 -7
  107. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  108. package/src/modes/controllers/input-controller.ts +10 -5
  109. package/src/modes/controllers/selector-controller.ts +30 -19
  110. package/src/modes/controllers/todo-command-controller.ts +1 -1
  111. package/src/modes/interactive-mode.ts +56 -9
  112. package/src/modes/print-mode.ts +5 -0
  113. package/src/modes/rpc/rpc-types.ts +1 -1
  114. package/src/modes/theme/theme.ts +48 -8
  115. package/src/modes/utils/keybinding-matchers.ts +10 -0
  116. package/src/modes/utils/ui-helpers.ts +1 -0
  117. package/src/plan-mode/plan-handoff.ts +37 -0
  118. package/src/priority.json +4 -0
  119. package/src/prompts/goals/goal-continuation.md +1 -1
  120. package/src/prompts/steering/user-interjection.md +10 -0
  121. package/src/prompts/system/agent-creation-architect.md +1 -26
  122. package/src/prompts/system/eager-todo.md +3 -3
  123. package/src/prompts/system/orchestrate-notice.md +5 -5
  124. package/src/prompts/system/plan-mode-approved.md +14 -17
  125. package/src/prompts/system/plan-mode-reference.md +3 -6
  126. package/src/prompts/system/subagent-system-prompt.md +11 -0
  127. package/src/prompts/system/system-prompt.md +143 -145
  128. package/src/prompts/system/title-system.md +3 -2
  129. package/src/prompts/system/workflow-notice.md +1 -1
  130. package/src/prompts/tools/browser.md +33 -30
  131. package/src/prompts/tools/render-mermaid.md +2 -2
  132. package/src/prompts/tools/search.md +1 -1
  133. package/src/prompts/tools/task.md +3 -1
  134. package/src/prompts/tools/{todo-write.md → todo.md} +5 -5
  135. package/src/sdk.ts +6 -21
  136. package/src/session/agent-session.ts +44 -21
  137. package/src/session/history-storage.ts +11 -18
  138. package/src/session/indexed-session-storage.ts +430 -0
  139. package/src/session/messages.ts +80 -0
  140. package/src/session/redis-session-storage.ts +66 -377
  141. package/src/session/session-manager.ts +109 -23
  142. package/src/session/session-storage.ts +148 -68
  143. package/src/session/sql-session-storage.ts +131 -382
  144. package/src/slash-commands/helpers/todo.ts +2 -2
  145. package/src/slash-commands/types.ts +27 -10
  146. package/src/task/executor.ts +9 -1
  147. package/src/task/index.ts +51 -1
  148. package/src/telemetry-export.ts +126 -0
  149. package/src/tiny/compiled-runtime.ts +179 -0
  150. package/src/tiny/text.ts +112 -1
  151. package/src/tiny/worker.ts +24 -2
  152. package/src/tools/ask.ts +133 -87
  153. package/src/tools/fetch.ts +17 -4
  154. package/src/tools/find.ts +2 -2
  155. package/src/tools/index.ts +6 -4
  156. package/src/tools/memory-recall.ts +1 -1
  157. package/src/tools/memory-reflect.ts +1 -1
  158. package/src/tools/path-utils.ts +16 -7
  159. package/src/tools/renderers.ts +2 -2
  160. package/src/tools/search.ts +6 -3
  161. package/src/tools/ssh.ts +26 -10
  162. package/src/tools/{todo-write.ts → todo.ts} +32 -35
  163. package/src/tools/write.ts +14 -2
  164. package/src/tui/status-line.ts +15 -4
  165. package/src/utils/session-color.ts +39 -14
  166. package/src/utils/title-generator.ts +9 -2
  167. package/src/web/search/index.ts +4 -2
  168. package/src/web/search/provider.ts +19 -35
  169. package/src/web/search/providers/base.ts +17 -0
  170. package/src/web/search/providers/exa.ts +111 -7
  171. package/src/web/search/providers/perplexity.ts +8 -4
  172. package/src/web/search/types.ts +74 -32
@@ -0,0 +1,430 @@
1
+ import { toError } from "@oh-my-pi/pi-utils";
2
+ import type { SessionStorage, SessionStorageStat, SessionStorageWriter } from "./session-storage";
3
+
4
+ export interface SessionStorageIndexEntry {
5
+ path: string;
6
+ size: number;
7
+ mtimeMs: number;
8
+ }
9
+
10
+ export interface SessionStorageBackend {
11
+ init(): Promise<void>;
12
+ loadIndex(): Promise<Iterable<SessionStorageIndexEntry>>;
13
+ readFull(path: string): Promise<string | null>;
14
+ readSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]>;
15
+ writeFull(path: string, content: string, mtimeMs: number): Promise<void>;
16
+ append(path: string, line: string, mtimeMs: number): Promise<void>;
17
+ truncate(path: string, mtimeMs: number): Promise<void>;
18
+ remove(paths: string[]): Promise<void>;
19
+ move(src: string, dst: string, mtimeMs: number): Promise<void>;
20
+ }
21
+
22
+ interface IndexEntry {
23
+ size: number;
24
+ mtimeMs: number;
25
+ }
26
+
27
+ interface EnqueueOptions {
28
+ trackDrain: boolean;
29
+ }
30
+
31
+ const RESOLVED = Promise.resolve();
32
+
33
+ function enoent(p: string): NodeJS.ErrnoException {
34
+ const err = new Error(`ENOENT: no such file, '${p}'`) as NodeJS.ErrnoException;
35
+ err.code = "ENOENT";
36
+ err.errno = -2;
37
+ err.path = p;
38
+ err.syscall = "open";
39
+ return err;
40
+ }
41
+
42
+ function matchesGlob(name: string, pattern: string): boolean {
43
+ if (pattern === "*") return true;
44
+ if (pattern.startsWith("*.")) return name.endsWith(pattern.slice(1));
45
+ return name === pattern;
46
+ }
47
+
48
+ function byteLength(text: string): number {
49
+ return Buffer.byteLength(text, "utf-8");
50
+ }
51
+
52
+ function normalizeByteLimit(maxBytes: number): number {
53
+ if (!(maxBytes > 0)) return 0;
54
+ return Math.trunc(maxBytes);
55
+ }
56
+
57
+ function uniquePaths(paths: readonly string[]): string[] {
58
+ const out: string[] = [];
59
+ const seen = new Set<string>();
60
+ for (const path of paths) {
61
+ if (seen.has(path)) continue;
62
+ seen.add(path);
63
+ out.push(path);
64
+ }
65
+ return out;
66
+ }
67
+
68
+ export class IndexedSessionStorage implements SessionStorage {
69
+ readonly #backend: SessionStorageBackend;
70
+ readonly #index = new Map<string, IndexEntry>();
71
+ readonly #writers = new Set<IndexedSessionStorageWriter>();
72
+ readonly #pathTails = new Map<string, Promise<void>>();
73
+ readonly #pathPending = new Map<string, Promise<void>>();
74
+ readonly #drainPending = new Set<Promise<void>>();
75
+ #nextMtimeMs = 0;
76
+ #firstDrainError: Error | undefined;
77
+
78
+ constructor(backend: SessionStorageBackend) {
79
+ this.#backend = backend;
80
+ }
81
+
82
+ async initialize(): Promise<void> {
83
+ await this.#backend.init();
84
+ await this.refresh();
85
+ }
86
+
87
+ async refresh(): Promise<void> {
88
+ await this.drain();
89
+ const rows = await this.#backend.loadIndex();
90
+ this.#index.clear();
91
+ for (const row of rows) {
92
+ this.#setIndex(row.path, row.size, row.mtimeMs);
93
+ }
94
+ }
95
+
96
+ async drain(): Promise<void> {
97
+ while (this.#drainPending.size > 0) {
98
+ await Promise.allSettled(this.#drainPending);
99
+ }
100
+ const error = this.#firstDrainError;
101
+ this.#firstDrainError = undefined;
102
+ if (error) throw error;
103
+ }
104
+
105
+ ensureDirSync(_dir: string): void {
106
+ // Indexed backends are flat: directories are derived from key prefixes.
107
+ }
108
+
109
+ existsSync(path: string): boolean {
110
+ return this.#index.has(path);
111
+ }
112
+
113
+ writeTextSync(path: string, content: string): void {
114
+ const mtimeMs = this.#allocMtimeMs();
115
+ this.#setIndex(path, byteLength(content), mtimeMs);
116
+ this.#enqueuePath(path, () => this.#backend.writeFull(path, content, mtimeMs), { trackDrain: true });
117
+ }
118
+
119
+ statSync(path: string): SessionStorageStat {
120
+ const entry = this.#index.get(path);
121
+ if (!entry) throw enoent(path);
122
+ return {
123
+ size: entry.size,
124
+ mtimeMs: entry.mtimeMs,
125
+ mtime: new Date(entry.mtimeMs),
126
+ };
127
+ }
128
+
129
+ listFilesSync(dir: string, pattern: string): string[] {
130
+ const prefix = dir.endsWith("/") ? dir : `${dir}/`;
131
+ const out: string[] = [];
132
+ for (const path of this.#index.keys()) {
133
+ if (!path.startsWith(prefix)) continue;
134
+ const name = path.slice(prefix.length);
135
+ if (name.includes("/") || name.includes("\\")) continue;
136
+ if (!matchesGlob(name, pattern)) continue;
137
+ out.push(path);
138
+ }
139
+ return out;
140
+ }
141
+
142
+ exists(path: string): Promise<boolean> {
143
+ return Promise.resolve(this.existsSync(path));
144
+ }
145
+
146
+ async readText(path: string): Promise<string> {
147
+ if (!this.#index.has(path)) throw enoent(path);
148
+ await this.#awaitPath(path);
149
+ const content = await this.#backend.readFull(path);
150
+ if (content === null) throw enoent(path);
151
+ return content;
152
+ }
153
+
154
+ async readTextSlices(path: string, prefixBytes: number, suffixBytes: number): Promise<[string, string]> {
155
+ if (!this.#index.has(path)) throw enoent(path);
156
+ const prefixLimit = normalizeByteLimit(prefixBytes);
157
+ const suffixLimit = normalizeByteLimit(suffixBytes);
158
+ if (prefixLimit === 0 && suffixLimit === 0) return ["", ""];
159
+ await this.#awaitPath(path);
160
+ return this.#backend.readSlices(path, prefixLimit, suffixLimit);
161
+ }
162
+
163
+ async writeText(path: string, content: string): Promise<void> {
164
+ await this.#awaitPath(path);
165
+ const previous = this.#index.get(path);
166
+ const mtimeMs = this.#allocMtimeMs();
167
+ this.#setIndex(path, byteLength(content), mtimeMs);
168
+ try {
169
+ await this.#enqueuePath(path, () => this.#backend.writeFull(path, content, mtimeMs), { trackDrain: false });
170
+ } catch (err) {
171
+ this.#restoreIndex(path, previous);
172
+ throw toError(err);
173
+ }
174
+ }
175
+
176
+ async rename(src: string, dst: string): Promise<void> {
177
+ await this.#awaitPath(src);
178
+ await this.#awaitPath(dst);
179
+ const entry = this.#index.get(src);
180
+ if (!entry) throw enoent(src);
181
+ const dstPrevious = this.#index.get(dst);
182
+ this.#index.delete(src);
183
+ this.#index.set(dst, { ...entry });
184
+ try {
185
+ await this.#enqueuePaths([src, dst], () => this.#backend.move(src, dst, entry.mtimeMs), { trackDrain: false });
186
+ } catch (err) {
187
+ this.#index.delete(dst);
188
+ this.#restoreIndex(dst, dstPrevious);
189
+ this.#index.set(src, entry);
190
+ throw toError(err);
191
+ }
192
+ }
193
+
194
+ async unlink(path: string): Promise<void> {
195
+ await this.#awaitPath(path);
196
+ const previous = this.#index.get(path);
197
+ if (!previous) throw enoent(path);
198
+ this.#index.delete(path);
199
+ try {
200
+ await this.#enqueuePath(path, () => this.#backend.remove([path]), { trackDrain: false });
201
+ } catch (err) {
202
+ this.#index.set(path, previous);
203
+ throw toError(err);
204
+ }
205
+ }
206
+
207
+ async deleteSessionWithArtifacts(sessionPath: string): Promise<void> {
208
+ await this.#awaitPath(sessionPath);
209
+ const sessionEntry = this.#index.get(sessionPath);
210
+ if (!sessionEntry) throw enoent(sessionPath);
211
+
212
+ const artifactsDir = sessionPath.slice(0, -6);
213
+ const prefix = artifactsDir.endsWith("/") ? artifactsDir : `${artifactsDir}/`;
214
+ const paths = [sessionPath];
215
+ for (const key of this.#index.keys()) {
216
+ if (key.startsWith(prefix)) paths.push(key);
217
+ }
218
+
219
+ for (const path of paths) await this.#awaitPath(path);
220
+
221
+ const previous = new Map<string, IndexEntry>();
222
+ for (const path of paths) {
223
+ const entry = this.#index.get(path);
224
+ if (entry) previous.set(path, entry);
225
+ this.#index.delete(path);
226
+ }
227
+
228
+ try {
229
+ await this.#enqueuePaths(paths, () => this.#backend.remove(paths), { trackDrain: false });
230
+ } catch (err) {
231
+ for (const [path, entry] of previous) this.#index.set(path, entry);
232
+ throw toError(err);
233
+ }
234
+ }
235
+
236
+ openWriter(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }): SessionStorageWriter {
237
+ const writer = new IndexedSessionStorageWriter(this, path, options);
238
+ this.#writers.add(writer);
239
+ return writer;
240
+ }
241
+
242
+ _writerClosed(writer: IndexedSessionStorageWriter): void {
243
+ this.#writers.delete(writer);
244
+ }
245
+
246
+ _truncateForWriter(path: string): number {
247
+ const mtimeMs = this.#allocMtimeMs();
248
+ this.#setIndex(path, 0, mtimeMs);
249
+ return mtimeMs;
250
+ }
251
+
252
+ _queueTruncate(path: string, mtimeMs: number, getError?: () => Error | undefined): Promise<void> {
253
+ return this.#enqueuePath(
254
+ path,
255
+ async () => {
256
+ const error = getError?.();
257
+ if (error) throw error;
258
+ await this.#backend.truncate(path, mtimeMs);
259
+ },
260
+ { trackDrain: true },
261
+ );
262
+ }
263
+
264
+ _appendForWriter(path: string, line: string): number {
265
+ const mtimeMs = this.#allocMtimeMs();
266
+ const existing = this.#index.get(path);
267
+ const size = (existing?.size ?? 0) + byteLength(line);
268
+ this.#setIndex(path, size, mtimeMs);
269
+ return mtimeMs;
270
+ }
271
+
272
+ _queueAppend(path: string, line: string, mtimeMs: number, getError?: () => Error | undefined): Promise<void> {
273
+ return this.#enqueuePath(
274
+ path,
275
+ async () => {
276
+ const error = getError?.();
277
+ if (error) throw error;
278
+ await this.#backend.append(path, line, mtimeMs);
279
+ },
280
+ { trackDrain: true },
281
+ );
282
+ }
283
+
284
+ #restoreIndex(path: string, entry: IndexEntry | undefined): void {
285
+ if (entry) {
286
+ this.#index.set(path, entry);
287
+ } else {
288
+ this.#index.delete(path);
289
+ }
290
+ }
291
+
292
+ #setIndex(path: string, size: number, mtimeMs: number): void {
293
+ this.#index.set(path, { size, mtimeMs });
294
+ if (mtimeMs > this.#nextMtimeMs) this.#nextMtimeMs = mtimeMs;
295
+ }
296
+
297
+ #allocMtimeMs(): number {
298
+ const now = Date.now();
299
+ const next = now > this.#nextMtimeMs ? now : this.#nextMtimeMs + 1;
300
+ this.#nextMtimeMs = next;
301
+ return next;
302
+ }
303
+
304
+ #enqueuePath(path: string, task: () => Promise<void>, options: EnqueueOptions): Promise<void> {
305
+ return this.#enqueuePaths([path], task, options);
306
+ }
307
+
308
+ #enqueuePaths(paths: readonly string[], task: () => Promise<void>, options: EnqueueOptions): Promise<void> {
309
+ const unique = uniquePaths(paths);
310
+ const previous = unique.map(path => this.#pathTails.get(path) ?? RESOLVED);
311
+ const operation = Promise.all(previous).then(task);
312
+ const tracked = operation.catch(err => {
313
+ const error = toError(err);
314
+ if (options.trackDrain && !this.#firstDrainError) this.#firstDrainError = error;
315
+ throw error;
316
+ });
317
+ const tail = tracked.catch(() => {});
318
+ for (const path of unique) {
319
+ this.#pathTails.set(path, tail);
320
+ this.#pathPending.set(path, tracked);
321
+ }
322
+ tail.finally(() => {
323
+ for (const path of unique) {
324
+ if (this.#pathTails.get(path) === tail) this.#pathTails.delete(path);
325
+ }
326
+ });
327
+ tracked
328
+ .finally(() => {
329
+ for (const path of unique) {
330
+ if (this.#pathPending.get(path) === tracked) this.#pathPending.delete(path);
331
+ }
332
+ })
333
+ .catch(() => {});
334
+ tracked.catch(() => {});
335
+ if (options.trackDrain) {
336
+ this.#drainPending.add(tracked);
337
+ tracked
338
+ .finally(() => {
339
+ this.#drainPending.delete(tracked);
340
+ })
341
+ .catch(() => {});
342
+ }
343
+ return tracked;
344
+ }
345
+
346
+ #awaitPath(path: string): Promise<void> {
347
+ return this.#pathPending.get(path) ?? RESOLVED;
348
+ }
349
+ }
350
+
351
+ class IndexedSessionStorageWriter implements SessionStorageWriter {
352
+ #storage: IndexedSessionStorage;
353
+ #path: string;
354
+ #closed = false;
355
+ #error: Error | undefined;
356
+ #onError: ((err: Error) => void) | undefined;
357
+ #pendingChain: Promise<void> = Promise.resolve();
358
+
359
+ constructor(
360
+ storage: IndexedSessionStorage,
361
+ path: string,
362
+ options?: { flags?: "a" | "w"; onError?: (err: Error) => void },
363
+ ) {
364
+ this.#storage = storage;
365
+ this.#path = path;
366
+ this.#onError = options?.onError;
367
+ if ((options?.flags ?? "a") === "w") {
368
+ const mtimeMs = storage._truncateForWriter(path);
369
+ this.#trackPromise(storage._queueTruncate(path, mtimeMs, () => this.#error));
370
+ }
371
+ }
372
+
373
+ #recordError(err: unknown): Error {
374
+ const error = toError(err);
375
+ if (!this.#error) this.#error = error;
376
+ this.#onError?.(error);
377
+ return error;
378
+ }
379
+
380
+ #trackPromise(promise: Promise<void>): Promise<void> {
381
+ const next = this.#pendingChain.then(async () => {
382
+ if (this.#error) throw this.#error;
383
+ try {
384
+ await promise;
385
+ } catch (err) {
386
+ throw this.#recordError(err);
387
+ }
388
+ });
389
+ this.#pendingChain = next.catch(() => {});
390
+ return next;
391
+ }
392
+
393
+ writeLineSync(line: string): void {
394
+ if (this.#closed) throw new Error("Writer closed");
395
+ if (this.#error) throw this.#error;
396
+ const mtimeMs = this.#storage._appendForWriter(this.#path, line);
397
+ this.#trackPromise(this.#storage._queueAppend(this.#path, line, mtimeMs, () => this.#error));
398
+ }
399
+
400
+ async writeLine(line: string): Promise<void> {
401
+ if (this.#closed) throw new Error("Writer closed");
402
+ if (this.#error) throw this.#error;
403
+ const mtimeMs = this.#storage._appendForWriter(this.#path, line);
404
+ await this.#trackPromise(this.#storage._queueAppend(this.#path, line, mtimeMs, () => this.#error));
405
+ }
406
+
407
+ async flush(): Promise<void> {
408
+ if (this.#error) throw this.#error;
409
+ await this.#pendingChain;
410
+ if (this.#error) throw this.#error;
411
+ }
412
+
413
+ async fsync(): Promise<void> {
414
+ await this.flush();
415
+ }
416
+
417
+ async close(): Promise<void> {
418
+ if (this.#closed) return;
419
+ this.#closed = true;
420
+ try {
421
+ await this.flush();
422
+ } finally {
423
+ this.#storage._writerClosed(this);
424
+ }
425
+ }
426
+
427
+ getError(): Error | undefined {
428
+ return this.#error;
429
+ }
430
+ }
@@ -18,7 +18,10 @@ import type {
18
18
  MessageAttribution,
19
19
  TextContent,
20
20
  ToolResultMessage,
21
+ UserMessage,
21
22
  } from "@oh-my-pi/pi-ai";
23
+ import { prompt } from "@oh-my-pi/pi-utils";
24
+ import userInterjectionTemplate from "../prompts/steering/user-interjection.md" with { type: "text" };
22
25
 
23
26
  export {
24
27
  type BranchSummaryMessage,
@@ -105,6 +108,83 @@ export function stripInternalDetailsFields<T>(details: T | undefined): T | undef
105
108
  return cleaned as T;
106
109
  }
107
110
 
111
+ function isSteeringUserMessage(message: AgentMessage | undefined): message is UserMessage & { steering: true } {
112
+ return message?.role === "user" && message.steering === true;
113
+ }
114
+
115
+ function userMessageWithoutSteering(message: UserMessage): UserMessage {
116
+ const { steering, ...rest } = message;
117
+ void steering;
118
+ return rest;
119
+ }
120
+
121
+ function renderSteeringEnvelope(message: string): string {
122
+ return prompt.render(userInterjectionTemplate, { message });
123
+ }
124
+
125
+ function getArrayContentText(content: (TextContent | ImageContent)[]): string {
126
+ let firstText: string | undefined;
127
+ let textParts: string[] | undefined;
128
+ for (const part of content) {
129
+ if (part.type !== "text") continue;
130
+ if (firstText === undefined) {
131
+ firstText = part.text;
132
+ continue;
133
+ }
134
+ if (textParts === undefined) {
135
+ textParts = [firstText];
136
+ }
137
+ textParts.push(part.text);
138
+ }
139
+ return textParts === undefined ? (firstText ?? "") : textParts.join("\n");
140
+ }
141
+
142
+ function getArrayContentImages(content: (TextContent | ImageContent)[]): ImageContent[] {
143
+ let images: ImageContent[] | undefined;
144
+ for (const part of content) {
145
+ if (part.type !== "image") continue;
146
+ if (images === undefined) images = [];
147
+ images.push(part);
148
+ }
149
+ return images ?? [];
150
+ }
151
+
152
+ function wrapSteeringUserMessage(message: UserMessage): UserMessage {
153
+ if (typeof message.content === "string") {
154
+ if (message.content.length === 0) return message;
155
+ return { ...userMessageWithoutSteering(message), content: renderSteeringEnvelope(message.content) };
156
+ }
157
+
158
+ const text = getArrayContentText(message.content);
159
+ if (text.length === 0) return message;
160
+ const content: (TextContent | ImageContent)[] = [{ type: "text", text: renderSteeringEnvelope(text) }];
161
+ content.push(...getArrayContentImages(message.content));
162
+ return { ...userMessageWithoutSteering(message), content };
163
+ }
164
+
165
+ export function wrapSteeringForModel(messages: AgentMessage[]): AgentMessage[] {
166
+ const last = messages[messages.length - 1];
167
+ if (!isSteeringUserMessage(last)) return messages;
168
+
169
+ let firstSteer = messages.length - 1;
170
+ while (firstSteer > 0 && isSteeringUserMessage(messages[firstSteer - 1])) {
171
+ firstSteer--;
172
+ }
173
+
174
+ let wrappedMessages: AgentMessage[] | undefined;
175
+ for (let i = firstSteer; i < messages.length; i++) {
176
+ const message = messages[i];
177
+ if (!isSteeringUserMessage(message)) continue;
178
+ const wrappedMessage = wrapSteeringUserMessage(message);
179
+ if (wrappedMessage === message) continue;
180
+ if (wrappedMessages === undefined) {
181
+ wrappedMessages = messages.slice();
182
+ }
183
+ wrappedMessages[i] = wrappedMessage;
184
+ }
185
+ return wrappedMessages ?? messages;
186
+ }
187
+
108
188
  function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
109
189
  if (message.prunedAt === undefined) {
110
190
  return message.content;