@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
@@ -599,79 +599,111 @@ export function truncateDiffByHunk(
599
599
  let keptHunks = 0;
600
600
 
601
601
  for (const seg of segments) {
602
+ if (kept.length >= maxLines) break;
602
603
  if (seg.isChange) {
604
+ if (keptHunks >= maxHunks) break;
603
605
  keptHunks++;
604
- if (keptHunks > maxHunks) break;
605
606
  }
606
- kept.push(...seg.lines);
607
- if (kept.length >= maxLines) break;
607
+ const take = Math.min(seg.lines.length, maxLines - kept.length);
608
+ for (let i = 0; i < take; i++) {
609
+ kept.push(seg.lines[i]!);
610
+ }
608
611
  }
609
612
 
610
- const keptStats = getDiffStats(kept.join("\n"));
611
613
  return {
612
614
  text: kept.join("\n"),
613
- hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
615
+ hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
614
616
  hiddenLines: Math.max(0, lines.length - kept.length),
615
617
  };
616
618
  }
617
619
 
618
620
  const contextBudget = maxLines - changeLineCount;
619
- const contextSegments = segments.filter(s => !s.isChange && !s.isEllipsis);
621
+ const contextSegments = segments.filter(s => !s.isChange);
620
622
  const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
621
623
 
622
624
  const kept: string[] = [];
623
625
  let keptHunks = 0;
626
+ let keptSourceLines = 0;
624
627
 
625
628
  if (totalContextLines <= contextBudget) {
626
629
  for (const seg of segments) {
627
630
  if (seg.isChange) {
631
+ if (keptHunks >= maxHunks) break;
628
632
  keptHunks++;
629
- if (keptHunks > maxHunks) break;
630
633
  }
631
634
  kept.push(...seg.lines);
635
+ keptSourceLines += seg.lines.length;
632
636
  }
633
637
  } else {
634
- const contextRatio = contextSegments.length > 0 ? contextBudget / totalContextLines : 0;
638
+ const contextRatio = totalContextLines > 0 ? contextBudget / totalContextLines : 0;
639
+ let remainingContextBudget = contextBudget;
635
640
 
636
641
  for (let i = 0; i < segments.length; i++) {
637
642
  const seg = segments[i];
638
643
 
639
644
  if (seg.isChange) {
645
+ if (keptHunks >= maxHunks) break;
640
646
  keptHunks++;
641
- if (keptHunks > maxHunks) break;
642
- kept.push(...seg.lines);
643
- } else if (seg.isEllipsis) {
644
647
  kept.push(...seg.lines);
648
+ keptSourceLines += seg.lines.length;
649
+ continue;
650
+ }
651
+ if (remainingContextBudget <= 0) continue;
652
+
653
+ const allowedLines = Math.min(
654
+ remainingContextBudget,
655
+ Math.max(1, Math.floor(seg.lines.length * contextRatio)),
656
+ );
657
+ const outputStart = kept.length;
658
+ let sourceLinesAdded = 0;
659
+
660
+ if (seg.isEllipsis || seg.lines.length <= allowedLines) {
661
+ for (let j = 0; j < allowedLines; j++) {
662
+ kept.push(seg.lines[j]!);
663
+ }
664
+ sourceLinesAdded = allowedLines;
645
665
  } else {
646
- const allowedLines = Math.max(1, Math.floor(seg.lines.length * contextRatio));
647
666
  const isBeforeChange = segments[i + 1]?.isChange;
648
667
  const isAfterChange = segments[i - 1]?.isChange;
649
668
 
650
669
  if (isBeforeChange && isAfterChange) {
651
- const half = Math.ceil(allowedLines / 2);
652
- if (seg.lines.length > allowedLines) {
653
- kept.push(...seg.lines.slice(0, half));
670
+ if (allowedLines >= 3) {
671
+ const sourceBudget = allowedLines - 1;
672
+ const firstCount = Math.ceil(sourceBudget / 2);
673
+ const lastCount = sourceBudget - firstCount;
674
+ kept.push(...seg.lines.slice(0, firstCount));
654
675
  kept.push("");
655
- kept.push(...seg.lines.slice(-half));
676
+ if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
677
+ sourceLinesAdded = sourceBudget;
656
678
  } else {
657
- kept.push(...seg.lines);
679
+ const firstCount = Math.ceil(allowedLines / 2);
680
+ const lastCount = allowedLines - firstCount;
681
+ kept.push(...seg.lines.slice(0, firstCount));
682
+ if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
683
+ sourceLinesAdded = allowedLines;
658
684
  }
659
685
  } else if (isBeforeChange) {
660
686
  kept.push(...seg.lines.slice(-allowedLines));
687
+ sourceLinesAdded = allowedLines;
661
688
  } else if (isAfterChange) {
662
689
  kept.push(...seg.lines.slice(0, allowedLines));
690
+ sourceLinesAdded = allowedLines;
663
691
  } else {
664
- kept.push(...seg.lines.slice(0, Math.min(allowedLines, 2)));
692
+ const take = Math.min(allowedLines, 2);
693
+ kept.push(...seg.lines.slice(0, take));
694
+ sourceLinesAdded = take;
665
695
  }
666
696
  }
697
+
698
+ keptSourceLines += sourceLinesAdded;
699
+ remainingContextBudget -= kept.length - outputStart;
667
700
  }
668
701
  }
669
702
 
670
- const keptStats = getDiffStats(kept.join("\n"));
671
703
  return {
672
704
  text: kept.join("\n"),
673
- hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
674
- hiddenLines: Math.max(0, lines.length - kept.length),
705
+ hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
706
+ hiddenLines: Math.max(0, lines.length - keptSourceLines),
675
707
  };
676
708
  }
677
709
 
@@ -8,14 +8,26 @@ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
8
8
  /** Whether a model transport can suppress native reasoning while private scratchpad thoughts are active. */
9
9
  export function supportsExternalThinking(model: Model | null | undefined): boolean {
10
10
  if (!model) return false;
11
+ const compat = model.compat;
11
12
  const requiresThinking =
12
13
  model.api === "anthropic-messages" &&
13
- model.compat !== undefined &&
14
- "requiresThinkingEnabled" in model.compat &&
15
- model.compat.requiresThinkingEnabled === true;
14
+ compat !== undefined &&
15
+ "requiresThinkingEnabled" in compat &&
16
+ compat.requiresThinkingEnabled === true;
16
17
  if (model.reasoning && (requiresThinking || (model.thinking?.requiresEffort && !model.thinking.suppressWhenOff))) {
17
18
  return false;
18
19
  }
20
+ // Transports that reject `reasoning.effort` (xAI Grok 4 and the other
21
+ // reasoning-only Responses models) cannot honour `forceReasoningOff`, so the
22
+ // scratchpad would run alongside native reasoning instead of replacing it.
23
+ if (
24
+ model.reasoning &&
25
+ compat !== undefined &&
26
+ (("omitReasoningEffort" in compat && compat.omitReasoningEffort === true) ||
27
+ ("supportsReasoningEffort" in compat && compat.supportsReasoningEffort === false))
28
+ ) {
29
+ return false;
30
+ }
19
31
  if (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
20
32
  return !model.reasoning || model.thinking?.mode === "budget" || model.thinking?.suppressWhenOff === true;
21
33
  }
@@ -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") {
@@ -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. */