@oh-my-pi/pi-coding-agent 17.3.3 → 17.3.5

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 (154) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/{CHANGELOG-s5t1xdmy.md → CHANGELOG-tt9k4jpr.md} +81 -0
  3. package/dist/cli.js +3724 -3705
  4. package/dist/docs-index.generated.txt +1 -1
  5. package/dist/types/config/model-discovery.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +10 -0
  8. package/dist/types/debug/report-bundle.d.ts +7 -2
  9. package/dist/types/extensibility/extensions/loader.d.ts +2 -0
  10. package/dist/types/extensibility/extensions/runner.d.ts +3 -3
  11. package/dist/types/extensibility/extensions/types.d.ts +2 -0
  12. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
  13. package/dist/types/launch/presence.d.ts +11 -0
  14. package/dist/types/lsp/client.d.ts +22 -0
  15. package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
  16. package/dist/types/markit/converters/pdf/index.d.ts +2 -1
  17. package/dist/types/mcp/config.d.ts +3 -1
  18. package/dist/types/modes/components/tool-execution.d.ts +3 -1
  19. package/dist/types/registry/agent-lifecycle.d.ts +11 -0
  20. package/dist/types/registry/agent-registry.d.ts +4 -0
  21. package/dist/types/session/agent-session-types.d.ts +0 -2
  22. package/dist/types/session/agent-session.d.ts +20 -0
  23. package/dist/types/session/date-cwd-reminder.d.ts +22 -0
  24. package/dist/types/session/irc-bridge.d.ts +2 -0
  25. package/dist/types/session/messages.d.ts +4 -0
  26. package/dist/types/session/session-loader.d.ts +18 -11
  27. package/dist/types/session/session-maintenance.d.ts +6 -2
  28. package/dist/types/session/session-manager.d.ts +19 -3
  29. package/dist/types/session/session-tools.d.ts +0 -1
  30. package/dist/types/session/turn-recovery.d.ts +7 -7
  31. package/dist/types/tools/browser/attach.d.ts +16 -0
  32. package/dist/types/tools/browser/launch.d.ts +2 -2
  33. package/dist/types/tools/browser.d.ts +1 -1
  34. package/dist/types/tools/builtin-names.d.ts +1 -1
  35. package/dist/types/tools/hub/jobs.d.ts +6 -0
  36. package/dist/types/tools/hub/types.d.ts +6 -0
  37. package/dist/types/tools/read-pdf.d.ts +15 -0
  38. package/dist/types/utils/external-editor.d.ts +7 -0
  39. package/dist/types/utils/markit.d.ts +3 -4
  40. package/dist/types/vibe/runtime.d.ts +3 -3
  41. package/package.json +13 -16
  42. package/scripts/build-binary.ts +0 -2
  43. package/scripts/bundle-dist.ts +0 -1
  44. package/src/auto-thinking/classifier.ts +37 -23
  45. package/src/cli/models-cli.ts +1 -1
  46. package/src/cli/read-cli.ts +2 -0
  47. package/src/commit/analysis/conventional.ts +15 -9
  48. package/src/commit/analysis/summary.ts +15 -9
  49. package/src/commit/changelog/generate.ts +15 -9
  50. package/src/commit/map-reduce/map-phase.ts +3 -19
  51. package/src/commit/map-reduce/reduce-phase.ts +15 -9
  52. package/src/config/model-discovery.ts +3 -3
  53. package/src/config/model-registry.ts +29 -16
  54. package/src/config/settings-schema.ts +13 -0
  55. package/src/debug/report-bundle.ts +25 -59
  56. package/src/discovery/claude-plugins.ts +2 -1
  57. package/src/discovery/claude.ts +4 -2
  58. package/src/discovery/helpers.ts +5 -0
  59. package/src/edit/renderer.ts +48 -19
  60. package/src/extensibility/extensions/loader.ts +9 -5
  61. package/src/extensibility/extensions/runner.ts +124 -21
  62. package/src/extensibility/extensions/types.ts +2 -0
  63. package/src/extensibility/extensions/wrapper.ts +16 -10
  64. package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
  65. package/src/extensibility/plugins/marketplace/manager.ts +15 -18
  66. package/src/irc/bus.ts +1 -1
  67. package/src/launch/broker.ts +8 -1
  68. package/src/launch/presence.ts +77 -1
  69. package/src/lsp/client.ts +35 -3
  70. package/src/lsp/clients/biome-client.ts +47 -93
  71. package/src/lsp/servers.ts +17 -11
  72. package/src/lsp/tool.ts +2 -4
  73. package/src/lsp/workspace-diagnostics.ts +24 -2
  74. package/src/markit/NOTICE +8 -8
  75. package/src/markit/converters/pdf/index.ts +14 -126
  76. package/src/mcp/client.ts +8 -9
  77. package/src/mcp/config.ts +50 -6
  78. package/src/memories/index.ts +29 -25
  79. package/src/mnemopi/backend.ts +13 -11
  80. package/src/modes/components/ask-dialog.ts +25 -5
  81. package/src/modes/components/tool-execution.ts +10 -6
  82. package/src/modes/components/welcome.ts +4 -1
  83. package/src/modes/controllers/extension-ui-controller.ts +52 -32
  84. package/src/modes/controllers/selector-controller.ts +6 -0
  85. package/src/modes/controllers/tan-command-controller.ts +1 -0
  86. package/src/modes/interactive-mode.ts +49 -10
  87. package/src/modes/rpc/rpc-client.ts +7 -0
  88. package/src/modes/theme/defaults/birch.json +2 -2
  89. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  90. package/src/prompts/system/checkpoint-active-notice.md +5 -0
  91. package/src/prompts/system/date-cwd-reminder.md +3 -0
  92. package/src/prompts/system/project-prompt.md +0 -1
  93. package/src/prompts/system/rewind-report.md +1 -3
  94. package/src/prompts/tools/browser.md +1 -0
  95. package/src/prompts/tools/rewind.md +1 -13
  96. package/src/registry/agent-lifecycle.ts +34 -0
  97. package/src/registry/agent-registry.ts +27 -2
  98. package/src/registry/persisted-agents.ts +40 -20
  99. package/src/sdk.ts +32 -2
  100. package/src/session/agent-session-types.ts +0 -2
  101. package/src/session/agent-session.ts +178 -46
  102. package/src/session/date-cwd-reminder.ts +77 -0
  103. package/src/session/irc-bridge.ts +5 -0
  104. package/src/session/messages.ts +5 -1
  105. package/src/session/session-loader.ts +106 -64
  106. package/src/session/session-maintenance.ts +189 -115
  107. package/src/session/session-manager.ts +142 -35
  108. package/src/session/session-persistence.ts +4 -4
  109. package/src/session/session-storage.ts +18 -5
  110. package/src/session/session-tools.ts +5 -10
  111. package/src/session/turn-recovery.ts +102 -20
  112. package/src/session/unexpected-stop-classifier.ts +33 -21
  113. package/src/system-prompt.ts +0 -5
  114. package/src/task/executor.ts +4 -15
  115. package/src/task/persisted-revive.ts +3 -6
  116. package/src/tools/ask.ts +10 -3
  117. package/src/tools/browser/attach.ts +80 -25
  118. package/src/tools/browser/launch.ts +44 -15
  119. package/src/tools/browser/relay/daemon.ts +3 -7
  120. package/src/tools/browser/render.ts +1 -1
  121. package/src/tools/browser/shared-daemon.ts +3 -7
  122. package/src/tools/browser.ts +5 -5
  123. package/src/tools/builtin-names.ts +7 -3
  124. package/src/tools/checkpoint.ts +1 -7
  125. package/src/tools/hub/index.ts +1 -1
  126. package/src/tools/hub/jobs.ts +20 -3
  127. package/src/tools/hub/types.ts +6 -0
  128. package/src/tools/read-pdf.ts +135 -0
  129. package/src/tools/read.ts +63 -37
  130. package/src/tools/render-utils.ts +53 -21
  131. package/src/tools/think.ts +15 -3
  132. package/src/tts/speech-enhancer.ts +19 -14
  133. package/src/utils/commit-message-generator.ts +14 -12
  134. package/src/utils/external-editor.ts +24 -5
  135. package/src/utils/markit.ts +6 -44
  136. package/src/utils/title-generator.ts +23 -19
  137. package/src/vibe/runtime.ts +32 -17
  138. package/dist/types/markit/converters/pdf/columns.d.ts +0 -35
  139. package/dist/types/markit/converters/pdf/extract.d.ts +0 -10
  140. package/dist/types/markit/converters/pdf/grid.d.ts +0 -25
  141. package/dist/types/markit/converters/pdf/headers.d.ts +0 -24
  142. package/dist/types/markit/converters/pdf/render.d.ts +0 -24
  143. package/dist/types/markit/converters/pdf/types.d.ts +0 -75
  144. package/dist/types/tools/read-pdf-images.d.ts +0 -12
  145. package/dist/types/utils/mupdf-wasm-embed.d.ts +0 -1
  146. package/scripts/embed-mupdf-wasm.ts +0 -67
  147. package/src/markit/converters/pdf/columns.ts +0 -103
  148. package/src/markit/converters/pdf/extract.ts +0 -598
  149. package/src/markit/converters/pdf/grid.ts +0 -780
  150. package/src/markit/converters/pdf/headers.ts +0 -106
  151. package/src/markit/converters/pdf/render.ts +0 -501
  152. package/src/markit/converters/pdf/types.ts +0 -84
  153. package/src/tools/read-pdf-images.ts +0 -250
  154. package/src/utils/mupdf-wasm-embed.ts +0 -12
