@bastani/atomic 0.9.3-alpha.6 → 0.9.4-alpha.1

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 +40 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +30 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +12 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +16 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +23 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +32 -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 +4299 -0
  37. package/package.json +7 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.1] - 2026-06-29
6
+
7
+ ### Changed
8
+
9
+ - 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.
10
+
11
+ ### Fixed
12
+
13
+ - 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.
14
+ - 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.
15
+ - 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.
16
+ - 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.
17
+
18
+ ## [0.9.3] - 2026-06-29
19
+
20
+ ### Breaking Changes
21
+
22
+ - Replaced the legacy exact-replacement `edit` tool API with the hashline-only `input` script schema; callers must use snapshot tags from `read`/`search`/`write`/`edit` instead of `path` + `edits[]` or `oldText`/`newText`.
23
+ - Tightened the model-facing `read`, `find`, and `search` schemas to the new builtin contracts (`read` path selectors, required `find.paths`, and the narrowed `search` option set).
24
+
25
+ ### Added
26
+
27
+ - Added first-run onboarding that routes pasted tickets, specs, and tasks into normal Atomic sessions with scope-estimation guidance and workflow handoff next steps.
28
+ - Added internal workflow-stage session marking so workflow-created sessions stay out of ordinary `/resume`, `atomic -r`, and `--continue` history while remaining accessible through workflow resume/status surfaces.
29
+ - Added first-class `find`/`search` builtins, hashline snapshot anchors, hashline edit scripts with stale-tag safety, a disabled-by-default Bash Interceptor toggle, Rust-backed `pty:true` bash execution, and native glob/grep/search bindings for oh-my-pi parity.
30
+
31
+ ### Changed
32
+
33
+ - Synced bundled upstream Pi runtime packages to `^0.80.2` and routed legacy pi-ai imports through the temporary `/compat` entrypoint, including virtual-module and Jiti aliases for first-party and user-installed extensions.
34
+ - Enabled provider/SDK retries by default, raised the standard HTTP idle timeout to 10 minutes, and added a 10-second connect-phase timeout so transient socket drops retry while unreachable hosts fail fast.
35
+ - Accounted for image content blocks in context-window estimates and compaction budgets, raised delegated workflow/subagent nesting to the shared five-level maximum, and exported the canonical schema-aware flattened-argument helper used by host and MCP tool execution.
36
+ - Updated workflow and user-facing documentation so structured, validation-heavy, implementation, debugging, migration, and loop-shaped requests are routed to workflows by default.
37
+
38
+ ### Fixed
39
+
40
+ - Fixed custom tool renderer cleanup, persisted-context replay after context compaction, and multiple first-run onboarding edge cases around placeholders, file references, isolated config discovery, saved tasks, `/import`, `/model`, and `/new`.
41
+ - Completed the oh-my-pi builtin parity pass across `read`, `write`, `find`, `search`, `edit`, `bash`, archive, document, URL, internal-resource, conflict, and SQLite selectors, including pagination, truncation metadata, native cache invalidation, copied-hashline stripping, async/PTY behavior, and cross-platform path handling.
42
+ - Hardened generated-file, URL, `local://`, SQLite, archive, native PTY/search, and bash-interceptor paths against traversal, SSRF, unsafe raw SQL/archive cases, stale cache writes, native panics, leaked async output, and provider-hostile argument shapes.
43
+ - Fixed compiled release binary packaging and cross-platform package tests by externalizing `mupdf`, preparing native bindings and fixtures in CI, running the coding-agent Vitest suite under Bun, and hardening Windows path, color, and process-spawn coverage.
44
+
5
45
  ## [0.9.3-alpha.6] - 2026-06-29
6
46
 
7
47
  ### Changed
