@bastani/atomic 0.9.57 → 0.9.58

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 (128) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/CHANGELOG.md +18 -0
  5. package/dist/builtin/subagents/README.md +3 -1
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/subagents/src/extension/index.bundle.mjs +202 -38
  8. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  9. package/dist/builtin/web-access/index.bundle.mjs +2 -2
  10. package/dist/builtin/web-access/package.json +1 -1
  11. package/dist/builtin/workflows/CHANGELOG.md +42 -0
  12. package/dist/builtin/workflows/package.json +1 -1
  13. package/dist/builtin/workflows/src/extension/index.bundle.mjs +885 -228
  14. package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +933 -175
  15. package/dist/builtin/workflows/src/index.js +630 -162
  16. package/dist/builtin/workflows/src/local-worker-entry.mjs +933 -175
  17. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +1 -1
  18. package/dist/builtin/workflows/src/shared/workflow-infrastructure-error.d.ts +13 -0
  19. package/dist/builtin/workflows/src/workers/public-options.d.ts +10 -0
  20. package/dist/core/agent-session-persistent-custom-messages.d.ts +5 -0
  21. package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
  22. package/dist/core/agent-session-persistent-custom-messages.js +16 -0
  23. package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
  24. package/dist/core/agent-session-tasks.d.ts.map +1 -1
  25. package/dist/core/agent-session-tasks.js +20 -0
  26. package/dist/core/agent-session-tasks.js.map +1 -1
  27. package/dist/core/child-session-options.d.ts.map +1 -1
  28. package/dist/core/child-session-options.js +3 -1
  29. package/dist/core/child-session-options.js.map +1 -1
  30. package/dist/core/sdk-types.d.ts +6 -1
  31. package/dist/core/sdk-types.d.ts.map +1 -1
  32. package/dist/core/sdk-types.js.map +1 -1
  33. package/dist/core/sdk.js +4 -2
  34. package/dist/core/sdk.js.map +1 -1
  35. package/dist/core/settings-defaults.d.ts +4 -0
  36. package/dist/core/settings-defaults.d.ts.map +1 -1
  37. package/dist/core/settings-defaults.js +4 -0
  38. package/dist/core/settings-defaults.js.map +1 -1
  39. package/dist/core/settings-manager-resource-accessors.d.ts +2 -0
  40. package/dist/core/settings-manager-resource-accessors.d.ts.map +1 -1
  41. package/dist/core/settings-manager-resource-accessors.js +6 -0
  42. package/dist/core/settings-manager-resource-accessors.js.map +1 -1
  43. package/dist/core/settings-schema.d.ts +9 -0
  44. package/dist/core/settings-schema.d.ts.map +1 -1
  45. package/dist/core/settings-schema.js +11 -0
  46. package/dist/core/settings-schema.js.map +1 -1
  47. package/dist/core/settings-types.d.ts +1 -1
  48. package/dist/core/settings-types.d.ts.map +1 -1
  49. package/dist/core/settings-types.js.map +1 -1
  50. package/dist/core/tasks/agent-adapter.d.ts +10 -1
  51. package/dist/core/tasks/agent-adapter.d.ts.map +1 -1
  52. package/dist/core/tasks/agent-adapter.js +20 -0
  53. package/dist/core/tasks/agent-adapter.js.map +1 -1
  54. package/dist/core/tasks/contracts.d.ts +6 -0
  55. package/dist/core/tasks/contracts.d.ts.map +1 -1
  56. package/dist/core/tasks/contracts.js.map +1 -1
  57. package/dist/core/tasks/heartbeat-notice.d.ts +64 -0
  58. package/dist/core/tasks/heartbeat-notice.d.ts.map +1 -0
  59. package/dist/core/tasks/heartbeat-notice.js +98 -0
  60. package/dist/core/tasks/heartbeat-notice.js.map +1 -0
  61. package/dist/core/tasks/heartbeat.d.ts +75 -0
  62. package/dist/core/tasks/heartbeat.d.ts.map +1 -0
  63. package/dist/core/tasks/heartbeat.js +440 -0
  64. package/dist/core/tasks/heartbeat.js.map +1 -0
  65. package/dist/core/tasks/supervisor.d.ts +2 -0
  66. package/dist/core/tasks/supervisor.d.ts.map +1 -1
  67. package/dist/core/tasks/supervisor.js +32 -4
  68. package/dist/core/tasks/supervisor.js.map +1 -1
  69. package/dist/core/tasks/task-view-source.d.ts +72 -0
  70. package/dist/core/tasks/task-view-source.d.ts.map +1 -0
  71. package/dist/core/tasks/task-view-source.js +155 -0
  72. package/dist/core/tasks/task-view-source.js.map +1 -0
  73. package/dist/core/tools/bash-pty-native.d.ts +2 -0
  74. package/dist/core/tools/bash-pty-native.d.ts.map +1 -1
  75. package/dist/core/tools/bash-pty-native.js +1 -0
  76. package/dist/core/tools/bash-pty-native.js.map +1 -1
  77. package/dist/core/tools/bash.d.ts +3 -0
  78. package/dist/core/tools/bash.d.ts.map +1 -1
  79. package/dist/core/tools/bash.js +20 -2
  80. package/dist/core/tools/bash.js.map +1 -1
  81. package/dist/core/tools/powershell.d.ts +16 -8
  82. package/dist/core/tools/powershell.d.ts.map +1 -1
  83. package/dist/index.d.ts +5 -3
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4 -2
  86. package/dist/index.js.map +1 -1
  87. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  88. package/dist/modes/interactive/components/chat-session-host-events.js +7 -0
  89. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  90. package/dist/modes/interactive/components/chat-session-host-state.d.ts +3 -0
  91. package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
  92. package/dist/modes/interactive/components/chat-session-host-state.js +4 -0
  93. package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
  94. package/dist/modes/interactive/components/chat-session-host-types.d.ts +11 -0
  95. package/dist/modes/interactive/components/chat-session-host-types.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/chat-session-host-types.js.map +1 -1
  97. package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
  98. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/chat-session-host.js +15 -8
  100. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  101. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  102. package/dist/modes/interactive/components/custom-message.js +12 -0
  103. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  104. package/dist/modes/interactive/components/task-detail.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/task-detail.js +5 -0
  106. package/dist/modes/interactive/components/task-detail.js.map +1 -1
  107. package/dist/modes/interactive/components/task-heartbeat-message.d.ts +14 -0
  108. package/dist/modes/interactive/components/task-heartbeat-message.d.ts.map +1 -0
  109. package/dist/modes/interactive/components/task-heartbeat-message.js +76 -0
  110. package/dist/modes/interactive/components/task-heartbeat-message.js.map +1 -0
  111. package/dist/modes/interactive/components/task-inspector.d.ts +6 -3
  112. package/dist/modes/interactive/components/task-inspector.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/task-inspector.js +53 -100
  114. package/dist/modes/interactive/components/task-inspector.js.map +1 -1
  115. package/dist/modes/interactive/skill-command-autocomplete.d.ts +22 -2
  116. package/dist/modes/interactive/skill-command-autocomplete.d.ts.map +1 -1
  117. package/dist/modes/interactive/skill-command-autocomplete.js +28 -9
  118. package/dist/modes/interactive/skill-command-autocomplete.js.map +1 -1
  119. package/docs/background-tasks.md +65 -1
  120. package/docs/sdk.md +3 -1
  121. package/docs/settings.md +12 -0
  122. package/docs/subagents/reference.md +2 -0
  123. package/docs/subagents.md +2 -0
  124. package/docs/tools.md +2 -1
  125. package/docs/workflows/operations.md +5 -3
  126. package/npm-shrinkwrap.json +32 -32
  127. package/package.json +3 -3
  128. package/schemas/settings.schema.json +24 -0