@@ -15,7 +15,7 @@
15
15
  * mechanical {@link SpeakableStream} cleanup — speech never blocks on the
16
16
  * model.
17
17
  */
18
- import { type AssistantMessage, completeSimple } from "@oh-my-pi/pi-ai";
18
+ import { type AssistantMessage, completeSimple, retryTransientCompletion } from "@oh-my-pi/pi-ai";
19
19
  import { logger, prompt } from "@oh-my-pi/pi-utils";
20
20
  import type { ModelRegistry } from "../config/model-registry";
21
21
  import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
@@ -82,20 +82,25 @@ export class SpeechEnhancer {
82
82
  if (!apiKey) return null;
83
83
  // Resolve metadata after getApiKey so the session-sticky credential is recorded first.
84
84
  const metadata = this.#deps.metadataResolver?.(model.provider);
85
- const timeout = AbortSignal.timeout(REWRITE_TIMEOUT_MS);
86
- const response = await completeSimple(
87
- model,
88
- {
89
- systemPrompt: [SYSTEM_PROMPT],
90
- messages: [{ role: "user", content: boundBlock(block), timestamp: Date.now() }],
91
- },
92
- {
93
- apiKey: registry.resolver(model, sessionId),
94
- maxTokens: ANSWER_MAX_TOKENS,
95
- disableReasoning: true,
96
- metadata,
97
- signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
85
+ const response = await retryTransientCompletion(
86
+ () => {
87
+ const timeout = AbortSignal.timeout(REWRITE_TIMEOUT_MS);
88
+ return completeSimple(
89
+ model,
90
+ {
91
+ systemPrompt: [SYSTEM_PROMPT],
92
+ messages: [{ role: "user", content: boundBlock(block), timestamp: Date.now() }],
93
+ },
94
+ {
95
+ apiKey: registry.resolver(model, sessionId),
96
+ maxTokens: ANSWER_MAX_TOKENS,
97
+ disableReasoning: true,
98
+ metadata,
99
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
100
+ },
101
+ );
98
102
  },
103
+ { signal },
99
104
  );
100
105
  if (response.stopReason === "error") {
101
106
  logger.debug("speech-enhancer: rewrite errored", { error: response.errorMessage });
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
6
  import type { Api, Model } from "@oh-my-pi/pi-ai";
7
- import { completeSimple } from "@oh-my-pi/pi-ai";
7
+ import { completeSimple, retryTransientCompletion } from "@oh-my-pi/pi-ai";
8
8
  import { logger, prompt } from "@oh-my-pi/pi-utils";
9
9
 
10
10
  import type { ModelRegistry } from "../config/model-registry";
@@ -106,17 +106,19 @@ export async function generateCommitMessage(
106
106
 
107
107
  try {
108
108
  const maxTokens = COMMIT_MAX_TOKENS;
109
- const response = await completeSimple(
110
- candidate.model,
111
- {
112
- systemPrompt: [COMMIT_SYSTEM_PROMPT],
113
- messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
114
- },
115
- {
116
- apiKey: registry.resolver(candidate.model, sessionId),
117
- maxTokens,
118
- reasoning: toReasoningEffort(candidate.thinkingLevel),
119
- },
109
+ const response = await retryTransientCompletion(() =>
110
+ completeSimple(
111
+ candidate.model,
112
+ {
113
+ systemPrompt: [COMMIT_SYSTEM_PROMPT],
114
+ messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
115
+ },
116
+ {
117
+ apiKey: registry.resolver(candidate.model, sessionId),
118
+ maxTokens,
119
+ reasoning: toReasoningEffort(candidate.thinkingLevel),
120
+ },
121
+ ),
120
122
  );
121
123
 
122
124
  if (response.stopReason === "error") {
@@ -33,6 +33,27 @@ export interface OpenInEditorOptions {
33
33
  trimTrailingNewline?: boolean;
34
34
  }
35
35
 
36
+ /** Subprocess argv and Windows quoting mode used to launch an external editor. */
37
+ export interface EditorSpawnCommand {
38
+ cmd: string[];
39
+ windowsVerbatimArguments: boolean;
40
+ }
41
+
42
+ /** Resolves shell argv without letting the host runtime re-quote the editor command. */
43
+ export function resolveEditorSpawnCommand(
44
+ editorCmd: string,
45
+ tmpFile: string,
46
+ platform: NodeJS.Platform = process.platform,
47
+ ): EditorSpawnCommand {
48
+ const windows = platform === "win32";
49
+ // cmd.exe strips the outer /s /c quote pair; Bun must pass the embedded
50
+ // editor/path quotes verbatim instead of applying argv escaping to them.
51
+ const cmd = windows
52
+ ? ["cmd.exe", "/d", "/s", "/c", `"${editorCmd} "${tmpFile}""`]
53
+ : [$which("sh") ?? "sh", "-c", `${editorCmd} "$1"`, "sh", tmpFile];
54
+ return { cmd, windowsVerbatimArguments: windows };
55
+ }
56
+
36
57
  /**
37
58
  * Opens `content` in the user's external editor and returns the edited text.
38
59
  * Returns `null` if the editor exits with a non-zero code.
@@ -50,15 +71,13 @@ export async function openInEditor(
50
71
  try {
51
72
  await Bun.write(tmpFile, content);
52
73
 
74
+ const spawnCommand = resolveEditorSpawnCommand(editorCmd, tmpFile);
53
75
  const [stdin, stdout, stderr] = options?.stdio ?? ["inherit", "inherit", "inherit"];
54
- const cmd =
55
- process.platform === "win32"
56
- ? ["cmd", "/c", `${editorCmd} "${tmpFile}"`]
57
- : [$which("sh") ?? "sh", "-c", `${editorCmd} "$1"`, "sh", tmpFile];
58
- const child = Bun.spawn(cmd, {
76
+ const child = Bun.spawn(spawnCommand.cmd, {
59
77
  stdin,
60
78
  stdout,
61
79
  stderr,
80
+ windowsVerbatimArguments: spawnCommand.windowsVerbatimArguments,
62
81
  });
63
82
  const exitCode = await child.exited;
64
83
  if (exitCode === 0) {
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { logger, untilAborted } from "@oh-my-pi/pi-utils";
2
+ import { untilAborted } from "@oh-my-pi/pi-utils";
3
3
  import type { ConversionResult, Markit, StreamInfo } from "../markit";
4
4
  import { ToolAbortError } from "../tools/tool-errors";
5
5
  import {
@@ -8,7 +8,6 @@ import {
8
8
  readMarkitConversionCache,
9
9
  writeMarkitConversionCache,
10
10
  } from "./markit-cache";
11
- import { loadEmbeddedMupdfWasm } from "./mupdf-wasm-embed";
12
11
 
13
12
  /**
14
13
  * File extensions markit can actually convert to markdown — one per registered
@@ -31,53 +30,16 @@ export interface MarkitConversionResult {
31
30
 
32
31
  export interface MarkitFileConversionOptions {
33
32
  /**
34
- * Directory the PDF converter writes extracted images/diagrams into. When
35
- * set, each embedded image is rendered to `<id>.png` and referenced by path
36
- * in the markdown; when unset, markit emits an `<!-- image: <id> ... -->`
37
- * placeholder comment instead.
33
+ * Directory converters may use for extracted image or diagram files. Since
34
+ * those files are conversion side effects, conversions using this option
35
+ * bypass the markdown cache.
38
36
  */
39
37
  imageDir?: string;
40
38
  }
41
39
 
42
- interface MuPdfWasmModuleConfig {
43
- print?: (...values: unknown[]) => void;
44
- printErr?: (...values: unknown[]) => void;
45
- wasmBinary?: Uint8Array;
46
- }
47
-
48
- function logMuPdfWasmOutput(stream: "stdout" | "stderr", values: unknown[]): void {
49
- const message = values.length === 1 && typeof values[0] === "string" ? values[0] : values.map(String).join(" ");
50
- logger.debug("mupdf wasm output", { stream, message });
51
- }
52
-
53
- // `$libmupdf_wasm_Module` is declared globally (as `any`) by the mupdf package.
54
- // Install print hooks before the WASM module initializes so its stdout/stderr
55
- // route to the file logger instead of corrupting the TUI.
56
- function installMuPdfWasmLogger(): void {
57
- const moduleConfig: MuPdfWasmModuleConfig = globalThis.$libmupdf_wasm_Module ?? {};
58
- moduleConfig.print = (...values: unknown[]) => logMuPdfWasmOutput("stdout", values);
59
- moduleConfig.printErr = (...values: unknown[]) => logMuPdfWasmOutput("stderr", values);
60
- globalThis.$libmupdf_wasm_Module = moduleConfig;
61
- }
62
-
63
- // Hand the WASM module its bytes directly when the compiled binary embedded them
64
- // (scripts/embed-mupdf-wasm.ts); a single-file binary has no node_modules for
65
- // mupdf to read `mupdf-wasm.wasm` from. Source/npm builds get undefined here and
66
- // mupdf loads its own wasm. Must run before the mupdf module evaluates.
67
- function installEmbeddedMupdfWasm(): void {
68
- const wasmBinary = loadEmbeddedMupdfWasm();
69
- if (!wasmBinary) return;
70
- const moduleConfig: MuPdfWasmModuleConfig = globalThis.$libmupdf_wasm_Module ?? {};
71
- moduleConfig.wasmBinary = wasmBinary;
72
- globalThis.$libmupdf_wasm_Module = moduleConfig;
73
- }
74
-
75
- installMuPdfWasmLogger();
76
-
77
40
  let markit: () => Markit | Promise<Markit> = async () => {
78
- // Lazy: keep the document engine (mammoth/mupdf) off the startup
79
- // import graph — it loads only when a document is first converted.
80
- installEmbeddedMupdfWasm();
41
+ // Lazy: keep the document engine off the startup import graph — it loads
42
+ // only when a document is first converted.
81
43
  const promise = import("../markit").then(({ Markit }) => {
82
44
  const instance = new Markit();
83
45
  markit = () => instance;
@@ -4,7 +4,7 @@
4
4
  import { dlopen, FFIType, ptr } from "bun:ffi";
5
5
  import * as path from "node:path";
6
6
 
7
- import { type Api, type AssistantMessage, completeSimple, type Model } from "@oh-my-pi/pi-ai";
7
+ import { type Api, type AssistantMessage, completeSimple, type Model, retryTransientCompletion } from "@oh-my-pi/pi-ai";
8
8
  import { StreamMarkupHealing } from "@oh-my-pi/pi-ai/utils/stream-markup-healing";
9
9
  import { isConPTYHosted } from "@oh-my-pi/pi-tui";
10
10
  import { isTerminalHeadless, logger, prompt } from "@oh-my-pi/pi-utils";
@@ -254,24 +254,28 @@ export async function generateTitleOnline(
254
254
  const maxTokens = TITLE_MAX_TOKENS;
255
255
  logger.debug("title-generator: request", { ...modelContext, maxTokens });
256
256
 
257
- const response = await completeSimple(
258
- model,
259
- {
260
- systemPrompt,
261
- messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
262
- },
263
- {
264
- apiKey: registry.resolver(model, sessionId),
265
- maxTokens,
266
- disableReasoning: true,
267
- // Greedy decode: titling is extraction, not generation. Backends that
268
- // default temperature high (e.g. Ollama's 0.8) otherwise garble names
269
- // from the message ("hashline" "HasHroshi"). Providers whose models
270
- // reject sampling params drop this via `supportsSamplingParams`.
271
- temperature: 0,
272
- metadata,
273
- signal,
274
- },
257
+ const response = await retryTransientCompletion(
258
+ () =>
259
+ completeSimple(
260
+ model,
261
+ {
262
+ systemPrompt,
263
+ messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
264
+ },
265
+ {
266
+ apiKey: registry.resolver(model, sessionId),
267
+ maxTokens,
268
+ disableReasoning: true,
269
+ // Greedy decode: titling is extraction, not generation. Backends that
270
+ // default temperature high (e.g. Ollama's 0.8) otherwise garble names
271
+ // from the message ("hashline" → "HasHroshi"). Providers whose models
272
+ // reject sampling params drop this via `supportsSamplingParams`.
273
+ temperature: 0,
274
+ metadata,
275
+ signal,
276
+ },
277
+ ),
278
+ { signal },
275
279
  );
276
280
 
277
281
  if (response.stopReason === "error") {
@@ -421,11 +421,17 @@ export class VibeSessionRegistry {
421
421
  }
422
422
 
423
423
  /**
424
- * Insert a bare worker record without the spawn/job machinery. Test-only —
425
- * lets {@link aggregateVibeWorkerTokensPerSecond} be exercised against a
426
- * fake roster + AgentRegistry session without driving a real turn.
424
+ * Insert a bare worker record without the spawn machinery. Test-only —
425
+ * lets focused runtime tests attach an optional synthetic in-flight job.
427
426
  */
428
- registerRecordForTests(record: { id: string; cli?: VibeCli; ownerId: string; state?: VibeSessionState }): void {
427
+ registerRecordForTests(record: {
428
+ id: string;
429
+ cli?: VibeCli;
430
+ ownerId: string;
431
+ state?: VibeSessionState;
432
+ jobId?: string;
433
+ }): void {
434
+ const now = Date.now();
429
435
  this.#records.set(record.id, {
430
436
  id: record.id,
431
437
  cli: record.cli ?? "fast",
@@ -434,8 +440,11 @@ export class VibeSessionRegistry {
434
440
  parentSessionFile: null,
435
441
  agent: getBundledAgent("sonic")!,
436
442
  state: record.state ?? "running",
437
- createdAt: Date.now(),
438
- lastActivityAt: Date.now(),
443
+ createdAt: now,
444
+ lastActivityAt: now,
445
+ turn: record.jobId
446
+ ? { jobId: record.jobId, message: "test turn", startedAt: now, trace: [], toolCount: 0 }
447
+ : undefined,
439
448
  queue: [],
440
449
  turnCount: 0,
441
450
  killed: false,
@@ -1113,25 +1122,31 @@ export class VibeSessionRegistry {
1113
1122
  if (job?.status === "running") runningJobs.push(job);
1114
1123
  }
1115
1124
 
1116
- let waited = false;
1125
+ let waitEndedByTimeout = false;
1117
1126
  if (runningJobs.length > 0 && collectSettled().length === 0) {
1118
- waited = true;
1119
1127
  const timeoutMs = Math.max(1, Math.trunc(args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
1120
1128
  const watchedJobIds = runningJobs.map(job => job.id);
1121
1129
  manager.watchJobs(watchedJobIds);
1122
- const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<void>();
1123
- const timeoutHandle = setTimeout(() => timeoutResolve(), timeoutMs);
1124
- const racePromises: Promise<unknown>[] = [...runningJobs.map(job => job.promise), timeoutPromise];
1130
+ const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<"timeout">();
1131
+ const timeoutHandle = setTimeout(() => timeoutResolve("timeout"), timeoutMs);
1132
+ const racePromises: Array<Promise<"settled" | "timeout" | "aborted">> = [
1133
+ ...runningJobs.map(job => job.promise.then(() => "settled" as const)),
1134
+ timeoutPromise,
1135
+ ];
1125
1136
  let abortCleanup: (() => void) | undefined;
1126
1137
  if (args.signal) {
1127
- const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<void>();
1128
- const onAbort = () => abortResolve();
1129
- args.signal.addEventListener("abort", onAbort, { once: true });
1130
- abortCleanup = () => args.signal?.removeEventListener("abort", onAbort);
1138
+ const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<"aborted">();
1139
+ const onAbort = () => abortResolve("aborted");
1140
+ if (args.signal.aborted) {
1141
+ onAbort();
1142
+ } else {
1143
+ args.signal.addEventListener("abort", onAbort, { once: true });
1144
+ abortCleanup = () => args.signal?.removeEventListener("abort", onAbort);
1145
+ }
1131
1146
  racePromises.push(abortPromise);
1132
1147
  }
1133
1148
  try {
1134
- await Promise.race(racePromises);
1149
+ waitEndedByTimeout = (await Promise.race(racePromises)) === "timeout";
1135
1150
  } finally {
1136
1151
  manager.unwatchJobs(watchedJobIds);
1137
1152
  clearTimeout(timeoutHandle);
@@ -1144,7 +1159,7 @@ export class VibeSessionRegistry {
1144
1159
  // Current in-flight state, independent of the snapshot: a session whose
1145
1160
  // watched turn settled may already be mid queued follow-up.
1146
1161
  const stillRunning = watched.filter(record => record.turn !== undefined).map(record => record.id);
1147
- return { settled, stillRunning, timedOut: waited && settled.length === 0 };
1162
+ return { settled, stillRunning, timedOut: waitEndedByTimeout && settled.length === 0 };
1148
1163
  }
1149
1164
 
1150
1165
  /** Detach one parent's process-local workers without tombstoning their persisted conversations. */
@@ -1,35 +0,0 @@
1
- /**
2
- * Multi-column layout detection and text box reordering.
3
- *
4
- * Many PDFs (legal documents, datasheets, academic papers) use two-column
5
- * layouts. Without column detection, text boxes are ordered by Y position
6
- * only, interleaving left and right column content.
7
- *
8
- * Algorithm:
9
- * 1. Collect left edges of all text boxes on the page
10
- * 2. Find the largest horizontal gap between consecutive left edges
11
- * 3. If gap > MIN_GAP_RATIO of the text width and both sides have
12
- * enough boxes → multi-column detected
13
- * 4. Assign each text box to a column based on its center X
14
- * 5. Return columns in reading order (left-to-right, top-to-bottom)
15
- *
16
- * This only detects the column structure. The caller is responsible for
17
- * processing each column's text boxes independently (table detection,
18
- * rendering, etc.).
19
- */
20
- import type { TextBox } from "./types.js";
21
- export interface ColumnLayout {
22
- /** Number of columns detected (1 = single column, 2+ = multi-column). */
23
- columnCount: number;
24
- /** Text boxes grouped by column, in reading order (left to right). */
25
- columns: TextBox[][];
26
- /** X positions of column boundaries (between columns). */
27
- boundaries: number[];
28
- }
29
- /**
30
- * Detect column layout and return text boxes grouped by column.
31
- *
32
- * For single-column pages, returns all boxes in one group.
33
- * For multi-column pages, returns boxes split by column in reading order.
34
- */
35
- export declare function detectColumns(textBoxes: TextBox[]): ColumnLayout;
@@ -1,10 +0,0 @@
1
- import type { ImageRegion, PageContent } from "./types.js";
2
- /**
3
- * Render an image region from a PDF page as a PNG buffer.
4
- * Uses mupdf's DrawDevice to render just the cropped area at 2x resolution.
5
- */
6
- export declare function renderImageRegion(input: Uint8Array, region: ImageRegion): Promise<Uint8Array>;
7
- /**
8
- * Extract text boxes and vector segments from all pages of a PDF buffer.
9
- */
10
- export declare function extractPages(input: Uint8Array): Promise<PageContent[]>;
@@ -1,25 +0,0 @@
1
- /**
2
- * Table grid detection from vector segments and text boxes.
3
- *
4
- * Ported from @oharato/pdf2md-ts with TypeScript types and without
5
- * CJK-specific borderless table heuristics. The core algorithm:
6
- *
7
- * 1. Classify segments as horizontal or vertical lines
8
- * 2. Group horizontal Y-lines into table groups (split by vertical gaps)
9
- * 3. For each group:
10
- * a. Full grid (H+V lines): build cells from grid intersections,
11
- * place text via raycasting
12
- * b. H-line only (no V lines): infer columns from text X positions
13
- * 4. Prune empty rows/cols
14
- *
15
- * Coordinate system: PDF native (bottom-left origin, Y increases upward).
16
- */
17
- import type { Segment, TableGrid, TextBox } from "./types.js";
18
- export interface GridResult {
19
- grids: TableGrid[];
20
- consumedIds: string[];
21
- }
22
- /**
23
- * Detect all table grids on a single page from its text boxes and segments.
24
- */
25
- export declare function resolveTableGrids(pageNumber: number, textBoxes: TextBox[], segments: Segment[]): GridResult;
@@ -1,24 +0,0 @@
1
- /**
2
- * Running header/footer detection and removal.
3
- *
4
- * Many PDFs have repeated text at the top or bottom of every page:
5
- * document titles, chapter names, page numbers, copyright notices.
6
- * These pollute the markdown output as false headings or noise.
7
- *
8
- * Algorithm:
9
- * 1. For each page, bucket text boxes by Y position (top/bottom zones)
10
- * 2. Collect the text content at each zone across all pages
11
- * 3. Text appearing on >20% of pages OR 8+ consecutive pages is a
12
- * running header/footer
13
- * 4. Remove matching text boxes before further processing
14
- */
15
- import type { PageContent } from "./types.js";
16
- /**
17
- * Detect and remove running headers and footers from all pages.
18
- * Mutates the pages array in place, removing header/footer text boxes.
19
- *
20
- * Uses two strategies:
21
- * 1. Global frequency: text appearing on > 20% of all pages
22
- * 2. Consecutive runs: text appearing on 8+ consecutive pages
23
- */
24
- export declare function stripHeadersFooters(pages: PageContent[]): void;
@@ -1,24 +0,0 @@
1
- /**
2
- * Markdown rendering for PDF pages.
3
- *
4
- * Converts table grids and free text boxes into markdown, handling:
5
- * - Table grid → markdown table (`| col | col |`)
6
- * - Free text → paragraphs with heading detection (by font size)
7
- * - Content ordering (top-to-bottom via Y coordinate)
8
- * - Paragraph wrap merging (lines broken across PDF line boundaries)
9
- * - Page number removal
10
- *
11
- * Ported from @oharato/pdf2md-ts, stripped of CJK/TDnet-specific logic.
12
- */
13
- import type { TableGrid, TextBox } from "./types.js";
14
- /**
15
- * Render a TableGrid as a markdown table.
16
- */
17
- export declare function renderTableToMarkdown(table: TableGrid): string;
18
- /**
19
- * Render one page's content: free text and tables interleaved top-to-bottom.
20
- */
21
- export declare function renderPageContent(freeTextBoxes: TextBox[], tables: TableGrid[], imageBlocks?: Array<{
22
- topY: number;
23
- markdown: string;
24
- }>, allTextBoxes?: TextBox[]): string;
@@ -1,75 +0,0 @@
1
- /** Bounding box in PDF coordinate space (origin = bottom-left). */
2
- export type Bounds = {
3
- left: number;
4
- right: number;
5
- /** Higher value = higher on the page. */
6
- top: number;
7
- bottom: number;
8
- };
9
- /** A text fragment with position and font metadata. */
10
- export type TextBox = {
11
- id: string;
12
- text: string;
13
- bounds: Bounds;
14
- pageNumber: number;
15
- /** Dominant font size in points. */
16
- fontSize: number;
17
- /** True if rendered bold (font name or rendering mode). */
18
- isBold: boolean;
19
- };
20
- /** A horizontal or vertical line segment extracted from vector graphics. */
21
- export type Segment = {
22
- id: string;
23
- x1: number;
24
- y1: number;
25
- x2: number;
26
- y2: number;
27
- };
28
- /** A single cell in a resolved table grid. */
29
- export type TableCell = {
30
- row: number;
31
- col: number;
32
- text: string;
33
- rowSpan: number;
34
- colSpan: number;
35
- };
36
- /** A resolved table grid ready for markdown rendering. */
37
- export type TableGrid = {
38
- pageNumber: number;
39
- rows: number;
40
- cols: number;
41
- cells: TableCell[];
42
- warnings: string[];
43
- /** Top Y coordinate (PDF space: larger = higher on page). */
44
- topY: number;
45
- /** True for tables detected without vector borders. */
46
- isBorderless: boolean;
47
- };
48
- /** An image/diagram region detected on a page. */
49
- export type ImageRegion = {
50
- id: string;
51
- pageNumber: number;
52
- /** Bounding box in mupdf coordinates (top-left origin). */
53
- bbox: {
54
- x: number;
55
- y: number;
56
- w: number;
57
- h: number;
58
- };
59
- /** Y position in PDF coordinates (bottom-left) for ordering. */
60
- topY: number;
61
- };
62
- /** Result of extracting content from a single PDF page. */
63
- export type PageContent = {
64
- pageNumber: number;
65
- textBoxes: TextBox[];
66
- segments: Segment[];
67
- images: ImageRegion[];
68
- };
69
- /** A block of rendered content (text paragraph or table). */
70
- export type ContentBlock = {
71
- topY: number;
72
- content: string;
73
- /** True if this line has wide gaps between text boxes (column headers). */
74
- isTabular?: boolean;
75
- };
@@ -1,12 +0,0 @@
1
- import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
- import type { ToolSession } from "../sdk.js";
3
- import type { ReadToolDetails } from "./read.js";
4
- export declare function rewritePdfImagePlaceholders(markdown: string, pdfPath: string): string;
5
- export declare function splitPdfImageMemberReadPath(readPath: string): {
6
- pdfPath: string;
7
- member: string;
8
- } | null;
9
- export declare function readPdfImageMember(session: ToolSession, autoResizeImages: boolean, absolutePdfPath: string, pdfDisplayPath: string, member: string, suffixResolution: {
10
- from: string;
11
- to: string;
12
- } | undefined, signal?: AbortSignal): Promise<AgentToolResult<ReadToolDetails>>;
@@ -1 +0,0 @@
1
- export declare function loadEmbeddedMupdfWasm(): Uint8Array | undefined;
@@ -1,67 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- // Embeds mupdf's `mupdf-wasm.wasm` into the compiled single-file binary.
4
- //
5
- // mupdf loads its wasm by reading the `mupdf-wasm.wasm` sibling of its own
6
- // module via `new URL(..., import.meta.url)` + `readFileSync`. A `bun --compile`
7
- // binary has no node_modules, so that read fails (`ENOENT .../mupdf-wasm.wasm`),
8
- // and marking mupdf `--external` instead makes `bun --compile` eagerly fail to
9
- // resolve the package at startup (the static `import * as mupdf` lives in a lazy
10
- // chunk but is hoisted). So the binary build bundles mupdf and embeds the wasm
11
- // bytes here, handing them to the WASM module as `$libmupdf_wasm_Module.wasmBinary`
12
- // (see src/utils/markit.ts).
13
- //
14
- // `--generate` copies the wasm next to src/utils/mupdf-wasm-embed.ts and rewrites
15
- // that module to import it via `with { type: "file" }`; `--reset` restores the
16
- // checked-in placeholder and removes the copy. The npm `dist/cli.js` bundle never
17
- // runs this — it keeps mupdf external and loads the wasm from node_modules.
18
-
19
- import * as fs from "node:fs/promises";
20
- import { createRequire } from "node:module";
21
- import * as path from "node:path";
22
-
23
- const utilsDir = path.join(import.meta.dir, "..", "src", "utils");
24
- const helperPath = path.join(utilsDir, "mupdf-wasm-embed.ts");
25
- const wasmCopyPath = path.join(utilsDir, "mupdf-wasm.wasm");
26
-
27
- const placeholder = `// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit by hand.
28
- //
29
- // Compiled single-file binaries cannot let mupdf resolve its \`mupdf-wasm.wasm\`
30
- // sibling from the read-only bunfs, so the binary build (scripts/build-binary.ts
31
- // and scripts/ci-release-build-binaries.ts) regenerates this module to embed the
32
- // wasm bytes via \`with { type: "file" }\` and copies the wasm next to it. Source
33
- // checkouts, \`bun test\`, and the npm \`dist/cli.js\` bundle keep mupdf external and
34
- // load the wasm from node_modules, so this placeholder returns undefined and the
35
- // build resets back to it afterward.
36
- export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
37
- \treturn undefined;
38
- }
39
- `;
40
-
41
- const generated = `// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit or commit.
42
- import { readFileSync } from "node:fs";
43
- import wasmPath from "./mupdf-wasm.wasm" with { type: "file" };
44
-
45
- export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
46
- \treturn readFileSync(wasmPath);
47
- }
48
- `;
49
-
50
- if (process.argv.includes("--reset")) {
51
- await Bun.write(helperPath, placeholder);
52
- try {
53
- await fs.unlink(wasmCopyPath);
54
- } catch (err) {
55
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
56
- }
57
- process.exit(0);
58
- }
59
-
60
- const wasmSource = path.join(path.dirname(createRequire(import.meta.url).resolve("mupdf")), "mupdf-wasm.wasm");
61
- const wasmFile = Bun.file(wasmSource);
62
- if (!(await wasmFile.exists())) {
63
- throw new Error(`mupdf wasm not found at ${wasmSource}; run \`bun install\` first.`);
64
- }
65
- await Bun.write(wasmCopyPath, wasmFile);
66
- await Bun.write(helperPath, generated);
67
- console.log(`Embedded mupdf wasm (${wasmFile.size} bytes) into ${path.relative(process.cwd(), wasmCopyPath)}`);