@@ -2,6 +2,36 @@
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
+
11
+ ## [0.9.3] - 2026-06-29
12
+
13
+ ### Added
14
+
15
+ - Added scoped Cursor image-input support for known multimodal Claude, Composer, Gemini, GPT, Kimi, and Grok 4.3 model families, including selected-image request serialization and mixed text/image MCP tool-result serialization.
16
+ - Added `cursor/grok-4.3` to the estimated fallback catalog.
17
+
18
+ ### Changed
19
+
20
+ - Aligned the Cursor provider dependency on upstream pi-ai `^0.80.2` and retargeted legacy provider/model imports to the `@earendil-works/pi-ai/compat` entrypoint.
21
+ - Resolved Cursor model context windows and max output tokens from Atomic's bundled pi-ai model catalog while preserving positive live limits, ignoring bogus non-positive values, and keeping conservative estimates only for Cursor-only models.
22
+ - Marked Cursor OAuth as **Cursor (Experimental)** in the `/login` picker.
23
+
24
+ ### Fixed
25
+
26
+ - Fixed effort-variant-only Cursor models failing no-thinking requests by recording and sending a concrete default variant instead of an unsendable synthesized base id.
27
+ - Preserved explicit `1M` Cursor context floors across fallback catalog matches and made the pi-ai catalog a runtime dependency so limit fallback remains available at provider startup.
28
+ - Exposed `xhigh` only for Cursor models whose live or estimated catalog includes a real `xhigh`/`max` variant, with saved `xhigh` selections falling back to the nearest sendable variant.
29
+ - Rejected empty or malformed base64 image payloads during Cursor selected-image and MCP image serialization while accepting valid MIME-wrapped base64 with whitespace.
30
+
31
+ ### Removed
32
+
33
+ - Removed outdated Cursor Grok 4.20 fallback entries and stopped advertising unsupported Grok-family Cursor IDs as image-capable.
34
+
5
35
  ## [0.9.3-alpha.6] - 2026-06-29
6
36
 
7
37
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/cursor",
3
- "version": "0.9.3-alpha.6",
3
+ "version": "0.9.4-alpha.1",
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-alpha.6",
43
+ "@bastani/atomic-natives": "0.9.4-alpha.1",
44
44
  "@bufbuild/protobuf": "^2.0.0",
45
45
  "@earendil-works/pi-ai": "^0.80.2"
46
46
  }
@@ -4,6 +4,18 @@ 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
+
13
+ ## [0.9.3] - 2026-06-29
14
+
15
+ ### Changed
16
+
17
+ - Published the stable Atomic 0.9.3 release for the intercom extension with its upstream pi TUI peer dependency aligned to `^0.80.2`; no intercom extension source changes were needed after 0.9.2.
18
+
7
19
  ## [0.9.3-alpha.6] - 2026-06-29
8
20
 
9
21
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.3-alpha.6",
3
+ "version": "0.9.4-alpha.1",
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,22 @@ 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
+
16
+ ## [0.9.3] - 2026-06-29
17
+
18
+ ### Changed
19
+
20
+ - Aligned the MCP extension peer dependencies with upstream pi `^0.80.2` and retargeted legacy provider/model imports to `@earendil-works/pi-ai/compat`, preserving MCP sampling behavior under the updated pi-ai package layout.
21
+
22
+ ### Fixed
23
+
24
+ - Made `unflattenToolArguments` schema-aware so literal dotted top-level argument keys such as `filter.name` are preserved unless the tool input schema proves the key represents a nested path, while bracket-indexed array/object keys are still reconstructed for direct-tool and proxy/gateway calls.
25
+
10
26
  ## [0.9.3-alpha.6] - 2026-06-29
11
27
 
12
28
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.3-alpha.6",
3
+ "version": "0.9.4-alpha.1",
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,29 @@
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
+
11
+ ## [0.9.3] - 2026-06-29
12
+
13
+ ### Added
14
+
15
+ - Added OpenRouter fallback coverage to bundled subagent definitions so delegated codebase analysis, research, debugging, simplification, and worker sessions can recover through OpenRouter-hosted frontier, GLM, and Gemini providers when native providers are unavailable.
16
+
17
+ ### Changed
18
+
19
+ - Aligned the subagents extension peer dependencies with upstream pi `^0.80.2` runtime packages and retargeted legacy provider/model imports to `@earendil-works/pi-ai/compat`.
20
+ - Raised the default and hard maximum subagent nesting budget to five delegated levels, extending nested-run observability to the same depth.
21
+
22
+ ### Fixed
23
+
24
+ - Prevented async and foreground subagent status widgets from flickering or unmounting during host UI refreshes, tall-panel redraws, and reset-and-hydrate cycles with active background runs.
25
+ - Fixed live subagent result animation cleanup so terminal workflow rows evict animation registry entries instead of leaving stale intervals.
26
+ - Synced upstream subagent hardening for compact tool-call summaries, nested fanout call/result history, duplicate dispatch rejection, provider-safe chain schemas, failed foreground diagnostics, builtin `search` MCP allowlist filtering, and updated builtin `find`/`search` tool guidance.
27
+
5
28
  ## [0.9.3-alpha.6] - 2026-06-29
