@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-1hmwwt45.md} +88 -0
  3. package/dist/cli.js +3156 -3163
  4. package/dist/docs-index.generated.txt +1 -1
  5. package/dist/types/cli/stats-cli.d.ts +1 -6
  6. package/dist/types/commands/stats.d.ts +4 -0
  7. package/dist/types/config/model-discovery.d.ts +2 -0
  8. package/dist/types/config/model-registry.d.ts +6 -1
  9. package/dist/types/config/settings-schema.d.ts +10 -0
  10. package/dist/types/debug/report-bundle.d.ts +7 -2
  11. package/dist/types/extensibility/extensions/loader.d.ts +2 -0
  12. package/dist/types/extensibility/extensions/runner.d.ts +24 -6
  13. package/dist/types/extensibility/extensions/types.d.ts +60 -0
  14. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
  15. package/dist/types/launch/presence.d.ts +11 -0
  16. package/dist/types/lsp/client.d.ts +22 -0
  17. package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
  18. package/dist/types/mcp/config.d.ts +3 -1
  19. package/dist/types/modes/components/tool-execution.d.ts +3 -1
  20. package/dist/types/registry/agent-lifecycle.d.ts +11 -0
  21. package/dist/types/registry/agent-registry.d.ts +4 -0
  22. package/dist/types/session/agent-session-types.d.ts +0 -2
  23. package/dist/types/session/agent-session.d.ts +20 -0
  24. package/dist/types/session/date-cwd-reminder.d.ts +22 -0
  25. package/dist/types/session/irc-bridge.d.ts +2 -0
  26. package/dist/types/session/messages.d.ts +4 -0
  27. package/dist/types/session/session-loader.d.ts +18 -11
  28. package/dist/types/session/session-maintenance.d.ts +6 -2
  29. package/dist/types/session/session-manager.d.ts +19 -3
  30. package/dist/types/session/session-tools.d.ts +0 -1
  31. package/dist/types/session/turn-recovery.d.ts +7 -7
  32. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
  33. package/dist/types/tools/browser/attach.d.ts +16 -0
  34. package/dist/types/tools/browser/launch.d.ts +2 -2
  35. package/dist/types/tools/browser.d.ts +1 -1
  36. package/dist/types/tools/builtin-names.d.ts +1 -1
  37. package/dist/types/tools/file-write-fallback.d.ts +124 -0
  38. package/dist/types/tools/hub/jobs.d.ts +6 -0
  39. package/dist/types/tools/hub/types.d.ts +6 -0
  40. package/dist/types/tools/index.d.ts +1 -0
  41. package/dist/types/tools/path-utils.d.ts +23 -0
  42. package/dist/types/vibe/runtime.d.ts +3 -3
  43. package/package.json +13 -13
  44. package/src/auto-thinking/classifier.ts +37 -23
  45. package/src/cli/models-cli.ts +1 -1
  46. package/src/cli/stats-cli.ts +6 -72
  47. package/src/commands/stats.ts +7 -4
  48. package/src/commit/analysis/conventional.ts +15 -9
  49. package/src/commit/analysis/summary.ts +15 -9
  50. package/src/commit/changelog/generate.ts +15 -9
  51. package/src/commit/map-reduce/map-phase.ts +3 -19
  52. package/src/commit/map-reduce/reduce-phase.ts +15 -9
  53. package/src/config/model-discovery.ts +3 -3
  54. package/src/config/model-registry.ts +29 -16
  55. package/src/config/settings-schema.ts +13 -0
  56. package/src/debug/report-bundle.ts +25 -59
  57. package/src/discovery/claude-plugins.ts +2 -1
  58. package/src/discovery/claude.ts +4 -2
  59. package/src/discovery/helpers.ts +5 -0
  60. package/src/edit/hashline/filesystem.ts +9 -3
  61. package/src/edit/modes/patch.ts +31 -5
  62. package/src/edit/renderer.ts +48 -19
  63. package/src/extensibility/extensions/loader.ts +20 -5
  64. package/src/extensibility/extensions/runner.ts +239 -24
  65. package/src/extensibility/extensions/types.ts +62 -0
  66. package/src/extensibility/extensions/wrapper.ts +26 -11
  67. package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
  68. package/src/extensibility/plugins/marketplace/manager.ts +15 -18
  69. package/src/irc/bus.ts +1 -1
  70. package/src/launch/broker.ts +8 -1
  71. package/src/launch/presence.ts +77 -1
  72. package/src/lsp/client.ts +35 -3
  73. package/src/lsp/clients/biome-client.ts +47 -93
  74. package/src/lsp/servers.ts +17 -11
  75. package/src/lsp/tool.ts +2 -4
  76. package/src/lsp/workspace-diagnostics.ts +24 -2
  77. package/src/lsp/writethrough.ts +20 -10
  78. package/src/mcp/config.ts +50 -6
  79. package/src/memories/index.ts +29 -25
  80. package/src/mnemopi/backend.ts +13 -11
  81. package/src/modes/components/ask-dialog.ts +25 -5
  82. package/src/modes/components/tool-execution.ts +10 -6
  83. package/src/modes/components/welcome.ts +4 -1
  84. package/src/modes/controllers/extension-ui-controller.ts +52 -32
  85. package/src/modes/controllers/selector-controller.ts +6 -0
  86. package/src/modes/controllers/tan-command-controller.ts +1 -0
  87. package/src/modes/interactive-mode.ts +49 -10
  88. package/src/modes/theme/defaults/birch.json +2 -2
  89. package/src/prompts/system/checkpoint-active-notice.md +5 -0
  90. package/src/prompts/system/date-cwd-reminder.md +3 -0
  91. package/src/prompts/system/project-prompt.md +0 -1
  92. package/src/prompts/system/rewind-report.md +1 -3
  93. package/src/prompts/tools/browser.md +1 -0
  94. package/src/prompts/tools/rewind.md +1 -13
  95. package/src/registry/agent-lifecycle.ts +34 -0
  96. package/src/registry/agent-registry.ts +27 -2
  97. package/src/registry/persisted-agents.ts +40 -20
  98. package/src/sdk.ts +32 -2
  99. package/src/session/agent-session-types.ts +0 -2
  100. package/src/session/agent-session.ts +178 -46
  101. package/src/session/date-cwd-reminder.ts +77 -0
  102. package/src/session/irc-bridge.ts +5 -0
  103. package/src/session/messages.ts +5 -1
  104. package/src/session/session-loader.ts +106 -64
  105. package/src/session/session-maintenance.ts +189 -115
  106. package/src/session/session-manager.ts +142 -35
  107. package/src/session/session-persistence.ts +4 -4
  108. package/src/session/session-storage.ts +18 -5
  109. package/src/session/session-tools.ts +5 -10
  110. package/src/session/turn-recovery.ts +102 -20
  111. package/src/session/unexpected-stop-classifier.ts +33 -21
  112. package/src/slash-commands/builtin-session.ts +1 -1
  113. package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
  114. package/src/system-prompt.ts +0 -5
  115. package/src/task/executor.ts +4 -15
  116. package/src/task/persisted-revive.ts +3 -6
  117. package/src/tools/ask.ts +10 -3
  118. package/src/tools/browser/attach.ts +80 -25
  119. package/src/tools/browser/launch.ts +44 -15
  120. package/src/tools/browser/relay/daemon.ts +3 -7
  121. package/src/tools/browser/render.ts +1 -1
  122. package/src/tools/browser/shared-daemon.ts +3 -7
  123. package/src/tools/browser.ts +5 -5
  124. package/src/tools/builtin-names.ts +7 -3
  125. package/src/tools/checkpoint.ts +1 -7
  126. package/src/tools/file-write-fallback.ts +467 -0
  127. package/src/tools/hub/index.ts +1 -1
  128. package/src/tools/hub/jobs.ts +20 -3
  129. package/src/tools/hub/types.ts +6 -0
  130. package/src/tools/index.ts +1 -0
  131. package/src/tools/path-utils.ts +79 -0
  132. package/src/tools/render-utils.ts +53 -21
  133. package/src/tools/think.ts +15 -3
  134. package/src/tts/speech-enhancer.ts +19 -14
  135. package/src/utils/commit-message-generator.ts +14 -12
  136. package/src/utils/title-generator.ts +23 -19
  137. package/src/vibe/runtime.ts +32 -17
