@bastani/atomic 0.9.15 → 0.9.16-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +10 -0
  3. package/dist/builtin/intercom/README.md +8 -8
  4. package/dist/builtin/intercom/contact-supervisor-tool.ts +68 -32
  5. package/dist/builtin/intercom/index-heavy.ts +1 -0
  6. package/dist/builtin/intercom/index.ts +21 -15
  7. package/dist/builtin/intercom/intercom-tool.ts +66 -10
  8. package/dist/builtin/intercom/package.json +1 -1
  9. package/dist/builtin/intercom/parent-ask-handoff.ts +72 -0
  10. package/dist/builtin/mcp/package.json +1 -1
  11. package/dist/builtin/subagents/CHANGELOG.md +13 -0
  12. package/dist/builtin/subagents/README.md +23 -12
  13. package/dist/builtin/subagents/agents/worker.md +2 -2
  14. package/dist/builtin/subagents/package.json +1 -1
  15. package/dist/builtin/subagents/skills/subagent/SKILL.md +10 -17
  16. package/dist/builtin/subagents/src/extension/index.ts +5 -30
  17. package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
  18. package/dist/builtin/subagents/src/extension/schemas.ts +2 -14
  19. package/dist/builtin/subagents/src/extension/tool-description.ts +1 -2
  20. package/dist/builtin/subagents/src/extension/tool-rendering.ts +53 -0
  21. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +1 -1
  22. package/dist/builtin/subagents/src/intercom/result-intercom.ts +4 -4
  23. package/dist/builtin/subagents/src/runs/foreground/execution-parent-ask-handoff.ts +78 -0
  24. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +7 -5
  25. package/dist/builtin/subagents/src/runs/foreground/notify.ts +5 -6
  26. package/dist/builtin/subagents/src/runs/foreground/parent-ask-output.ts +48 -0
  27. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-burst-display.ts +46 -0
  28. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-burst.ts +403 -0
  29. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +48 -9
  30. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-input.ts +3 -1
  31. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-live-update.ts +11 -0
  32. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +164 -93
  33. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +36 -29
  34. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parent-ask-projection.ts +11 -0
  35. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +42 -17
  36. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +3 -121
  37. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +7 -2
  38. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +30 -220
  39. package/dist/builtin/subagents/src/runs/inprocess/control-status.ts +0 -34
  40. package/dist/builtin/subagents/src/runs/inprocess/index.ts +0 -12
  41. package/dist/builtin/subagents/src/runs/inprocess/prompt-behavior.ts +1 -1
  42. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +2 -141
  43. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +8 -0
  44. package/dist/builtin/subagents/src/shared/status-format.ts +3 -3
  45. package/dist/builtin/subagents/src/shared/types-config.ts +41 -0
  46. package/dist/builtin/subagents/src/shared/types-foreground-state.ts +7 -15
  47. package/dist/builtin/subagents/src/shared/types-results.ts +3 -1
  48. package/dist/builtin/subagents/src/shared/types-runtime.ts +1 -11
  49. package/dist/builtin/subagents/src/tui/render-progress.ts +2 -2
  50. package/dist/builtin/subagents/src/tui/render-result-compact.ts +2 -2
  51. package/dist/builtin/subagents/src/tui/render-result.ts +38 -14
  52. package/dist/builtin/subagents/src/tui/render-status-progress.ts +1 -1
  53. package/dist/builtin/web-access/package.json +1 -1
  54. package/dist/builtin/workflows/CHANGELOG.md +10 -0
  55. package/dist/builtin/workflows/README.md +1 -1
  56. package/dist/builtin/workflows/package.json +1 -1
  57. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +548 -39
  58. package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +529 -65
  59. package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +30 -8
  60. package/dist/builtin/workflows/src/durable/dbos-local-postgres.ts +36 -3
  61. package/dist/builtin/workflows/src/durable/local-command.ts +68 -10
  62. package/dist/builtin/workflows/src/extension/dispatcher.ts +7 -1
  63. package/dist/builtin/workflows/src/extension/extension-factory.ts +2 -2
  64. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1094 -298
  65. package/dist/builtin/workflows/src/extension/public-types.ts +2 -2
  66. package/dist/builtin/workflows/src/extension/render-result.ts +16 -2
  67. package/dist/builtin/workflows/src/extension/runtime.ts +8 -1
  68. package/dist/builtin/workflows/src/extension/workflow-request-abort.ts +15 -0
  69. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +4 -3
  70. package/dist/builtin/workflows/src/extension/workflow-tool-registration.ts +107 -13
  71. package/dist/builtin/workflows/src/extension/workflow-tool.ts +22 -12
  72. package/dist/builtin/workflows/src/tui/widget.ts +25 -6
  73. package/dist/core/event-bus.d.ts.map +1 -1
  74. package/dist/core/event-bus.js +19 -3
  75. package/dist/core/event-bus.js.map +1 -1
  76. package/docs/intercom.md +28 -26
  77. package/docs/subagents.md +15 -11
  78. package/docs/workflows.md +27 -10
  79. package/npm-shrinkwrap.json +32 -32
  80. package/package.json +3 -3
  81. package/dist/builtin/subagents/src/runs/inprocess/attempt-handles.ts +0 -104
  82. /package/dist/builtin/subagents/src/runs/foreground/{subagent-executor-resume.ts → subagent-executor-cwd.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, statSync } from "node:fs";
