@bastani/atomic 0.9.3 → 0.9.4-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +6 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  13. package/dist/builtin/workflows/README.md +4 -4
  14. package/dist/builtin/workflows/package.json +1 -1
  15. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +4 -2
  16. package/dist/builtin/workflows/src/engine/run-returned-status.ts +41 -0
  17. package/dist/builtin/workflows/src/engine/run.ts +6 -4
  18. package/dist/builtin/workflows/src/extension/config-file-loader.ts +1 -1
  19. package/dist/builtin/workflows/src/extension/config-loader.ts +1 -1
  20. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +37 -12
  21. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +1 -1
  22. package/dist/builtin/workflows/src/shared/store-public-types.ts +3 -2
  23. package/dist/builtin/workflows/src/shared/store-run-methods.ts +6 -1
  24. package/dist/core/http-dispatcher.d.ts +3 -3
  25. package/dist/core/http-dispatcher.d.ts.map +1 -1
  26. package/dist/core/http-dispatcher.js +19 -6
  27. package/dist/core/http-dispatcher.js.map +1 -1
  28. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  29. package/dist/core/settings-manager-basic-accessors.js +1 -1
  30. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  31. package/dist/modes/print-mode.d.ts.map +1 -1
  32. package/dist/modes/print-mode.js +15 -13
  33. package/dist/modes/print-mode.js.map +1 -1
  34. package/docs/settings.md +4 -4
  35. package/docs/workflows.md +5 -3
  36. package/npm-shrinkwrap.json +4292 -0
  37. package/package.json +7 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.2] - 2026-06-29
6
+
7
+ ### Fixed
8
+
9
+ - Hardened the coding-agent npm shrinkwrap generator so release publishes resolve the already-published `@bastani/atomic-natives` registry metadata and generated platform optional packages before packing `@bastani/atomic`, while versionless main keeps deterministic placeholder native package entries for shrinkwrap checks.
10
+
11
+ ## [0.9.4-alpha.1] - 2026-06-29
12
+
13
+ ### Changed
14
+
15
+ - Restored upstream Pi's provider retry default by leaving `retry.provider.maxRetries` unset/zero unless users configure it explicitly, so Atomic's agent-level retry observes provider transport failures directly instead of the SDK retrying them first.
16
+
17
+ ### Fixed
18
+
19
+ - Aligned Atomic's global HTTP dispatcher with upstream Pi by removing the fixed 10-second connect-phase timeout and installing undici's fetch implementation alongside the configured proxy dispatcher, avoiding spurious provider `Connection error` failures behind policy/proxy egress layers.
20
+ - Made headless JSON print mode return a nonzero exit code when the final assistant turn ends with `stopReason: "error"` or `"aborted"`, so Pier and other harnesses can mark provider failures as agent errors.
21
+ - Declared `lru-cache` as a direct runtime dependency and added publish-time `npm-shrinkwrap.json` generation, matching upstream Pi's deterministic npm installs and fixing strict pnpm global installs that could not resolve Atomic's hashline and fetch-tool cache imports.
22
+ - Updated user-facing workflow documentation for the blocked lifecycle notice state, the default `workflowNotifications.notifyOn` set, and the reserved top-level workflow output `status` convention for returned `failed`/`blocked` run statuses.
23
+
5
24
  ## [0.9.3] - 2026-06-29
6
25
 
7
26
  ### Breaking Changes
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.1] - 2026-06-29
6
+
7
+ ### Changed
8
+
9
+ - Published a synchronized Atomic 0.9.4-alpha.1 prerelease for the Cursor provider package; no functional Cursor provider changes were made after 0.9.3.
10
+
5
11
  ## [0.9.3] - 2026-06-29
6
12
 
7
13
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/cursor",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-alpha.2",
4
4
  "private": true,
5
5
  "description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
6
6
  "contributors": [
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@bastani/atomic-natives": "0.9.3",
43
+ "@bastani/atomic-natives": "0.9.4-alpha.2",
44
44
  "@bufbuild/protobuf": "^2.0.0",
45
45
  "@earendil-works/pi-ai": "^0.80.2"
46
46
  }
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.4-alpha.1] - 2026-06-29
8
+
9
+ ### Changed
10
+
11
+ - Published a synchronized Atomic 0.9.4-alpha.1 prerelease for the intercom extension; no intercom extension changes were made after 0.9.3.
12
+
7
13
  ## [0.9.3] - 2026-06-29
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.4-alpha.1] - 2026-06-29
11
+
12
+ ### Changed
13
+
14
+ - Published a synchronized Atomic 0.9.4-alpha.1 prerelease for the MCP extension; no MCP extension changes were made after 0.9.3.
15
+
10
16
  ## [0.9.3] - 2026-06-29
11
17
 
12
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.1] - 2026-06-29
6
+
7
+ ### Changed
8
+
9
+ - Published a synchronized Atomic 0.9.4-alpha.1 prerelease for the subagents extension; no subagents extension changes were made after 0.9.3.
10
+
5
11
  ## [0.9.3] - 2026-06-29