@@ -8939,6 +8939,13 @@ function subagentChildCardRecord(childId, taskId) {
8939
8939
  const id = childId ?? childIdsByTaskId.get(taskId);
8940
8940
  return id === undefined ? undefined : latest.get(id);
8941
8941
  }
8942
+ function observedSubagentRunIds(ownerSessionId) {
8943
+ const runIds = new Set;
8944
+ for (const record of latest.values())
8945
+ if (record.owner?.sessionId === ownerSessionId)
8946
+ runIds.add(record.runId);
8947
+ return runIds;
8948
+ }
8942
8949
  function watchSubagentChildCard(card, invalidate, key, ownerSessionId) {
8943
8950
  invalidators.set(card, invalidate);
8944
8951
  const cards = watchers.get(key) ?? new Set;
@@ -10820,6 +10827,7 @@ ${launch.message}` : input.task,
10820
10827
  },
10821
10828
  wait: { kind: "background" },
10822
10829
  runtime: input.runtime,
10830
+ ...input.heartbeatMs === undefined ? {} : { heartbeatMs: input.heartbeatMs },
10823
10831
  onTerminal: launch.onTerminal,
10824
10832
  ...input.schedule ? { schedule: input.schedule } : {},
10825
10833
  ...input.outputText ? { outputText: input.outputText } : {},
@@ -10834,7 +10842,8 @@ ${launch.message}` : input.task,
10834
10842
  agent: input.agent,
10835
10843
  task: input.intentTask ?? input.task,
10836
10844
  cwd: input.options.cwd ?? input.cwd,
10837
- ...input.options.modelRoute ? { routerSelection: input.options.modelRoute.routerSelection } : {}
10845
+ ...input.options.modelRoute ? { routerSelection: input.options.modelRoute.routerSelection } : {},
10846
+ ...input.heartbeatMs === undefined ? {} : { heartbeatMs: input.heartbeatMs }
10838
10847
  }, input.operation ?? `${input.options.runId}:${input.options.index ?? 0}`, (context) => {
10839
10848
  dispatched = true;
10840
10849
  queuedChildren.delete(context.ref.taskId);
@@ -11182,6 +11191,7 @@ async function runForegroundParallelTasks(input) {
11182
11191
  options: runOptions,
11183
11192
  runtime: input.runtime,
11184
11193
  wait: input.wait,
11194
+ ...(task.heartbeatMs ?? input.heartbeatMs) === undefined ? {} : { heartbeatMs: task.heartbeatMs ?? input.heartbeatMs },
11185
11195
  schedule,
11186
11196
  onTerminal: (child) => {
11187
11197
  terminalChild = child;
@@ -11822,7 +11832,10 @@ import {
11822
11832
  import {
11823
11833
  SubagentControl
11824
11834
  } from "@bastani/atomic-natives";
11825
- import { clampThinkingLevel } from "@bastani/pi-ai/compat";
11835
+ import {
11836
+ clampThinkingLevel,
11837
+ getSupportedThinkingLevels
11838
+ } from "@bastani/pi-ai/compat";
11826
11839
 
11827
11840
  // dist/builtin/subagents/src/agents/skills.ts
11828
11841
  import * as fs14 from "node:fs";
@@ -12563,6 +12576,23 @@ function createInProcessChildPromptBehavior(policy) {
12563
12576
  }
12564
12577
 
12565
12578
  // dist/builtin/subagents/src/runs/inprocess/runner.ts
12579
+ var SUBAGENT_SESSION_START_TIMEOUT_MS = 5 * 60000;
12580
+ function formatStartTimeout(ms) {
12581
+ return ms % 60000 === 0 ? `${ms / 60000} min` : `${Math.max(1, Math.round(ms / 1000))}s`;
12582
+ }
12583
+ var DEFAULT_THINKING_LEVEL = "medium";
12584
+ function explicitEffortFor(model, requested, settings) {
12585
+ if (getSupportedThinkingLevels(model).includes(requested))
12586
+ return requested;
12587
+ return clampThinkingLevel(model, settings.getModelThinkingLevel(model.provider, model.id) ?? settings.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);
12588
+ }
12589
+ function pinFallbackEffort(entry, requested, settings, resolve) {
12590
+ const resolved = resolve?.(entry);
12591
+ if (!resolved)
12592
+ return entry;
12593
+ const { baseModel } = splitKnownThinkingSuffix(entry.trim());
12594
+ return `${baseModel}:${explicitEffortFor(resolved.model, resolved.thinkingLevel ?? requested, settings)}`;
12595
+ }
12566
12596
  function modelIdForCandidate(candidate, fallbackModel) {
12567
12597
  return candidate.modelId ?? (candidate.model ? `${candidate.model.provider}/${candidate.model.id}` : undefined) ?? (fallbackModel ? `${fallbackModel.provider}/${fallbackModel.id}` : undefined);
12568
12598
  }
@@ -13015,12 +13045,23 @@ class SubagentControlRuntime {
13015
13045
  delivered = new Set;
13016
13046
  deliveredEnvelopes = new Map;
13017
13047
  killedChildren = new Set;
13048
+ childIds = new Map;
13018
13049
  nextAttemptId = 1;
13019
13050
  constructor(parent, sessionRoot, runningCapacity) {
13020
13051
  this.parent = { ...parent };
13021
13052
  this.sessionRoot = trustedSessionRoot(this.parent, sessionRoot);
13022
13053
  this.native = new SubagentControl(this.parent.path, runningCapacity);
13023
13054
  }
13055
+ childIdOf(pathValue) {
13056
+ return this.childIds.get(pathValue);
13057
+ }
13058
+ pathOfChildId(childId) {
13059
+ let latest;
13060
+ for (const [pathValue, id] of this.childIds)
13061
+ if (id === childId)
13062
+ latest = pathValue;
13063
+ return latest;
13064
+ }
13024
13065
  registerAgents(agents) {
13025
13066
  for (const agent of agents)
13026
13067
  this.native.registerAgent(agent.name);
@@ -13038,6 +13079,8 @@ class SubagentControlRuntime {
13038
13079
  if (!native.child)
13039
13080
  return refusal(native);
13040
13081
  const identity = native.child;
13082
+ if (spec.childId !== undefined)
13083
+ this.childIds.set(identity.path, spec.childId);
13041
13084
  const childModePolicy = resolveChildModePolicy(spec);
13042
13085
  const sessionDir = spec.sessionManager?.getSessionDir() || sessionDirectory(this.sessionRoot, identity.path);
13043
13086
  mkdirSync7(sessionDir, { recursive: true });
@@ -13180,9 +13223,34 @@ class SubagentControlRuntime {
13180
13223
  signals.interrupt.addEventListener("abort", interruptListener, {
13181
13224
  once: true
13182
13225
  });
13226
+ const startTimeoutMs = admitted.spec.sessionStartTimeoutMs ?? SUBAGENT_SESSION_START_TIMEOUT_MS;
13227
+ const startDeadline = new AbortController;
13228
+ let startPhase = "opening its session";
13229
+ const startTimer = setTimeout(() => startDeadline.abort(new Error(`Subagent session did not start within ${formatStartTimeout(startTimeoutMs)} (still ${startPhase}); it was stopped before its task ran.`)), startTimeoutMs);
13230
+ const beforeStartDeadline = (phase, work, abandon) => {
13231
+ startPhase = phase;
13232
+ return new Promise((resolve, reject) => {
13233
+ const expire = () => {
13234
+ work.then((late) => abandon?.(late), () => {
13235
+ return;
13236
+ });
13237
+ reject(startDeadline.signal.reason);
13238
+ };
13239
+ if (startDeadline.signal.aborted)
13240
+ return expire();
13241
+ startDeadline.signal.addEventListener("abort", expire, { once: true });
13242
+ work.then((value) => {
13243
+ startDeadline.signal.removeEventListener("abort", expire);
13244
+ resolve(value);
13245
+ }, (error) => {
13246
+ startDeadline.signal.removeEventListener("abort", expire);
13247
+ reject(error);
13248
+ });
13249
+ });
13250
+ };
13183
13251
  try {
13184
13252
  const workflow = workflowMetadataFromContext(admitted.spec.parent?.orchestrationContext);
13185
- const sessionManager = admitted.sessionManager ? await SessionManager3.open(admitted.sessionManager.getSessionStore(), admitted.sessionManager.getSessionId(), admitted.policy.cwd) : await SessionManager3.create(admitted.policy.cwd, admitted.sessionDir, workflow ? { internal: true, workflow } : { internal: true });
13253
+ const sessionManager = await beforeStartDeadline("opening its session", admitted.sessionManager ? SessionManager3.open(admitted.sessionManager.getSessionStore(), admitted.sessionManager.getSessionId(), admitted.policy.cwd) : SessionManager3.create(admitted.policy.cwd, admitted.sessionDir, workflow ? { internal: true, workflow } : { internal: true }));
13186
13254
  transcriptManager = sessionManager;
13187
13255
  taskHooks?.bindTranscript?.(sessionManager);
13188
13256
  const childSession = {
@@ -13192,7 +13260,7 @@ class SubagentControlRuntime {
13192
13260
  taskHooks?.bindChildSession?.(childSession);
13193
13261
  admitted.spec.onChildSession?.(childSession);
13194
13262
  if (workflow)
13195
- await sessionManager.markSessionInternal(workflow);
13263
+ await beforeStartDeadline("opening its session", sessionManager.markSessionInternal(workflow));
13196
13264
  const launchLeafId = sessionManager.getLeafId();
13197
13265
  ownMessages = () => messagesSinceLaunch(sessionManager, launchLeafId);
13198
13266
  let created;
@@ -13218,6 +13286,13 @@ class SubagentControlRuntime {
13218
13286
  } : inherited.extensionBindings;
13219
13287
  const agentDir = inherited.agentDir ?? getAgentDir2();
13220
13288
  const settingsManager = inherited.settingsManager ?? SettingsManager.create(admitted.policy.cwd, agentDir);
13289
+ const requested = admitted.spec.requestedThinking;
13290
+ const startSuffix = splitKnownThinkingSuffix(candidate.modelId ?? "").thinkingSuffix.slice(1);
13291
+ const startEffort = THINKING_LEVELS.find((level) => level === startSuffix) ?? requested;
13292
+ const explicitEffort = requested === undefined || startEffort === undefined ? {} : {
13293
+ ...inherited.model ? { thinkingLevel: explicitEffortFor(inherited.model, startEffort, settingsManager) } : {},
13294
+ fallbackModels: (inherited.fallbackModels ?? settingsManager.getFallbackModels() ?? []).map((entry) => pinFallbackEffort(entry, requested, settingsManager, admitted.spec.resolveCandidateModel))
13295
+ };
13221
13296
  const resourceLoader = new DefaultResourceLoader(inProcessChildResourceLoaderOptions({
13222
13297
  cwd: admitted.policy.cwd,
13223
13298
  agentDir,
@@ -13227,7 +13302,7 @@ class SubagentControlRuntime {
13227
13302
  resourceLoaderInheritanceSnapshot: admitted.spec.parent?.resourceLoaderInheritanceSnapshot,
13228
13303
  orchestrationContext: admitted.spec.parent?.orchestrationContext
13229
13304
  }));
13230
- await resourceLoader.reload();
13305
+ await beforeStartDeadline("loading its resources", resourceLoader.reload());
13231
13306
  const selectedSkills = resolveSkillsFromCatalog([...admitted.policy.skills], resourceLoader.getSkillCatalog(), admitted.policy.cwd);
13232
13307
  skillReport = {
13233
13308
  ...selectedSkills.resolved.length > 0 ? { skills: selectedSkills.resolved.map((skill) => skill.name) } : {},
@@ -13239,26 +13314,28 @@ class SubagentControlRuntime {
13239
13314
  const skillInjection = buildSkillInjection(selectedSkills.resolved);
13240
13315
  const promptBehavior = createInProcessChildPromptBehavior(admitted.policy);
13241
13316
  const systemPromptTransform = createInProcessChildSystemPromptTransform(admitted.policy, skillInjection);
13242
- created = {
13243
- session: (await createAgentSession({
13244
- ...inherited,
13245
- extensionBindings,
13246
- customTools: admitted.policy.customTools ?? inherited.customTools,
13247
- resourceLoader,
13248
- resourceAdmissionSignal: AbortSignal.any([signals.abort, signals.interrupt]),
13249
- subagentPolicy: {
13250
- ...admitted.policy,
13251
- executionEnded: executionEnded.signal,
13252
- ...!childAllowsIntercom(inherited) ? { intercom: undefined, intercomGroup: undefined } : {}
13253
- },
13254
- settingsManager,
13255
- orchestrationContext: admitted.spec.parent?.orchestrationContext,
13256
- sessionManager,
13257
- systemPromptTransform,
13258
- initialContextTransform: promptBehavior.initialContextTransform
13259
- })).session
13260
- };
13317
+ created = await beforeStartDeadline("creating its agent session, which includes any wait for a resource-admission permit", createAgentSession({
13318
+ ...inherited,
13319
+ ...explicitEffort,
13320
+ extensionBindings,
13321
+ customTools: admitted.policy.customTools ?? inherited.customTools,
13322
+ resourceLoader,
13323
+ resourceAdmissionSignal: AbortSignal.any([signals.abort, signals.interrupt, startDeadline.signal]),
13324
+ subagentPolicy: {
13325
+ ...admitted.policy,
13326
+ executionEnded: executionEnded.signal,
13327
+ ...!childAllowsIntercom(inherited) ? { intercom: undefined, intercomGroup: undefined } : {}
13328
+ },
13329
+ settingsManager,
13330
+ orchestrationContext: admitted.spec.parent?.orchestrationContext,
13331
+ sessionManager,
13332
+ systemPromptTransform,
13333
+ initialContextTransform: promptBehavior.initialContextTransform
13334
+ }), (late) => void late.session.dispose().catch(() => {
13335
+ return;
13336
+ }));
13261
13337
  }
13338
+ clearTimeout(startTimer);
13262
13339
  session = created.session;
13263
13340
  const transcriptSession = session;
13264
13341
  let observedStreamingMessage;
@@ -13490,6 +13567,7 @@ class SubagentControlRuntime {
13490
13567
  ...skillReport
13491
13568
  };
13492
13569
  } finally {
13570
+ clearTimeout(startTimer);
13493
13571
  executionEnded.abort();
13494
13572
  signals.abort.removeEventListener("abort", abortListener);
13495
13573
  signals.interrupt.removeEventListener("abort", interruptListener);
@@ -13677,11 +13755,24 @@ function findSubagentControl(id) {
13677
13755
  if (direct)
13678
13756
  return direct;
13679
13757
  for (const control of controls.values()) {
13758
+ if (control.pathOfChildId(id) !== undefined)
13759
+ return control;
13680
13760
  if (control.native.listChildren().some((child) => child.path === id))
13681
13761
  return control;
13682
13762
  }
13683
13763
  return;
13684
13764
  }
13765
+ function subagentChildPath(control, id) {
13766
+ return control.pathOfChildId(id) ?? id;
13767
+ }
13768
+ function subagentChildIdOfPath(id) {
13769
+ for (const control of controls.values()) {
13770
+ const childId = control.childIdOf(id);
13771
+ if (childId !== undefined)
13772
+ return childId;
13773
+ }
13774
+ return;
13775
+ }
13685
13776
  function listSubagentControls() {
13686
13777
  return [...controls.values()];
13687
13778
  }
@@ -14027,6 +14118,8 @@ async function runSingleInProcess(runtimeCwd, agent, task, options) {
14027
14118
  const resolvedCandidate = candidate ? options.resolveCandidateModel?.(candidate) : undefined;
14028
14119
  if (options.modelRoute && !resolvedCandidate)
14029
14120
  throw new Error("Subagent auto-selected model could not be resolved before execution. Retry explicitly with the current catalog.");
14121
+ const requestedSuffix = splitKnownThinkingSuffix(options.modelOverride ?? agent.model ?? "").thinkingSuffix.slice(1);
14122
+ const requestedThinking = options.modelRoute ? undefined : THINKING_LEVELS.find((level) => level === requestedSuffix);
14030
14123
  const orchestrationContext = workflowOrchestrationContext(options);
14031
14124
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
14032
14125
  const parent = {
@@ -14053,6 +14146,7 @@ ${task}`);
14053
14146
  const testSession = options.testSession ?? defaultTestSession();
14054
14147
  const spec = {
14055
14148
  taskName: agent.name,
14149
+ childId: `${options.runId}:${options.index ?? 0}`,
14056
14150
  task,
14057
14151
  agent,
14058
14152
  cwd,
@@ -14066,6 +14160,11 @@ ${task}`);
14066
14160
  ...options.progressPath ? { progressPath: options.progressPath } : {},
14067
14161
  ...options.progressArtifactPath ? { progressArtifactPath: options.progressArtifactPath } : options.progressPath && artifactsDir ? { progressArtifactPath: options.progressPath } : {},
14068
14162
  ...artifactPaths?.outputPath ? { outputArtifactPath: artifactPaths.outputPath } : {},
14163
+ ...options.sessionStartTimeoutMs === undefined ? {} : { sessionStartTimeoutMs: options.sessionStartTimeoutMs },
14164
+ ...requestedThinking === undefined ? {} : {
14165
+ requestedThinking,
14166
+ ...options.resolveCandidateModel ? { resolveCandidateModel: options.resolveCandidateModel } : {}
14167
+ },
14069
14168
  tools: agent.tools,
14070
14169
  mcpDirectTools: agent.mcpDirectTools,
14071
14170
  skills: options.skills ?? agent.skills,
@@ -15334,6 +15433,9 @@ async function recordReportedGoneSubagentChildren(pi, outcomes) {
15334
15433
  }
15335
15434
  }
15336
15435
 
15436
+ // dist/builtin/subagents/src/runs/foreground/subagent-executor.ts
15437
+ import { validateTaskHeartbeatMs } from "@bastani/atomic";
15438
+
15337
15439
  // dist/builtin/subagents/src/agents/agent-management.ts
15338
15440
  import * as fs15 from "node:fs";
15339
15441
  import * as path16 from "node:path";
@@ -15892,34 +15994,52 @@ function handleSubagentControlNotice(input) {
15892
15994
  function canonicalChildren(control) {
15893
15995
  return [...control.listChildren()].sort((left, right) => left.path.localeCompare(right.path));
15894
15996
  }
15997
+ function settledCause(control, path, status) {
15998
+ if (status !== "error" && status !== "interrupted")
15999
+ return;
16000
+ const cause = control.getDeliveredResult(path)?.cause?.replace(/\s+/g, " ").trim();
16001
+ return cause || undefined;
16002
+ }
15895
16003
  function childLines(control, id) {
15896
- if (!control)
15897
- return [];
15898
16004
  if (id) {
15899
- const child = control.findChild(id);
16005
+ const path = subagentChildPath(control, id);
16006
+ const child = control.findChild(path);
15900
16007
  if (!child)
15901
16008
  return [];
15902
- const delivered = control.getDeliveredResult(id);
16009
+ const delivered = control.getDeliveredResult(path);
16010
+ const childId = control.childIdOf(path);
16011
+ const cause = settledCause(control, path, child.status);
15903
16012
  return [
16013
+ ...childId ? [`Id: ${childId}`] : [],
15904
16014
  `Child: ${child.path}`,
15905
16015
  `Parent: ${child.parentPath}`,
15906
16016
  `Task: ${child.taskName}`,
15907
16017
  `Depth: ${child.depth}`,
15908
16018
  `Status: ${child.status}`,
16019
+ ...cause ? [`Cause: ${cause}`] : [],
15909
16020
  `Residency: ${child.loaded ? "loaded" : "cold"}`,
15910
16021
  ...delivered?.sessionFile ? [`Session: ${delivered.sessionFile}`] : []
15911
16022
  ];
15912
16023
  }
15913
- return control.listChildren().map((child) => `${child.path} — ${child.status} (${child.loaded ? "loaded" : "cold"})`);
16024
+ return control.listChildren().map((child) => {
16025
+ const childId = control.childIdOf(child.path);
16026
+ const cause = settledCause(control, child.path, child.status);
16027
+ return `${child.path} — ${child.status} (${child.loaded ? "loaded" : "cold"})${childId ? ` · id ${childId}` : ""}${cause ? ` · ${cause}` : ""}`;
16028
+ });
15914
16029
  }
15915
16030
  function statusGroup(control, id) {
16031
+ const path = id === undefined ? undefined : subagentChildPath(control, id);
15916
16032
  return {
15917
16033
  parentPath: control.parent.path,
15918
- children: canonicalChildren(control).filter((child) => !id || id === control.parent.path || child.path === id).map((child) => {
16034
+ children: canonicalChildren(control).filter((child) => !path || path === control.parent.path || child.path === path).map((child) => {
15919
16035
  const delivered = control.getDeliveredResult(child.path);
15920
16036
  const metadata = control.getChildMetadata(child.path) ?? delivered;
16037
+ const childId = control.childIdOf(child.path);
16038
+ const failure = settledCause(control, child.path, child.status);
15921
16039
  return {
15922
16040
  ...child,
16041
+ ...childId === undefined ? {} : { childId },
16042
+ ...failure === undefined ? {} : { failure },
15923
16043
  sessionFile: delivered?.sessionFile,
15924
16044
  ...metadata?.model === undefined ? {} : { model: metadata.model },
15925
16045
  ...metadata?.thinking === undefined ? {} : { thinking: metadata.thinking }
@@ -15927,10 +16047,11 @@ function statusGroup(control, id) {
15927
16047
  })
15928
16048
  };
15929
16049
  }
15930
- function inspectInProcessChildStatus(id) {
16050
+ function inspectInProcessChildStatus(id, ownsRun) {
16051
+ const owned = (control) => ownsRun?.(control.parent.path) ?? true;
15931
16052
  if (id) {
15932
16053
  const control = findSubagentControl(id);
15933
- if (!control)
16054
+ if (!control || !owned(control))
15934
16055
  return;
15935
16056
  const text = id === control.parent.path ? [`Parent: ${control.parent.path}`, ...childLines(control)].join(`
15936
16057
  `) : childLines(control, id).join(`
@@ -15942,7 +16063,7 @@ function inspectInProcessChildStatus(id) {
15942
16063
  details: { mode: "management", results: [], statusGroups: [statusGroup(control, id)] }
15943
16064
  };
15944
16065
  }
15945
- const controls = listSubagentControls();
16066
+ const controls = listSubagentControls().filter(owned);
15946
16067
  const lines = controls.flatMap((control) => [`Parent: ${control.parent.path}`, ...childLines(control)]);
15947
16068
  if (lines.length === 0)
15948
16069
  return;
@@ -16257,7 +16378,8 @@ function topLevelSingleTask(item) {
16257
16378
  ...item.params.modelConstraints !== undefined ? { modelConstraints: item.params.modelConstraints } : {},
16258
16379
  ...item.params.taskNeeds !== undefined ? { taskNeeds: item.params.taskNeeds } : {},
16259
16380
  ...item.params.skill !== undefined ? { skill: item.params.skill } : {},
16260
- ...item.params.group !== undefined ? { group: item.params.group } : {}
16381
+ ...item.params.group !== undefined ? { group: item.params.group } : {},
16382
+ ...item.params.heartbeatMs !== undefined ? { heartbeatMs: item.params.heartbeatMs } : {}
16261
16383
  };
16262
16384
  }
16263
16385
  function existingParallelTask(item, task) {
@@ -16266,7 +16388,8 @@ function existingParallelTask(item, task) {
16266
16388
  ...task,
16267
16389
  cwd: originatingTaskCwd(callCwd, task.cwd),
16268
16390
  [BURST_TASK_DISCOVERY_CWD]: callCwd,
16269
- ...task.group === undefined && item.params.group !== undefined ? { group: item.params.group } : {}
16391
+ ...task.group === undefined && item.params.group !== undefined ? { group: item.params.group } : {},
16392
+ ...task.heartbeatMs === undefined && item.params.heartbeatMs !== undefined ? { heartbeatMs: item.params.heartbeatMs } : {}
16270
16393
  };
16271
16394
  }
16272
16395
  function mergeBurst(items) {
@@ -16992,6 +17115,7 @@ async function runParallelPath(data, deps) {
16992
17115
  const detachedNotificationScope = { orchestrationContext: ctx.orchestrationContext };
16993
17116
  const results = await runForegroundParallelTasks({
16994
17117
  wait: params.wait,
17118
+ ...params.heartbeatMs === undefined ? {} : { heartbeatMs: params.heartbeatMs },
16995
17119
  schedule: params[BURST_EXECUTION_SCHEDULE],
16996
17120
  onDetachedExit: (index, result) => {
16997
17121
  if (workflowStageAcceptsDetachedNotification(detachedNotificationScope)) {
@@ -17340,6 +17464,7 @@ async function runSinglePath(data, deps) {
17340
17464
  intentTask: handoffTaskContext,
17341
17465
  options: runOptions,
17342
17466
  wait: params.wait,
17467
+ ...params.heartbeatMs === undefined ? {} : { heartbeatMs: params.heartbeatMs },
17343
17468
  schedule: params[BURST_EXECUTION_SCHEDULE],
17344
17469
  runtime: deps.runtime,
17345
17470
  recordChild: createSubagentChildRecordSink(deps.pi, ctx),
@@ -17551,10 +17676,22 @@ function liveTaskIds(host) {
17551
17676
  function ownedRecords(ctx) {
17552
17677
  return readSubagentChildRecords(ctx.sessionManager.getEntries(), subagentChildOwner(ctx).sessionId);
17553
17678
  }
17679
+ async function subagentRunsLaunchedBy(ctx) {
17680
+ const owner = subagentChildOwner(ctx).sessionId;
17681
+ await subagentChildRecordsAppended(owner);
17682
+ const runIds = observedSubagentRunIds(owner);
17683
+ for (const record of ownedRecords(ctx).values())
17684
+ runIds.add(record.runId);
17685
+ return runIds;
17686
+ }
17554
17687
  function resolveRecord(records, id) {
17555
17688
  const direct = findSubagentChildRecord(records, id);
17556
17689
  if (direct)
17557
17690
  return direct;
17691
+ const listedAs = subagentChildIdOfPath(id.trim());
17692
+ const byPath = listedAs === undefined ? undefined : records.get(listedAs);
17693
+ if (byPath)
17694
+ return byPath;
17558
17695
  const inRun = [...records.values()].filter((record) => record.runId === id.trim());
17559
17696
  if (inRun.length === 1)
17560
17697
  return inRun[0];
@@ -17895,7 +18032,8 @@ async function handleManagementRequest(input) {
17895
18032
  if (action === "status") {
17896
18033
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
17897
18034
  const foreground = getForegroundControl(deps.state, targetRunId);
17898
- const live = inspectInProcessChildStatus(targetRunId) ?? (foreground ? foregroundStatusResult(foreground) : undefined);
18035
+ const launched = await subagentRunsLaunchedBy(ctx);
18036
+ const live = inspectInProcessChildStatus(targetRunId, (runId) => launched.has(runId)) ?? (foreground ? foregroundStatusResult(foreground) : undefined);
17899
18037
  if (awaitingResume.length > 0) {
17900
18038
  const liveText = live?.content.map((part) => part.type === "text" ? part.text : "").join(`
17901
18039
  `);
@@ -17926,6 +18064,22 @@ function inferExecutionMode(params) {
17926
18064
  return "parallel";
17927
18065
  return "single";
17928
18066
  }
18067
+ function invalidHeartbeatResult(params) {
18068
+ try {
18069
+ if (params.heartbeatMs !== undefined)
18070
+ validateTaskHeartbeatMs(params.heartbeatMs);
18071
+ for (const [index, task] of (params.tasks ?? []).entries())
18072
+ if (task.heartbeatMs !== undefined)
18073
+ validateTaskHeartbeatMs(task.heartbeatMs, `tasks[${index}].heartbeatMs`);
18074
+ return;
18075
+ } catch (error) {
18076
+ return {
18077
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
18078
+ isError: true,
18079
+ details: { mode: inferExecutionMode(params), results: [] }
18080
+ };
18081
+ }
18082
+ }
17929
18083
  function duplicateSubagentCallResult(params) {
17930
18084
  return {
17931
18085
  content: [
@@ -17999,7 +18153,7 @@ function createSubagentExecutor(rawDeps) {
17999
18153
  duplicateResult: duplicateSubagentCallResult
18000
18154
  });
18001
18155
  return {
18002
- execute: (id, params, signal, onUpdate, ctx) => params.action ? execute(id, params, signal, onUpdate, ctx) : executeWithBurstCollection(id, params, signal, onUpdate, ctx)
18156
+ execute: async (id, params, signal, onUpdate, ctx) => params.action ? execute(id, params, signal, onUpdate, ctx) : invalidHeartbeatResult(params) ?? executeWithBurstCollection(id, params, signal, onUpdate, ctx)
18003
18157
  };
18004
18158
  }
18005
18159
 
@@ -19237,11 +19391,13 @@ function childStatusLines(child, expanded, theme, width) {
19237
19391
  return [
19238
19392
  heading,
19239
19393
  ...metadata,
19394
+ ...child.childId ? [theme.fg("dim", ` Id: ${displayText(child.childId)}`)] : [],
19240
19395
  theme.fg("dim", ` Path: ${displayText(child.path)}`),
19241
19396
  theme.fg("dim", ` Parent: ${displayText(child.parentPath)} · Depth: ${child.depth}`),
19242
19397
  theme.fg("dim", ` Task: ${displayText(child.taskName)}`),
19243
19398
  theme.fg("dim", ` Residency: ${child.loaded ? "loaded" : "cold"}`),
19244
19399
  ...child.cause ? [theme.fg("muted", ` Cause: ${displayText(child.cause)}`)] : [],
19400
+ ...child.failure ? [theme.fg("error", ` Error: ${displayText(child.failure)}`)] : [],
19245
19401
  ...child.sessionFile ? [theme.fg("dim", ` Session: ${displayText(child.sessionFile)}`)] : []
19246
19402
  ];
19247
19403
  }
@@ -19709,6 +19865,12 @@ var MaxOutputSchema = Type.Object({
19709
19865
  bytes: Type.Optional(Type.Number()),
19710
19866
  lines: Type.Optional(Type.Number())
19711
19867
  }, { additionalProperties: false });
19868
+ function heartbeatSchema(scope) {
19869
+ return Type.Number({
19870
+ minimum: 0,
19871
+ description: `Heartbeat cadence in ms for ${scope} while it runs (default 15 min; 0 disables; min 60000).`
19872
+ });
19873
+ }
19712
19874
  var GroupSchema = Type.Union([Type.String(), Type.Boolean()], {
19713
19875
  description: "Intercom group for spawned children. A named string joins that group; boolean `true` or the trimmed, case-insensitive string sentinel `true`/`auto` auto-generates one shared UUID group per parallel set. The names `true` and `auto` are reserved; use a different literal group name. Defaults to the current session/stage's group. Only applied when the child has intercom access; contact_supervisor still reaches the supervisor across groups."
19714
19876
  });
@@ -19727,7 +19889,8 @@ var TaskItem = Type.Object({
19727
19889
  modelConstraints: Type.Optional(ModelConstraintsSchema),
19728
19890
  taskNeeds: Type.Optional(TaskNeedsSchema),
19729
19891
  skill: Type.Optional(SkillOverride),
19730
- group: Type.Optional(GroupSchema)
19892
+ group: Type.Optional(GroupSchema),
19893
+ heartbeatMs: Type.Optional(heartbeatSchema("this task"))
19731
19894
  });
19732
19895
  var ControlOverrides = Type.Object({
19733
19896
  enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
@@ -19764,6 +19927,7 @@ var SubagentParams = Type.Object({
19764
19927
  budgetMs: Type.Optional(Type.Number({ description: "Foreground wait budget in milliseconds (kind='foreground' only)." }))
19765
19928
  }, { additionalProperties: false })),
19766
19929
  budgetMs: Type.Optional(Type.Number({ description: "Foreground wait budget in milliseconds." })),
19930
+ heartbeatMs: Type.Optional(heartbeatSchema("each child; a task's own wins")),
19767
19931
  agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
19768
19932
  task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
19769
19933
  action: Type.Optional(Type.String({
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.58] - 2026-11-01
8
+
9
+ ### Fixed
10
+
11
+ - PDF text extraction no longer prints pdf.js font warnings such as `Warning: TT: undefined function: 32` over the interactive terminal UI when fetching PDFs with unusual embedded fonts. Extraction errors are still reported.
12
+
13
+ ## [0.9.58-alpha.1] - 2026-11-01
14
+
15
+ ### Fixed
16
+
17
+ - PDF text extraction no longer prints pdf.js font warnings such as `Warning: TT: undefined function: 32` over the interactive terminal UI when fetching PDFs with unusual embedded fonts. Extraction errors are still reported.
18
+
7
19
  ## [0.9.52] - 2026-10-29
8
20
 
9
21
  ### Fixed
@@ -72560,7 +72560,7 @@ async function extractPDFToMarkdown(buffer, url, options = {}) {
72560
72560
  filename
72561
72561
  } = options;
72562
72562
  const safeMaxPages = Number.isFinite(maxPages) ? Math.max(1, Math.floor(maxPages)) : DEFAULT_MAX_PAGES;
72563
- const pdf = await getDocumentProxy(new Uint8Array(buffer));
72563
+ const pdf = await getDocumentProxy(new Uint8Array(buffer), { verbosity: PDF_JS_VERBOSITY_ERRORS });
72564
72564
  const metadata = await pdf.getMetadata();
72565
72565
  const metadataInfo = metadata.info && typeof metadata.info === "object" ? metadata.info : null;
72566
72566
  const metaTitle = typeof metadataInfo?.Title === "string" ? metadataInfo.Title : undefined;
@@ -72650,7 +72650,7 @@ function isPDF(url, contentType) {
72650
72650
  return false;
72651
72651
  }
72652
72652
  }
72653
- var DEFAULT_MAX_PAGES = 100, DEFAULT_OUTPUT_DIR;
72653
+ var DEFAULT_MAX_PAGES = 100, PDF_JS_VERBOSITY_ERRORS = 0, DEFAULT_OUTPUT_DIR;
72654
72654
  var init_pdf_extract = __esm(() => {
72655
72655
  init_dist();
72656
72656
  DEFAULT_OUTPUT_DIR = join4(homedir4(), "Downloads");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.57",
3
+ "version": "0.9.58",
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,48 @@ 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.58] - 2026-11-01
10
+
11
+ ### Added
12
+
13
+ - `workflows.localWorkers.maxLoadPerCpu` (SDK `localWorkers.maxLoadPerCpu`): while the machine's load per CPU is above it, the stages of pooled children start one at a time, at most one every 5 seconds in the order they asked, instead of all at once while earlier ones are still starting. Running stages and their subagents and tool processes are not held. Default `3`; `0` turns it off. It is not saved with the root. ([#4146](https://github.com/bastani-inc/atomic/pull/4146))
14
+
15
+ ### Fixed
16
+
17
+ - A message sent to a stage on a local worker (`workflows.localWorkers`) while its session was still starting no longer fails the stage with "generation closed before the stage finished; its tool calls were refused" when the stage has `fallbackModels`.
18
+ - The chat of a stage on a local worker completes `/skill:` commands with that stage's skills, as an in-process stage chat does, instead of warning "Skill discovery unavailable: this stage host does not expose its session command metadata." While the stage's session is still starting, it says its skills are loading.
19
+ - In the chat of a stage on a local worker, `/compact` compacts that stage's session and a `!` command runs on the worker in the stage's working directory, with its output streaming into the chat and Escape stopping it even while the pool is paused, as in the chat of an in-process stage. After `/compact` the open chat shows the compacted transcript. Before, `/compact` was sent to the model as plain text and a `!` command failed with "no live agent session on this stage". `/tasks` there lists the stage's agents and shells on its worker with their live status, opens their output and transcripts, sends them input and stops them, and the chat's task footer counts them, as in the chat of an in-process stage. An agent task's reply shows while it streams, tasks can be inspected and stopped while the pool is paused, and a stop that cannot reach the worker says so. Before, `/tasks` showed an empty list.
20
+ - A stage on a local worker reads as queued, with what it is doing (`creating session 32s`), on its graph node, in the graph header, the stage list, `/workflow status` and the `workflow` tool's stage list until its session has started, instead of reading as running. Its chat says it is starting on its local worker instead of showing a blank transcript, streams the stage as soon as the session starts, and opens the live view by itself when the worker cannot serve one yet. A message sent meanwhile waits for the session instead of failing with "The connection to stage … was lost" and "1 message not delivered".
21
+ - A root that runs children on local workers (`workflows.localWorkers`) no longer fails all of them with `Stale lease for run <id>` when its own process renews the root's lease late. A busy session, such as one that reloads its workflows and starts several other runs with in-process agent stages, could push the root's renewal past the pool's 90-second window. The pool treated that late renewal as a lost lease: it failed every running child, and every child the root started afterwards failed at once, while the root itself kept running. The pool now waits for the renewal and its workers keep their children; only a lease that was released or taken over by another holder ends them. Each run also renews its lease on its own, so a busy or blocked run no longer delays another run's renewal, and a renewal that fails without losing the lease, such as one that meets a briefly locked database, is retried on the next heartbeat instead of giving up the run. ([#4138](https://github.com/bastani-inc/atomic/pull/4138))
22
+ - With `workflows.localWorkers` configured, a root starts its pool's coordinator process when it runs its first pooled `ctx.workflow` child, or when it resumes a pool that already ran. A root that only runs `ctx.task` stages and `ctx.tool` steps no longer starts one, so launching such runs no longer leaves an idle coordinator process for each of them. ([#4138](https://github.com/bastani-inc/atomic/pull/4138))
23
+ - Stages on local workers (`workflows.localWorkers`) no longer fail with `Worker exited before delivery acknowledgement` while their worker is still running. On a busy machine a stage's startup delivery of queued Intercom messages could make no progress for a minute; the root then ended the worker's delivery request while the delivery kept going, and the next message reached a closed connection, failing the stage at `preparing message delivery` with an unknown, terminal failure. The delivery now keeps its connection to the worker until it finishes, and the stage still starts after a minute without progress, as an in-process stage does. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
24
+ - A worker whose work lease ran out only because the machine was too busy to process its renewal in time keeps its child. Its next journal write, usage report or completion is no longer refused with `Stale work lease`, which ended the child's attempt and could fail the child as `killed`; a session it starts no longer fails with `Stale resource admission attempt`, and waits for the renewal instead. Only an attempt that recovery handed to another worker is refused. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
25
+ - A fault of the pool itself that reaches a running pooled child, such as a journal write the database refused, replays the child from its last checkpoint (up to the usual three attempts) instead of failing its stage. A stage that does fail on a pool fault is classified with that fault as its failure code (`worker_lost`, `stale_work_lease`, …), recoverable and retryable, instead of `unknown`, and resume runs it again. A child whose attempt was fenced is no longer completed with the aborted run's result. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
26
+ - A stage on a local worker that ended keeps the worker lease it ended under: its worker's later lease renewals for the child's other stages no longer show it as renewed after it ended. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
27
+ - `/workflow pause <run>` and the `workflow` tool's `pause` pause a root's stages on local workers even while the root's own `ctx.tool` steps or in-process stages run, instead of reporting `No active stages to pause`. Pause, `pause --all` and resume no longer fail with `Worker control acknowledgement unresolved` when workers confirm slowly on a busy machine: the pause is recorded at once, workers pause their stages as they next check in, and the result says how many stages have not confirmed yet. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
28
+ - Resuming a root that runs children on local workers runs again the children an earlier process failed on a fault of the pool itself (a stage that lost its worker's delivery or lease on a busy machine, recorded as `Worker exited before delivery acknowledgement`, `Stale work lease` or `Stale resource admission attempt`), instead of replaying their failure. This includes runs paused or failed by 0.9.57. Pausing a root whose children have durable progress now says it can be resumed from a new process once this one exits, instead of `Resume with /workflow resume on this live process … Cross-process resume requires durable checkpoint or pending prompt progress`. ([#4146](https://github.com/bastani-inc/atomic/pull/4146))
29
+
30
+ ## [0.9.58-alpha.1] - 2026-11-01
31
+
32
+ ### Added
33
+
34
+ - `workflows.localWorkers.maxLoadPerCpu` (SDK `localWorkers.maxLoadPerCpu`): while the machine's load per CPU is above it, the stages of pooled children start one at a time, at most one every 5 seconds in the order they asked, instead of all at once while earlier ones are still starting. Running stages and their subagents and tool processes are not held. Default `3`; `0` turns it off. It is not saved with the root. ([#4146](https://github.com/bastani-inc/atomic/pull/4146))
35
+
36
+ ### Fixed
37
+
38
+ - A message sent to a stage on a local worker (`workflows.localWorkers`) while its session was still starting no longer fails the stage with "generation closed before the stage finished; its tool calls were refused" when the stage has `fallbackModels`.
39
+ - The chat of a stage on a local worker completes `/skill:` commands with that stage's skills, as an in-process stage chat does, instead of warning "Skill discovery unavailable: this stage host does not expose its session command metadata." While the stage's session is still starting, it says its skills are loading.
40
+ - In the chat of a stage on a local worker, `/compact` compacts that stage's session and a `!` command runs on the worker in the stage's working directory, with its output streaming into the chat and Escape stopping it even while the pool is paused, as in the chat of an in-process stage. After `/compact` the open chat shows the compacted transcript. Before, `/compact` was sent to the model as plain text and a `!` command failed with "no live agent session on this stage". `/tasks` there lists the stage's agents and shells on its worker with their live status, opens their output and transcripts, sends them input and stops them, and the chat's task footer counts them, as in the chat of an in-process stage. An agent task's reply shows while it streams, tasks can be inspected and stopped while the pool is paused, and a stop that cannot reach the worker says so. Before, `/tasks` showed an empty list.
41
+ - A stage on a local worker reads as queued, with what it is doing (`creating session 32s`), on its graph node, in the graph header, the stage list, `/workflow status` and the `workflow` tool's stage list until its session has started, instead of reading as running. Its chat says it is starting on its local worker instead of showing a blank transcript, streams the stage as soon as the session starts, and opens the live view by itself when the worker cannot serve one yet. A message sent meanwhile waits for the session instead of failing with "The connection to stage … was lost" and "1 message not delivered".
42
+ - A root that runs children on local workers (`workflows.localWorkers`) no longer fails all of them with `Stale lease for run <id>` when its own process renews the root's lease late. A busy session, such as one that reloads its workflows and starts several other runs with in-process agent stages, could push the root's renewal past the pool's 90-second window. The pool treated that late renewal as a lost lease: it failed every running child, and every child the root started afterwards failed at once, while the root itself kept running. The pool now waits for the renewal and its workers keep their children; only a lease that was released or taken over by another holder ends them. Each run also renews its lease on its own, so a busy or blocked run no longer delays another run's renewal, and a renewal that fails without losing the lease, such as one that meets a briefly locked database, is retried on the next heartbeat instead of giving up the run. ([#4138](https://github.com/bastani-inc/atomic/pull/4138))
43
+ - With `workflows.localWorkers` configured, a root starts its pool's coordinator process when it runs its first pooled `ctx.workflow` child, or when it resumes a pool that already ran. A root that only runs `ctx.task` stages and `ctx.tool` steps no longer starts one, so launching such runs no longer leaves an idle coordinator process for each of them. ([#4138](https://github.com/bastani-inc/atomic/pull/4138))
44
+ - Stages on local workers (`workflows.localWorkers`) no longer fail with `Worker exited before delivery acknowledgement` while their worker is still running. On a busy machine a stage's startup delivery of queued Intercom messages could make no progress for a minute; the root then ended the worker's delivery request while the delivery kept going, and the next message reached a closed connection, failing the stage at `preparing message delivery` with an unknown, terminal failure. The delivery now keeps its connection to the worker until it finishes, and the stage still starts after a minute without progress, as an in-process stage does. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
45
+ - A worker whose work lease ran out only because the machine was too busy to process its renewal in time keeps its child. Its next journal write, usage report or completion is no longer refused with `Stale work lease`, which ended the child's attempt and could fail the child as `killed`; a session it starts no longer fails with `Stale resource admission attempt`, and waits for the renewal instead. Only an attempt that recovery handed to another worker is refused. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
46
+ - A fault of the pool itself that reaches a running pooled child, such as a journal write the database refused, replays the child from its last checkpoint (up to the usual three attempts) instead of failing its stage. A stage that does fail on a pool fault is classified with that fault as its failure code (`worker_lost`, `stale_work_lease`, …), recoverable and retryable, instead of `unknown`, and resume runs it again. A child whose attempt was fenced is no longer completed with the aborted run's result. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
47
+ - A stage on a local worker that ended keeps the worker lease it ended under: its worker's later lease renewals for the child's other stages no longer show it as renewed after it ended. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
48
+ - `/workflow pause <run>` and the `workflow` tool's `pause` pause a root's stages on local workers even while the root's own `ctx.tool` steps or in-process stages run, instead of reporting `No active stages to pause`. Pause, `pause --all` and resume no longer fail with `Worker control acknowledgement unresolved` when workers confirm slowly on a busy machine: the pause is recorded at once, workers pause their stages as they next check in, and the result says how many stages have not confirmed yet. ([#4145](https://github.com/bastani-inc/atomic/pull/4145))
49
+ - Resuming a root that runs children on local workers runs again the children an earlier process failed on a fault of the pool itself (a stage that lost its worker's delivery or lease on a busy machine, recorded as `Worker exited before delivery acknowledgement`, `Stale work lease` or `Stale resource admission attempt`), instead of replaying their failure. This includes runs paused or failed by 0.9.57. Pausing a root whose children have durable progress now says it can be resumed from a new process once this one exits, instead of `Resume with /workflow resume on this live process … Cross-process resume requires durable checkpoint or pending prompt progress`. ([#4146](https://github.com/bastani-inc/atomic/pull/4146))
50
+
9
51
  ## [0.9.57] - 2026-10-31
10
52
 
11
53
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.57",
3
+ "version": "0.9.58",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [