@bastani/atomic 0.9.46-alpha.1 → 0.9.46

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 (64) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +11 -0
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/CHANGELOG.md +11 -0
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/subagents/src/extension/index.bundle.mjs +2 -2
  8. package/dist/builtin/web-access/package.json +1 -1
  9. package/dist/builtin/workflows/CHANGELOG.md +40 -0
  10. package/dist/builtin/workflows/package.json +1 -1
  11. package/dist/builtin/workflows/src/extension/index.bundle.mjs +55 -28
  12. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +55 -28
  13. package/dist/builtin/workflows/src/index.js +55 -28
  14. package/dist/builtin/workflows/src/local-worker-entry.mjs +55 -28
  15. package/dist/core/agent-session-tasks.d.ts.map +1 -1
  16. package/dist/core/agent-session-tasks.js +8 -1
  17. package/dist/core/agent-session-tasks.js.map +1 -1
  18. package/dist/core/extensions/ui-types.d.ts +9 -1
  19. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  20. package/dist/core/extensions/ui-types.js.map +1 -1
  21. package/dist/core/model-resolver-defaults.js +2 -2
  22. package/dist/core/model-resolver-defaults.js.map +1 -1
  23. package/dist/core/package-manager-npm.d.ts.map +1 -1
  24. package/dist/core/package-manager-npm.js +7 -3
  25. package/dist/core/package-manager-npm.js.map +1 -1
  26. package/dist/core/tasks/completion.d.ts +6 -1
  27. package/dist/core/tasks/completion.d.ts.map +1 -1
  28. package/dist/core/tasks/completion.js +33 -18
  29. package/dist/core/tasks/completion.js.map +1 -1
  30. package/dist/core/tasks/pause.d.ts +2 -1
  31. package/dist/core/tasks/pause.d.ts.map +1 -1
  32. package/dist/core/tasks/pause.js +3 -2
  33. package/dist/core/tasks/pause.js.map +1 -1
  34. package/dist/core/tools/grep.d.ts.map +1 -1
  35. package/dist/core/tools/grep.js +6 -4
  36. package/dist/core/tools/grep.js.map +1 -1
  37. package/dist/core/tools/shell-task-status.d.ts.map +1 -1
  38. package/dist/core/tools/shell-task-status.js +2 -1
  39. package/dist/core/tools/shell-task-status.js.map +1 -1
  40. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  41. package/dist/modes/interactive/components/assistant-message.js +20 -12
  42. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  43. package/dist/modes/interactive/interactive-extension-custom-ui.js +1 -1
  44. package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
  45. package/dist/modes/interactive/interactive-extension-dialogs.js +10 -4
  46. package/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
  47. package/dist/modes/interactive/interactive-mode-surface.d.ts +1 -0
  48. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  49. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  50. package/dist/modes/interactive-engine/engine-custom-ui.d.ts +3 -0
  51. package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
  52. package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
  53. package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
  54. package/dist/modes/interactive-engine/protocol.d.ts +6 -0
  55. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  56. package/dist/modes/interactive-engine/protocol.js +9 -0
  57. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  58. package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
  59. package/dist/modes/interactive-engine/remote-component.js +3 -2
  60. package/dist/modes/interactive-engine/remote-component.js.map +1 -1
  61. package/docs/llama-cpp.md +25 -14
  62. package/docs/tui.md +2 -0
  63. package/npm-shrinkwrap.json +32 -32
  64. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.46] - 2026-10-27
6
+
7
+ Stages on local workers can now be watched, steered, paused and answered live; large pooled workflows stay responsive and recover cleanly from rate limits, worker crashes and resumes; and GPT-6.1 Terra is available.
8
+
9
+ - **Live chat for pooled stages:** opening a running stage on a local worker shows its live transcript. Enter steers, Ctrl+F queues a follow-up, `/workflow pause <run> <stage>` pauses just that stage, and `ctx.ui` and `ask_user_question` prompts are answered from the stage chat. When a worker dies, the chat reconnects to its replacement, a pending prompt is asked again, and every pending message is delivered exactly once.
10
+ - **Responsive at scale:** a 150-worker fan-out no longer stalls the root while children start or finish, even with the graph and a stage chat open. One child that keeps losing its worker now fails alone instead of taking its siblings with it.
11
+ - **Rate limits handled the way providers ask:** `Retry-After` is honored on every provider path, including Google and Bedrock. A quota-exhausted model moves straight to the next fallback, a chain where every model is throttled waits for the earliest cooldown, and a limit one local worker meets now holds back the other workers and the root too.
12
+ - **Failures classified by what happened:** a child workflow's failure no longer depends on its name, and SDK callers get the classification in `RunResult.failure`.
13
+ - **Pools that survive crashes and resumes:** resuming a quit root no longer fails in-flight children, journal writers are served in order instead of timing out, and workflow runs survive a deleted `jiti` cache.
14
+ - **New models:** GPT-6.1 Terra on OpenAI, Azure and Codex, with a Fast tier on the OpenAI API, and correct reasoning-effort levels for GPT-6 models on Bedrock.
15
+ - **Hardening from upstream pi:** OAuth logins accept only trusted URLs, terminal titles and the login dialog strip control characters, and the CLI auth file is written with mode 0600.
16
+
17
+ ### Added
18
+
19
+ - GPT-6.1 Terra is available as `openai/gpt-6.1-terra`, `openai-codex/gpt-6.1-terra` and `azure/gpt-6.1-terra`, with a derived `openai/gpt-6.1-terra-fast` choice. It supports `off` through `max` (not `minimal`), and `openai-codex/gpt-6.1-terra` has a 500K context window ([pi 363ad78b](https://github.com/earendil-works/pi/commit/363ad78bf0625e1dfbe848429782f5e07bb1584e))
20
+ - `@bastani/atomic` exports `observeProviderRateLimits()`, `providerRateLimitObservations()` and `applyProviderRateLimitObservation()` with the `ProviderRateLimitObservation` type, so another process on the same host can apply the provider and model cooldowns and quota-exhausted marks this process recorded. An applied limit only ever extends the matching provider or model limit and counts no failure ([#4023](https://github.com/bastani-inc/atomic/issues/4023))
21
+ - `ctx.ui.custom()` accepts `blocked: { kind, message }` so an inline component that waits for an answer reports Atomic as blocked through OSC 7501 program status while it is visible, including in the default isolated mode. `kind` is `"question"`, `"permission"` or `"auth"`; overlays ignore the option ([pi #11428](https://github.com/earendil-works/pi/issues/11428))
22
+
23
+ ### Changed
24
+
25
+ - The `openai` and `openai-api` providers default to `gpt-6.1-sol` instead of `gpt-5.5`, matching `openai-codex` ([pi #11467](https://github.com/earendil-works/pi/pull/11467))
26
+
27
+ ### Fixed
28
+
29
+ - The `Full output:` path of a truncated shell result, both a foreground command and a `wait` on a background task, now sits on its own line and links to the file. Before, it followed its label on the same line, so word wrap could split a path containing spaces (such as `C:\Users\Jane Doe\AppData\Local\Temp\…`) partway through and the copied path no longer worked. A path wider than the terminal still wraps, but its link still opens the whole file ([#3828](https://github.com/bastani-inc/atomic/issues/3828))
30
+ - A workflow child run whose stage had launched a background subagent no longer fails with `This extension ctx is stale after session replacement or reload` after the stage's model fallback replaces its session. The subagent's auto-routed model check, which the child also uses to pick its next fallback model, no longer reads the replaced session. The child now moves on to its next fallback model, and the run keeps the stage's real error, such as a rate limit. A subagent that waited for a free slot and starts after the replacement no longer fails to launch: it takes its settings from the replacement session, or from the launching session's last state when that session closed with no replacement ([#3981](https://github.com/bastani-inc/atomic/issues/3981))
31
+ - When the model that picks `auto` models (the router) answers with an HTTP 429 or an exhausted quota (402), the routing error now keeps that status, a rate-limit or quota code and the server's `Retry-After` hint. Before, it reported only `provider request failed`, so a workflow stage whose routing hit a rate limit was blocked as a provider outage. Now it is blocked as a rate limit or exhausted quota. The provider's error text is still never kept ([#4031](https://github.com/bastani-inc/atomic/issues/4031))
32
+ - Provider cooldowns now honor `Retry-After` on every provider path, including Anthropic-format models, instead of guessing an exponential backoff. HTTP 402 and quota-exhaustion codes (`insufficient_quota`, `quota_exceeded`, `usage_limit*`) are classified as the new `quota_exhausted` failure kind: the account counts as unavailable until its reset, and the turn moves to a configured fallback model instead of retrying the same model. When a rate-limit response names the model that hit the limit, only that model cools down; other models on the provider stay usable. Limits that do not name a model still cool the whole provider ([#3984](https://github.com/bastani-inc/atomic/issues/3984))
33
+ - A workflow stage whose model fallback replaced its session no longer fails the replacement's prompt with `Agent is already processing. Specify streamingBehavior…` when an Intercom message reaches the stage during the swap. Like a fresh stage, the replacement now holds turn-starting messages as context for the stage prompt's turn ([#3982](https://github.com/bastani-inc/atomic/issues/3982))
34
+ - Fixed a `.ts` storage adapter that serves both `sessions.storage` and `workflows.journal` failing to import `@bastani/atomic/workflows` when sessions loaded first; the journal now gets the running Atomic's workflows module ([#3973](https://github.com/bastani-inc/atomic/issues/3973))
35
+ - ES module and TypeScript files a storage adapter imports by relative path now get the running Atomic's `@bastani/atomic/workflows`, `@bastani/atomic/storage`, and `@bastani/atomic`, as the adapter's entry file does. Before, a `.mjs` file or a `.js` file in a `"type": "module"` adapter package could resolve these imports from `node_modules` instead, so errors such as `StaleLeaseError` thrown from it failed Atomic's `instanceof` checks. CommonJS files other than the entry file still resolve these imports from `node_modules` ([#3972](https://github.com/bastani-inc/atomic/issues/3972))
36
+ - Fixed subagents launched with `context: "fork"` finishing instantly as "completed" with the parent's last message as their output. A forked child's history now stops before the parent's in-flight turn that is launching it, so providers that sign thinking no longer reject the child's first request as a modified turn. A child's output now comes only from messages it produced itself, and a child whose own turns all failed, or that ran none, is reported as failed with the provider's error instead of completing with inherited text. A child that completed a turn without final text, such as a tool-only child, still completes ([#3987](https://github.com/bastani-inc/atomic/issues/3987))
37
+ - GitHub Copilot offers fewer models that fail on the first request with `model_not_available_for_integrator`: models enabled during `/login` are offered only if the model list re-read afterwards marks them usable, and an older login without a stored model list re-reads it on the next networked model refresh (until then, including offline, it keeps offering the catalog). Models you define for `github-copilot` in `models.json` stay offered. If Copilot still rejects a model this way, a fallback chain moves on to its next model, and the error reads "<model> isn't available to Atomic's GitHub Copilot integration; pick another model or run /login to refresh the model list" followed by the raw error ([#3998](https://github.com/bastani-inc/atomic/issues/3998))
38
+ - `tool_search` and codemode `searchTools()` rank a tool first when the query is its name, either the full name or the part after a `__` separator (`thread_read` for `mcp__app__thread_read`). Before, a shorter description that shared the name's words could rank above it ([pi #11331](https://github.com/earendil-works/pi/issues/11331))
39
+ - Writers to one SQLite journal in different processes now take its write lock in turn, so a busy process that commits and begins again can no longer starve the others, and a transaction waiting for the lock fails only when the lock is held for the whole lock wait with no other connection committing, instead of after 30 s however busy the lock was with real work. Large local worker pools no longer lose children to `database is locked … gave up after waiting 30000 ms for the write lock` under contention ([#3978](https://github.com/bastani-inc/atomic/issues/3978))
40
+ - The login dialog opens only web (`http:`/`https:`) sign-in URLs, and no longer prints control characters from sign-in, device-code or info URLs, which could end a terminal hyperlink and send escape sequences to the terminal. The terminal title likewise replaces control characters in session names and extension titles ([pi 89c921d8](https://github.com/earendil-works/pi/commit/89c921d8b56b59e768b26e7f48b6832978119310))
41
+ - A Bedrock `ServiceQuotaExceededException` and a Google error body reporting a per-day quota violation now count as an exhausted quota, so model fallback moves straight to the next model instead of retrying the same one, and a throttled Bedrock request (`ThrottlingException`, shown as `Throttling error: …`) now counts as a rate limit, so its `Retry-After` cools the model down for other requests. The classifier also reads the error code a provider failure diagnostic records.
42
+ - `tool_search` and codemode `searchTools()` now find a tool whose name is made only of stop words (for example `is`) when the query names it; other stop-word-only queries still return nothing.
43
+ - The `grep` tool numbers `context` lines the way ripgrep does when a file contains a lone carriage return: before, every line after it was off by one, so the wrong line showed as the match and the last context line went missing. Context and match lines no longer show the carriage return ([pi #11398](https://github.com/earendil-works/pi/issues/11398))
44
+ - An open extension dialog (`confirm`, `select`, `input`, `editor`, or an inline `ctx.ui.custom()` component) stays visible and answerable when an extension calls `ctx.ui.setEditorComponent()`. Before, the new editor replaced it on screen and the dialog waited until its timeout, or forever. The new editor appears when the last dialog closes ([pi #11431](https://github.com/earendil-works/pi/issues/11431))
45
+ - Installing, updating or removing an npm-hosted Atomic package no longer prints npm audit and funding messages. `npm audit fix` would have installed Atomic peer packages, because it runs without `--legacy-peer-deps` ([pi #11469](https://github.com/earendil-works/pi/pull/11469))
46
+ - Hidden thinking with an empty label (for example after `ctx.ui.setHiddenThinkingLabel("")`) no longer draws blank rows: before, a message with thinking drew two blank rows before a tool call and three before its text ([pi #11402](https://github.com/earendil-works/pi/issues/11402))
47
+ - A background shell stopped by a workflow stage or run pause is now recorded with the `pause` stop reason, and its completion notice says `Background shell interrupted by a pause` and asks to redo or continue the interrupted step, instead of "This execution has ended. Do not restart it". The shell's kind is saved with its completion intent, so a notice replayed after a restart keeps that wording; an intent saved by an earlier version asks to redo or continue the interrupted step and never offers a subagent resume call. Shells stopped with `/tasks` or `kill` keep the ended-for-good wording ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
48
+ - The `wait` and `kill` results for a shell stopped by a pause now say `stopped by a pause` instead of `paused · awaiting resume`, since a shell does not resume ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
49
+
5
50
  ## [0.9.46-alpha.1] - 2026-10-27
6
51
 
7
52
  Stages on local workers can now be watched, steered, paused and answered live; large pooled workflows stay responsive and recover cleanly from rate limits, worker crashes and resumes; and GPT-6.1 Terra is available.
@@ -4,6 +4,17 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.46] - 2026-10-27
8
+
9
+ ### Fixed
10
+
11
+ - The session that launched a workflow run can now send to that run's stages by exact path (`workflow:<rootRunId>/<stage>`) and by pattern (`workflow:<rootRunId>/**`), and sees them in `intercom list`, without joining the run's invocation group. Before, the send was refused with `Target workflow run is in a different intercom group`, because the run's route is registered on a separate connection and the broker could not tell that the sender had launched the run. The launching session now proves this with a secret that only its own process holds, never the run's stages. Its group memberships do not change, so the run's stages still cannot message it. A session that joins `workflow:<rootRunId>` keeps the same authority, and every other session is still refused, now with an error that names the group to join ([#4006](https://github.com/bastani-inc/atomic/issues/4006))
12
+ - A send to a recipient that is still saving the message, such as a stage on a local worker whose journal commit waits behind other writers, now waits for that save instead of reporting after 8 s that the outcome is uncertain. The recipient reports progress while it saves; a recipient that dies stops reporting, so its sender still hears the outcome is uncertain, and no save holds a sender longer than 2 minutes. This also holds when the recipient is connected through another broker node ([#3983](https://github.com/bastani-inc/atomic/issues/3983))
13
+ - A connection to the Intercom broker that the operating system refuses because the broker socket's listen backlog is full, as happens when a large pooled fan-out starts many sessions at once, is now retried briefly instead of failing that session's Intercom ([#3904](https://github.com/bastani-inc/atomic/issues/3904))
14
+ - A session that launched a workflow run, or that joined `workflow:<rootRunId>` from another group, now receives the failure notice when a message it queued to one of the run's pending stages becomes undeliverable (for example because the stage was skipped). The notice used to reach only senders registered in the run's own group, so these senders never learned their message was dropped. The notice still goes only to the session that sent the message ([#4006](https://github.com/bastani-inc/atomic/issues/4006))
15
+ - A send to a session on another broker node that quits cleanly while the send is being routed is now queued for that session and delivered when it reconnects, as a send made after the quit is. Before, when the sender's broker still saw the session as connected, the sender waited 8 s for a receipt that could never come and was told the outcome was uncertain, although the message had not reached anyone.
16
+ - A workflow stage registered outside a run (another run's stage, or an isolated subgroup stage of the same run) that tries to send to the run's stages is now told `A workflow stage in <its group> cannot send to stages of workflow:<rootRunId>; joining that group does not grant it control`, instead of being told to join a group that would not help ([#4006](https://github.com/bastani-inc/atomic/issues/4006))
17
+
7
18
  ## [0.9.46-alpha.1] - 2026-10-27
8
19
 
9
20
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.46-alpha.1",
3
+ "version": "0.9.46",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.46-alpha.1",
3
+ "version": "0.9.46",
4
4
  "private": true,
5
5
  "description": "Atomic's pi-compatible MCP integration for server connections, OAuth, tools, resources, and terminal management.",
6
6
  "contributors": [
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.46] - 2026-10-27
6
+
7
+ ### Fixed
8
+
9
+ - A subagent that waited for a free slot (`concurrency`, or a burst of launches) or was restored, and that starts after its parent workflow stage's session was replaced by model fallback, now launches instead of failing with `This extension ctx is stale after session replacement or reload`. It takes its settings from the replacement session, and starts in the directory it was launched for ([#3981](https://github.com/bastani-inc/atomic/issues/3981))
10
+ - A detached subagent that exits after its parent workflow stage's session was replaced by model fallback still sends its completion notice. Before, the notice was lost and the workflow run failed with `This extension ctx is stale after session replacement or reload` ([#3981](https://github.com/bastani-inc/atomic/issues/3981))
11
+ - A resumed subagent child now receives its resume message and the messages it had queued and not read exactly as written. They used to go through `prompt()` or `followUp()` depending on whether the child had held messages, so `/review …` was replaced by a prompt template's contents, a registered command such as `/workflow` either ran or failed the child with `Extension command "/workflow" cannot be queued`, and text the child had already received was expanded a second time. Only an original task the child never read is still prompted the way a fresh launch prompts it ([#4027](https://github.com/bastani-inc/atomic/pull/4027)).
12
+ - A subagent child paused, quit or stopped by its owner while it waited for its run's running capacity, before its session existed, can be resumed and restored again: it used to report `could not be resumed: no session was recorded for it`. A child admitted behind its run's concurrency limit and stopped that way no longer receives its task twice when it is restored. Resuming or restoring a child that had not started now says it started fresh with its task instead of claiming it resumed from its own session ([#4027](https://github.com/bastani-inc/atomic/pull/4027)).
13
+ - A subagent whose model is pinned with a thinking suffix (such as `provider/model:low`) no longer shows that suffix as part of its model on its launch and status cards once its session has started; the cards show `provider/model` and the thinking level beside it, as they do before the session starts ([#4009](https://github.com/bastani-inc/atomic/issues/4009))
14
+ - A paused subagent child resumed without a message, and a child restored after a restart, are now told to redo or continue a command or step the pause or restart interrupted, as a resumed workflow stage is ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
15
+
5
16
  ## [0.9.46-alpha.1] - 2026-10-27
6
17
 
7
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.46-alpha.1",
3
+ "version": "0.9.46",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -14866,7 +14866,7 @@ async function maybeBuildForegroundIntercomReceipt(input) {
14866
14866
  }
14867
14867
 
14868
14868
  // dist/builtin/subagents/src/runs/foreground/subagent-child-restore.ts
14869
- var SUBAGENT_RESUME_CONTINUATION_PROMPT = "Atomic restarted while you were working, and your session has been restored. Continue where you left off. If you believe you are finished with your original task, stop.";
14869
+ var SUBAGENT_RESUME_CONTINUATION_PROMPT = "Atomic restarted while you were working, and your session has been restored. Continue where you left off. If the restart interrupted a command or step you were running, redo or continue that step as needed. If you believe you are finished with your original task, stop.";
14870
14870
  var SUBAGENT_AWAIT_INTERCOM_PEER_EVENT = "subagent:await-intercom-peer";
14871
14871
  var SUBAGENT_INTERCOM_PEER_TIMEOUT_MS = 15000;
14872
14872
  var SUBAGENT_AWAIT_CHILD_RESTORATION_EVENT = "subagent:await-child-restoration";
@@ -17484,7 +17484,7 @@ async function runSinglePath(data, deps) {
17484
17484
  }
17485
17485
 
17486
17486
  // dist/builtin/subagents/src/runs/foreground/subagent-run-control.ts
17487
- var SUBAGENT_RUN_CONTROL_CONTINUATION_PROMPT = "Continue where you left off. If you believe you are finished with your original task (or a redefined task if the user told you), stop.";
17487
+ var SUBAGENT_RUN_CONTROL_CONTINUATION_PROMPT = "Continue where you left off. If a pause interrupted a command or step you were running, redo or continue that step as needed. If you believe you are finished with your original task (or a redefined task if the user told you), stop.";
17488
17488
  var NON_RESUMABLE_STATUSES = new Map([
17489
17489
  ["killed", "it was cancelled for good (an owner or batch cancellation, or the removed kill action)"],
17490
17490
  [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.46-alpha.1",
3
+ "version": "0.9.46",
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,46 @@ 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.46] - 2026-10-27
10
+
11
+ ### Added
12
+
13
+ - `RunResult.failure` (type `RunFailureClassification`, exported from `@bastani/atomic/workflows`) describes how a failed or blocked run's failure was classified: `kind`, `code`, `retryable`, `resumable`, `recoverability`, `disposition` and `retryAfterMs`. It is absent for runs that did not fail and for results recorded by earlier releases ([#4031](https://github.com/bastani-inc/atomic/issues/4031))
14
+ - Opening a running stage that runs on a local worker (`workflows.localWorkers`) from the workflow graph or `/workflow attach` now shows its live chat: the transcript so far, the reply as it streams, and tool calls, with `on local worker` in the chat header. It no longer shows "Post-mortem chat is available only after the stage completes". Only stages you have open stream to the root, so pools with many workers send nothing extra for stages nobody watches. ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
15
+ - Enter (steer) and Ctrl+F (follow-up) in the chat of a stage that runs on a local worker now reach that stage and report the same delivery as an in-process stage. Each message is admitted at most once, even when a reply is lost and the send is retried. The stage's graph node shows `✉ N queued` with no chat open. SDK runs with `localWorkers` now accept `stageControlRegistry`, whose handles for pooled stages steer, queue follow-ups and stream events the same way ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
16
+ - A single stage on a local worker can now be paused and resumed (`/workflow pause <run> <stage>`, stage chat, `session.workflows.pause/resume` with `stageId`) without pausing the pool; it keeps its worker's lease while paused. When the worker running an open stage dies or its attempt is fenced, the chat shows `RECONNECTING`, reattaches to the replacement attempt, and redelivers once each, in send order and in their original mode (a steer still steers), the messages the replacement does not have ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
17
+ - Children on local workers can now ask for input. Their `ctx.ui` prompts show on their stage and are answered from the stage chat or the workflow tool's `answer` action, and a stage's `ask_user_question` shows its form in the stage chat as for an in-process stage; each answer reaches only the prompt it names on the worker's current attempt. Previously `ctx.ui` in a pooled child failed the child. SDK runs with `localWorkers` now accept `ui`, which answers those prompts (a rejecting `ui` fails the child with its error), and `stageUiBroker`, which receives the stages' `ask_user_question` forms (a broker that rejects a form fails that question in the stage with its error). `ctx.ui.custom` in a pooled child fails it with an error naming the limitation. When a worker dies, the replacement asks again a `ctx.ui` prompt that was still pending, and does not ask again one already answered. A stage's `ask_user_question` still waiting when its worker dies (or when a durable run resumes a stage) shows its form again on the replacement, and the answer becomes that call's result; previously the call was reported to the model as interrupted and the stage could finish without the answer ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
18
+ - Added a GPT-6.1 Terra reference to the prompt-engineering skill, based on OpenAI's updated GPT-6 guide, covering when to choose Terra, effort selection, migration from GPT-5.6 Terra, computer-use and writing prompts, and selective use of the shared templates observed on Astra.
19
+
20
+ ### Fixed
21
+
22
+ - Starting a large local worker pool (`workflows.localWorkers`) no longer freezes the root's input and rendering for hundreds of milliseconds while the workers start. When a workflow starts many pooled children at once, such as `Promise.all` over 150 `ctx.workflow` calls, their journal work used to run back to back in one turn of the event loop, and each child's dispatch listed and sorted every checkpoint of the root. While many journal operations are queued, journal work now gives the event loop a turn after 8 ms, checked before each operation, and a child's dispatch looks up only its own boundary. When those children finish at once, completing each one no longer reads every checkpoint of the root, and the sweep for undeliverable stage messages checks run ownership only for runs that have messages to settle, and children finishing together complete in turns of the event loop instead of all at once
23
+ - An Intercom send to a workflow path pattern that matches a live or pending stage, such as `workflow:<run>/**/work-2` for a stage in a child on a local worker, no longer reports `notInKnownSet` ("not in the workflow's known stage set"). The message was delivered, but the known set came only from the launch-time scan of the workflow source, which cannot see stages of children loaded with `loadWorkerWorkflow`. Stages that already exist and can receive messages now count as known; a target that matches neither the scan nor any such stage still gets the warning.
24
+ - Large pooled fan-outs no longer stall the root's main thread when the workflow graph or a stage chat is open: typing in a stage chat during dispatch, wave turnover and run end of 150+ local workers stayed responsive only with nothing attached. Each store change used to re-project, re-freeze, re-expand and re-serialize every run of the root for the graph snapshot, the graph view, the attach pane, the workflow activity bridge and the BACKGROUND widget. Unchanged runs and stages now keep their frozen graph projections and expansions, the graph view and attach pane catch up once before they render or handle input, an open stage chat projects only its own run, the activity bridge publishes each root's final activity once per turn of the event loop (reading published activity, a root run's own lifecycle event and `waitForSettled` always see it first), store-driven widget refreshes spend at most 8 ms per 50 ms after the first four, and the coordinator gets the root's boundaries once per turn instead of once per enqueue and published batch. A pooled child whose worker was replaced also keeps its last known stages, marked `reconnecting`, until the replacement republishes them, instead of briefly showing no stages ([#4009](https://github.com/bastani-inc/atomic/issues/4009))
25
+ - Stages on local workers (`workflows.localWorkers`) now share provider rate limits. When one worker's stage gets an HTTP 429 or an exhausted quota (HTTP 402), the other workers and the root apply the same cooldown or unavailable mark, with the same provider or model scope and the same `Retry-After` end. They hold requests to that provider or model back, or move their fallback walk past it, instead of each sending requests until it gets its own 429. A worker that starts later gets the limits still running. Limits are sent only when a request is rate limited, at most once per provider or model every 250 ms ([#4023](https://github.com/bastani-inc/atomic/issues/4023))
26
+ - Text typed into a stage chat reaches the stage the same way whether the stage is idle, streaming, or still preparing its turn. Only `/skill:` commands expand. Before, Enter or Ctrl+F while the stage was busy expanded prompt templates such as `/review` and ran registered extension commands, or refused them, while the same text sent to an idle stage arrived as typed
27
+ - A message given when resuming a stage that was paused in the middle of its workflow prompt is now delivered as typed, as it is when resuming an idle stage chat. It used to go through the stage prompt's own options, which expand prompt templates and run registered extension commands
28
+ - A failed child workflow (`ctx.workflow`), or a failed stage, no longer has its failure classified from workflow or stage names. Before, a name containing words such as `rate-limit`, `overloaded` or `quota` could turn an ordinary failure, such as a tool that throws `ENOENT`, into a resumable rate limit or provider outage. The root then stayed `running`, blocked for a resume that could not help, and `run()` returned status `running` with the error. A parent now classifies a failed child from the child's own classification (`RunResult.failure`), at any nesting depth, for in-process children and children on local workers, including a child result a resumed root reads back from the journal. A stage's model-resolution failure is classified from the routing error it wraps. A child that really fails on a rate limit still blocks the root resumably. Error messages are unchanged ([#4031](https://github.com/bastani-inc/atomic/issues/4031))
29
+ - A stage whose model hits an exhausted quota (HTTP 402) or a rate limit that resets beyond the retry-delay cap now moves straight to the next fallback model, without spending retries or cooldown-wait rounds on it. A model-specific rate limit no longer delays a fallback on the same provider, and an HTTP 529 overload waits for its `Retry-After` like a 503 ([#3984](https://github.com/bastani-inc/atomic/issues/3984))
30
+ - A stage whose whole model and fallback chain returns HTTP 429 now waits for the earliest recorded provider cooldown and retries, instead of failing after one attempt per model. The wait is bounded: at most `retry.maxRetries` extra rounds per prompt, and only when the earliest cooldown fits the retry-delay cap (`retry.provider.maxRetryDelayMs`, else `retry.maxAgentDelayMs`, else 60 s). A persistent 429 still fails the stage once those rounds are spent ([#3984](https://github.com/bastani-inc/atomic/issues/3984))
31
+ - A workflow run whose stage replaced its session through model fallback no longer fails with `This extension ctx is stale after session replacement` and hides the stage's real error (such as a rate limit). An unhandled stale extension-context rejection no longer fails the run. It is logged to stderr with its stack and the run id, so the code that kept the retired session's context can be found ([#3981](https://github.com/bastani-inc/atomic/issues/3981))
32
+ - Disposing an SDK session right after a workflow run reports `completed`, without awaiting `session.workflows.waitForSettled(runId)`, no longer prints one `atomic-workflows: released stage listener failed Error: SessionClosed` warning per stage ([#3966](https://github.com/bastani-inc/atomic/issues/3966))
33
+ - A workflow stage that has sent its first prompt no longer reads `startup first-dispatch (0s total, 0s on current step; dispatched)` in the workflow tool's `stages` and `stage` output, which looked like a stalled stage for the whole run. The line now reads `startup complete (dispatched after 2s)` ([#3967](https://github.com/bastani-inc/atomic/issues/3967))
34
+ - A local-worker pool no longer fails every in-flight child with `Local worker process cleanup unconfirmed; permits quarantined` when one worker exits while its tool process groups are still running. The coordinator now reaps that worker's orphaned process groups (SIGTERM, then SIGKILL, with a bounded wait), holds the child's work lease so no other worker can claim it meanwhile, releases the permits of groups that are gone or hold only zombies, and replays only that worker's child on a replacement. If a group is still alive when the bound expires, only that child fails, with a retryable `WorkflowInfrastructureError` (`worker_cleanup_unconfirmed`) that names the process groups; their permits stay quarantined and the child is not replayed alongside them, also not by recovery after a stalled worker is stopped or by a durable resume (both read the quarantine from the journal), and closing the pool reports those groups. A durable resume reaps such an attempt's groups with the same bounded sequence, then replays it in the resumed root or fails only that child ([#3986](https://github.com/bastani-inc/atomic/issues/3986))
35
+ - A direct Intercom send to a live stage in a local worker pool no longer routinely fails with `The recipient did not confirm that it saved the message in time` while the pool is busy. Saving a message to a pooled stage's session no longer waits for the worker to re-read the root workflow's journal or for a usage-total transaction; it is confirmed once the coordinator has committed it ([#3983](https://github.com/bastani-inc/atomic/issues/3983))
36
+ - Resuming a quit or crashed root whose children run in `workflows.localWorkers` no longer fails in-flight children with `Session not found`. Resume used to make the previous run's pooled children claimable before the resumed root had enqueued them again, so workers crashed on `Stale work lease`, the pool failed with `Local worker retry limit`, and later children ran in the root process, which cannot see pooled stage sessions. A pooled child is now claimed only after the resumed root enqueues it, a child started after the pool has failed gets the pool's error instead of running in the root process, the three-attempt retry limit counts only crashes within one run of the root (a child resumed many times keeps its full budget), and a worker whose attempt was handed to another worker ends that attempt and serves new work instead of crashing and using up a retry ([#3979](https://github.com/bastani-inc/atomic/issues/3979))
37
+ - Large local worker pools no longer lose child workflows to `database is locked … gave up after waiting 30000 ms for the write lock` after a few hours. Resource admission reconciliation, which every pooled worker runs before it starts a command, read the whole resource permit history twice for every live worker while it held the journal's write lock. With 160 workers and tens of thousands of permits, each pass held the lock for nearly a second. Reconciliation now uses indexes, keeps only the most recent 256 released permits, and takes the write lock only when it has a permit to fence or prune ([#3978](https://github.com/bastani-inc/atomic/issues/3978))
38
+ - Workflow runs and local worker definitions load again after something such as a `/tmp` cleanup deletes the `jiti` transpile cache directory, instead of failing with `ENOENT: no such file or directory, open '/tmp/jiti/workflows-<name>.<hash>.cjs'` until Atomic restarts ([#3985](https://github.com/bastani-inc/atomic/issues/3985))
39
+ - A sticky Intercom send to a workflow pattern target (for example `workflow:<rootRunId>/**`) no longer fails newly starting pooled stages with `Extension startup failed: … pending-stage message <id> changed during delivery`. A stage drain whose transcript snapshot was taken before a slow session append finished used to deliver the already-receipted message again and then fail. The drain now treats an existing delivery receipt for that stage and session as success, so the message reaches each stage exactly once ([#3980](https://github.com/bastani-inc/atomic/issues/3980))
40
+ - A workflow that starts many child workflows at once, such as `Promise.all` over 150 `ctx.workflow` calls, no longer stalls the root session's input and rendering while the children start. Each workflow store change used to re-read every run and stage to detect lifecycle transitions, so a fan-out's cost grew with the square of its width; only the run, stage or tool node that changed is now re-read. Lifecycle events are unchanged
41
+ - A finished stage that ran on a local worker now reopens for post-mortem chat. It used to report `The retained session is missing, unreadable, or invalid` in the same session and SESSION UNAVAILABLE after a restart, because its session lives in the workflow journal rather than the host's session store ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
42
+ - A workflow heartbeat no longer goes missing when its wake-up timer fires a moment early. The early wake-up found nothing due and rescheduled, and if the boundary passed while that pass was running, the heartbeat moved to the following interval instead of being sent. The scheduler now reads the clock once per pass, so a boundary that comes due during it is still delivered
43
+ - An idle local worker that meets its root's released lease (between the root's teardown and the pool's close) now ends cleanly instead of crashing, and the pool no longer spawns replacements that crash the same way ([#3979](https://github.com/bastani-inc/atomic/issues/3979))
44
+ - A stage chat open on a `ctx.ui` prompt or `ask_user_question` form of a stage on a local worker (`workflows.localWorkers`) now takes the answer after that worker dies: it shows the prompt the replacement worker asks again and delivers your answer to it once. Before, the open chat kept a prompt that dropped typed text and ignored Enter until you closed and reopened the stage. An answer you submit after the worker died but before the replacement asks again is now kept and given to the replacement's identical prompt instead of being lost, so you are not asked twice. The re-asked `ask_user_question` form no longer shows "Tool execution was interrupted after it started…" above it. A replacement worker whose child asks a different `ctx.ui` question at the same point (its text depends on data) no longer fails the child with `Pool topology scope: direct child source orders are not unique`: the unanswered question of the dead worker is replaced, an open stage chat moves to the new question, and an answer held for the old question is discarded. A child that really takes different steps when it runs again now fails with an error that says so in plain words. The forms of `ask_user_question` and the readiness gate are now built by one helper for in-process and local-worker stages, so they cannot drift apart ([#4022](https://github.com/bastani-inc/atomic/issues/4022))
45
+ - A child on a local worker that loses its worker three times now fails alone with a retryable `WorkflowInfrastructureError` (`worker_retry_limit`) that names its child run and definition export, as #3986 already did for a child whose processes survive cleanup. Before, it failed the whole pool within milliseconds: every other child, including one waiting on a prompt whose worker was never touched, was rejected with `Local worker retry limit`. The same applies to a child whose every attempt the root refused as stale. The failed child, its nested runs and their unfinished stages show failed with its error, so the graph counts none of them as active. Workers that keep crashing without work, worker startup failures and a coordinator that dies still fail the pool. When a pool closes without a quit, the root's unfinished work rows end `failed` instead of staying `ready` or `leased`; quarantined attempts keep their leases, and a resumed root replays failed work it enqueues again ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
46
+ - The post-mortem chat of a completed or failed stage that ran on a local worker no longer shows `on local worker` in its header. Post-mortem chat runs in the root, so the header now matches an in-process stage's ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
47
+ - Pausing a stage or a run (`/workflow pause <run> [stage]`) no longer tells the stage that its interrupted background shell ended for good. The shell's notice now says a pause interrupted it and that the step should be redone or continued, and the "Continue where you left off" resume prompt says to redo or continue a step the pause interrupted. Previously the notice said "This execution has ended. Do not restart it", and a resumed stage could stop early with its remaining work never run ([#3996](https://github.com/bastani-inc/atomic/issues/3996))
48
+
9
49
  ## [0.9.46-alpha.1] - 2026-10-27
10
50
 
11
51
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.46-alpha.1",
3
+ "version": "0.9.46",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -13256,7 +13256,7 @@ var init_stage_question_form = __esm(() => {
13256
13256
  });
13257
13257
 
13258
13258
  // dist/builtin/workflows/src/shared/resume-continuation.ts
13259
- var RESUME_CONTINUATION_PROMPT = "Continue where you left off. If you believe you are finished with your original task (or a redefined task if the user told you), stop.";
13259
+ var RESUME_CONTINUATION_PROMPT = "Continue where you left off. If a pause interrupted a command or step you were running, redo or continue that step as needed. If you believe you are finished with your original task (or a redefined task if the user told you), stop.";
13260
13260
 
13261
13261
  // dist/builtin/workflows/src/runs/foreground/executor-hil.ts
13262
13262
  import { createHash as createHash2 } from "node:crypto";
@@ -67675,13 +67675,19 @@ class HeldPromptAnswers {
67675
67675
  if (held.runId === runId && generation > held.generation && !present.has(key))
67676
67676
  this.held.delete(key);
67677
67677
  }
67678
+ awaitsLaterAttempt(runId, stageId, generation) {
67679
+ const held = this.held.get(JSON.stringify([runId, stageId]));
67680
+ return held !== undefined && generation <= held.generation;
67681
+ }
67678
67682
  takePrompt(runId, stageId, prompt, generation) {
67679
67683
  const key = JSON.stringify([runId, stageId]);
67680
67684
  const held = this.held.get(key);
67681
67685
  if (held === undefined || generation <= held.generation)
67682
67686
  return;
67683
67687
  this.held.delete(key);
67684
- return held.kind === "prompt" && held.identity === promptIdentity(prompt) ? { response: held.response } : undefined;
67688
+ if (held.kind !== "prompt" || held.identity !== promptIdentity(prompt))
67689
+ return;
67690
+ return "error" in held ? { error: held.error } : { response: held.response };
67685
67691
  }
67686
67692
  takeQuestion(runId, stageId, question, generation) {
67687
67693
  const key = JSON.stringify([runId, stageId]);
@@ -67689,7 +67695,9 @@ class HeldPromptAnswers {
67689
67695
  if (held === undefined || generation <= held.generation)
67690
67696
  return;
67691
67697
  this.held.delete(key);
67692
- return held.kind === "question" && held.identity === questionIdentity(question) ? { answer: held.answer } : undefined;
67698
+ if (held.kind !== "question" || held.identity !== questionIdentity(question))
67699
+ return;
67700
+ return "error" in held ? { error: held.error } : { answer: held.answer };
67693
67701
  }
67694
67702
  clear() {
67695
67703
  this.held.clear();
@@ -71148,18 +71156,39 @@ class LocalWorkerPool {
71148
71156
  const key = JSON.stringify([run.id, stage.id, prompt.id]);
71149
71157
  if (this.routedPrompts.has(key))
71150
71158
  continue;
71159
+ if (this.heldAnswers.awaitsLaterAttempt(run.id, stage.id, generation))
71160
+ continue;
71151
71161
  this.routedPrompts.add(key);
71152
71162
  const held = this.heldAnswers.takePrompt(run.id, stage.id, prompt, generation);
71153
- if (held !== undefined)
71163
+ if (held !== undefined && "response" in held)
71154
71164
  store?.resolveStagePendingPrompt(run.id, stage.id, prompt.id, held.response);
71155
- const fail = (error) => this.answerPooledStagePrompt(run.id, stage.id, {
71156
- kind: "failed",
71157
- promptId: prompt.id,
71158
- error: error.message
71159
- });
71165
+ const fail = async (error) => {
71166
+ const result = await this.answerPooledStagePrompt(run.id, stage.id, {
71167
+ kind: "failed",
71168
+ promptId: prompt.id,
71169
+ error: error.message
71170
+ });
71171
+ if (!result.ok && result.refusal !== "StageNotLive") {
71172
+ this.heldAnswers.hold(run.id, stage.id, {
71173
+ kind: "prompt",
71174
+ generation,
71175
+ promptId: prompt.id,
71176
+ identity: promptIdentity(prompt),
71177
+ error: error.message
71178
+ });
71179
+ this.applyHeldAnswer(run.id, stage.id, generation);
71180
+ }
71181
+ return result;
71182
+ };
71183
+ let failedElsewhere = held !== undefined && "error" in held;
71160
71184
  const heldLater = Promise.withResolvers();
71161
- this.routedPromptAnswers.set(key, heldLater.resolve);
71162
- const answered = held !== undefined ? Promise.resolve(held.response) : prompt.kind === "custom" ? Promise.reject(new Error(POOLED_CUSTOM_PROMPT_UNAVAILABLE)) : Promise.race([
71185
+ this.routedPromptAnswers.set(key, (outcome) => {
71186
+ if ("response" in outcome)
71187
+ return heldLater.resolve(outcome.response);
71188
+ failedElsewhere = true;
71189
+ heldLater.reject(new Error(outcome.error));
71190
+ });
71191
+ const answered = held !== undefined ? "response" in held ? Promise.resolve(held.response) : Promise.reject(new Error(held.error)) : prompt.kind === "custom" ? Promise.reject(new Error(POOLED_CUSTOM_PROMPT_UNAVAILABLE)) : Promise.race([
71163
71192
  heldLater.promise,
71164
71193
  this.promptUi !== undefined ? askStagePromptUi(this.promptUi, prompt) : store?.awaitStagePendingPrompt(run.id, stage.id, prompt.id) ?? Promise.reject(new Error("No workflow store answers this prompt."))
71165
71194
  ]);
@@ -71181,9 +71210,9 @@ class LocalWorkerPool {
71181
71210
  this.applyHeldAnswer(run.id, stage.id, generation);
71182
71211
  }
71183
71212
  return result;
71184
- }, (error) => this.promptUi !== undefined || prompt.kind === "custom" ? fail(error) : undefined);
71213
+ }, (error) => failedElsewhere || this.promptUi !== undefined || prompt.kind === "custom" ? fail(error) : undefined);
71185
71214
  answer.then((result) => {
71186
- if (result?.ok === false && result.refusal === "StaleAttempt")
71215
+ if (result?.ok === false && result.refusal !== "StageNotLive")
71187
71216
  this.routedPrompts.delete(key);
71188
71217
  }).catch(() => this.routedPrompts.delete(key));
71189
71218
  }
@@ -71219,8 +71248,10 @@ class LocalWorkerPool {
71219
71248
  if (question?.promptId !== promptId || signal.aborted)
71220
71249
  return signal.aborted;
71221
71250
  const held = this.heldAnswers.takeQuestion(runId, stageId, question, generation);
71222
- if (held !== undefined)
71223
- return this.sendQuestionAnswer(runId, stageId, question, generation, held.answer, route);
71251
+ if (held !== undefined) {
71252
+ const sent = await this.sendQuestionOutcome(runId, stageId, question, generation, held, route);
71253
+ return sent && "error" in held ? "failed" : sent;
71254
+ }
71224
71255
  const broker = this.questionBroker ?? currentStageUiBroker();
71225
71256
  let answered;
71226
71257
  let failure;
@@ -71244,19 +71275,15 @@ class LocalWorkerPool {
71244
71275
  broker.clearStagePrompt(runId, stageId);
71245
71276
  }
71246
71277
  if (failure !== undefined) {
71247
- await this.answerPooledStagePrompt(runId, stageId, { kind: "failed", promptId, error: failure.message });
71248
- return "failed";
71278
+ const sent = await this.sendQuestionOutcome(runId, stageId, question, generation, { error: failure.message }, route);
71279
+ return sent ? "failed" : false;
71249
71280
  }
71250
71281
  if (answered === undefined)
71251
71282
  return true;
71252
- return this.sendQuestionAnswer(runId, stageId, question, generation, { raw: answered.value }, route);
71283
+ return this.sendQuestionOutcome(runId, stageId, question, generation, { answer: { raw: answered.value } }, route);
71253
71284
  }
71254
- async sendQuestionAnswer(runId, stageId, question, generation, answer, route) {
71255
- const result = await this.answerPooledStagePrompt(runId, stageId, {
71256
- kind: "input",
71257
- promptId: question.promptId,
71258
- answer
71259
- });
71285
+ async sendQuestionOutcome(runId, stageId, question, generation, outcome, route) {
71286
+ const result = await this.answerPooledStagePrompt(runId, stageId, "error" in outcome ? { kind: "failed", promptId: question.promptId, error: outcome.error } : { kind: "input", promptId: question.promptId, answer: outcome.answer });
71260
71287
  if (result.ok || result.refusal === "StageNotLive")
71261
71288
  return true;
71262
71289
  this.heldAnswers.hold(runId, stageId, {
@@ -71264,7 +71291,7 @@ class LocalWorkerPool {
71264
71291
  generation,
71265
71292
  promptId: question.promptId,
71266
71293
  identity: questionIdentity(question),
71267
- answer
71294
+ ...outcome
71268
71295
  });
71269
71296
  const key = JSON.stringify([runId, stageId]);
71270
71297
  const active = this.routedQuestions.get(key);
@@ -71272,7 +71299,7 @@ class LocalWorkerPool {
71272
71299
  this.routedQuestions.delete(key);
71273
71300
  else if (active !== undefined && active.promptId === question.promptId && active.generation > generation) {
71274
71301
  this.routedQuestions.delete(key);
71275
- active.controller.abort(new Error("The question was answered before its worker was replaced."));
71302
+ active.controller.abort(new Error("The question was settled before its worker was replaced."));
71276
71303
  }
71277
71304
  this.applyHeldAnswer(runId, stageId, generation);
71278
71305
  return false;
@@ -71293,9 +71320,9 @@ class LocalWorkerPool {
71293
71320
  const held = this.heldAnswers.takePrompt(runId, stageId, prompt, generation);
71294
71321
  if (held === undefined)
71295
71322
  return;
71296
- if (this.promptUi === undefined)
71323
+ if (this.promptUi === undefined && "response" in held)
71297
71324
  this.store?.resolveStagePendingPrompt(runId, stageId, prompt.id, held.response);
71298
- deliver(held.response);
71325
+ deliver(held);
71299
71326
  }
71300
71327
  async pausePooledStage(watch) {
71301
71328
  if (this.closed || this.coordinatorLost !== undefined)
@@ -10755,7 +10755,7 @@ var init_stage_question_form = __esm(() => {
10755
10755
  });
10756
10756
 
10757
10757
  // dist/builtin/workflows/src/shared/resume-continuation.ts
10758
- var RESUME_CONTINUATION_PROMPT = "Continue where you left off. If you believe you are finished with your original task (or a redefined task if the user told you), stop.";
10758
+ var RESUME_CONTINUATION_PROMPT = "Continue where you left off. If a pause interrupted a command or step you were running, redo or continue that step as needed. If you believe you are finished with your original task (or a redefined task if the user told you), stop.";
10759
10759
 
10760
10760
  // dist/builtin/workflows/src/runs/foreground/executor-hil.ts
10761
10761
  import { createHash as createHash2 } from "node:crypto";
@@ -64239,13 +64239,19 @@ class HeldPromptAnswers {
64239
64239
  if (held.runId === runId && generation > held.generation && !present.has(key))
64240
64240
  this.held.delete(key);
64241
64241
  }
64242
+ awaitsLaterAttempt(runId, stageId, generation) {
64243
+ const held = this.held.get(JSON.stringify([runId, stageId]));
64244
+ return held !== undefined && generation <= held.generation;
64245
+ }
64242
64246
  takePrompt(runId, stageId, prompt, generation) {
64243
64247
  const key = JSON.stringify([runId, stageId]);
64244
64248
  const held = this.held.get(key);
64245
64249
  if (held === undefined || generation <= held.generation)
64246
64250
  return;
64247
64251
  this.held.delete(key);
64248
- return held.kind === "prompt" && held.identity === promptIdentity(prompt) ? { response: held.response } : undefined;
64252
+ if (held.kind !== "prompt" || held.identity !== promptIdentity(prompt))
64253
+ return;
64254
+ return "error" in held ? { error: held.error } : { response: held.response };
64249
64255
  }
64250
64256
  takeQuestion(runId, stageId, question, generation) {
64251
64257
  const key = JSON.stringify([runId, stageId]);
@@ -64253,7 +64259,9 @@ class HeldPromptAnswers {
64253
64259
  if (held === undefined || generation <= held.generation)
64254
64260
  return;
64255
64261
  this.held.delete(key);
64256
- return held.kind === "question" && held.identity === questionIdentity(question) ? { answer: held.answer } : undefined;
64262
+ if (held.kind !== "question" || held.identity !== questionIdentity(question))
64263
+ return;
64264
+ return "error" in held ? { error: held.error } : { answer: held.answer };
64257
64265
  }
64258
64266
  clear() {
64259
64267
  this.held.clear();
@@ -67803,18 +67811,39 @@ class LocalWorkerPool {
67803
67811
  const key = JSON.stringify([run.id, stage.id, prompt.id]);
67804
67812
  if (this.routedPrompts.has(key))
67805
67813
  continue;
67814
+ if (this.heldAnswers.awaitsLaterAttempt(run.id, stage.id, generation))
67815
+ continue;
67806
67816
  this.routedPrompts.add(key);
67807
67817
  const held = this.heldAnswers.takePrompt(run.id, stage.id, prompt, generation);
67808
- if (held !== undefined)
67818
+ if (held !== undefined && "response" in held)
67809
67819
  store?.resolveStagePendingPrompt(run.id, stage.id, prompt.id, held.response);
67810
- const fail = (error) => this.answerPooledStagePrompt(run.id, stage.id, {
67811
- kind: "failed",
67812
- promptId: prompt.id,
67813
- error: error.message
67814
- });
67820
+ const fail = async (error) => {
67821
+ const result = await this.answerPooledStagePrompt(run.id, stage.id, {
67822
+ kind: "failed",
67823
+ promptId: prompt.id,
67824
+ error: error.message
67825
+ });
67826
+ if (!result.ok && result.refusal !== "StageNotLive") {
67827
+ this.heldAnswers.hold(run.id, stage.id, {
67828
+ kind: "prompt",
67829
+ generation,
67830
+ promptId: prompt.id,
67831
+ identity: promptIdentity(prompt),
67832
+ error: error.message
67833
+ });
67834
+ this.applyHeldAnswer(run.id, stage.id, generation);
67835
+ }
67836
+ return result;
67837
+ };
67838
+ let failedElsewhere = held !== undefined && "error" in held;
67815
67839
  const heldLater = Promise.withResolvers();
67816
- this.routedPromptAnswers.set(key, heldLater.resolve);
67817
- const answered = held !== undefined ? Promise.resolve(held.response) : prompt.kind === "custom" ? Promise.reject(new Error(POOLED_CUSTOM_PROMPT_UNAVAILABLE)) : Promise.race([
67840
+ this.routedPromptAnswers.set(key, (outcome) => {
67841
+ if ("response" in outcome)
67842
+ return heldLater.resolve(outcome.response);
67843
+ failedElsewhere = true;
67844
+ heldLater.reject(new Error(outcome.error));
67845
+ });
67846
+ const answered = held !== undefined ? "response" in held ? Promise.resolve(held.response) : Promise.reject(new Error(held.error)) : prompt.kind === "custom" ? Promise.reject(new Error(POOLED_CUSTOM_PROMPT_UNAVAILABLE)) : Promise.race([
67818
67847
  heldLater.promise,
67819
67848
  this.promptUi !== undefined ? askStagePromptUi(this.promptUi, prompt) : store?.awaitStagePendingPrompt(run.id, stage.id, prompt.id) ?? Promise.reject(new Error("No workflow store answers this prompt."))
67820
67849
  ]);
@@ -67836,9 +67865,9 @@ class LocalWorkerPool {
67836
67865
  this.applyHeldAnswer(run.id, stage.id, generation);
67837
67866
  }
67838
67867
  return result;
67839
- }, (error) => this.promptUi !== undefined || prompt.kind === "custom" ? fail(error) : undefined);
67868
+ }, (error) => failedElsewhere || this.promptUi !== undefined || prompt.kind === "custom" ? fail(error) : undefined);
67840
67869
  answer.then((result) => {
67841
- if (result?.ok === false && result.refusal === "StaleAttempt")
67870
+ if (result?.ok === false && result.refusal !== "StageNotLive")
67842
67871
  this.routedPrompts.delete(key);
67843
67872
  }).catch(() => this.routedPrompts.delete(key));
67844
67873
  }
@@ -67874,8 +67903,10 @@ class LocalWorkerPool {
67874
67903
  if (question?.promptId !== promptId || signal.aborted)
67875
67904
  return signal.aborted;
67876
67905
  const held = this.heldAnswers.takeQuestion(runId, stageId, question, generation);
67877
- if (held !== undefined)
67878
- return this.sendQuestionAnswer(runId, stageId, question, generation, held.answer, route);
67906
+ if (held !== undefined) {
67907
+ const sent = await this.sendQuestionOutcome(runId, stageId, question, generation, held, route);
67908
+ return sent && "error" in held ? "failed" : sent;
67909
+ }
67879
67910
  const broker = this.questionBroker ?? currentStageUiBroker();
67880
67911
  let answered;
67881
67912
  let failure;
@@ -67899,19 +67930,15 @@ class LocalWorkerPool {
67899
67930
  broker.clearStagePrompt(runId, stageId);
67900
67931
  }
67901
67932
  if (failure !== undefined) {
67902
- await this.answerPooledStagePrompt(runId, stageId, { kind: "failed", promptId, error: failure.message });
67903
- return "failed";
67933
+ const sent = await this.sendQuestionOutcome(runId, stageId, question, generation, { error: failure.message }, route);
67934
+ return sent ? "failed" : false;
67904
67935
  }
67905
67936
  if (answered === undefined)
67906
67937
  return true;
67907
- return this.sendQuestionAnswer(runId, stageId, question, generation, { raw: answered.value }, route);
67938
+ return this.sendQuestionOutcome(runId, stageId, question, generation, { answer: { raw: answered.value } }, route);
67908
67939
  }
67909
- async sendQuestionAnswer(runId, stageId, question, generation, answer, route) {
67910
- const result = await this.answerPooledStagePrompt(runId, stageId, {
67911
- kind: "input",
67912
- promptId: question.promptId,
67913
- answer
67914
- });
67940
+ async sendQuestionOutcome(runId, stageId, question, generation, outcome, route) {
67941
+ const result = await this.answerPooledStagePrompt(runId, stageId, "error" in outcome ? { kind: "failed", promptId: question.promptId, error: outcome.error } : { kind: "input", promptId: question.promptId, answer: outcome.answer });
67915
67942
  if (result.ok || result.refusal === "StageNotLive")
67916
67943
  return true;
67917
67944
  this.heldAnswers.hold(runId, stageId, {
@@ -67919,7 +67946,7 @@ class LocalWorkerPool {
67919
67946
  generation,
67920
67947
  promptId: question.promptId,
67921
67948
  identity: questionIdentity(question),
67922
- answer
67949
+ ...outcome
67923
67950
  });
67924
67951
  const key = JSON.stringify([runId, stageId]);
67925
67952
  const active = this.routedQuestions.get(key);
@@ -67927,7 +67954,7 @@ class LocalWorkerPool {
67927
67954
  this.routedQuestions.delete(key);
67928
67955
  else if (active !== undefined && active.promptId === question.promptId && active.generation > generation) {
67929
67956
  this.routedQuestions.delete(key);
67930
- active.controller.abort(new Error("The question was answered before its worker was replaced."));
67957
+ active.controller.abort(new Error("The question was settled before its worker was replaced."));
67931
67958
  }
67932
67959
  this.applyHeldAnswer(runId, stageId, generation);
67933
67960
  return false;
@@ -67948,9 +67975,9 @@ class LocalWorkerPool {
67948
67975
  const held = this.heldAnswers.takePrompt(runId, stageId, prompt, generation);
67949
67976
  if (held === undefined)
67950
67977
  return;
67951
- if (this.promptUi === undefined)
67978
+ if (this.promptUi === undefined && "response" in held)
67952
67979
  this.store?.resolveStagePendingPrompt(runId, stageId, prompt.id, held.response);
67953
- deliver(held.response);
67980
+ deliver(held);
67954
67981
  }
67955
67982
  async pausePooledStage(watch) {
67956
67983
  if (this.closed || this.coordinatorLost !== undefined)