6
12
 
7
13
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.4-alpha.1] - 2026-06-29
8
+
9
+ ### Changed
10
+
11
+ - Published a synchronized Atomic 0.9.4-alpha.1 prerelease for the web-access extension; no web-access extension changes were made after 0.9.3.
12
+
7
13
  ## [0.9.3] - 2026-06-29
8
14
 
9
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.4-alpha.1] - 2026-06-29
10
+
11
+ ### Fixed
12
+
13
+ - Fixed workflow lifecycle notices for structured workflow outputs whose returned `status` is `failed` or `blocked`: the runtime now records those runs as failed/blocked instead of successful completions, preserves their returned output, carries blocked summaries into lifecycle notice reasons, marks returned failures non-resumable with explicit terminal failure metadata, and emits failure/blocked lifecycle cards rather than success/checkmark notices.
14
+
9
15
  ## [0.9.3] - 2026-06-29
10
16
 
11
17
  ### Breaking Changes
@@ -31,18 +31,18 @@ Adding workflow files under `.atomic/workflows/` (project scope) or `~/.atomic/a
31
31
 
32
32
  ### Workflow lifecycle notifications
33
33
 
34
- Workflow lifecycle notices are enabled by default. They send steer prompts into the main chat/model context when a run completes or fails. Awaiting-input prompts are tracked for dedupe/restore, but they do not wake the main chat agent. Configure lifecycle tracking in the same extension config file:
34
+ Workflow lifecycle notices are enabled by default. They send steer prompts into the main chat/model context when a run completes, fails, or ends blocked. Awaiting-input prompts are tracked for dedupe/restore, but they do not wake the main chat agent. Configure lifecycle tracking in the same extension config file:
35
35
 
36
36
  ```json
37
37
  {
38
38
  "workflowNotifications": {
39
39
  "enabled": true,
40
- "notifyOn": ["completed", "failed", "awaiting_input"]
40
+ "notifyOn": ["completed", "failed", "blocked", "awaiting_input"]
41
41
  }
42
42
  }
43
43
  ```
44
44
 
45
- Set `enabled` to `false` to disable all lifecycle notices, or narrow `notifyOn` to a non-empty list of selected events. Completion and failure lifecycle notices are emitted for top-level workflow runs, use steer delivery, and wake an idle model so the lifecycle update enters the model context when it happens. Nested child workflow completion/failure is reflected inside the expanded parent graph instead of producing separate top-level completion cards. Awaiting-input states are tracked for dedupe/restore, but workflows do not enqueue main-chat `/workflow connect` cards for them; prompt state remains visible through workflow status/connect surfaces, avoiding stale actionable cards if a prompt resolves while the main chat is streaming.
45
+ Set `enabled` to `false` to disable all lifecycle notices, or narrow `notifyOn` to a non-empty list of selected events. Completion, failure, and blocked lifecycle notices are emitted for top-level workflow runs, use steer delivery, and wake an idle model so the lifecycle update enters the model context when it happens. Nested child workflow completion/failure is reflected inside the expanded parent graph instead of producing separate top-level completion cards. Awaiting-input states are tracked for dedupe/restore, but workflows do not enqueue main-chat `/workflow connect` cards for them; prompt state remains visible through workflow status/connect surfaces, avoiding stale actionable cards if a prompt resolves while the main chat is streaming.
46
46
 
47
47
  When a stage human-in-the-loop prompt is answered from the workflow TUI/stage chat, workflows also emits a separate display-only `workflows:hil-answer-notice` custom message. It records the answer for user-visible audit, but it does not wake the main agent, enter LLM context, or authorize answering later workflow prompts. Answers sent by the main-chat `workflow` tool do not emit this notice because the tool result already tells the main agent what happened.
48
48
 
@@ -736,7 +736,7 @@ Config-based discovery (`~/.atomic/agent/extensions/workflow/config.json` or `.a
736
736
  },
737
737
  "workflowNotifications": {
738
738
  "enabled": true,
739
- "notifyOn": ["completed", "failed", "awaiting_input"]
739
+ "notifyOn": ["completed", "failed", "blocked", "awaiting_input"]
740
740
  }
741
741
  }