@@ -2,14 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { getBlobsDir, isEnoent, parseJsonlLenient } from "@oh-my-pi/pi-utils";
3
3
  import { BlobStore, isBlobRef, resolveImageData, resolveImageDataUrl } from "./blob-store";
4
4
  import { buildSessionContext } from "./session-context";
5
- import {
6
- type FileEntry,
7
- type RawFileEntry,
8
- SESSION_TITLE_SLOT_BYTES,
9
- type SessionEntry,
10
- type SessionHeader,
11
- type SessionTitleSlotEntry,
12
- } from "./session-entries";
5
+ import type { FileEntry, RawFileEntry, SessionEntry, SessionHeader } from "./session-entries";
13
6
  import { migrateToCurrentVersion } from "./session-migrations";
14
7
  import { isImageBlock, isImageDataPayload } from "./session-persistence";
15
8
  import { FileSessionStorage, type SessionStorage } from "./session-storage";
@@ -33,6 +26,15 @@ export interface VisitEntriesFromFileStreamOptions {
33
26
  yieldEveryBytes?: number;
34
27
  /** Yield to the macrotask queue after this many entries have been visited. */
35
28
  yieldEveryEntries?: number;
29
+ /** Called once for every malformed JSONL record skipped by the stream. */
30
+ onMalformedRecord?: () => void;
31
+ }
32
+
33
+ /** Parsed session entries plus corruption metadata needed by writable loaders. */
34
+ export interface SessionLoadResult {
35
+ entries: FileEntry[];
36
+ titleSlot: SessionTitleUpdate | undefined;
37
+ malformedRecords: number;
36
38
  }
37
39
 
38
40
  function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
@@ -42,31 +44,35 @@ function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpda
42
44
  return { body: content.slice(newlineIndex + 1), slot };
43
45
  }
44
46
 
45
- function foldTitleSlot(entries: FileEntry[], slot: SessionTitleUpdate | undefined): FileEntry[] {
46
- if (!slot || entries.length === 0) return entries;
47
- const header = entries[0] as SessionHeader;
48
- if (header.type !== "session" || typeof header.id !== "string") return entries;
47
+ function isValidSessionHeader(entry: FileEntry | undefined): entry is SessionHeader {
48
+ return entry?.type === "session" && typeof entry.id === "string";
49
+ }
50
+
51
+ function applyTitleSlot(entry: FileEntry | undefined, slot: SessionTitleUpdate | undefined): void {
52
+ if (!slot || !isValidSessionHeader(entry)) return;
49
53
  if (slot.title && slot.title.length > 0) {
50
- header.title = slot.title;
54
+ entry.title = slot.title;
51
55
  } else {
52
- delete header.title;
56
+ delete entry.title;
53
57
  }
54
58
  if (slot.source) {
55
- header.titleSource = slot.source;
59
+ entry.titleSource = slot.source;
56
60
  } else {
57
- delete header.titleSource;
61
+ delete entry.titleSource;
58
62
  }
59
- return entries;
60
63
  }
61
64
 
62
65
  /** Parse session JSONL while stripping and folding the optional fixed title slot. */
63
- export function parseSessionContent(content: string): {
64
- entries: FileEntry[];
65
- titleSlot: SessionTitleUpdate | undefined;
66
- } {
66
+ export function parseSessionContent(content: string): SessionLoadResult {
67
67
  const { body, slot } = splitTitleSlot(content);
68
- const entries = parseJsonlLenient<RawFileEntry>(body) as FileEntry[];
69
- return { entries: foldTitleSlot(entries, slot), titleSlot: slot };
68
+ let malformedRecords = 0;
69
+ const entries = parseJsonlLenient<RawFileEntry>(body, {
70
+ onMalformedRecord: () => {
71
+ malformedRecords++;
72
+ },
73
+ }) as FileEntry[];
74
+ applyTitleSlot(entries[0], slot);
75
+ return { entries, titleSlot: slot, malformedRecords };
70
76
  }
71
77
 
72
78
  /** Parse session JSONL and visit each entry without retaining prior entries. */
@@ -77,6 +83,7 @@ export async function visitEntriesFromFileStream(
77
83
  ): Promise<SessionTitleUpdate | undefined> {
78
84
  let titleSlot: SessionTitleUpdate | undefined;
79
85
  let sawFirstLine = false;
86
+ let sawFirstEntry = false;
80
87
  let bytesSinceYield = 0;
81
88
  let entriesSinceYield = 0;
82
89
  let recordsSeen = 0;
@@ -121,8 +128,13 @@ export async function visitEntriesFromFileStream(
121
128
  stopped = true;
122
129
  break;
123
130
  }
131
+ const entry = value as FileEntry;
132
+ if (!sawFirstEntry) {
133
+ sawFirstEntry = true;
134
+ applyTitleSlot(entry, titleSlot);
135
+ }
124
136
  try {
125
- if (visit(value as FileEntry) === false) {
137
+ if (visit(entry) === false) {
126
138
  stopped = true;
127
139
  break;
128
140
  }
@@ -143,6 +155,15 @@ export async function visitEntriesFromFileStream(
143
155
  // Malformed record: skip past the next newline and continue.
144
156
  const nextNewline = buffer.indexOf(0x0a, read);
145
157
  if (nextNewline === -1) break; // rest of the bad line not yet received
158
+ let nonWhitespace = false;
159
+ for (let index = read; index < nextNewline; index++) {
160
+ const byte = buffer[index];
161
+ if (byte !== 0x09 && byte !== 0x0d && byte !== 0x20) {
162
+ nonWhitespace = true;
163
+ break;
164
+ }
165
+ }
166
+ if (nonWhitespace) options.onMalformedRecord?.();
146
167
  recordsSeen++;
147
168
  buffer = buffer.subarray(nextNewline + 1);
148
169
  if (recordsSeen >= maxRecords) {
@@ -203,64 +224,85 @@ export async function visitEntriesFromFileStream(
203
224
  }
204
225
 
205
226
  /** Exported for testing — the ≥8MiB streaming path (works on any file size). */
206
- export async function loadEntriesFromFileStream(filePath: string): Promise<{
207
- entries: FileEntry[];
208
- titleSlot: SessionTitleUpdate | undefined;
209
- }> {
227
+ export async function loadEntriesFromFileStream(filePath: string): Promise<SessionLoadResult> {
210
228
  const entries: FileEntry[] = [];
211
- const titleSlot = await visitEntriesFromFileStream(filePath, entry => {
212
- entries.push(entry);
213
- });
214
- return { entries: foldTitleSlot(entries, titleSlot), titleSlot };
229
+ let malformedRecords = 0;
230
+ const titleSlot = await visitEntriesFromFileStream(
231
+ filePath,
232
+ entry => {
233
+ entries.push(entry);
234
+ },
235
+ {
236
+ onMalformedRecord: () => {
237
+ malformedRecords++;
238
+ },
239
+ },
240
+ );
241
+ return { entries, titleSlot, malformedRecords };
215
242
  }
216
243
 
217
- /** Read only the fixed-size head window to detect a physical title slot. */
218
- export async function readTitleSlotFromFile(
244
+ /** Exported for compaction.test.ts */
245
+ export function parseSessionEntries(content: string): FileEntry[] {
246
+ return parseSessionContent(content).entries;
247
+ }
248
+
249
+ function shouldStreamEntries(storage: SessionStorage, size: number): boolean {
250
+ return storage instanceof FileSessionStorage && size >= STREAM_LOAD_THRESHOLD_BYTES;
251
+ }
252
+
253
+ async function loadWithKnownSize(filePath: string, storage: SessionStorage, size: number): Promise<SessionLoadResult> {
254
+ const loaded = shouldStreamEntries(storage, size)
255
+ ? await loadEntriesFromFileStream(filePath)
256
+ : parseSessionContent(await storage.readText(filePath));
257
+ return isValidSessionHeader(loaded.entries[0]) ? loaded : { ...loaded, entries: [] };
258
+ }
259
+
260
+ /** Load and validate a session while retaining malformed-record diagnostics. */
261
+ export async function loadSessionFile(
219
262
  filePath: string,
220
263
  storage: SessionStorage = new FileSessionStorage(),
221
- ): Promise<SessionTitleSlotEntry | undefined> {
222
- let head: string;
264
+ ): Promise<SessionLoadResult> {
223
265
  try {
224
- [head] = await storage.readTextSlices(filePath, SESSION_TITLE_SLOT_BYTES, 0);
266
+ return await loadWithKnownSize(filePath, storage, storage.statSync(filePath).size);
225
267
  } catch (err) {
226
- if (isEnoent(err)) return undefined;
268
+ if (isEnoent(err)) return { entries: [], titleSlot: undefined, malformedRecords: 0 };
227
269
  throw err;
228
270
  }
229
- const newlineIndex = head.indexOf("\n");
230
- if (newlineIndex < 0) return undefined;
231
- return parseTitleSlotLine(head.slice(0, newlineIndex));
232
- }
233
- /** Exported for compaction.test.ts */
234
- export function parseSessionEntries(content: string): FileEntry[] {
235
- return parseSessionContent(content).entries;
236
271
  }
237
272
 
238
- /** Exported for testing */
273
+ /** Load the valid entries from a session file, skipping malformed records. */
239
274
  export async function loadEntriesFromFile(
240
275
  filePath: string,
241
276
  storage: SessionStorage = new FileSessionStorage(),
242
277
  ): Promise<FileEntry[]> {
243
- let loaded: { entries: FileEntry[]; titleSlot: SessionTitleUpdate | undefined };
244
- try {
245
- const stat = storage.statSync(filePath);
246
- loaded =
247
- storage instanceof FileSessionStorage && stat.size >= STREAM_LOAD_THRESHOLD_BYTES
248
- ? await loadEntriesFromFileStream(filePath)
249
- : parseSessionContent(await storage.readText(filePath));
250
- } catch (err) {
251
- if (isEnoent(err)) return [];
252
- throw err;
253
- }
254
- const { entries } = loaded;
278
+ return (await loadSessionFile(filePath, storage)).entries;
279
+ }
255
280
 
256
- // Validate session header
257
- if (entries.length === 0) return entries;
258
- const header = entries[0] as SessionHeader;
259
- if (header.type !== "session" || typeof header.id !== "string") {
260
- return [];
281
+ /**
282
+ * Visit session entries, using bounded streaming for large file-backed journals.
283
+ * Small files and non-file backends keep the existing full-load path.
284
+ */
285
+ export async function visitEntriesFromFile(
286
+ filePath: string,
287
+ visit: (entry: FileEntry) => void | boolean,
288
+ storage: SessionStorage = new FileSessionStorage(),
289
+ ): Promise<void> {
290
+ const size = storage.statSync(filePath).size;
291
+ if (shouldStreamEntries(storage, size)) {
292
+ let sawFirstEntry = false;
293
+ await visitEntriesFromFileStream(filePath, entry => {
294
+ if (!sawFirstEntry) {
295
+ sawFirstEntry = true;
296
+ if (!isValidSessionHeader(entry)) return false;
297
+ }
298
+ return visit(entry);
299
+ });
300
+ return;
261
301
  }
262
302
 
263
- return entries;
303
+ for (const entry of (await loadWithKnownSize(filePath, storage, size)).entries) {
304
+ if (visit(entry) === false) return;
305
+ }
264
306
  }
265
307
 
266
308
  /**