@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.
- package/CHANGELOG.md +19 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +6 -0
- package/dist/builtin/workflows/README.md +4 -4
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +4 -2
- package/dist/builtin/workflows/src/engine/run-returned-status.ts +41 -0
- package/dist/builtin/workflows/src/engine/run.ts +6 -4
- package/dist/builtin/workflows/src/extension/config-file-loader.ts +1 -1
- package/dist/builtin/workflows/src/extension/config-loader.ts +1 -1
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +37 -12
- package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +1 -1
- package/dist/builtin/workflows/src/shared/store-public-types.ts +3 -2
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +6 -1
- package/dist/core/http-dispatcher.d.ts +3 -3
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +19 -6
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-basic-accessors.js +1 -1
- package/dist/core/settings-manager-basic-accessors.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +15 -13
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/settings.md +4 -4
- package/docs/workflows.md +5 -3
- package/npm-shrinkwrap.json +4292 -0
- 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
|
+
"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.
|
|
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
|
+
"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
|
+
"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
|
+
"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
|
+
"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
|
|
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
|
|
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
|
```
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
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:
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|
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"
|
|
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
|
-
:
|
|
370
|
-
|
|
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
|
-
:
|
|
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 ||
|
|
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
|
-
*
|
|
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;
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
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,
|
|
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"]}
|