6
29
 
7
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.3-alpha.6",
3
+ "version": "0.9.4-alpha.1",
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,18 @@ 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
+
13
+ ## [0.9.3] - 2026-06-29
14
+
15
+ ### Changed
16
+
17
+ - Published the stable Atomic 0.9.3 release for the web-access extension with its upstream pi TUI peer dependency aligned to `^0.80.2` and legacy pi-ai summary/model helper imports retargeted to the `/compat` entrypoint; no web-access extension source changes were needed after 0.9.2.
18
+
7
19
  ## [0.9.3-alpha.6] - 2026-06-29
8
20
 
9
21
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.3-alpha.6",
3
+ "version": "0.9.4-alpha.1",
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,38 @@ 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
+
15
+ ## [0.9.3] - 2026-06-29
16
+
17
+ ### Breaking Changes
18
+
19
+ - Enabled workflow durability by default with a zero-infrastructure per-workflow file backend under `~/.atomic/workflow-durable`, replacing the previous process-local in-memory default and removing the old `ATOMIC_WORKFLOW_DURABLE_DIR` opt-in path. Explicit in-memory durability remains available through custom/test backends or `ATOMIC_WORKFLOW_DURABLE=0`/`false`/`off`/`memory`.
20
+
21
+ ### Added
22
+
23
+ - Added `StageContext.sendUserMessage(content, { deliverAs? })` so workflow authors can inject follow-on user turns into live stage sessions after `prompt()` resolves while preserving MCP scope, abort handling, and structured-output guards.
24
+ - Added cross-session durable workflow resumability with cached `ctx.tool`, `ctx.ui`, `ctx.stage`/`ctx.task`/`ctx.chain`/`ctx.parallel`, and child-workflow checkpoints, plus `/workflow resume` discovery of durable workflow history from session metadata and the durable backend.
25
+ - Added optional DBOS-backed durability through the lazily loaded `@dbos-inc/dbos-sdk`, including DBOS read-side hydration for resumable workflows when `DBOS_SYSTEM_DATABASE_URL` is set.
26
+ - Added OpenRouter fallback coverage to builtin `goal`, `ralph`, `deep-research-codebase`, and `open-claude-design` workflow model configs.
27
+
28
+ ### Changed
29
+
30
+ - Marked workflow stage sessions as internal so they are excluded from normal Atomic resume history while remaining inspectable and resumable through workflow-specific commands and tools.
31
+ - Revised builtin `goal`, `ralph`, and `open-claude-design` prompts to emphasize full objective completion, stable non-ordinal artifacts, a shared five-level subagent nesting budget, and model-facing workflow guidance that routes non-trivial structured work to workflows by default.
32
+ - Aligned the workflows extension peer dependency with upstream pi TUI `^0.80.2`.
33
+
34
+ ### Fixed
35
+
36
+ - Fixed workflow graph and attached-stage chat interactions, including direct mouse-click activation, mouse/trackpad wheel capture, copy-mode toggling, stale `Working...` spinners, terminal-row animation cleanup, and post-terminal subagent progress handling.
37
+ - Hardened durable resume, replay, checkpoint, and listing behavior across file and DBOS backends, including pending prompts, terminal/non-resumable states, stale cache entries, repeated quit/resume cycles, child workflow scope isolation, checkpoint identity hashing, retries, cancellation, and durable write failures.
38
+ - Fixed `/workflow resume` picker behavior for live, paused, failed, and cross-session durable workflows so it matches the standard Atomic resume selector and handles empty, headless, mixed live/durable, and cache-only states consistently.
39
+ - Fixed builtin workflow continuity and configuration edge cases, including `open-claude-design` generator/feedback forking, isolated workflow config discovery, durable schema-backed stage replay, parallel fail-fast finalization, and custom-backend propagation into child workflows.
40
+
9
41
  ## [0.9.3-alpha.6] - 2026-06-29
10
42
 
11
43
  ### Added
@@ -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-alpha.6",
3
+ "version": "0.9.4-alpha.1",
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