@bastani/atomic 0.9.34 → 0.9.35-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  4. package/dist/builtin/mcp/OAUTH.md +1 -1
  5. package/dist/builtin/mcp/package.json +1 -1
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/web-access/package.json +1 -1
  8. package/dist/builtin/workflows/CHANGELOG.md +11 -0
  9. package/dist/builtin/workflows/package.json +1 -1
  10. package/dist/builtin/workflows/src/extension/index.bundle.mjs +209 -77
  11. package/dist/builtin/workflows/src/index.js +28 -9
  12. package/dist/core/compaction/utils.d.ts +6 -0
  13. package/dist/core/compaction/utils.d.ts.map +1 -1
  14. package/dist/core/compaction/utils.js +6 -7
  15. package/dist/core/compaction/utils.js.map +1 -1
  16. package/dist/core/tools/read.d.ts.map +1 -1
  17. package/dist/core/tools/read.js +3 -1
  18. package/dist/core/tools/read.js.map +1 -1
  19. package/dist/extensions/codemode/tool.js +1 -1
  20. package/dist/extensions/codemode/tool.js.map +1 -1
  21. package/dist/extensions/mcp/client/oauth/errors.d.ts +3 -2
  22. package/dist/extensions/mcp/client/oauth/errors.d.ts.map +1 -1
  23. package/dist/extensions/mcp/client/oauth/errors.js +3 -1
  24. package/dist/extensions/mcp/client/oauth/errors.js.map +1 -1
  25. package/dist/extensions/mcp/client/oauth/flow.d.ts.map +1 -1
  26. package/dist/extensions/mcp/client/oauth/flow.js +5 -4
  27. package/dist/extensions/mcp/client/oauth/flow.js.map +1 -1
  28. package/docs/codemode.md +1 -1
  29. package/docs/compaction/reference.md +2 -1
  30. package/docs/models/reference.md +3 -1
  31. package/docs/sdk.md +11 -1
  32. package/docs/workflows/operations.md +3 -1
  33. package/npm-shrinkwrap.json +32 -32
  34. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.35-alpha.2] - 2026-10-12