2
2
  import { basename, dirname, join, relative, resolve } from "node:path";
3
3
  import {
4
4
  type AgentSession,
@@ -45,7 +45,6 @@ import {
45
45
  type MaxOutputConfig,
46
46
  truncateOutput,
47
47
  } from "../../shared/types.js";
48
- import { type InProcessAttemptResumeOutcome, registerInProcessAttempt } from "./attempt-handles.js";
49
48
  import { type ChildModePolicy, resolveChildModePolicy } from "./child-policy.js";
50
49
  import { createInProcessChildPromptBehavior, createInProcessChildSystemPromptTransform } from "./prompt-behavior.js";
51
50
 
@@ -673,9 +672,6 @@ export class SubagentControlRuntime {
673
672
  readonly native: SubagentControl;
674
673
  readonly parent: ParentContext;
675
674
  readonly sessionRoot: string;
676
- private readonly sessions = new Map<string, AgentSession>();
677
- private readonly specs = new Map<string, ChildSpec>();
678
- private readonly sessionFiles = new Map<string, string>();
679
675
  private readonly runningAttempts = new Map<number, RunningAttempt>();
680
676
  private readonly attemptTokens = new Map<string, number>();
681
677
  private readonly attemptTerminators = new Map<string, (cause: TerminationCauseName) => Promise<void>>();
@@ -713,7 +709,6 @@ export class SubagentControlRuntime {
713
709
  ? dirname(spec.sessionFile)
714
710
  : sessionDirectory(this.sessionRoot, identity.path);
715
711
  mkdirSync(sessionDir, { recursive: true });
716
- this.specs.set(identity.path, spec);
717
712
  return {
718
713
  admitted: AdmittedChild.create({
719
714
  identity,
@@ -896,10 +891,9 @@ export class SubagentControlRuntime {
896
891
  })
897
892
  ).session,
898
893
  };
894
+ await created.session.extensionRunner.emit({ type: "session_start", reason: "startup" });
899
895
  }
900
896
  session = created.session;
901
- if (session.sessionFile) this.sessionFiles.set(admitted.identity.path, session.sessionFile);
902
- this.sessions.set(admitted.identity.path, session);
903
897
  const initialModelId = modelIdForSession(session) ?? candidateModelId;
904
898
  effectiveModelId = initialModelId;
905
899
  effectiveThinking =
@@ -1070,7 +1064,6 @@ export class SubagentControlRuntime {
1070
1064
  } catch {
1071
1065
  // Session teardown must not replace the attempt result.
1072
1066
  }
1073
- this.sessions.delete(admitted.identity.path);
1074
1067
  if (this.attemptTokens.get(admitted.identity.path) === token)
1075
1068
  this.attemptTokens.delete(admitted.identity.path);
1076
1069
  if (this.attemptTerminators.get(admitted.identity.path) === terminate)
@@ -1142,56 +1135,6 @@ export class SubagentControlRuntime {
1142
1135
  return running.child.identity.path;
1143
1136
  }
1144
1137
 
1145
- reloadColdChild(pathValue: string, message: string): AdmittedResult {
1146
- let sessionDir: string;
1147
- try {
1148
- sessionDir = sessionDirectory(this.sessionRoot, pathValue);
1149
- } catch (error) {
1150
- return {
1151
- refusal: {
1152
- kind: "invalidCwd",
1153
- reason: error instanceof Error ? error.message : String(error),
1154
- },
1155
- };
1156
- }
1157
- const identity = this.native.listChildren().find((child) => child.path === pathValue);
1158
- if (!identity)
1159
- return {
1160
- refusal: { kind: "unknownAgent", reason: "unknown child identity" },
1161
- };
1162
- const sessionFile = this.sessionFiles.get(pathValue) ?? this.findSessionFile(sessionDir);
1163
- if (!sessionFile)
1164
- return {
1165
- refusal: {
1166
- kind: "invalidCwd",
1167
- reason: "child session file is missing",
1168
- },
1169
- };
1170
- sessionDir = dirname(sessionFile);
1171
- const native = this.native.reloadColdChild(pathValue, message);
1172
- if (!native.child) return refusal(native);
1173
- const previous = this.specs.get(pathValue);
1174
- if (!previous)
1175
- return {
1176
- refusal: {
1177
- kind: "unknownAgent",
1178
- reason: "child policy is not resident",
1179
- },
1180
- };
1181
- const spec: ChildSpec = { ...previous, task: message };
1182
- this.specs.set(pathValue, spec);
1183
- return {
1184
- admitted: AdmittedChild.create({
1185
- identity: native.child,
1186
- spec,
1187
- policy: { ...this.policyFor(spec, native.child.depth) },
1188
- sessionDir,
1189
- sessionFile,
1190
- control: this,
1191
- }),
1192
- };
1193
- }
1194
-
1195
1138
  getChildMetadata(pathValue: string): ChildRuntimeMetadata | undefined {
1196
1139
  const running = [...this.runningAttempts.values()].find(
1197
1140
  (attempt) =>
@@ -1209,34 +1152,6 @@ export class SubagentControlRuntime {
1209
1152
  };
1210
1153
  }
1211
1154
 
1212
- /** Expose a direct child's live attempt so `interrupt`/`resume` can reach it by run id or path. */
1213
- registerAttempt(runId: string, running: RunningAttempt, candidate: ModelCandidate): void {
1214
- registerInProcessAttempt({
1215
- runId,
1216
- path: running.child.identity.path,
1217
- status: () => running.status,
1218
- interrupt: () => this.terminateChildAttempt(running, "interrupt"),
1219
- resume: async (message): Promise<InProcessAttemptResumeOutcome> => {
1220
- const admission = this.reloadColdChild(running.child.identity.path, message);
1221
- if (!admission.admitted) {
1222
- throw new Error(admission.refusal?.reason ?? "child cold reload was refused");
1223
- }
1224
- const neverAbort = new AbortController().signal;
1225
- const resumed = this.startAttempt(admission.admitted, candidate, {
1226
- abort: neverAbort,
1227
- interrupt: neverAbort,
1228
- });
1229
- const outcome = await resumed.promise;
1230
- return {
1231
- status: outcome.status,
1232
- path: outcome.path,
1233
- sessionFile: outcome.sessionFile,
1234
- envelope: outcome.envelope,
1235
- };
1236
- },
1237
- });
1238
- }
1239
-
1240
1155
  async terminateChildAttempt(running: RunningAttempt, cause: TerminationCauseName): Promise<void> {
1241
1156
  if (running.status !== "running" && running.status !== "continued") return;
1242
1157
  await running.terminate?.(cause);
@@ -1285,50 +1200,11 @@ export class SubagentControlRuntime {
1285
1200
  await this.terminateChildAttempt(running, "interrupt");
1286
1201
  return true;
1287
1202
  }
1288
- async resumeChild(
1289
- pathValue: string,
1290
- message: string,
1291
- candidate: ModelCandidate,
1292
- signals: AttemptSignals = {
1293
- abort: new AbortController().signal,
1294
- interrupt: new AbortController().signal,
1295
- },
1296
- ): Promise<AttemptOutcome> {
1297
- const admission = this.reloadColdChild(pathValue, message);
1298
- if (!admission.admitted) {
1299
- const reason = admission.refusal?.reason ?? "cold child reload was refused";
1300
- return {
1301
- status: "error",
1302
- cause: reason,
1303
- stats: { ...EMPTY_STATS, sessionId: pathValue },
1304
- path: pathValue,
1305
- envelope: boundedEnvelope(reason),
1306
- };
1307
- }
1308
- const running = this.startAttempt(admission.admitted, candidate, signals);
1309
- return running.promise;
1310
- }
1311
1203
 
1312
1204
  subscribe(pathValue: string, callback: (status: ChildStatus) => void): void {
1313
1205
  this.native.subscribeChildStatus(pathValue, callback);
1314
1206
  }
1315
1207
 
1316
- private policyFor(spec: ChildSpec, depth: number): ChildPolicy {
1317
- return {
1318
- ...resolveChildModePolicy(spec),
1319
- cwd: spec.cwd,
1320
- tools: spec.tools ?? spec.agent.tools,
1321
- excludedTools: spec.excludedTools,
1322
- mcpDirectTools: spec.mcpDirectTools ?? spec.agent.mcpDirectTools,
1323
- skills: [...(spec.skills ?? spec.agent.skills ?? [])],
1324
- customTools: spec.customTools,
1325
- model: spec.model,
1326
- thinkingLevel: spec.thinkingLevel ?? (spec.agent.thinking as ChildPolicy["thinkingLevel"]),
1327
- intercomGroup: spec.parent?.intercomGroup,
1328
- depth,
1329
- };
1330
- }
1331
-
1332
1208
  private async terminateRunningAttempt(running: RunningAttempt, cause: TerminationCauseName): Promise<void> {
1333
1209
  const terminate = this.attemptTerminators.get(running.child.identity.path);
1334
1210
  if (terminate) {
@@ -1346,17 +1222,6 @@ export class SubagentControlRuntime {
1346
1222
  // The runner's signal path owns the race with terminal completion.
1347
1223
  }
1348
1224
  }
1349
-
1350
- private findSessionFile(sessionDir: string): string | undefined {
1351
- try {
1352
- const entry = readdirSync(sessionDir, { withFileTypes: true }).find(
1353
- (candidate) => candidate.isFile() && candidate.name.endsWith(".jsonl"),
1354
- );
1355
- return entry ? join(sessionDir, entry.name) : undefined;
1356
- } catch {
1357
- return undefined;
1358
- }
1359
- }
1360
1225
  }
1361
1226
 
1362
1227
  export function createSubagentControl(parent: ParentContext, sessionRoot?: string): SubagentControlRuntime {
@@ -1398,10 +1263,6 @@ export function continue_detached(
1398
1263
  return control.continueDetached(running, reason);
1399
1264
  }
1400
1265
 
1401
- export function reload_cold_child(control: SubagentControlRuntime, pathValue: string, message: string): AdmittedResult {
1402
- return control.reloadColdChild(pathValue, message);
1403
- }
1404
-
1405
1266
  export function terminate_child_attempt(
1406
1267
  control: SubagentControlRuntime,
1407
1268
  running: RunningAttempt,
@@ -59,4 +59,12 @@ export function aggregateParallelOutputs(
59
59
  .join("\n\n");
60
60
  }
61
61
 
62
+ export function formatParallelResultContent(
63
+ results: ParallelTaskResult[],
64
+ headerFormat: (index: number, agent: string) => string,
65
+ ): string {
66
+ const ok = results.filter((result) => result.status === "ok").length;
67
+ return `${ok}/${results.length} succeeded\n\n${aggregateParallelOutputs(results, headerFormat)}`;
68
+ }
69
+
62
70
  export const MAX_PARALLEL_CONCURRENCY = 4;
@@ -1,6 +1,6 @@
1
1
  import type { ActivityState } from "./types.js";
2
2
 
3
- type StepStatus = "pending" | "running" | "complete" | "completed" | "failed" | "paused";
3
+ type StepStatus = "pending" | "running" | "complete" | "completed" | "failed" | "interrupted";
4
4
  type StepStatusLike = { status: StepStatus };
5
5
 
6
6
  function formatActivityAge(ms: number): string {
@@ -41,10 +41,10 @@ export function formatParallelOutcome(
41
41
  const running = steps.filter((step) => step.status === "running").length;
42
42
  const done = steps.filter((step) => isCompletedStepStatus(step.status)).length;
43
43
  const failed = steps.filter((step) => step.status === "failed").length;
44
- const paused = steps.filter((step) => step.status === "paused").length;
44
+ const interrupted = steps.filter((step) => step.status === "interrupted").length;
45
45
  const parts = [`${done}/${total} done`];
46
46
  if (options.showRunning !== false && running > 0) parts.unshift(formatAgentRunningLabel(running));
47
47
  if (failed > 0) parts.push(`${failed} failed`);
48
- if (paused > 0) parts.push(`${paused} paused`);
48
+ if (interrupted > 0) parts.push(`${interrupted} interrupted`);
49
49
  return parts.join(" · ");
50
50
  }
@@ -41,6 +41,7 @@ export interface IntercomEventBus {
41
41
  }
42
42
 
43
43
  export const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
44
+ export const PARENT_ASK_HANDOFF_REQUEST_EVENT = "subagent:parent-ask-handoff-request";
44
45
  export const SUBAGENT_COMPLETE_EVENT = "subagent:complete";
45
46
  export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
46
47
  export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
@@ -49,6 +50,44 @@ export const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
49
50
  export const SUBAGENT_TERMINAL_ORDERING_BARRIER_EVENT = "subagent:terminal-ordering-barrier";
50
51
  export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
51
52
 
53
+ export type ParentAskKind = "decision" | "interview" | "intercom";
54
+
55
+ export interface ParentAskInterviewQuestion extends Record<string, unknown> {
56
+ id: string;
57
+ type: "single" | "multi" | "text" | "image" | "info";
58
+ question: string;
59
+ options?: unknown[];
60
+ }
61
+
62
+ export interface ParentAskInterviewRequest extends Record<string, unknown> {
63
+ title?: string;
64
+ description?: string;
65
+ questions: ParentAskInterviewQuestion[];
66
+ }
67
+
68
+ export interface ParentAskAttachment {
69
+ type: "file" | "snippet" | "context";
70
+ name: string;
71
+ content: string;
72
+ language?: string;
73
+ }
74
+
75
+ export interface ParentAskHandoffRequest {
76
+ runId: string;
77
+ index: number;
78
+ agent: string;
79
+ childIntercomTarget: string;
80
+ orchestratorTarget: string;
81
+ kind: ParentAskKind;
82
+ question: string;
83
+ attachments?: ParentAskAttachment[];
84
+ interview?: ParentAskInterviewRequest;
85
+ resolvedTargetId?: string;
86
+ /** Original delegated task supplied by the parent model for a fresh-child handoff. */
87
+ taskContext?: string;
88
+ claimed: boolean;
89
+ }
90
+
52
91
  // ============================================================================
53
92
  // Execution Options
54
93
  // ============================================================================
@@ -64,6 +103,8 @@ export interface RunSyncOptions {
64
103
  intercomDetachSignal?: AbortSignal;
65
104
  /** Releases every active foreground sibling only after this exact child accepts a detach commit. */
66
105
  onIntercomDetachCommit?: () => void;
106
+ /** Claims a blocking ask from this exact child and ends it with a fresh-child handoff. */
107
+ onParentAskHandoff?: (request: ParentAskHandoffRequest) => void;
67
108
  onUpdate?: (r: AgentToolResult<Details>) => void;
68
109
  onControlEvent?: (event: ControlEvent) => void;
69
110
  controlConfig?: ResolvedControlConfig;
@@ -7,29 +7,21 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionContext } from "@bastani/atomic";
10
- import type { ActivityState, SingleResult, SubagentResultStatus, SubagentRunMode } from "./types-results.js";
10
+ import type { ParentAskHandoffRequest } from "./types-config.js";
11
11
 
12
- export interface ForegroundResumeChild {
13
- agent: string;
14
- index: number;
15
- sessionFile?: string;
16
- status: SubagentResultStatus;
17
- result?: SingleResult;
12
+ export interface ForegroundParentAskHandoff {
13
+ askingChildIndex: number;
14
+ releasedChildIndices: number[];
15
+ unlaunchedChildIndices: number[];
16
+ request: ParentAskHandoffRequest;
18
17
  }
19
18
 
20
- export interface ForegroundResumeRun {
21
- runId: string;
22
- mode: SubagentRunMode;
23
- cwd: string;
24
- updatedAt: number;
25
- children: ForegroundResumeChild[];
26
- }
19
+ import type { ActivityState, SubagentRunMode } from "./types-results.js";
27
20
 
28
21
  export interface SubagentState {
29
22
  baseCwd: string;
30
23
  currentSessionId: string | null;
31
24
  subagentInProgress?: boolean;
32
- foregroundRuns?: Map<string, ForegroundResumeRun>;
33
25
  foregroundControls: Map<
34
26
  string,
35
27
  {
@@ -91,7 +91,7 @@ export interface ControlEvent {
91
91
  recentFailureSummary?: string;
92
92
  }
93
93
 
94
- export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
94
+ export type SubagentResultStatus = "completed" | "failed" | "interrupted" | "detached";
95
95
  export type SubagentRunMode = "single" | "parallel";
96
96
 
97
97
  export interface SubagentResultIntercomChild {
@@ -220,6 +220,8 @@ export interface Details {
220
220
  controlEvents?: ControlEvent[];
221
221
  progress?: AgentProgress[];
222
222
  totalSteps?: number;
223
+ /** Parent ask ended the current child and returned a fresh-subagent handoff. */
224
+ parentAskYielded?: boolean;
223
225
  progressSummary?: ProgressSummary;
224
226
  artifacts?: {
225
227
  dir: string;
@@ -81,17 +81,7 @@ export const SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
81
81
  export const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
82
82
  export const SLASH_SUBAGENT_UPDATE_EVENT = "subagent:slash:update";
83
83
  export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
84
- export const SUBAGENT_ACTIONS = [
85
- "list",
86
- "get",
87
- "create",
88
- "update",
89
- "delete",
90
- "status",
91
- "interrupt",
92
- "resume",
93
- "doctor",
94
- ] as const;
84
+ export const SUBAGENT_ACTIONS = ["list", "get", "create", "update", "delete", "status", "interrupt", "doctor"] as const;
95
85
 
96
86
  export const DEFAULT_FORK_PREAMBLE =
97
87
  "You are a delegated subagent running from a fork of the parent session. " +
@@ -27,7 +27,7 @@ export function buildMultiProgressLabel(
27
27
 
28
28
  if (details.mode === "parallel") {
29
29
  const statuses = new Array(totalCount).fill("pending") as Array<
30
- "pending" | "running" | "completed" | "failed" | "paused" | "detached"
30
+ "pending" | "running" | "completed" | "failed" | "interrupted" | "detached"
31
31
  >;
32
32
  for (const progress of details.progress ?? []) {
33
33
  if (progress.index >= 0 && progress.index < totalCount) statuses[progress.index] = progress.status;
@@ -42,7 +42,7 @@ export function buildMultiProgressLabel(
42
42
  const status =
43
43
  result.progress?.status ??
44
44
  (result.interrupted || result.status === "interrupted"
45
- ? "paused"
45
+ ? "interrupted"
46
46
  : result.detached || result.status === "continued"
47
47
  ? "detached"
48
48
  : result.status === "ok"
@@ -87,7 +87,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, pulse
87
87
  const hasRunning =
88
88
  d.progress?.some((p) => p.status === "running") || d.results.some((r) => r.progress?.status === "running");
89
89
  const failed = d.results.some((r) => r.status === "error" && r.progress?.status !== "running");
90
- const paused = d.results.some(
90
+ const interruptedOrDetached = d.results.some(
91
91
  (r) =>
92
92
  (r.interrupted ||
93
93
  r.detached ||
@@ -117,7 +117,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, pulse
117
117
  ? theme.fg("accent", pulseGlyph(pulseFrame))
118
118
  : failed
119
119
  ? theme.fg("error", "✗")
120
- : paused
120
+ : interruptedOrDetached
121
121
  ? theme.fg("warning", "■")
122
122
  : theme.fg("success", "✓");
123
123
  const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
@@ -24,6 +24,19 @@ import {
24
24
  snapshotNowForProgress,
25
25
  } from "./render-status-progress.js";
26
26
 
27
+ function parentAskOutput(result: AgentToolResult<Details>): string | undefined {
28
+ if (!result.details?.parentAskYielded) return undefined;
29
+ return result.content.find((part) => part.type === "text")?.text;
30
+ }
31
+
32
+ function appendParentAskOutput(component: Component, output: string | undefined, theme: Theme): Component {
33
+ if (!output) return component;
34
+ const container = new Container();
35
+ container.addChild(component);
36
+ container.addChild(new Text(theme.fg("accent", output), 0, 0));
37
+ return container;
38
+ }
39
+
27
40
  export function renderLiveSubagentResult(
28
41
  result: AgentToolResult<Details>,
29
42
  options: { expanded: boolean; isPartial: boolean },
@@ -79,18 +92,26 @@ export function renderSubagentResult(
79
92
 
80
93
  const expanded = options.expanded;
81
94
  const mdTheme = getMarkdownTheme();
95
+ const handoffOutput = parentAskOutput(result);
82
96
 
83
97
  if (d.mode === "single" && d.results.length === 1) {
84
98
  const r = d.results[0];
85
- if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.pulseFrame);
99
+ if (!expanded)
100
+ return appendParentAskOutput(
101
+ renderSingleCompact(d, r, theme, options.now, options.pulseFrame),
102
+ handoffOutput,
103
+ theme,
104
+ );
86
105
  const isRunning = r.progress?.status === "running";
87
106
  const icon = isRunning
88
107
  ? theme.fg("warning", "running")
89
- : r.detached || r.status === "continued"
90
- ? theme.fg("warning", "detached")
91
- : r.status === "ok"
92
- ? theme.fg("success", "ok")
93
- : theme.fg("error", "failed");
108
+ : d.parentAskYielded
109
+ ? theme.fg("warning", "yielded")
110
+ : r.detached || r.status === "continued"
111
+ ? theme.fg("warning", "detached")
112
+ : r.status === "ok"
113
+ ? theme.fg("success", "ok")
114
+ : theme.fg("error", "failed");
94
115
  const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
95
116
  const output = r.truncation?.text || getSingleResultOutput(r);
96
117
 
@@ -183,10 +204,11 @@ export function renderSubagentResult(
183
204
  c.addChild(new Spacer(1));
184
205
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
185
206
  }
186
- return c;
207
+ return appendParentAskOutput(c, handoffOutput, theme);
187
208
  }
188
209
 
189
- if (!expanded) return renderMultiCompact(d, theme, options.now, options.pulseFrame);
210
+ if (!expanded)
211
+ return appendParentAskOutput(renderMultiCompact(d, theme, options.now, options.pulseFrame), handoffOutput, theme);
190
212
 
191
213
  const hasRunning =
192
214
  d.progress?.some((p) => p.status === "running") || d.results.some((r) => r.progress?.status === "running");
@@ -201,11 +223,13 @@ export function renderSubagentResult(
201
223
  );
202
224
  const icon = hasRunning
203
225
  ? theme.fg("warning", "running")
204
- : hasEmptyWithoutTarget
205
- ? theme.fg("warning", "warning")
206
- : ok === d.results.length
207
- ? theme.fg("success", "ok")
208
- : theme.fg("error", "failed");
226
+ : d.parentAskYielded
227
+ ? theme.fg("warning", "yielded")
228
+ : hasEmptyWithoutTarget
229
+ ? theme.fg("warning", "warning")
230
+ : ok === d.results.length
231
+ ? theme.fg("success", "ok")
232
+ : theme.fg("error", "failed");
209
233
 
210
234
  const totalSummary =
211
235
  d.progressSummary ||
@@ -410,5 +434,5 @@ export function renderSubagentResult(
410
434
  c.addChild(new Spacer(1));
411
435
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`)), 0, 0));
412
436
  }
413
- return c;
437
+ return appendParentAskOutput(c, handoffOutput, theme);
414
438
  }
@@ -135,7 +135,7 @@ export function firstOutputLine(text: string): string {
135
135
  export function resultStatusLine(result: Details["results"][number], output: string): string {
136
136
  if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
137
137
  if (result.status === "continued") return "Continued";
138
- if (result.interrupted || result.status === "interrupted") return "Paused";
138
+ if (result.interrupted || result.status === "interrupted") return "Interrupted";
139
139
  if (result.status === "skipped") return result.error ? `Skipped: ${result.error}` : "Skipped";
140
140
  if (result.status === "error") return `Error: ${result.error ?? (firstOutputLine(output) || "failed")}`;
141
141
  if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.15",
3
+ "version": "0.9.16-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,16 @@ 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.16-alpha.1] - 2026-08-23
10
+
11
+ ### Fixed
12
+
13
+ - Fixed Windows embedded Postgres startup waiting forever when the daemon inherited `pg_ctl` pipes by directly spawning Postgres behind an opaque native process lease. Orderly shutdown now sends fast shutdown and bounded-waits on that exact retained child/HANDLE, preventing mutable-pidfile and PID-reuse races while leaving attached clusters untouched; failed or timed-out shutdown retains the lease for retry. Linux root runtime copies are now exact, root-owned, read/execute-only generations for the dropped Postgres account, with source and post-rename validation, atomically replaced monotonic setup heartbeats, final publication-lease checks, and bounded fail-closed corruption handling ([#2547](https://github.com/bastani-inc/atomic/issues/2547), [#2544](https://github.com/bastani-inc/atomic/pull/2544) by [@darionco](https://github.com/darionco)).
14
+
15
+ - Fixed active zero-stage workflows whose only work is durable `ctx.tool` calls being under-represented in the below-editor `BACKGROUND` panel. Normal-width cards now show a single pending or running tool node by name and status; when multiple nodes are live, their total appears before the width-clamped node details. The narrow collapsed form shows only the live-tool count, and no synthetic stage is created. The panel mounts for tool-only runs loaded after installation, keeps updating through workflow-resource reload, and remounts from the adopted run store after full `/reload`. Installed builds can evaluate the extension and host SDK as separate jiti module copies; their canonical session-bus map is now process-shared, preventing a replacement generation from adopting an empty store and dropping the live job, cancellation, and tool-control owners. Same-process extension reload preserves the live durable tool callback so its active node settles normally alongside cached completed siblings; a real process exit continues to require explicit durable resume for unfinished callbacks.
16
+
17
+ - Fixed a never-settling public workflow tool request blocking the interactive agent indefinitely. All 15 workflow actions now share one hard 30-second request deadline that aborts cancellable work, emits one structured timeout error, discards late settlement, and never retries. Timed-out mutations report an unknown outcome and direct callers to inspect workflow status before retrying; a timed-out run that already allocated detached execution returns its exact run id for that inspection, while background run/resume execution remains independent after its acknowledgement ([#2605](https://github.com/bastani-inc/atomic/issues/2605)).
18
+
9
19
  ## [0.9.15] - 2026-08-21
10
20
 
11
21
  Cumulative release of the `0.9.15-alpha.1` prerelease. The summary below covers the user-visible outcome of that work; the per-change detail remains in the prerelease section below.
@@ -662,7 +662,7 @@ Named launches return only after startup admission, while the admitted workflow
662
662
 
663
663
  Graceful quit is idempotent for already-paused runs and preserves unresolved `ctx.ui` prompts in DBOS. Pausing or interrupting a stage also holds every queued steering and follow-up item in place: no queued turn, late context-bearing delivery, or workflow continuation starts while the stage is paused, and the existing `resume` action releases the items once in their existing per-queue order without queue release itself starting a provider turn. Stable author-callsite-and-composed-nested-scope reservations are created before prompting and released by exact current-format token generation after answer checkpoint, rejection, or abort. Answering while quit/paused cannot advance workflow code until explicit resume.
664
664
 
665
- Workflow durability requires DBOS/Postgres. Atomic configures and launches DBOS lazily on the first workflow action, reuses that process-wide instance, and awaits readiness before durable execution or control. Initialization or persistence failures fail the workflow action; no alternate backend is selected. `DBOS_SYSTEM_DATABASE_URL` selects an existing database when supplied; otherwise Atomic runs DBOS against its own embedded Postgres (npm-distributed binaries, detached `pg_ctl` daemon under `~/.atomic/postgres` on port 5439, shared across sessions and never stopped by Atomic), with DBOS's `dbos-db` Docker container only as a platform fallback. Concurrent Atomic sessions safely share one database: unique per-process executor ids, owner/heartbeat metadata on running workflows, and first-writer-wins claims on contended status transitions prevent double dispatch. Running workflows never appear as resume targets in any session; stale-heartbeat (crashed) ones surface as red `crashed` rows, paused rows render yellow, failed/blocked red, completed green, and the open picker live-updates on local changes plus a bounded cross-session poll.
665
+ Workflow durability uses DBOS/Postgres as its only persistent backend. Atomic initializes it lazily on the first workflow action, reuses that process-wide instance, and awaits readiness before durable execution or control. `DBOS_SYSTEM_DATABASE_URL` selects an existing database when supplied. Otherwise Atomic first provisions its embedded Postgres (npm-distributed binaries, directly spawned under `~/.atomic/postgres` on port 5439, shared across sessions) and tries DBOS's reusable `dbos-db` Docker container if embedded provisioning fails without leaving retained-process cleanup pending. Linux root execution clears inherited supplementary groups before dropping Postgres to an unprivileged primary GID/UID; when that account cannot traverse the installed binaries, Atomic publishes one exact package-content runtime generation under a root-owned cache, where it stays readable/executable but not writable by Postgres. Reuse and publication re-snapshot the current source, validate the deterministic path after rename, and fail closed on source mutation, corruption, or setup-lease displacement without accumulating unbounded repair copies. If DBOS still cannot become ready, Atomic logs a loud warning and runs against one process-local in-memory backend: workflow execution continues, but its state does not survive the process and `/workflow resume` after exit has nothing to restore. Once DBOS is ready, query or write failures fail the workflow action and do not switch backends. Abrupt Atomic exit releases the retained native process lease without killing Postgres. Orderly shutdown sends fast shutdown and waits on that exact retained process; sessions attached to an already-running cluster never own or stop it. Concurrent Atomic sessions safely share one database: unique per-process executor ids, owner/heartbeat metadata on running workflows, and first-writer-wins claims on contended status transitions prevent double dispatch. Running workflows never appear as resume targets in any session; stale-heartbeat (crashed) ones surface as red `crashed` rows, paused rows render yellow, failed/blocked red, completed green, and the open picker live-updates on local changes plus a bounded cross-session poll.
666
666
 
667
667
  DBOS is the only durable catalog for resume, completed inspection, deletion, and targeted lookup. Session JSONL files remain chat transcripts only. Atomic reads one current durable format and does not convert prior local state or pre-current DBOS records. A completed current-format child checkpoint created before boundary-start or invocation-fingerprint identity existed is shown only when its child checkpoints reciprocally prove the same root, parent run, boundary, child owner, and scope. Active checkpoints without a provable invocation fingerprint, and malformed, duplicate, stale, nonreciprocal, mixed, aliased, cyclic, orphaned, or unsupported topology, fail closed before cache exposure or child/control dispatch: Atomic does not invent a child link or execute child code to repair it.
668
668
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.15",
3
+ "version": "0.9.16-alpha.1",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [