@bastani/atomic 0.9.45 → 0.9.46-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.
- package/CHANGELOG.md +34 -0
- package/dist/builtin/intercom/CHANGELOG.md +11 -0
- package/dist/builtin/intercom/broker/broker.ts +124 -10
- package/dist/builtin/intercom/broker/client.ts +177 -25
- package/dist/builtin/intercom/broker/multinode-router.ts +228 -27
- package/dist/builtin/intercom/broker/multinode-state-store.ts +10 -0
- package/dist/builtin/intercom/broker/recipient-receipts.ts +38 -4
- package/dist/builtin/intercom/broker/send-handler.ts +9 -2
- package/dist/builtin/intercom/broker/sql-outbox-store.ts +29 -4
- package/dist/builtin/intercom/group.ts +25 -0
- package/dist/builtin/intercom/index.bundle.mjs +177 -35
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/retry-policy.ts +7 -0
- package/dist/builtin/intercom/types.ts +21 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +10 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +150 -54
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +39 -0
- package/dist/builtin/workflows/builtin/{chunk-6901nsd6.js → chunk-6jpwc09k.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-vwx023ge.js → chunk-qhm35ntx.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-jvna28mq.js → chunk-zhyqnf5g.js} +331 -68
- package/dist/builtin/workflows/builtin/goal.js +2 -2
- package/dist/builtin/workflows/builtin/index.js +3 -3
- package/dist/builtin/workflows/builtin/ralph.js +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/prompt-engineer/SKILL.md +1 -0
- package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6.md +2 -2
- package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_sol.md +2 -2
- package/dist/builtin/workflows/skills/prompt-engineer/references/gpt_6_1_terra.md +77 -0
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/journal/internal.d.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +7076 -3386
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +11075 -7261
- package/dist/builtin/workflows/src/index.js +6768 -3605
- package/dist/builtin/workflows/src/local-worker-entry.mjs +11075 -7261
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +19 -0
- package/dist/core/agent-session-boundaries.d.ts.map +1 -1
- package/dist/core/agent-session-boundaries.js +1 -0
- package/dist/core/agent-session-boundaries.js.map +1 -1
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +2 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +15 -2
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +3 -2
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts +4 -2
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +13 -8
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-recovery.d.ts.map +1 -1
- package/dist/core/agent-session-recovery.js +92 -1
- package/dist/core/agent-session-recovery.js.map +1 -1
- package/dist/core/agent-session-transfer.d.ts.map +1 -1
- package/dist/core/agent-session-transfer.js +5 -0
- package/dist/core/agent-session-transfer.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/copilot-errors.d.ts +6 -0
- package/dist/core/copilot-errors.d.ts.map +1 -0
- package/dist/core/copilot-errors.js +17 -0
- package/dist/core/copilot-errors.js.map +1 -0
- package/dist/core/execution-model-router.d.ts.map +1 -1
- package/dist/core/execution-model-router.js +12 -2
- package/dist/core/execution-model-router.js.map +1 -1
- package/dist/core/extensions/context-types.d.ts +7 -0
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/runner-context.d.ts.map +1 -1
- package/dist/core/extensions/runner-context.js +6 -0
- package/dist/core/extensions/runner-context.js.map +1 -1
- package/dist/core/extensions/workflow-activity-hub.d.ts +4 -0
- package/dist/core/extensions/workflow-activity-hub.d.ts.map +1 -1
- package/dist/core/extensions/workflow-activity-hub.js +26 -5
- package/dist/core/extensions/workflow-activity-hub.js.map +1 -1
- package/dist/core/extensions/workflow-events.d.ts +5 -0
- package/dist/core/extensions/workflow-events.d.ts.map +1 -1
- package/dist/core/extensions/workflow-events.js.map +1 -1
- package/dist/core/messages.d.ts +5 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +1 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-fallback-failures.d.ts +1 -1
- package/dist/core/model-fallback-failures.d.ts.map +1 -1
- package/dist/core/model-fallback-failures.js +37 -3
- package/dist/core/model-fallback-failures.js.map +1 -1
- package/dist/core/model-runtime-streaming.js +2 -2
- package/dist/core/model-runtime-streaming.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +19 -11
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/provider-rate-limits.d.ts +55 -6
- package/dist/core/provider-rate-limits.d.ts.map +1 -1
- package/dist/core/provider-rate-limits.js +182 -29
- package/dist/core/provider-rate-limits.js.map +1 -1
- package/dist/core/provider-request-admission.d.ts.map +1 -1
- package/dist/core/provider-request-admission.js +42 -18
- package/dist/core/provider-request-admission.js.map +1 -1
- package/dist/core/storage/node-sqlite.d.ts.map +1 -1
- package/dist/core/storage/node-sqlite.js +176 -19
- package/dist/core/storage/node-sqlite.js.map +1 -1
- package/dist/core/storage/provider.d.ts +4 -2
- package/dist/core/storage/provider.d.ts.map +1 -1
- package/dist/core/storage/provider.js +94 -5
- package/dist/core/storage/provider.js.map +1 -1
- package/dist/core/structured-output/index.d.ts.map +1 -1
- package/dist/core/structured-output/index.js +15 -8
- package/dist/core/structured-output/index.js.map +1 -1
- package/dist/core/structured-output/provider-failure.d.ts +17 -0
- package/dist/core/structured-output/provider-failure.d.ts.map +1 -0
- package/dist/core/structured-output/provider-failure.js +28 -0
- package/dist/core/structured-output/provider-failure.js.map +1 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +13 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/shell-task-render.d.ts +1 -0
- package/dist/core/tools/shell-task-render.d.ts.map +1 -1
- package/dist/core/tools/shell-task-render.js +7 -2
- package/dist/core/tools/shell-task-render.js.map +1 -1
- package/dist/extensions/tool-search/tool.d.ts +5 -0
- package/dist/extensions/tool-search/tool.d.ts.map +1 -1
- package/dist/extensions/tool-search/tool.js +38 -5
- package/dist/extensions/tool-search/tool.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +3 -2
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +42 -14
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-extension-context.js +2 -1
- package/dist/modes/interactive/interactive-extension-context.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +5 -6
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/rpc/rpc-client-process.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-process.js +8 -1
- package/dist/modes/rpc/rpc-client-process.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/utils/ansi.d.ts +2 -0
- package/dist/utils/ansi.d.ts.map +1 -1
- package/dist/utils/ansi.js +5 -0
- package/dist/utils/ansi.js.map +1 -1
- package/docs/codemode.md +1 -1
- package/docs/intercom/reference.md +3 -3
- package/docs/models/evals.md +31 -12
- package/docs/models/reference.md +23 -51
- package/docs/models.md +3 -4
- package/docs/providers.md +5 -15
- package/docs/settings.md +1 -1
- package/docs/storage.md +2 -2
- package/docs/tools.md +1 -1
- package/docs/workflows/api-reference.md +20 -1
- package/docs/workflows/operations.md +17 -4
- package/npm-shrinkwrap.json +64 -44
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.46-alpha.1] - 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
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 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))
|
|
25
|
+
- 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))
|
|
26
|
+
- 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))
|
|
27
|
+
- 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))
|
|
28
|
+
- 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))
|
|
29
|
+
- 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))
|
|
30
|
+
- 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))
|
|
31
|
+
- 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))
|
|
32
|
+
- 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))
|
|
33
|
+
- `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))
|
|
34
|
+
- 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))
|
|
35
|
+
- 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))
|
|
36
|
+
- 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.
|
|
37
|
+
- `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.
|
|
38
|
+
|
|
5
39
|
## [0.9.45] - 2026-10-25
|
|
6
40
|
|
|
7
41
|
Custom storage adapters now plug into workflow journals as first-class citizens, and large local worker pools start reliably on busy machines.
|
|
@@ -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-alpha.1] - 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.44] - 2026-10-25
|
|
8
19
|
|
|
9
20
|
### Changed
|
|
@@ -72,7 +72,7 @@ import { OUTBOX_BACKUP_POLL_INTERVAL_MS } from "./multinode-state-store.js";
|
|
|
72
72
|
import { SupervisorChannelCache } from "./supervisor-channel.js";
|
|
73
73
|
import { SessionIdReclaim } from "./session-id-reclaim.js";
|
|
74
74
|
import { type HeldSend, RestoringRecipients } from "./restoring-recipients.js";
|
|
75
|
-
import { hasGroup, normalizeGroup, normalizeGroups } from "../group.js";
|
|
75
|
+
import { hasGroup, normalizeGroup, normalizeGroups, workflowInvocationRefusalFor } from "../group.js";
|
|
76
76
|
import { handleBrokerPresence } from "./presence-handler.js";
|
|
77
77
|
import {
|
|
78
78
|
knownGroupSummaries,
|
|
@@ -121,6 +121,11 @@ interface PendingStageRouteRegistration {
|
|
|
121
121
|
readonly capability: string;
|
|
122
122
|
/** Registered by a restarted owner for an interrupted run; yields to the process that resumes it (#3760). */
|
|
123
123
|
readonly dormant?: true;
|
|
124
|
+
/**
|
|
125
|
+
* #4006: held only by the process that launched the run, never by its stages. Presenting it in
|
|
126
|
+
* `claim_workflow_invocation` grants that session the invocation's control without membership.
|
|
127
|
+
*/
|
|
128
|
+
readonly launcherCapability?: string;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
interface WorkflowRosterRegistration {
|
|
@@ -347,6 +352,11 @@ class IntercomBroker {
|
|
|
347
352
|
private liveWorkflowStageRoutes = new Map<string, LiveWorkflowStageRouteRegistration>();
|
|
348
353
|
private workflowRosters = new Map<string, WorkflowRosterRegistration>();
|
|
349
354
|
private liveWorkflowStageRouteActivations = new Map<string, LiveWorkflowStageRouteActivation>();
|
|
355
|
+
/**
|
|
356
|
+
* #4006: runs each connected session launched, proven with the run's launcher capability. Keyed by session,
|
|
357
|
+
* then run, holding the capability claimed; a claim lapses when the run's route carries another one.
|
|
358
|
+
*/
|
|
359
|
+
private workflowInvocationClaims = new Map<string, Map<string, string>>();
|
|
350
360
|
/** Multi-node S5: set only when the agent directory's settings select a top-level shared `storage`. */
|
|
351
361
|
private readonly sharedStorageSetting = readSharedStorageSetting(dirname(INTERCOM_DIR));
|
|
352
362
|
private readonly routeRemote: RemoteSendRouter | undefined =
|
|
@@ -649,17 +659,57 @@ class IntercomBroker {
|
|
|
649
659
|
}
|
|
650
660
|
|
|
651
661
|
/**
|
|
652
|
-
* Invocation control may come from the invocation owner
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
* parent-control capability for a
|
|
662
|
+
* Invocation control may come from the invocation owner, from the session that
|
|
663
|
+
* launched the run (#4006), or from an ordinary host session that explicitly
|
|
664
|
+
* joined the invocation group. A workflow stage registered under any workflow
|
|
665
|
+
* group cannot turn mutable membership into a parent-control capability for a
|
|
666
|
+
* sibling (or for another workflow run).
|
|
656
667
|
*/
|
|
657
668
|
private canControlWorkflowInvocation(sender: ConnectedSession, invocationGroup: string): boolean {
|
|
669
|
+
return this.controlsWorkflowInvocationByMembership(sender, invocationGroup) || this.launchedWorkflowInvocation(sender, invocationGroup);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private controlsWorkflowInvocationByMembership(sender: ConnectedSession, invocationGroup: string): boolean {
|
|
658
673
|
if (!hasGroup(sessionGroups(sender.info), invocationGroup)) return false;
|
|
659
674
|
const registrationGroup = normalizeGroup(sender.registrationGroup);
|
|
660
675
|
return registrationGroup === invocationGroup || !registrationGroup.startsWith("workflow:");
|
|
661
676
|
}
|
|
662
677
|
|
|
678
|
+
/** #4006: the sender holds a current claim on a run whose route is registered under `invocationGroup`. */
|
|
679
|
+
private launchedWorkflowInvocation(sender: ConnectedSession, invocationGroup: string): boolean {
|
|
680
|
+
const claims = this.workflowInvocationClaims.get(sender.info.id);
|
|
681
|
+
if (claims === undefined) return false;
|
|
682
|
+
const group = normalizeGroup(invocationGroup);
|
|
683
|
+
for (const [runId, capability] of claims) {
|
|
684
|
+
const route = this.pendingStageRoutes.get(runId);
|
|
685
|
+
if (route?.launcherCapability === capability && normalizeGroup(route.group) === group) return true;
|
|
686
|
+
}
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Only the holder of a run's launcher capability can claim to have launched it. That capability is
|
|
692
|
+
* separate from the route capability every stage of the run holds, so a stage cannot claim.
|
|
693
|
+
*/
|
|
694
|
+
private claimWorkflowInvocation(sessionId: string, runId: string, capability: string): boolean {
|
|
695
|
+
const route = this.pendingStageRoutes.get(runId);
|
|
696
|
+
if (
|
|
697
|
+
route?.launcherCapability === undefined ||
|
|
698
|
+
route.launcherCapability !== capability ||
|
|
699
|
+
!normalizeGroup(route.group).startsWith("workflow:")
|
|
700
|
+
) {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
const claims = this.workflowInvocationClaims.get(sessionId) ?? new Map<string, string>();
|
|
704
|
+
// Claims on runs whose routes are gone can never authorize again.
|
|
705
|
+
for (const claimedRunId of claims.keys()) {
|
|
706
|
+
if (!this.pendingStageRoutes.has(claimedRunId)) claims.delete(claimedRunId);
|
|
707
|
+
}
|
|
708
|
+
claims.set(runId, capability);
|
|
709
|
+
this.workflowInvocationClaims.set(sessionId, claims);
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
663
713
|
private canControlLiveWorkflowStage = (
|
|
664
714
|
sender: ConnectedSession,
|
|
665
715
|
target: ConnectedSession,
|
|
@@ -723,7 +773,12 @@ class IntercomBroker {
|
|
|
723
773
|
const entries: WorkflowFutureStageRosterEntry[] = [];
|
|
724
774
|
for (const [runId, roster] of this.workflowRosters) {
|
|
725
775
|
if (roster.possibleStages === undefined || roster.possibleStages.length === 0) continue;
|
|
726
|
-
if (
|
|
776
|
+
if (
|
|
777
|
+
![...requesterGroups].some((group) => invocationOwnsGroup(roster.group, group)) &&
|
|
778
|
+
!this.launchedWorkflowInvocation(requester, roster.group)
|
|
779
|
+
) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
727
782
|
if (selected !== undefined && selected !== roster.group) continue;
|
|
728
783
|
for (const row of roster.possibleStages) {
|
|
729
784
|
if (this.isNonAgentWorkflowTarget(row.target)) continue;
|
|
@@ -899,10 +954,15 @@ class IntercomBroker {
|
|
|
899
954
|
type: "delivery_failed",
|
|
900
955
|
messageId: route.message.id,
|
|
901
956
|
...(route.attemptId ? { attemptId: route.attemptId } : {}),
|
|
902
|
-
|
|
957
|
+
// A workflow stage registered outside the invocation can't gain control by joining; say so.
|
|
958
|
+
reason: workflowInvocationRefusalFor(route.from.registrationGroup, ownerRegistration.group),
|
|
903
959
|
});
|
|
904
960
|
return true;
|
|
905
961
|
}
|
|
962
|
+
// #4006: the run's host checks membership again before queueing; say when the broker authorized the launcher instead.
|
|
963
|
+
const invocationControl =
|
|
964
|
+
!this.controlsWorkflowInvocationByMembership(route.from, ownerRegistration.group) &&
|
|
965
|
+
this.launchedWorkflowInvocation(route.from, ownerRegistration.group);
|
|
906
966
|
const requestId = randomUUID();
|
|
907
967
|
const timeout = setTimeout(() => {
|
|
908
968
|
const pending = this.pendingStageAcknowledgments.get(requestId);
|
|
@@ -944,6 +1004,7 @@ class IntercomBroker {
|
|
|
944
1004
|
target: route.target,
|
|
945
1005
|
message: route.message,
|
|
946
1006
|
...(route.liveTargetId === undefined ? {} : { live: true }),
|
|
1007
|
+
...(invocationControl ? { invocationControl: true as const } : {}),
|
|
947
1008
|
})
|
|
948
1009
|
) {
|
|
949
1010
|
// The owner's socket stopped accepting frames between resolving the route
|
|
@@ -1220,6 +1281,14 @@ class IntercomBroker {
|
|
|
1220
1281
|
reason: UNCERTAIN_RECEIPT_REASON,
|
|
1221
1282
|
});
|
|
1222
1283
|
},
|
|
1284
|
+
hold: (waiting, timeoutMs) => {
|
|
1285
|
+
writeMessageIfOpen(waiting.socket, {
|
|
1286
|
+
type: "send_held",
|
|
1287
|
+
messageId: pending.messageId,
|
|
1288
|
+
...(waiting.attemptId === undefined ? {} : { attemptId: waiting.attemptId }),
|
|
1289
|
+
timeoutMs,
|
|
1290
|
+
});
|
|
1291
|
+
},
|
|
1223
1292
|
},
|
|
1224
1293
|
sender,
|
|
1225
1294
|
);
|
|
@@ -1375,8 +1444,7 @@ class IntercomBroker {
|
|
|
1375
1444
|
target === undefined ||
|
|
1376
1445
|
!isAgentRecipient(target.info) ||
|
|
1377
1446
|
target.info.id === currentId ||
|
|
1378
|
-
|
|
1379
|
-
!hasGroup(sessionGroups(target.info), routeGroup)
|
|
1447
|
+
!this.canReceivePendingStageNotification(target, clientMessage.to, routeGroup)
|
|
1380
1448
|
) {
|
|
1381
1449
|
this.failPendingStageNotification(socket, message.id, attemptId, "Session not found");
|
|
1382
1450
|
return;
|
|
@@ -1459,6 +1527,19 @@ class IntercomBroker {
|
|
|
1459
1527
|
}
|
|
1460
1528
|
}
|
|
1461
1529
|
|
|
1530
|
+
/**
|
|
1531
|
+
* A pending-stage notice reaches a session registered in the run's group, or (#4006) the exact
|
|
1532
|
+
* sender session that controls the run from outside it: the launcher holding a current claim, or
|
|
1533
|
+
* an ordinary session that joined `workflow:<rootRunId>`. Isolated stages never qualify, and the
|
|
1534
|
+
* legacy name/return-address fallbacks stay limited to the run's own group.
|
|
1535
|
+
*/
|
|
1536
|
+
private canReceivePendingStageNotification(target: ConnectedSession, to: string, routeGroup: string): boolean {
|
|
1537
|
+
if (normalizeGroup(target.registrationGroup) === routeGroup && hasGroup(sessionGroups(target.info), routeGroup)) {
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
return target.info.id === to && this.canControlWorkflowInvocation(target, routeGroup);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1462
1543
|
private settlePendingStageNotification(
|
|
1463
1544
|
currentId: string,
|
|
1464
1545
|
requestId: string,
|
|
@@ -1636,6 +1717,8 @@ class IntercomBroker {
|
|
|
1636
1717
|
acknowledgesPendingStageRoutes: true as const,
|
|
1637
1718
|
refusesPendingStageRoutesInPlace: true as const,
|
|
1638
1719
|
brokerStatus: true as const,
|
|
1720
|
+
claimsWorkflowInvocations: true as const,
|
|
1721
|
+
extendsSaves: true as const,
|
|
1639
1722
|
});
|
|
1640
1723
|
if (isAgentRecipient(info)) this.broadcastToMemberships({ type: "session_joined", session: info }, sessionGroups(info), id);
|
|
1641
1724
|
// #3761: messages written to this recipient's previous connection that it never confirmed saving go out again.
|
|
@@ -1835,10 +1918,13 @@ class IntercomBroker {
|
|
|
1835
1918
|
typeof clientMessage.group !== "string" ||
|
|
1836
1919
|
typeof clientMessage.capability !== "string" ||
|
|
1837
1920
|
!clientMessage.capability ||
|
|
1838
|
-
(clientMessage.requestId !== undefined && typeof clientMessage.requestId !== "string")
|
|
1921
|
+
(clientMessage.requestId !== undefined && typeof clientMessage.requestId !== "string") ||
|
|
1922
|
+
(clientMessage.launcherCapability !== undefined &&
|
|
1923
|
+
(typeof clientMessage.launcherCapability !== "string" || !clientMessage.launcherCapability))
|
|
1839
1924
|
) {
|
|
1840
1925
|
throw new Error("Invalid pending-stage route registration");
|
|
1841
1926
|
}
|
|
1927
|
+
const launcherCapability = clientMessage.launcherCapability;
|
|
1842
1928
|
const routeRequestId = clientMessage.requestId;
|
|
1843
1929
|
// The route is applied synchronously, so this answer proves the update is visible to every later request.
|
|
1844
1930
|
const acknowledgeRoute = (): void => {
|
|
@@ -1955,6 +2041,7 @@ class IntercomBroker {
|
|
|
1955
2041
|
group: ownerGroup,
|
|
1956
2042
|
capability: clientMessage.capability,
|
|
1957
2043
|
...(dormant ? { dormant: true as const } : {}),
|
|
2044
|
+
...(launcherCapability === undefined ? {} : { launcherCapability }),
|
|
1958
2045
|
});
|
|
1959
2046
|
if (yieldedDormant !== undefined) {
|
|
1960
2047
|
const revoked = this.sessions.get(yieldedDormant.sessionId);
|
|
@@ -1975,6 +2062,24 @@ class IntercomBroker {
|
|
|
1975
2062
|
break;
|
|
1976
2063
|
}
|
|
1977
2064
|
|
|
2065
|
+
case "claim_workflow_invocation": {
|
|
2066
|
+
if (
|
|
2067
|
+
typeof clientMessage.requestId !== "string" ||
|
|
2068
|
+
typeof clientMessage.runId !== "string" ||
|
|
2069
|
+
typeof clientMessage.capability !== "string" ||
|
|
2070
|
+
!clientMessage.capability ||
|
|
2071
|
+
currentId === null
|
|
2072
|
+
) {
|
|
2073
|
+
throw new Error("Invalid workflow invocation claim");
|
|
2074
|
+
}
|
|
2075
|
+
writeMessageIfOpen(socket, {
|
|
2076
|
+
type: "workflow_invocation_claimed",
|
|
2077
|
+
requestId: clientMessage.requestId,
|
|
2078
|
+
claimed: this.claimWorkflowInvocation(currentId, clientMessage.runId, clientMessage.capability),
|
|
2079
|
+
});
|
|
2080
|
+
break;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1978
2083
|
case "register_live_workflow_stage_route": {
|
|
1979
2084
|
if (
|
|
1980
2085
|
typeof clientMessage.requestId !== "string" ||
|
|
@@ -2092,6 +2197,14 @@ class IntercomBroker {
|
|
|
2092
2197
|
break;
|
|
2093
2198
|
}
|
|
2094
2199
|
|
|
2200
|
+
case "saving": {
|
|
2201
|
+
if (typeof clientMessage.messageId !== "string") throw new Error("Invalid saving message");
|
|
2202
|
+
const recipient = this.sessions.get(currentId);
|
|
2203
|
+
if (recipient?.acknowledgesReceipt === true && !this.multiNode?.saving(recipient, clientMessage.messageId))
|
|
2204
|
+
this.receipts.extend(clientMessage.messageId, deliveryTargetIdentity(recipient));
|
|
2205
|
+
break;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2095
2208
|
case "send_pending_stage_notification": {
|
|
2096
2209
|
this.handlePendingStageNotificationSend(socket, clientMessage, currentId);
|
|
2097
2210
|
break;
|
|
@@ -2384,6 +2497,7 @@ class IntercomBroker {
|
|
|
2384
2497
|
for (const [requestId, activation] of this.liveWorkflowStageRouteActivations) {
|
|
2385
2498
|
if (activation.sessionId === sessionId) this.liveWorkflowStageRouteActivations.delete(requestId);
|
|
2386
2499
|
}
|
|
2500
|
+
this.workflowInvocationClaims.delete(sessionId);
|
|
2387
2501
|
this.sessions.delete(sessionId);
|
|
2388
2502
|
this.multiNode?.sessionLeft(departed);
|
|
2389
2503
|
this.sessionIdReclaim.retire(sessionId, departed.registrationReturnAddress);
|