742
742
  ```
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.3",
3
+ "version": "0.9.4-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -33,11 +33,13 @@ export async function finalizeDurableTerminalStatus(input: DurableTerminalFinali
33
33
  if (!input.isRoot) return;
34
34
  const status = input.runSnapshot.status;
35
35
  const isExitTerminal = input.runSnapshot.exited === true && status !== "running";
36
- if (status !== "failed" && status !== "killed" && !isExitTerminal) return;
36
+ const isReturnedBlockedTerminal = status === "blocked" && input.runSnapshot.endedAt !== undefined;
37
+ if (status !== "failed" && status !== "killed" && !isExitTerminal && !isReturnedBlockedTerminal) return;
37
38
 
38
39
  const durableStatus = toDurableStatus(status);
39
40
  if (durableStatus !== undefined) {
40
- input.durableBackend.setWorkflowStatus(input.runId, durableStatus, undefined, input.runSnapshot.resumable);
41
+ const resumable = status === "blocked" ? false : input.runSnapshot.resumable;
42
+ input.durableBackend.setWorkflowStatus(input.runId, durableStatus, undefined, resumable);
41
43
  }
42
44
  try {
43
45
  await input.durableBackend.flush?.();
@@ -0,0 +1,41 @@
1
+ import type { RunEndMetadata } from "../shared/store-public-types.js";
2
+ import type { WorkflowOutputValues } from "../shared/types.js";
3
+
4
+ export interface ReturnedRunStatus {
5
+ readonly status: "completed" | "failed" | "blocked";
6
+ readonly error?: string;
7
+ readonly metadata?: RunEndMetadata;
8
+ }
9
+
10
+ export function classifyReturnedRunStatus(result: WorkflowOutputValues | undefined): ReturnedRunStatus {
11
+ const returnedStatus = result?.["status"];
12
+ if (returnedStatus !== "failed" && returnedStatus !== "blocked") {
13
+ return { status: "completed" };
14
+ }
15
+
16
+ const summary = result?.["summary"];
17
+ const error = typeof summary === "string" && summary.trim().length > 0
18
+ ? summary.trim()
19
+ : `Workflow returned status ${JSON.stringify(returnedStatus)}.`;
20
+ return {
21
+ status: returnedStatus,
22
+ error,
23
+ metadata: returnedStatus === "failed"
24
+ ? returnedFailureMetadata(error)
25
+ : returnedBlockedMetadata(),
26
+ };
27
+ }
28
+
29
+ function returnedFailureMetadata(error: string): RunEndMetadata {
30
+ return {
31
+ failureKind: "unknown",
32
+ failureRecoverability: "non_recoverable",
33
+ failureDisposition: "terminal_failed",
34
+ failureMessage: error,
35
+ resumable: false,
36
+ };
37
+ }
38
+
39
+ function returnedBlockedMetadata(): RunEndMetadata {
40
+ return { resumable: false };
41
+ }
@@ -41,6 +41,7 @@ import {
41
41
  import { assertWorkflowRunOutputs, normalizeWorkflowRunOutput } from "../runs/foreground/executor-outputs.js";
42
42
  import { isWorkflowDefinition, workflowDefinitionRequirementMessage } from "../runs/foreground/executor-child-helpers.js";
43
43
  import { getDurableBackend } from "../durable/factory.js";
44
+ import { classifyReturnedRunStatus } from "./run-returned-status.js";
44
45
  import { createToolPrimitive, createCheckpointIdGenerator } from "../durable/tool-primitive.js";
45
46
  import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
46
47
  import { createDurableStagePrimitive, createDurableTaskPrimitive, recordStageCheckpoint, createStageReplayKeyGenerator, recordCachedStageWithTracker } from "../durable/stage-primitive.js";
@@ -423,15 +424,16 @@ export async function run<
423
424
  assertWorkflowRunOutputs(def.name, result, def.outputs);
424
425
  assertWorkflowCreatedStage(runSnapshot);
425
426
  await durableBackend.flush?.();
426
- const recorded = activeStore.recordRunEnd(runId, "completed", result);
427
- appendRunEndWhenRecorded(opts.persistence, recorded, { runId, status: "completed", result, ts: Date.now() });
428
- durableBackend.setWorkflowStatus(runId, "completed");
427
+ const returned = classifyReturnedRunStatus(result);
428
+ const recorded = activeStore.recordRunEnd(runId, returned.status, result, returned.error, returned.metadata);
429
+ appendRunEndWhenRecorded(opts.persistence, recorded, { runId, status: returned.status, result, ...(returned.error !== undefined ? { error: returned.error } : {}), ...(returned.metadata ?? {}), ts: Date.now() });
430
+ durableBackend.setWorkflowStatus(runId, returned.status, undefined, returned.metadata?.resumable);
429
431
  await durableBackend.flush?.();
430
432
  if (opts.persistence && durableBackend.persistent) {
431
433
  const cacheEntry = durableBackend.toCacheEntry(runId);
432
434
  if (cacheEntry) persistDurableCacheEntry(opts.persistence, cacheEntry);
433
435
  }
434
- return reconcileTerminalRunResult(runId, runSnapshot, activeStore, { status: "completed", result }, opts.onRunEnd);
436
+ return reconcileTerminalRunResult(runId, runSnapshot, activeStore, { status: returned.status, result, error: returned.error }, opts.onRunEnd);
435
437
  } catch (err) {
436
438
  const selectedExit = findWorkflowExitSignal(err, exitScope) ?? findWorkflowExitSignal(ownController.signal.reason, exitScope);
437
439
  if (selectedExit !== undefined) return await finalizers.finalizeWorkflowExit(selectedExit);
@@ -68,7 +68,7 @@ function validateConfig(value: unknown): string | null {
68
68
  }
69
69
  for (const item of notifyOn) {
70
70
  if (!isWorkflowLifecycleNoticeKind(item)) {
71
- return `"workflowNotifications.notifyOn" entries must be "completed", "failed", or "awaiting_input", got ${JSON.stringify(item)}`;
71
+ return `"workflowNotifications.notifyOn" entries must be "completed", "failed", "blocked", or "awaiting_input", got ${JSON.stringify(item)}`;
72
72
  }
73
73
  }
74
74
  }
@@ -182,7 +182,7 @@ export const WORKFLOW_CONFIG_DEFAULTS = {
182
182
  resumeInFlight: "ask" as const,
183
183
  workflowNotifications: {
184
184
  enabled: true,
185
- notifyOn: ["completed", "failed", "awaiting_input"] as const,
185
+ notifyOn: ["completed", "failed", "blocked", "awaiting_input"] as const,
186
186
  },
187
187
  } as const;
188
188
 
@@ -21,11 +21,12 @@ import { renderWorkflowNoticeCard, type WorkflowNoticeTone } from "../tui/workfl
21
21
  export const LIFECYCLE_NOTICE_CUSTOM_TYPE = "workflows:lifecycle-notice";
22
22
  export const LIFECYCLE_NOTICE_SNIPPET_LIMIT = 240;
23
23
 
24
- export type WorkflowLifecycleNoticeKind = "completed" | "failed" | "awaiting_input";
24
+ export type WorkflowLifecycleNoticeKind = "completed" | "failed" | "blocked" | "awaiting_input";
25
25
 
26
26
  export const WORKFLOW_LIFECYCLE_NOTICE_KINDS = [
27
27
  "completed",
28
28
  "failed",
29
+ "blocked",
29
30
  "awaiting_input",
30
31
  ] as const satisfies readonly WorkflowLifecycleNoticeKind[];
31
32
 
@@ -95,8 +96,9 @@ export function seedWorkflowLifecycleNotificationState(
95
96
  ): void {
96
97
  for (const run of snapshot.runs) {
97
98
  if (!isTopLevelWorkflowRun(run)) continue;
98
- if ((run.status === "completed" || run.status === "failed") && run.endedAt !== undefined) {
99
- state.deliveredTerminalRuns.add(terminalRunKey(run.status, run.id));
99
+ const noticeKind = terminalNoticeKind(run);
100
+ if (noticeKind !== undefined && run.endedAt !== undefined) {
101
+ state.deliveredTerminalRuns.add(terminalRunKey(noticeKind, run.id));
100
102
  }
101
103
  if (run.pendingPrompt !== undefined) {
102
104
  state.deliveredInputPrompts.add(runAwaitingInputKey(run.id, run.pendingPrompt));
@@ -186,9 +188,10 @@ export function installWorkflowLifecycleNotifications(
186
188
 
187
189
  const emitTerminalNoticeOnce = (
188
190
  run: RunSnapshot,
189
- kind: "completed" | "failed",
191
+ kind: "completed" | "failed" | "blocked",
190
192
  ): void => {
191
- if (run.status !== kind || run.endedAt === undefined || !notifyOn.has(kind)) {
193
+ const noticeKind = terminalNoticeKind(run);
194
+ if (noticeKind !== kind || run.endedAt === undefined || !notifyOn.has(kind)) {
192
195
  return;
193
196
  }
194
197
 
@@ -231,6 +234,7 @@ export function installWorkflowLifecycleNotifications(
231
234
  if (!isTopLevelWorkflowRun(run)) continue;
232
235
  emitTerminalNoticeOnce(run, "completed");
233
236
  emitTerminalNoticeOnce(run, "failed");
237
+ emitTerminalNoticeOnce(run, "blocked");
234
238
 
235
239
  if (!notifyOn.has("awaiting_input")) continue;
236
240
  emitRunAwaitingInputNoticeOnce(run);
@@ -273,6 +277,10 @@ export function formatWorkflowLifecycleNoticeText(details: WorkflowLifecycleNoti
273
277
  const errorText = details.error ? `: ${details.error}` : "";
274
278
  return `✗ Workflow "${workflowName}" failed (run ${details.runId}${stageText})${errorText}. Inspect: /workflow status ${details.runId}`;
275
279
  }
280
+ if (details.kind === "blocked") {
281
+ const errorText = details.error ? `: ${details.error}` : "";
282
+ return `! Workflow "${workflowName}" ended blocked (run ${details.runId})${errorText}. Inspect: /workflow status ${details.runId}`;
283
+ }
276
284
  const prompt = details.promptMessage ? ` Prompt: ${details.promptMessage}` : "";
277
285
  if (details.scope === "run") {
278
286
  return `? Workflow "${workflowName}" needs input (run ${details.runId}).${prompt} Respond: /workflow connect ${details.runId} to answer this run-level prompt.`;
@@ -286,18 +294,19 @@ export function formatWorkflowLifecycleNoticeText(details: WorkflowLifecycleNoti
286
294
 
287
295
  function makeTerminalNotice(
288
296
  run: RunSnapshot,
289
- kind: "completed" | "failed",
297
+ kind: "completed" | "failed" | "blocked",
290
298
  ): WorkflowLifecycleNoticeDetails {
291
299
  const failedStage = run.failedStageId
292
300
  ? run.stages.find((stage) => stage.id === run.failedStageId)
293
301
  : undefined;
302
+ const error = run.error ?? (kind === "blocked" ? run.exitReason : undefined);
294
303
  return {
295
304
  kind,
296
305
  scope: "run",
297
306
  runId: run.id,
298
307
  workflowName: run.name,
299
308
  status: run.status,
300
- ...(run.error ? { error: truncateSnippet(run.error) } : {}),
309
+ ...(error ? { error: truncateSnippet(error) } : {}),
301
310
  ...(run.failedStageId ? { failedStageId: run.failedStageId } : {}),
302
311
  ...(failedStage ? { stageId: failedStage.id, stageName: failedStage.name } : {}),
303
312
  ...(run.durationMs !== undefined ? { durationMs: run.durationMs } : {}),
@@ -320,7 +329,15 @@ function jsonString(value: string): string {
320
329
  return JSON.stringify(value);
321
330
  }
322
331
 
323
- function terminalRunKey(kind: "completed" | "failed", runId: string): string {
332
+ function terminalNoticeKind(run: RunSnapshot): "completed" | "failed" | "blocked" | undefined {
333
+ if (run.status === "failed" || run.status === "blocked") return run.status;
334
+ if (run.status !== "completed") return undefined;
335
+ const returnedStatus = run.result?.["status"];
336
+ if (returnedStatus === "failed" || returnedStatus === "blocked") return returnedStatus;
337
+ return "completed";
338
+ }
339
+
340
+ function terminalRunKey(kind: "completed" | "failed" | "blocked", runId: string): string {
324
341
  return `${kind}:${runId}`;
325
342
  }
326
343
 
@@ -361,19 +378,27 @@ function renderLifecycleNoticeCard(
361
378
  details: WorkflowLifecycleNoticeDetails,
362
379
  opts: { width: number; theme?: GraphTheme; fallbackText: string },
363
380
  ): string[] {
364
- const tone: WorkflowNoticeTone = details.kind === "failed" ? "error" : details.kind === "awaiting_input" ? "warning" : "success";
381
+ const tone: WorkflowNoticeTone = details.kind === "failed"
382
+ ? "error"
383
+ : details.kind === "awaiting_input" || details.kind === "blocked"
384
+ ? "warning"
385
+ : "success";
365
386
  const title = details.kind === "failed"
366
387
  ? "WORKFLOW FAILED"
367
388
  : details.kind === "awaiting_input"
368
389
  ? "WORKFLOW INPUT"
369
- : "WORKFLOW COMPLETE";
370
- const glyph = details.kind === "failed" ? "✗" : details.kind === "awaiting_input" ? "?" : "✓";
390
+ : details.kind === "blocked"
391
+ ? "WORKFLOW BLOCKED"
392
+ : "WORKFLOW COMPLETE";
393
+ const glyph = details.kind === "failed" ? "✗" : details.kind === "awaiting_input" ? "?" : details.kind === "blocked" ? "!" : "✓";
371
394
  const stage = details.stageName ?? details.failedStageId ?? details.stageId;
372
395
  const headline = details.kind === "failed"
373
396
  ? `Workflow "${details.workflowName}" failed`
374
397
  : details.kind === "awaiting_input"
375
398
  ? `Workflow "${details.workflowName}" needs input`
376
- : `Workflow "${details.workflowName}" completed`;
399
+ : details.kind === "blocked"
400
+ ? `Workflow "${details.workflowName}" ended blocked`
401
+ : `Workflow "${details.workflowName}" completed`;
377
402
  return renderWorkflowNoticeCard({
378
403
  title,
379
404
  glyph,
@@ -335,7 +335,7 @@ export function restoreTerminalRuns(entries: readonly SessionEntry[], store: Sto
335
335
  const exitReason = end["exitReason"];
336
336
  const resumable = end["resumable"];
337
337
  const restoredAuthorExit = isWorkflowExitTerminalStatus(status) &&
338
- (exited === true || status !== "completed" || typeof exitReason === "string" || resumable === false);
338
+ (exited === true || typeof exitReason === "string");
339
339
  if (status === "completed" && !restoredAuthorExit && stages.some((stage) => stage.status !== "completed")) continue;
340
340
  store.recordRunStart({
341
341
  id: runId,
@@ -83,8 +83,9 @@ export interface Store {
83
83
  * Records the end of a run.
84
84
  * Returns `true` if state changed, `false` if the run was not found or
85
85
  * already in a terminal state (completed | failed | killed | skipped | cancelled | blocked).
86
- * `result` is applied for intentional success/exit statuses (completed | skipped | cancelled | blocked).
87
- * `error` is only applied for status "failed" | "killed".
86
+ * `result` is applied for intentional success/exit statuses (completed | skipped | cancelled | blocked)
87
+ * and for workflows that intentionally return a failed status with structured outputs.
88
+ * `error` is applied for status "failed" | "killed" | "blocked".
88
89
  */
89
90
  recordRunEnd(
90
91
  runId: string,
@@ -82,11 +82,12 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
82
82
  run.pausedAt = undefined;
83
83
  }
84
84
  run.durationMs = elapsedRunMs(run, run.endedAt);
85
+ if (result !== undefined && shouldStoreRunResult(status)) run.result = result;
85
86
  const wasBlocked = run.blockedAt !== undefined || run.failureDisposition === "active_blocked";
86
87
  delete run.blockedAt;
87
88
  if (status === "completed" || status === "skipped" || status === "cancelled" || status === "blocked") {
88
- if (result !== undefined) run.result = result;
89
89
  clearRunFailureMetadata(run);
90
+ if (status === "blocked" && error !== undefined) run.error = error;
90
91
  if (metadata !== undefined) applyRunEndMetadata(run, metadata);
91
92
  } else {
92
93
  if (wasBlocked && error === undefined) delete run.error;
@@ -227,3 +228,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
227
228
  },
228
229
  };
229
230
  }
231
+
232
+ function shouldStoreRunResult(status: RunStatus): boolean {
233
+ return status === "completed" || status === "skipped" || status === "cancelled" || status === "blocked" || status === "failed";
234
+ }
@@ -1,6 +1,4 @@
1
1
  export declare const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600000;
2
- /** Connect-phase timeout so a black-holed/firewalled host fails fast instead of hanging until the OS TCP timeout. */
3
- export declare const HTTP_CONNECT_TIMEOUT_MS = 10000;
4
2
  export declare const HTTP_IDLE_TIMEOUT_CHOICES: readonly [{
5
3
  readonly label: "30 sec";
6
4
  readonly timeoutMs: 30000;
@@ -28,7 +26,9 @@ export declare function formatHttpIdleTimeoutMs(timeoutMs: number): string;
28
26
  * Keep HTTP/2 disabled for now because some Node/undici combinations have
29
27
  * produced stream-reset crashes, and use a configurable idle timeout so stale
30
28
  * connections are eventually reclaimed while long-running requests remain
31
- * supported.
29
+ * supported. Do not install a fixed connect-phase timeout here: under Pier and
30
+ * other policy/proxy layers, CONNECT establishment can be slower than normal
31
+ * internet egress and should surface through the provider/agent retry path.
32
32
  */
33
33
  export declare function configureHttpDispatcher(timeoutMs?: number): void;
34
34
  //# sourceMappingURL=http-dispatcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-dispatcher.d.ts","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAEpD,qHAAqH;AACrH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,eAAO,MAAM,yBAAyB;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,KAAM;;aAClC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,KAAM;;aACjC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,MAAO;;aAClC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,MAAO;;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,OAAS;;aACrC,KAAK,EAAE,UAAU;aAAE,SAAS,EAAE,CAAC;EACxB,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAKzE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,MAAqC,GAAG,IAAI,CAmB9F","sourcesContent":["import { EnvHttpProxyAgent, setGlobalDispatcher } from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\n/** Connect-phase timeout so a black-holed/firewalled host fails fast instead of hanging until the OS TCP timeout. */\nexport const HTTP_CONNECT_TIMEOUT_MS = 10_000;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tsetGlobalDispatcher(\n\t\tnew EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t\tconnect: { timeout: HTTP_CONNECT_TIMEOUT_MS },\n\t\t}),\n\t);\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
1
+ {"version":3,"file":"http-dispatcher.d.ts","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,4BAA4B,SAAU,CAAC;AAKpD,eAAO,MAAM,yBAAyB;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,KAAM;;aAClC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,KAAM;;aACjC,KAAK,EAAE,OAAO;aAAE,SAAS,EAAE,MAAO;;aAClC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,MAAO;;aACnC,KAAK,EAAE,QAAQ;aAAE,SAAS,EAAE,OAAS;;aACrC,KAAK,EAAE,UAAU;aAAE,SAAS,EAAE,CAAC;EACxB,CAAC;AAEX,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAKzE;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,GAAE,MAAqC,GAAG,IAAI,CA+B9F","sourcesContent":["import * as undici from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\nconst originalGlobalFetch = globalThis.fetch;\nlet installedGlobalFetch: typeof globalThis.fetch | undefined;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported. Do not install a fixed connect-phase timeout here: under Pier and\n * other policy/proxy layers, CONNECT establishment can be slower than normal\n * internet egress and should surface through the provider/agent retry path.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\n\t// Keep fetch and the dispatcher on the same undici implementation. Some Node\n\t// releases use a bundled fetch that can ignore the npm undici dispatcher or\n\t// otherwise behave differently from the configured dispatcher used by SDKs.\n\t// If a caller replaced fetch after module load, preserve that deliberate\n\t// override.\n\tconst shouldInstallGlobals = installedGlobalFetch === undefined\n\t\t? globalThis.fetch === originalGlobalFetch\n\t\t: globalThis.fetch === installedGlobalFetch;\n\tif (shouldInstallGlobals) {\n\t\tundici.install?.();\n\t\tinstalledGlobalFetch = globalThis.fetch;\n\t}\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
@@ -1,8 +1,8 @@
1
- import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
1
+ import * as undici from "undici";
2
2
  import { installCopilotGeminiReasoningInterceptor } from "./copilot-gemini-reasoning.js";
3
3
  export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;
4
- /** Connect-phase timeout so a black-holed/firewalled host fails fast instead of hanging until the OS TCP timeout. */
5
- export const HTTP_CONNECT_TIMEOUT_MS = 10_000;
4
+ const originalGlobalFetch = globalThis.fetch;
5
+ let installedGlobalFetch;
6
6
  export const HTTP_IDLE_TIMEOUT_CHOICES = [
7
7
  { label: "30 sec", timeoutMs: 30_000 },
8
8
  { label: "1 min", timeoutMs: 60_000 },
@@ -30,19 +30,32 @@ export function formatHttpIdleTimeoutMs(timeoutMs) {
30
30
  * Keep HTTP/2 disabled for now because some Node/undici combinations have
31
31
  * produced stream-reset crashes, and use a configurable idle timeout so stale
32
32
  * connections are eventually reclaimed while long-running requests remain
33
- * supported.
33
+ * supported. Do not install a fixed connect-phase timeout here: under Pier and
34
+ * other policy/proxy layers, CONNECT establishment can be slower than normal
35
+ * internet egress and should surface through the provider/agent retry path.
34
36
  */
35
37
  export function configureHttpDispatcher(timeoutMs = DEFAULT_HTTP_IDLE_TIMEOUT_MS) {
36
38
  const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);
37
39
  if (normalizedTimeoutMs === undefined) {
38
40
  throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
39
41
  }
40
- setGlobalDispatcher(new EnvHttpProxyAgent({
42
+ undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent({
41
43
  allowH2: false,
42
44
  bodyTimeout: normalizedTimeoutMs,
43
45
  headersTimeout: normalizedTimeoutMs,
44
- connect: { timeout: HTTP_CONNECT_TIMEOUT_MS },
45
46
  }));
47
+ // Keep fetch and the dispatcher on the same undici implementation. Some Node
48
+ // releases use a bundled fetch that can ignore the npm undici dispatcher or
49
+ // otherwise behave differently from the configured dispatcher used by SDKs.
50
+ // If a caller replaced fetch after module load, preserve that deliberate
51
+ // override.
52
+ const shouldInstallGlobals = installedGlobalFetch === undefined
53
+ ? globalThis.fetch === originalGlobalFetch
54
+ : globalThis.fetch === installedGlobalFetch;
55
+ if (shouldInstallGlobals) {
56
+ undici.install?.();
57
+ installedGlobalFetch = globalThis.fetch;
58
+ }
46
59
  // Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound
47
60
  // SSE stream so multi-turn Copilot Gemini tool use does not stall on empty
48
61
  // completions. Idempotent and scoped to `*.githubcopilot.com` event streams.
@@ -1 +1 @@
1
- {"version":3,"file":"http-dispatcher.js","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,wCAAwC,EAAE,MAAM,+BAA+B,CAAC;AAEzF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,qHAAqH;AACrH,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;IACrC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IACvC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;IACzC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE;CAC1B,CAAC;AAEX,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB;IACxD,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtF,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,SAAS,GAAG,IAAI,MAAM,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,SAAS,GAAW,4BAA4B;IACvF,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,mBAAmB,CAClB,IAAI,iBAAiB,CAAC;QACrB,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,mBAAmB;QAChC,cAAc,EAAE,mBAAmB;QACnC,OAAO,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE;KAC7C,CAAC,CACF,CAAC;IAEF,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,wCAAwC,EAAE,CAAC;AAC5C,CAAC","sourcesContent":["import { EnvHttpProxyAgent, setGlobalDispatcher } from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\n/** Connect-phase timeout so a black-holed/firewalled host fails fast instead of hanging until the OS TCP timeout. */\nexport const HTTP_CONNECT_TIMEOUT_MS = 10_000;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tsetGlobalDispatcher(\n\t\tnew EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t\tconnect: { timeout: HTTP_CONNECT_TIMEOUT_MS },\n\t\t}),\n\t);\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}
1
+ {"version":3,"file":"http-dispatcher.js","sourceRoot":"","sources":["../../src/core/http-dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,wCAAwC,EAAE,MAAM,+BAA+B,CAAC;AAEzF,MAAM,CAAC,MAAM,4BAA4B,GAAG,OAAO,CAAC;AAEpD,MAAM,mBAAmB,GAAG,UAAU,CAAC,KAAK,CAAC;AAC7C,IAAI,oBAAyD,CAAC;AAE9D,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE;IACtC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;IACrC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE;IACtC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IACvC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;IACzC,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,EAAE;CAC1B,CAAC;AAEX,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,SAAiB;IACxD,MAAM,MAAM,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;IACtF,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,GAAG,SAAS,GAAG,IAAI,MAAM,CAAC;AAClC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,SAAS,GAAW,4BAA4B;IACvF,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,CAAC,mBAAmB,CACzB,IAAI,MAAM,CAAC,iBAAiB,CAAC;QAC5B,OAAO,EAAE,KAAK;QACd,WAAW,EAAE,mBAAmB;QAChC,cAAc,EAAE,mBAAmB;KACnC,CAAC,CACF,CAAC;IAEF,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY;IACZ,MAAM,oBAAoB,GAAG,oBAAoB,KAAK,SAAS;QAC9D,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,mBAAmB;QAC1C,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,oBAAoB,CAAC;IAC7C,IAAI,oBAAoB,EAAE,CAAC;QAC1B,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACnB,oBAAoB,GAAG,UAAU,CAAC,KAAK,CAAC;IACzC,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,wCAAwC,EAAE,CAAC;AAC5C,CAAC","sourcesContent":["import * as undici from \"undici\";\nimport { installCopilotGeminiReasoningInterceptor } from \"./copilot-gemini-reasoning.ts\";\n\nexport const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 600_000;\n\nconst originalGlobalFetch = globalThis.fetch;\nlet installedGlobalFetch: typeof globalThis.fetch | undefined;\n\nexport const HTTP_IDLE_TIMEOUT_CHOICES = [\n\t{ label: \"30 sec\", timeoutMs: 30_000 },\n\t{ label: \"1 min\", timeoutMs: 60_000 },\n\t{ label: \"5 min\", timeoutMs: 300_000 },\n\t{ label: \"10 min\", timeoutMs: 600_000 },\n\t{ label: \"30 min\", timeoutMs: 1_800_000 },\n\t{ label: \"Disabled\", timeoutMs: 0 },\n] as const;\n\nexport function parseHttpIdleTimeoutMs(value: unknown): number | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.floor(value);\n}\n\nexport function formatHttpIdleTimeoutMs(timeoutMs: number): string {\n\tconst choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs);\n\tif (choice) {\n\t\treturn choice.label;\n\t}\n\treturn `${timeoutMs / 1000} sec`;\n}\n\n/**\n * Configure the global undici dispatcher used by fetch and SDK HTTP clients.\n *\n * Keep HTTP/2 disabled for now because some Node/undici combinations have\n * produced stream-reset crashes, and use a configurable idle timeout so stale\n * connections are eventually reclaimed while long-running requests remain\n * supported. Do not install a fixed connect-phase timeout here: under Pier and\n * other policy/proxy layers, CONNECT establishment can be slower than normal\n * internet egress and should surface through the provider/agent retry path.\n */\nexport function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {\n\tconst normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);\n\tif (normalizedTimeoutMs === undefined) {\n\t\tthrow new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);\n\t}\n\n\tundici.setGlobalDispatcher(\n\t\tnew undici.EnvHttpProxyAgent({\n\t\t\tallowH2: false,\n\t\t\tbodyTimeout: normalizedTimeoutMs,\n\t\t\theadersTimeout: normalizedTimeoutMs,\n\t\t}),\n\t);\n\n\t// Keep fetch and the dispatcher on the same undici implementation. Some Node\n\t// releases use a bundled fetch that can ignore the npm undici dispatcher or\n\t// otherwise behave differently from the configured dispatcher used by SDKs.\n\t// If a caller replaced fetch after module load, preserve that deliberate\n\t// override.\n\tconst shouldInstallGlobals = installedGlobalFetch === undefined\n\t\t? globalThis.fetch === originalGlobalFetch\n\t\t: globalThis.fetch === installedGlobalFetch;\n\tif (shouldInstallGlobals) {\n\t\tundici.install?.();\n\t\tinstalledGlobalFetch = globalThis.fetch;\n\t}\n\n\t// Bridge CAPI Gemini thought signatures (`reasoning_opaque`) on the inbound\n\t// SSE stream so multi-turn Copilot Gemini tool use does not stall on empty\n\t// completions. Idempotent and scoped to `*.githubcopilot.com` event streams.\n\tinstallCopilotGeminiReasoningInterceptor();\n}\n"]}