6
+
7
+ ### Fixed
8
+
9
+ - Fixed `read` reporting success when it refuses a selected range as too large. The refusal is now an error result, so models see a failed read with guidance to read a smaller range, and code mode's `tools.read(...)` throws instead of resolving with the refusal text as if it were the file's contents ([#3574](https://github.com/bastani-inc/atomic/issues/3574)).
10
+
11
+ ## [0.9.35-alpha.1] - 2026-10-12
12
+
13
+ ### Added
14
+
15
+ - `session.workflows.resume(runId)` now adopts a paused run that was launched through the model-facing `workflow` tool once its owning session has been disposed, in the same process or after the owning process exits. Disposing a session quits its in-flight runs at their last checkpoint and durably records that it released its checkpointed paused runs; the adopting session becomes the owner and the run continues under the same UUID. Runs whose owner is still open, runs whose owner crashed without disposing, and finished runs are still refused ([#3566](https://github.com/bastani-inc/atomic/issues/3566)).
16
+ - Switching sessions with `/new`, `/fork`, or `/resume` now releases the previous session's paused workflow runs, the same way disposing a session does. The model in the new session can inspect them with the `workflow` tool's `status` and `stages` actions and resume them, and `session.workflows.resume(runId)` can adopt them; adoption, including the `/workflow resume <id>` slash command, transfers ownership to the resuming session. The new session lists the released runs with their `/workflow resume <id>` commands, and, in the process that released it, a run another session already adopted now reports that it is owned by another session. `/reload` still keeps runs executing and releases nothing ([#3569](https://github.com/bastani-inc/atomic/issues/3569)).
17
+
18
+ ### Changed
19
+
20
+ - Changed `serializeConversation()` (branch summaries, resume-picker session summaries, and the policy-refusal summary fallback) to omit thinking blocks; replayed thinking is often not in the live context, so including it could make a summarization request larger than the context it summarizes and fail. Verbatim line compaction still serializes thinking lines.
21
+ - Changed `openai-codex/gpt-6.1-sol` to a 500,000-token context window (up from 272,000) with no long-context price tier on Standard, Fast, or Ultrafast. OpenAI raised GPT-6.1 Sol's default context to 500k in Codex & Work for ChatGPT subscriptions and does not charge long-context fees when usage draws from the subscription. `openai/gpt-6.1-sol` and the other Codex models are unchanged.
22
+
23
+ ### Fixed
24
+
25
+ - Fixed the code mode tool description claiming every failed tool call throws; a tool that declares an output schema resolves with its value on failure too (for example `bash` with a non-zero exit), so scripts should check its fields.
26
+
5
27
  ## [0.9.34] - 2026-10-11
6
28
 
7
29
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.34",
3
+ "version": "0.9.35-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.35-alpha.1] - 2026-10-12
11
+
12
+ ### Fixed
13
+
14
+ - Fixed OAuth exchanging an authorization code whose `iss` parameter could not be checked because no authorization server metadata was discovered. It now throws `OAuthIssuerMismatchError`, whose `expected` is `undefined` in that case (RFC 9207). Reported by [@AizenvoltPrime](https://github.com/AizenvoltPrime).
15
+
10
16
  ## [0.9.28] - 2026-10-08
11
17
 
12
18
  ### Fixed
@@ -71,7 +71,7 @@ Atomic normally finds the authorization server through protected resource metada
71
71
 
72
72
  The document may use RFC 8414 or OpenID Connect discovery. Atomic trusts it as configured, including its issuer, so only point it at a document you trust. The URL must use HTTPS except for HTTP on `localhost`, `127.0.0.1`, or `[::1]`.
73
73
 
74
- An authorization response's `iss` must name the flow's authorization server. If the metadata promises `authorization_response_iss_parameter_supported`, the response must include `iss`. Mismatched or required-but-missing issuers are rejected before code exchange under RFC 9207. Pasted redirect URLs have the same checks as browser callbacks.
74
+ An authorization response's `iss` must name the flow's authorization server. If the metadata promises `authorization_response_iss_parameter_supported`, the response must include `iss`. Mismatched or required-but-missing issuers are rejected before code exchange under RFC 9207, and when no authorization server metadata was discovered any `iss` is rejected because there is no issuer to compare it with. Pasted redirect URLs have the same checks as browser callbacks.
75
75
 
76
76
  ## Tokens and refresh
77
77
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.34",
3
+ "version": "0.9.35-alpha.2",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.34",
3
+ "version": "0.9.35-alpha.2",
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": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.34",
3
+ "version": "0.9.35-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,17 @@ 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.35-alpha.1] - 2026-10-12
10
+
11
+ ### Added
12
+
13
+ - `session.workflows.resume(runId)` now adopts a paused run that was launched through the model-facing `workflow` tool once its owning session has been disposed, in the same process or after the owning process exits. Disposing a session quits its in-flight runs at their last checkpoint and durably records that it released its checkpointed paused runs; the adopting session becomes the owner and the run continues under the same UUID. Runs whose owner is still open, runs whose owner crashed without disposing, and finished runs are still refused ([#3566](https://github.com/bastani-inc/atomic/issues/3566)).
14
+ - Switching sessions with `/new`, `/fork`, or `/resume` now releases the previous session's paused workflow runs, the same way disposing a session does. The model in the new session can inspect them with the `workflow` tool's `status` and `stages` actions and resume them, and `session.workflows.resume(runId)` can adopt them; adoption, including the `/workflow resume <id>` slash command, transfers ownership to the resuming session. The new session lists the released runs with their `/workflow resume <id>` commands, and, in the process that released it, a run another session already adopted now reports that it is owned by another session. `/reload` still keeps runs executing and releases nothing ([#3569](https://github.com/bastani-inc/atomic/issues/3569)).
15
+
16
+ ### Fixed
17
+
18
+ - Fixed an SDK session started after another session in the same process was disposed falling back to non-durable workflows with `Managed Postgres health observer is stopped.`. Releasing the last session now discards the stopped workflow database configuration, so the next session starts a fresh managed Postgres and launches durably ([#3565](https://github.com/bastani-inc/atomic/issues/3565)).
19
+
9
20
  ## [0.9.33] - 2026-10-10
10
21
 
11
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.34",
3
+ "version": "0.9.35-alpha.2",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -1001,6 +1001,7 @@ class InMemoryDurableBackend {
1001
1001
  ...handle.invocationCwd !== undefined ? { invocationCwd: handle.invocationCwd } : existing?.handle.invocationCwd !== undefined ? { invocationCwd: existing.handle.invocationCwd } : {},
1002
1002
  ...handle.origin !== undefined ? { origin: handle.origin } : existing?.handle.origin !== undefined ? { origin: existing.handle.origin } : {},
1003
1003
  modelOwner: authoritativeOwner ? handle.modelOwner : existing?.handle.modelOwner ?? handle.modelOwner,
1004
+ ...handle.releasedModelOwner !== undefined ? { releasedModelOwner: handle.releasedModelOwner } : {},
1004
1005
  ...handle.workflowCwd !== undefined ? { workflowCwd: handle.workflowCwd } : existing?.handle.workflowCwd !== undefined ? { workflowCwd: existing.handle.workflowCwd } : {},
1005
1006
  ...handle.repositoryRoot !== undefined ? { repositoryRoot: handle.repositoryRoot } : existing?.handle.repositoryRoot !== undefined ? { repositoryRoot: existing.handle.repositoryRoot } : {},
1006
1007
  ...handle.gitWorktreeRoot !== undefined ? { gitWorktreeRoot: handle.gitWorktreeRoot } : existing?.handle.gitWorktreeRoot !== undefined ? { gitWorktreeRoot: existing.handle.gitWorktreeRoot } : {},
@@ -1214,14 +1215,20 @@ class InMemoryDurableBackend {
1214
1215
  if (pendingPrompts !== undefined)
1215
1216
  this.promptReservations.delete(workflowId);
1216
1217
  }
1217
- async transitionWorkflowStatus(workflowId, expected, status, pendingPrompts, resumable, expectedUpdatedAt, modelOwner) {
1218
+ async transitionWorkflowStatus(workflowId, expected, status, pendingPrompts, resumable, expectedUpdatedAt, modelOwner, releasedModelOwner) {
1218
1219
  const record = this.workflows.get(workflowId);
1219
1220
  const current = record?.handle;
1220
1221
  if (current === undefined || !expected.includes(current.status) || expectedUpdatedAt !== undefined && current.updatedAt !== expectedUpdatedAt)
1221
1222
  return false;
1222
1223
  this.setWorkflowStatus(workflowId, status, pendingPrompts, resumable);
1223
- if (modelOwner !== undefined && record !== undefined)
1224
- record.handle = { ...record.handle, modelOwner };
1224
+ if (record !== undefined) {
1225
+ const { releasedModelOwner: _previousRelease, ...settled } = record.handle;
1226
+ record.handle = {
1227
+ ...settled,
1228
+ ...modelOwner !== undefined ? { modelOwner } : {},
1229
+ ...releasedModelOwner !== undefined ? { releasedModelOwner } : {}
1230
+ };
1231
+ }
1225
1232
  return true;
1226
1233
  }
1227
1234
  adjustPendingPrompts(workflowId, delta) {
@@ -1303,6 +1310,7 @@ class InMemoryDurableBackend {
1303
1310
  ...h.invocationCwd !== undefined ? { invocationCwd: h.invocationCwd } : {},
1304
1311
  ...h.origin !== undefined ? { origin: h.origin } : {},
1305
1312
  modelOwner: h.modelOwner,
1313
+ ...h.releasedModelOwner !== undefined ? { releasedModelOwner: h.releasedModelOwner } : {},
1306
1314
  ...h.workflowCwd !== undefined ? { workflowCwd: h.workflowCwd } : {},
1307
1315
  ...h.repositoryRoot !== undefined ? { repositoryRoot: h.repositoryRoot } : {},
1308
1316
  ...h.gitWorktreeRoot !== undefined ? { gitWorktreeRoot: h.gitWorktreeRoot } : {},
@@ -4344,6 +4352,7 @@ function encodeMetadata(metadata) {
4344
4352
  ...metadata.failedToolNodeId !== undefined ? { failedToolNodeId: metadata.failedToolNodeId } : {},
4345
4353
  ...metadata.origin !== undefined ? { origin: metadata.origin } : {},
4346
4354
  modelOwner: metadata.modelOwner,
4355
+ ...metadata.releasedModelOwner !== undefined ? { releasedModelOwner: metadata.releasedModelOwner } : {},
4347
4356
  ...metadata.invocationCwd !== undefined ? { invocationCwd: metadata.invocationCwd } : {},
4348
4357
  ...metadata.workflowCwd !== undefined ? { workflowCwd: metadata.workflowCwd } : {},
4349
4358
  ...metadata.repositoryRoot !== undefined ? { repositoryRoot: metadata.repositoryRoot } : {},
@@ -4392,7 +4401,7 @@ function parseDurableWorkflowMetadata(value2, workflowId) {
4392
4401
  return;
4393
4402
  const possibleStages = coercePossibleStages(serialized.possibleStages);
4394
4403
  const metadata = value2;
4395
- if (metadata.workflowId !== workflowId || typeof metadata.workflowId !== "string" || typeof metadata.name !== "string" || typeof metadata.inputs !== "object" || metadata.inputs === null || Array.isArray(metadata.inputs) || typeof metadata.status !== "string" || !isDurableWorkflowStatus(metadata.status) || typeof metadata.completedCheckpoints !== "number" || typeof metadata.createdAt !== "number" || typeof metadata.pendingPrompts !== "number" || typeof metadata.promptReservationEpoch !== "string" || typeof metadata.updatedAt !== "number" || metadata.ownerExecutorId !== undefined && typeof metadata.ownerExecutorId !== "string" || metadata.transitionClaimId !== undefined && typeof metadata.transitionClaimId !== "string" || metadata.legacyRecoveryPending !== undefined && metadata.legacyRecoveryPending !== true || metadata.sessionFile !== undefined && typeof metadata.sessionFile !== "string" || metadata.label !== undefined && typeof metadata.label !== "string" || metadata.rootWorkflowId !== undefined && typeof metadata.rootWorkflowId !== "string" || metadata.resumable !== undefined && typeof metadata.resumable !== "boolean" || metadata.exited !== undefined && typeof metadata.exited !== "boolean" || metadata.exitReason !== undefined && typeof metadata.exitReason !== "string" || metadata.error !== undefined && typeof metadata.error !== "string" || metadata.failureKind !== undefined && !isWorkflowFailureKind(metadata.failureKind) || metadata.failureCode !== undefined && !isWorkflowFailureCode(metadata.failureCode) || metadata.failureRecoverability !== undefined && !isWorkflowFailureRecoverability(metadata.failureRecoverability) || metadata.failureDisposition !== undefined && !isWorkflowFailureDisposition(metadata.failureDisposition) || metadata.failedToolNodeId !== undefined && typeof metadata.failedToolNodeId !== "string" || metadata.invocationCwd !== undefined && typeof metadata.invocationCwd !== "string" || metadata.workflowCwd !== undefined && typeof metadata.workflowCwd !== "string" || metadata.repositoryRoot !== undefined && typeof metadata.repositoryRoot !== "string" || metadata.gitWorktreeRoot !== undefined && typeof metadata.gitWorktreeRoot !== "string")
4404
+ if (metadata.workflowId !== workflowId || typeof metadata.workflowId !== "string" || typeof metadata.name !== "string" || typeof metadata.inputs !== "object" || metadata.inputs === null || Array.isArray(metadata.inputs) || typeof metadata.status !== "string" || !isDurableWorkflowStatus(metadata.status) || typeof metadata.completedCheckpoints !== "number" || typeof metadata.createdAt !== "number" || typeof metadata.pendingPrompts !== "number" || typeof metadata.promptReservationEpoch !== "string" || typeof metadata.updatedAt !== "number" || metadata.ownerExecutorId !== undefined && typeof metadata.ownerExecutorId !== "string" || metadata.transitionClaimId !== undefined && typeof metadata.transitionClaimId !== "string" || metadata.legacyRecoveryPending !== undefined && metadata.legacyRecoveryPending !== true || metadata.sessionFile !== undefined && typeof metadata.sessionFile !== "string" || metadata.label !== undefined && typeof metadata.label !== "string" || metadata.rootWorkflowId !== undefined && typeof metadata.rootWorkflowId !== "string" || metadata.resumable !== undefined && typeof metadata.resumable !== "boolean" || metadata.exited !== undefined && typeof metadata.exited !== "boolean" || metadata.exitReason !== undefined && typeof metadata.exitReason !== "string" || metadata.error !== undefined && typeof metadata.error !== "string" || metadata.failureKind !== undefined && !isWorkflowFailureKind(metadata.failureKind) || metadata.failureCode !== undefined && !isWorkflowFailureCode(metadata.failureCode) || metadata.failureRecoverability !== undefined && !isWorkflowFailureRecoverability(metadata.failureRecoverability) || metadata.failureDisposition !== undefined && !isWorkflowFailureDisposition(metadata.failureDisposition) || metadata.failedToolNodeId !== undefined && typeof metadata.failedToolNodeId !== "string" || metadata.releasedModelOwner !== undefined && typeof metadata.releasedModelOwner !== "string" || metadata.invocationCwd !== undefined && typeof metadata.invocationCwd !== "string" || metadata.workflowCwd !== undefined && typeof metadata.workflowCwd !== "string" || metadata.repositoryRoot !== undefined && typeof metadata.repositoryRoot !== "string" || metadata.gitWorktreeRoot !== undefined && typeof metadata.gitWorktreeRoot !== "string")
4396
4405
  return;
4397
4406
  const { origin, possibleStages: rawPossibleStages, ...metadataWithoutOrigin } = metadata;
4398
4407
  return {
@@ -36438,7 +36447,9 @@ async function configureDbosDurableBackend(config) {
36438
36447
  throw new Error("Existing workflow executor has no database ownership fence; restart the host before recovering workflows");
36439
36448
  let launch = owner.active?.launch ?? (() => sdk.launch());
36440
36449
  let checkReady;
36441
- if (existing === undefined) {
36450
+ if (existing === undefined || owner.active === undefined) {
36451
+ if (owner.executorFence?.closed)
36452
+ owner.executorId = undefined;
36442
36453
  const url = effectiveSystemDatabaseUrl(config?.systemDatabaseUrl);
36443
36454
  await Promise.resolve().then(() => init_dbos_admission_config());
36444
36455
  const database = configureAdmissionDatabase(sdk, {
@@ -36918,7 +36929,7 @@ class DbosDurableBackend {
36918
36929
  await this.writeMetadata(workflowId);
36919
36930
  });
36920
36931
  }
36921
- async transitionWorkflowStatus(workflowId, expected, status, pendingPrompts, resumable, expectedUpdatedAt, modelOwner) {
36932
+ async transitionWorkflowStatus(workflowId, expected, status, pendingPrompts, resumable, expectedUpdatedAt, modelOwner, releasedModelOwner) {
36922
36933
  this.ensureGeneration();
36923
36934
  let records = [];
36924
36935
  const transition = () => transitionDbosWorkflowStatus({
@@ -36935,13 +36946,14 @@ class DbosDurableBackend {
36935
36946
  const pendingPrompts = this.promptReservations.hydrate(workflowId, entry.pendingPrompts, records, entry.promptReservationEpoch);
36936
36947
  this.applyMetadata(workflowId, { ...entry, pendingPrompts });
36937
36948
  },
36938
- claim: (authoritative, generation) => this.claimStatusTransition(workflowId, authoritative, generation, status, pendingPrompts, resumable, modelOwner),
36949
+ claim: (authoritative, generation) => this.claimStatusTransition(workflowId, authoritative, generation, status, pendingPrompts, resumable, modelOwner, releasedModelOwner),
36939
36950
  write: async () => {
36940
36951
  const handle = this.mem.getWorkflow(workflowId);
36941
36952
  if (handle !== undefined)
36942
36953
  this.mem.registerWorkflow({
36943
36954
  ...handle,
36944
36955
  legacyRecoveryPending: undefined,
36956
+ releasedModelOwner,
36945
36957
  ...modelOwner !== undefined ? { modelOwner } : {}
36946
36958
  }, true);
36947
36959
  this.setWorkflowStatus(workflowId, status, pendingPrompts, resumable);
@@ -36953,7 +36965,7 @@ class DbosDurableBackend {
36953
36965
  await this.flush(workflowId);
36954
36966
  return await this.sdk.withWorkflowClaim(workflowId, transition);
36955
36967
  }
36956
- async claimStatusTransition(workflowId, authoritative, generation, status, pendingPrompts, resumable, modelOwner) {
36968
+ async claimStatusTransition(workflowId, authoritative, generation, status, pendingPrompts, resumable, modelOwner, releasedModelOwner) {
36957
36969
  const stepName = claimMetadataStepName(generation);
36958
36970
  const transitionClaimId = crypto.randomUUID();
36959
36971
  const claim = {
@@ -36962,6 +36974,7 @@ class DbosDurableBackend {
36962
36974
  ...pendingPrompts !== undefined ? { pendingPrompts } : {},
36963
36975
  ...resumable !== undefined ? { resumable } : {},
36964
36976
  ...modelOwner === undefined ? {} : { modelOwner },
36977
+ releasedModelOwner,
36965
36978
  legacyRecoveryPending: undefined,
36966
36979
  ownerExecutorId: this.executorId,
36967
36980
  transitionClaimId,
@@ -40577,8 +40590,11 @@ async function shutdownDbos() {
40577
40590
  }
40578
40591
  if (errors.length > 0)
40579
40592
  throw combinedShutdownFailure(errors);
40580
- if (wasReady)
40593
+ if (wasReady) {
40581
40594
  slot.state = "shut_down";
40595
+ slot.configured = undefined;
40596
+ slot.active = undefined;
40597
+ }
40582
40598
  })().catch(async (error) => {
40583
40599
  slot.failure = await durabilityFailure("shutdown", error);
40584
40600
  slot.state = "failed";
@@ -91770,6 +91786,44 @@ init_dbos_lifecycle();
91770
91786
  init_factory();
91771
91787
  import { registerExtensionContextRetirementEffect, sessionScopedExtensionState as sessionScopedExtensionState2 } from "@bastani/atomic";
91772
91788
 
91789
+ // dist/builtin/workflows/src/durable/sdk-owner-release.ts
91790
+ var releasedRuns = new WeakMap;
91791
+ async function releasePausedRunsOwnedBy(backend, modelOwner) {
91792
+ const settled = await Promise.allSettled(backend.listResumableWorkflows().filter((entry) => entry.status === "paused" && entry.modelOwner === modelOwner).map(async (entry) => {
91793
+ const released = await backend.transitionWorkflowStatus(entry.workflowId, ["paused"], "paused", undefined, undefined, undefined, undefined, modelOwner);
91794
+ return released ? entry.workflowId : undefined;
91795
+ }));
91796
+ const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
91797
+ if (failures.length > 0)
91798
+ throw new AggregateError(failures, failures.map(String).join("; "));
91799
+ const releasedIds = settled.flatMap((result) => result.status === "fulfilled" && result.value !== undefined ? [result.value] : []);
91800
+ let remembered = releasedRuns.get(backend);
91801
+ if (remembered === undefined) {
91802
+ remembered = new Set;
91803
+ releasedRuns.set(backend, remembered);
91804
+ }
91805
+ for (const runId of releasedIds)
91806
+ remembered.add(runId);
91807
+ return releasedIds;
91808
+ }
91809
+ async function reclaimReleasedRunsOwnedBy(backend, modelOwner) {
91810
+ const settled = await Promise.allSettled(backend.listResumableWorkflows().filter((entry) => entry.status === "paused" && entry.modelOwner === modelOwner).flatMap((entry) => {
91811
+ const handle = backend.getWorkflow(entry.workflowId);
91812
+ return handle?.releasedModelOwner === modelOwner ? [handle] : [];
91813
+ }).map(async (handle) => {
91814
+ const reclaimed = await backend.transitionWorkflowStatus(handle.workflowId, ["paused"], "paused", undefined, undefined, handle.updatedAt);
91815
+ if (reclaimed)
91816
+ releasedRuns.get(backend)?.delete(handle.workflowId);
91817
+ }));
91818
+ const failures = settled.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
91819
+ if (failures.length > 0)
91820
+ throw new AggregateError(failures, failures.map(String).join("; "));
91821
+ }
91822
+ var FOREIGN_INSTANCE_MESSAGE = "Workflow instance belongs to another caller/session.";
91823
+ function foreignInstanceMessage(backend, runId) {
91824
+ return releasedRuns.get(backend)?.has(runId) === true && backend.getWorkflow(runId)?.releasedModelOwner === undefined ? `Workflow ${runId} is now owned by another session; it was already adopted after its previous session was switched away.` : FOREIGN_INSTANCE_MESSAGE;
91825
+ }
91826
+
91773
91827
  // dist/builtin/workflows/src/durable/sdk-recovery-scope.ts
91774
91828
  init_dbos_owner_fence();
91775
91829
  init_dbos_sdk_handle();
@@ -91790,6 +91844,12 @@ function isSdkDurableRunInScope(handle, cwd) {
91790
91844
  function isSdkCrashedRunAdoptable(handle, cwd) {
91791
91845
  return isSdkDurableRunInScope(handle, cwd) && handle.status === "running" && handle.ownerExecutorId !== undefined && handle.ownerExecutorId !== getAtomicExecutorId() && !isLiveRunningWorkflow(handle) && isDatabaseExecutor(handle.ownerExecutorId) && handle.ownerLiveness === "dead";
91792
91846
  }
91847
+ function isSdkReleasedRunAdoptable(handle, cwd) {
91848
+ return isSdkDurableRunInScope(handle, cwd) && handle.status === "paused" && handle.releasedModelOwner !== undefined && handle.releasedModelOwner === handle.modelOwner;
91849
+ }
91850
+ function isSdkRunAdoptable(handle, cwd) {
91851
+ return isSdkCrashedRunAdoptable(handle, cwd) || isSdkReleasedRunAdoptable(handle, cwd);
91852
+ }
91793
91853
 
91794
91854
  // dist/builtin/workflows/src/engine/run-durable-admission.ts
91795
91855
  init_dbos_admission();
@@ -93721,6 +93781,9 @@ async function markDurableQuit(runId, run, resumable = true) {
93721
93781
  if (backend === undefined)
93722
93782
  return "not_needed";
93723
93783
  await backend.settleWorkflowAdmission?.(runId);
93784
+ const durable = backend.getWorkflow(runId);
93785
+ if (durable?.status === "paused" && durable.releasedModelOwner !== undefined)
93786
+ return "not_needed";
93724
93787
  const transitioned = await transitionDurableWorkflowStatus(backend, runId, ["running", "paused"], "paused", undefined, resumable);
93725
93788
  if (!transitioned)
93726
93789
  return "refused";
@@ -109350,8 +109413,8 @@ async function resumeDurableWorkflowClaimed(workflowId, deps, catalog) {
109350
109413
  }
109351
109414
  const requestedOwner = deps.baseRunOpts.modelOwner;
109352
109415
  const adopting = requestedOwner !== undefined && requestedOwner !== handle.modelOwner && (handle.modelOwner !== undefined || handle.origin === "agent");
109353
- if (adopting && !isSdkCrashedRunAdoptable(handle, deps.baseRunOpts.cwd)) {
109354
- return { ok: false, reason: "owned_elsewhere", message: "Workflow instance belongs to another caller/session." };
109416
+ if (adopting && !isSdkRunAdoptable(handle, deps.baseRunOpts.cwd)) {
109417
+ return { ok: false, reason: "owned_elsewhere", message: foreignInstanceMessage(backend, resolved.workflowId) };
109355
109418
  }
109356
109419
  const def = handle.invocationCwd === undefined ? deps.registry.get(handle.name) : await deps.resolveDefinition?.(handle.name, handle.invocationCwd) ?? deps.registry.get(handle.name);
109357
109420
  if (def === undefined) {
@@ -110528,6 +110591,61 @@ function openWorkflowResumeSelector(ui, liveRuns, hydrate, options = {}) {
110528
110591
  });
110529
110592
  }
110530
110593
 
110594
+ // dist/builtin/workflows/src/extension/workflow-instance-owner.ts
110595
+ init_factory();
110596
+ var anonymousCallers = new WeakMap;
110597
+
110598
+ class WorkflowInstanceOwnershipError extends Error {
110599
+ constructor(message) {
110600
+ super(message);
110601
+ this.name = "WorkflowInstanceOwnershipError";
110602
+ }
110603
+ }
110604
+ function workflowCaller(ctx) {
110605
+ const sessionId = ctx.sessionId ?? ctx.sessionManager?.getSessionId?.();
110606
+ if (sessionId)
110607
+ return sessionId;
110608
+ const caller = ctx.sessionManager ?? ctx;
110609
+ let id = anonymousCallers.get(caller);
110610
+ if (id === undefined) {
110611
+ id = crypto.randomUUID();
110612
+ anonymousCallers.set(caller, id);
110613
+ }
110614
+ return id;
110615
+ }
110616
+ function assertWorkflowInstanceOwner(id, ctx, store) {
110617
+ const runs = store.runs();
110618
+ const backend = getDurableBackend();
110619
+ const seen = new Set;
110620
+ const assertOne = (runId) => {
110621
+ if (seen.has(runId))
110622
+ return;
110623
+ seen.add(runId);
110624
+ const local = runs.find((run) => run.id === runId);
110625
+ const durable = backend.getWorkflow(runId);
110626
+ const modelOwner = durable?.modelOwner ?? local?.modelOwner;
110627
+ if (modelOwner !== undefined) {
110628
+ if (modelOwner !== workflowCaller(ctx))
110629
+ throw new WorkflowInstanceOwnershipError(foreignInstanceMessage(backend, runId));
110630
+ } else if ((durable?.origin ?? local?.origin) === "agent") {
110631
+ throw new WorkflowInstanceOwnershipError("Workflow instance ownership is unavailable; another caller cannot assume authority.");
110632
+ }
110633
+ const parentId = durable?.rootWorkflowId ?? local?.parentRunId;
110634
+ if (parentId !== undefined)
110635
+ assertOne(parentId);
110636
+ };
110637
+ assertOne(id);
110638
+ const descendants = [id];
110639
+ for (const parentId of descendants) {
110640
+ for (const run of runs) {
110641
+ if (run.parentRunId !== parentId || descendants.includes(run.id))
110642
+ continue;
110643
+ assertOne(run.id);
110644
+ descendants.push(run.id);
110645
+ }
110646
+ }
110647
+ }
110648
+
110531
110649
  // dist/builtin/workflows/src/shared/types.ts
110532
110650
  var INTERACTIVE_WORKFLOW_POLICY = Object.freeze({
110533
110651
  mode: "interactive",
@@ -110605,7 +110723,11 @@ async function handleDurableResume(target, ctx, reporter, deps, preparedCatalog)
110605
110723
  fail(completedAttempt.message);
110606
110724
  return true;
110607
110725
  }
110608
- const result = await runtime.resumeDurableWorkflow(target, { policy, actor: "user" });
110726
+ const result = await runtime.resumeDurableWorkflow(target, {
110727
+ policy,
110728
+ actor: "user",
110729
+ ...releasedRunAdoption("workflowId" in resolved ? resolved.workflowId : target, ctx)
110730
+ });
110609
110731
  if (result.ok) {
110610
110732
  print(result.message);
110611
110733
  if (policy.allowInputPicker)
@@ -110680,10 +110802,15 @@ function isExplicitResumeCandidate(run, sessionRuns) {
110680
110802
  return true;
110681
110803
  return isWorkflowRunResumable(workflowRunResumeCandidate(run, sessionRuns)) || run.endedAt === undefined && run.status === "running";
110682
110804
  }
110805
+ function releasedRunAdoption(workflowId, ctx) {
110806
+ const handle = getDurableBackend().getLoadableWorkflow(workflowId);
110807
+ return handle !== undefined && isSdkReleasedRunAdoptable(handle, ctx.cwd) ? { modelOwner: workflowCaller(ctx) } : {};
110808
+ }
110683
110809
  async function resumeDurableTarget(workflowId, ctx, reporter, deps, runtime) {
110684
110810
  const result = await runtime.resumeDurableWorkflow(workflowId, {
110685
110811
  policy: workflowPolicyFromContext(ctx),
110686
- actor: "user"
110812
+ actor: "user",
110813
+ ...releasedRunAdoption(workflowId, ctx)
110687
110814
  });
110688
110815
  if (!result.ok)
110689
110816
  reporter.error(result.message);
@@ -110715,61 +110842,6 @@ function openCompleted(runtime, workflowId, catalog, beforeRestoreCompleted, sto
110715
110842
  }, catalog);
110716
110843
  }
110717
110844
 
110718
- // dist/builtin/workflows/src/extension/workflow-instance-owner.ts
110719
- init_factory();
110720
- var anonymousCallers = new WeakMap;
110721
-
110722
- class WorkflowInstanceOwnershipError extends Error {
110723
- constructor(message) {
110724
- super(message);
110725
- this.name = "WorkflowInstanceOwnershipError";
110726
- }
110727
- }
110728
- function workflowCaller(ctx) {
110729
- const sessionId = ctx.sessionId ?? ctx.sessionManager?.getSessionId?.();
110730
- if (sessionId)
110731
- return sessionId;
110732
- const caller = ctx.sessionManager ?? ctx;
110733
- let id = anonymousCallers.get(caller);
110734
- if (id === undefined) {
110735
- id = crypto.randomUUID();
110736
- anonymousCallers.set(caller, id);
110737
- }
110738
- return id;
110739
- }
110740
- function assertWorkflowInstanceOwner(id, ctx, store) {
110741
- const runs = store.runs();
110742
- const backend = getDurableBackend();
110743
- const seen = new Set;
110744
- const assertOne = (runId) => {
110745
- if (seen.has(runId))
110746
- return;
110747
- seen.add(runId);
110748
- const local = runs.find((run) => run.id === runId);
110749
- const durable = backend.getWorkflow(runId);
110750
- const modelOwner = durable?.modelOwner ?? local?.modelOwner;
110751
- if (modelOwner !== undefined) {
110752
- if (modelOwner !== workflowCaller(ctx))
110753
- throw new WorkflowInstanceOwnershipError("Workflow instance belongs to another caller/session.");
110754
- } else if ((durable?.origin ?? local?.origin) === "agent") {
110755
- throw new WorkflowInstanceOwnershipError("Workflow instance ownership is unavailable; another caller cannot assume authority.");
110756
- }
110757
- const parentId = durable?.rootWorkflowId ?? local?.parentRunId;
110758
- if (parentId !== undefined)
110759
- assertOne(parentId);
110760
- };
110761
- assertOne(id);
110762
- const descendants = [id];
110763
- for (const parentId of descendants) {
110764
- for (const run of runs) {
110765
- if (run.parentRunId !== parentId || descendants.includes(run.id))
110766
- continue;
110767
- assertOne(run.id);
110768
- descendants.push(run.id);
110769
- }
110770
- }
110771
- }
110772
-
110773
110845
  // dist/builtin/workflows/src/extension/extension-lifecycle.ts
110774
110846
  var startupRecoveries = new Set;
110775
110847
  function trackStartupRecovery(recovery) {
@@ -110809,6 +110881,18 @@ function sessionSwitchQuitConfirmation(reason, inFlightWorkflowCount) {
110809
110881
  message: `Continuing quits ${runs} now. ${pronoun} stops at its last checkpoint and can be resumed later with /workflow resume.`
110810
110882
  };
110811
110883
  }
110884
+ async function releasePausedRunsOfSession(sessionCaller) {
110885
+ const backend = getAvailableDurableBackend();
110886
+ return sessionCaller !== undefined && backend !== undefined ? releasePausedRunsOwnedBy(backend, sessionCaller) : [];
110887
+ }
110888
+ function releasedRunsNotice(runs) {
110889
+ const count = runs.length === 1 ? "1 workflow run" : `${runs.length} workflow runs`;
110890
+ return [
110891
+ `Released ${count} from the previous session. Resume ${runs.length === 1 ? "it" : "any"} here:`,
110892
+ ...runs.map((run) => ` /workflow resume ${run.id} (${run.name})`)
110893
+ ].join(`
110894
+ `);
110895
+ }
110812
110896
  function eventReason(event) {
110813
110897
  return typeof event === "object" && event !== null && "reason" in event ? event.reason : undefined;
110814
110898
  }
@@ -110958,9 +111042,31 @@ function registerWorkflowLifecycleHandlers(pi, deps) {
110958
111042
  if (errors.length)
110959
111043
  throw new AggregateError(errors, "Workflow observation retirement failed");
110960
111044
  };
111045
+ let sessionCaller;
111046
+ let switchHandoff;
111047
+ const handOffRunsToSuccessor = () => {
111048
+ switchHandoff ??= attemptAll([
111049
+ ...[...lifetime.generations].map((quitRuns) => () => quitRuns("switch")),
111050
+ async () => {
111051
+ const released = await releasePausedRunsOfSession(sessionCaller);
111052
+ lifetime.releasedRunIds = [...new Set([...lifetime.releasedRunIds ?? [], ...released])];
111053
+ },
111054
+ flushDbos
111055
+ ]);
111056
+ lifetime.activeSwitchHandoff = switchHandoff;
111057
+ return switchHandoff;
111058
+ };
110961
111059
  pi.on("session_start", async (event, ctx) => {
110962
- if (ctx)
111060
+ const departingHandoff = lifetime.pendingSwitchHandoff;
111061
+ lifetime.pendingSwitchHandoff = handOffRunsToSuccessor;
111062
+ if (ctx) {
110963
111063
  registerExtensionContextRetirementEffect(ctx, closeObservation);
111064
+ sessionCaller = workflowCaller(ctx);
111065
+ }
111066
+ if (isSessionSwitch(eventReason(event)) && departingHandoff !== undefined && lifetime.closing === undefined)
111067
+ await departingHandoff().catch(() => {
111068
+ return;
111069
+ });
110964
111070
  lifetime.release ??= getDurableBackendProcessOwner().injectedBackend === undefined ? acquireDbosLease() : async () => {};
110965
111071
  const reason = typeof event === "object" && event !== null && "reason" in event ? event.reason : undefined;
110966
111072
  const stopsWorkflows = replacementStopsWorkflows(reason) && !(reason === "startup" && lifetime.startupStarted);
@@ -110989,6 +111095,28 @@ function registerWorkflowLifecycleHandlers(pi, deps) {
110989
111095
  deps.storeWidgetRef.current?.();
110990
111096
  deps.storeWidgetRef.current = installStoreWidget({ ui: ctx.ui }, store);
110991
111097
  }
111098
+ if (isSessionSwitch(reason) && sessionCaller !== undefined) {
111099
+ const backend = getAvailableDurableBackend();
111100
+ if (backend !== undefined)
111101
+ await reclaimReleasedRunsOwnedBy(backend, sessionCaller).catch(() => {
111102
+ return;
111103
+ });
111104
+ }
111105
+ if (reason !== "reload") {
111106
+ const releasedRunIds = lifetime.releasedRunIds;
111107
+ lifetime.releasedRunIds = undefined;
111108
+ if (isSessionSwitch(reason) && releasedRunIds !== undefined && releasedRunIds.length > 0) {
111109
+ const backend = getAvailableDurableBackend();
111110
+ const cwd = ctx?.cwd ?? ctx?.sessionManager?.getCwd?.() ?? runtimeState.resolveInvocationCwd();
111111
+ const adoptable = releasedRunIds.flatMap((runId) => {
111112
+ const handle = backend?.getWorkflow(runId);
111113
+ return handle !== undefined && isSdkReleasedRunAdoptable(handle, cwd) ? [{ id: runId, name: handle.name }] : [];
111114
+ });
111115
+ lifetime.releasedRunIds = adoptable.map((run) => run.id);
111116
+ if (adoptable.length > 0)
111117
+ ctx?.ui?.notify?.(releasedRunsNotice(adoptable), "info");
111118
+ }
111119
+ }
110992
111120
  runtimeState.updateHostStageSessionDir(ctx?.sessionManager ?? pi.sessionManager);
110993
111121
  const generation = ++recoveryGeneration;
110994
111122
  if (stopsWorkflows)
@@ -111011,8 +111139,12 @@ function registerWorkflowLifecycleHandlers(pi, deps) {
111011
111139
  const closeGeneration = closeObservation;
111012
111140
  if (replacementStopsWorkflows(reason)) {
111013
111141
  lifetime.closing ??= attemptAll([
111142
+ () => lifetime.activeSwitchHandoff?.catch(() => {
111143
+ return;
111144
+ }),
111014
111145
  closeGeneration,
111015
111146
  ...lifetime.generations,
111147
+ () => releasePausedRunsOfSession(sessionCaller),
111016
111148
  () => {
111017
111149
  lifetime.generations.clear();
111018
111150
  },
@@ -111020,11 +111152,7 @@ function registerWorkflowLifecycleHandlers(pi, deps) {
111020
111152
  ]);
111021
111153
  await lifetime.closing;
111022
111154
  } else if (isSessionSwitch(reason)) {
111023
- await attemptAll([
111024
- closeGeneration,
111025
- ...[...lifetime.generations].map((quitRuns) => () => quitRuns("switch")),
111026
- flushDbos
111027
- ]);
111155
+ await attemptAll([closeGeneration, handOffRunsToSuccessor]);
111028
111156
  } else {
111029
111157
  await attemptAll([closeGeneration, () => stageControlRegistry.clearDetached(), flushDbos]);
111030
111158
  }
@@ -124493,8 +124621,14 @@ function makeExecuteWorkflowTool(runtime, reloadWorkflowResources, ensureWorkflo
124493
124621
  };
124494
124622
  }
124495
124623
  const recoveryCwd = typeof ctx.cwd === "string" ? ctx.cwd : undefined;
124624
+ const adoptsReleasedRun = (id) => {
124625
+ const handle = getDurableBackend().getLoadableWorkflow(id);
124626
+ return handle !== undefined && isSdkReleasedRunAdoptable(handle, recoveryCwd);
124627
+ };
124496
124628
  const authorize = (id) => {
124497
124629
  const handle = getDurableBackend().getLoadableWorkflow(id);
124630
+ if ((action === "resume" || action === "status" || action === "stages") && handle !== undefined && isSdkReleasedRunAdoptable(handle, recoveryCwd))
124631
+ return;
124498
124632
  if (access === "sdk" && isFullRunId(args.runId?.trim() ?? "") && (action === "status" || action === "stages" || action === "resume" && handle !== undefined && isSdkCrashedRunAdoptable(handle, recoveryCwd)) && !store.runs().some((run) => run.id === id) && handle !== undefined && isSdkDurableRunInScope(handle, recoveryCwd))
124499
124633
  return;
124500
124634
  assertWorkflowInstanceOwner(id, ctx, store);
@@ -124516,11 +124650,9 @@ function makeExecuteWorkflowTool(runtime, reloadWorkflowResources, ensureWorkflo
124516
124650
  const awaitRequest = (operation) => raceAbort(operation, signal);
124517
124651
  const getRuntime = () => {
124518
124652
  const resolved = typeof runtime === "function" ? runtime(ctx) : runtime;
124519
- if (access !== "sdk")
124520
- return resolved;
124521
124653
  return {
124522
124654
  ...resolved,
124523
- resumeDurableWorkflow: (id, options) => resolved.resumeDurableWorkflow(id, { ...options, modelOwner: workflowCaller(ctx) })
124655
+ resumeDurableWorkflow: (id, options) => resolved.resumeDurableWorkflow(id, access === "sdk" || adoptsReleasedRun(id) ? { ...options, modelOwner: workflowCaller(ctx) } : options)
124524
124656
  };
124525
124657
  };
124526
124658
  const ensureWorkflowResourcesVisible = async () => {