@bastani/atomic 0.9.13 → 0.9.14-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 (96) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/i-have-adhd/package.json +1 -1
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/web-access/package.json +1 -1
  7. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  8. package/dist/builtin/workflows/README.md +9 -0
  9. package/dist/builtin/workflows/builtin/adversarial-verification.ts +4 -0
  10. package/dist/builtin/workflows/builtin/classify-and-act.ts +4 -0
  11. package/dist/builtin/workflows/builtin/fan-out-and-synthesize.ts +4 -0
  12. package/dist/builtin/workflows/builtin/generate-and-filter.ts +4 -0
  13. package/dist/builtin/workflows/builtin/goal.ts +4 -0
  14. package/dist/builtin/workflows/builtin/loop-until-done.ts +4 -0
  15. package/dist/builtin/workflows/builtin/open-claude-design.ts +4 -0
  16. package/dist/builtin/workflows/builtin/ralph.ts +4 -0
  17. package/dist/builtin/workflows/builtin/tournament.ts +4 -0
  18. package/dist/builtin/workflows/package.json +1 -1
  19. package/dist/builtin/workflows/src/authoring/workflow.ts +9 -0
  20. package/dist/builtin/workflows/src/authoring.d.ts +3 -0
  21. package/dist/builtin/workflows/src/durable/completed-catalog.ts +13 -2
  22. package/dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts +153 -0
  23. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +28 -0
  24. package/dist/builtin/workflows/src/engine/run.ts +1 -0
  25. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +138 -0
  26. package/dist/builtin/workflows/src/extension/index.bundle.mjs +766 -4
  27. package/dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts +197 -0
  28. package/dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts +115 -0
  29. package/dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts +962 -0
  30. package/dist/builtin/workflows/src/sdk-surface.ts +9 -0
  31. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +1 -0
  32. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +1 -0
  33. package/dist/builtin/workflows/src/shared/persistence-restore.ts +12 -0
  34. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +1 -0
  35. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +1 -0
  36. package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.d.ts +21 -0
  37. package/dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts +25 -0
  38. package/dist/core/extensions/index.d.ts +1 -0
  39. package/dist/core/extensions/index.d.ts.map +1 -1
  40. package/dist/core/extensions/index.js +1 -0
  41. package/dist/core/extensions/index.js.map +1 -1
  42. package/dist/core/extensions/ui-types.d.ts +26 -0
  43. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  44. package/dist/core/extensions/ui-types.js +17 -1
  45. package/dist/core/extensions/ui-types.js.map +1 -1
  46. package/dist/core/tools/ask-user-question/ask-user-question.d.ts +21 -0
  47. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  48. package/dist/core/tools/ask-user-question/ask-user-question.js +34 -9
  49. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  50. package/dist/core/tools/ask-user-question/view/components/multi-select-view.d.ts.map +1 -1
  51. package/dist/core/tools/ask-user-question/view/components/multi-select-view.js +5 -2
  52. package/dist/core/tools/ask-user-question/view/components/multi-select-view.js.map +1 -1
  53. package/dist/core/tools/ask-user-question/view/components/submit-picker.d.ts.map +1 -1
  54. package/dist/core/tools/ask-user-question/view/components/submit-picker.js +3 -1
  55. package/dist/core/tools/ask-user-question/view/components/submit-picker.js.map +1 -1
  56. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  57. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +8 -1
  58. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  59. package/dist/index-extensions.d.ts +1 -1
  60. package/dist/index-extensions.d.ts.map +1 -1
  61. package/dist/index-extensions.js +1 -1
  62. package/dist/index-extensions.js.map +1 -1
  63. package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts +95 -0
  64. package/dist/modes/interactive/components/reserved-bottom-overlay.d.ts.map +1 -0
  65. package/dist/modes/interactive/components/reserved-bottom-overlay.js +234 -0
  66. package/dist/modes/interactive/components/reserved-bottom-overlay.js.map +1 -0
  67. package/dist/modes/interactive/interactive-extension-custom-ui.js +94 -6
  68. package/dist/modes/interactive/interactive-extension-custom-ui.js.map +1 -1
  69. package/dist/modes/interactive/interactive-mode-base.d.ts +3 -0
  70. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  71. package/dist/modes/interactive/interactive-mode-base.js +5 -1
  72. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  73. package/dist/modes/interactive/interactive-mode-surface.d.ts +2 -0
  74. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  76. package/dist/modes/interactive/interactive-tui.d.ts +3 -0
  77. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  78. package/dist/modes/interactive/interactive-tui.js +14 -0
  79. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  80. package/dist/modes/interactive-engine/engine-custom-ui.d.ts +2 -0
  81. package/dist/modes/interactive-engine/engine-custom-ui.d.ts.map +1 -1
  82. package/dist/modes/interactive-engine/engine-custom-ui.js +1 -0
  83. package/dist/modes/interactive-engine/engine-custom-ui.js.map +1 -1
  84. package/dist/modes/interactive-engine/protocol.d.ts +1 -0
  85. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  86. package/dist/modes/interactive-engine/protocol.js +1 -0
  87. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  88. package/dist/modes/interactive-engine/remote-component.d.ts +3 -1
  89. package/dist/modes/interactive-engine/remote-component.d.ts.map +1 -1
  90. package/dist/modes/interactive-engine/remote-component.js +3 -2
  91. package/dist/modes/interactive-engine/remote-component.js.map +1 -1
  92. package/docs/extensions.md +27 -0
  93. package/docs/keybindings.md +1 -0
  94. package/docs/workflows.md +34 -0
  95. package/npm-shrinkwrap.json +29 -29
  96. package/package.json +2 -2
@@ -1,3 +1,5 @@
1
+ import { getDurableBackend } from "../durable/factory.js";
2
+ import { readWorkflowHeartbeatAnchor, recordWorkflowHeartbeatAnchor } from "../durable/workflow-heartbeat-anchor.js";
1
3
  import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
2
4
  import type { StageAdapters } from "../runs/foreground/stage-runner.js";
3
5
  import type { SessionManager } from "../shared/persistence-restore.js";
@@ -11,6 +13,7 @@ import type {
11
13
  WorkflowPersistencePort,
12
14
  WorkflowRuntimeConfig,
13
15
  } from "../shared/types.js";
16
+ import type { WorkflowHeartbeatIdentity } from "../shared/workflow-heartbeat-contract.js";
14
17
  import {
15
18
  type ConfigLoadResult,
16
19
  loadWorkflowConfig,
@@ -35,11 +38,57 @@ import {
35
38
  import type { ExtensionAPI, PiModelContext } from "./public-types.js";
36
39
  import { createExtensionRuntime, type ExtensionRuntime } from "./runtime.js";
37
40
  import { createStatusWriter, type StatusWriter } from "./status-writer.js";
41
+ import { registerWorkflowHeartbeatRenderer } from "./workflow-heartbeat-notice.js";
42
+ import {
43
+ createWorkflowHeartbeatSchedulerState,
44
+ installWorkflowHeartbeatScheduler,
45
+ isWorkflowHeartbeatTerminalRun,
46
+ resetWorkflowHeartbeatSchedulerState,
47
+ type WorkflowHeartbeatAnchorStore,
48
+ type WorkflowHeartbeatScheduler,
49
+ workflowHeartbeatConsumedIdentity,
50
+ workflowHeartbeatContextInvalidation,
51
+ } from "./workflow-heartbeat-scheduler.js";
38
52
  import { workflowModelCatalogFromContext } from "./workflow-model-catalog.js";
39
53
  import { makeMcpPort, makePersistencePort } from "./workflow-ports.js";
40
54
  import { createWorkflowReloadCoordinator } from "./workflow-reload-coordinator.js";
41
55
  import { type WorkflowReloadReport, workflowReloadDiagnostics } from "./workflow-reload-report.js";
42
56
 
57
+ /**
58
+ * Best-effort durable cadence-anchor store for workflow heartbeats.
59
+ *
60
+ * `getDurableBackend()` throws `DbosNotReadyError` until a backend exists, and
61
+ * the scheduler installs at `session_start`, before any workflow has run — so
62
+ * both sides swallow failure and fall back to the run's own `startedAt`.
63
+ */
64
+ function durableWorkflowHeartbeatAnchorStore(): WorkflowHeartbeatAnchorStore {
65
+ return {
66
+ readAnchorAt(runId) {
67
+ try {
68
+ return readWorkflowHeartbeatAnchor(getDurableBackend(), runId);
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ },
73
+ async recordAnchorAt(runId, record) {
74
+ try {
75
+ return await recordWorkflowHeartbeatAnchor(getDurableBackend(), {
76
+ runId,
77
+ anchorAt: record.anchorAt,
78
+ // The scheduler only records a positive cadence; a run launched
79
+ // disabled never reaches this seam.
80
+ intervalMinutes: record.intervalMinutes ?? 0,
81
+ now: Date.now(),
82
+ });
83
+ } catch {
84
+ // A backend that is not ready must not break a background pass; the
85
+ // caller retries on the next schedule pass.
86
+ return false;
87
+ }
88
+ },
89
+ };
90
+ }
91
+
43
92
  export interface WorkflowExtensionRuntimeState {
44
93
  persistenceRef: { current: WorkflowPersistencePort | undefined };
45
94
  mcpPort: WorkflowMcpPort | undefined;
@@ -48,6 +97,7 @@ export interface WorkflowExtensionRuntimeState {
48
97
  discoveryRef: { current: DiscoveryResult | null };
49
98
  lifecycleNotificationState: ReturnType<typeof createWorkflowLifecycleNotificationState>;
50
99
  hilAnswerNotificationState: ReturnType<typeof createWorkflowHilAnswerNotificationState>;
100
+ workflowHeartbeatSchedulerState: ReturnType<typeof createWorkflowHeartbeatSchedulerState>;
51
101
  /** Seed lifecycle notification state before completed historical snapshots are inserted. */
52
102
  beforeRestoreCompleted(snapshots: readonly RunSnapshot[]): void;
53
103
  runtimeForContext(ctx?: PiModelContext): ExtensionRuntime;
@@ -83,10 +133,12 @@ export function createWorkflowExtensionRuntimeState(
83
133
  let statusWriterRef: StatusWriter = createStatusWriter(store, runtimeConfigRef.current);
84
134
  let lifecycleNotificationsUnsubscribe: (() => void) | null = null;
85
135
  let hilAnswerNotificationsUnsubscribe: (() => void) | null = null;
136
+ let workflowHeartbeatScheduler: WorkflowHeartbeatScheduler | null = null;
86
137
  let notificationsActive = false;
87
138
  let notificationGeneration = 0;
88
139
  const lifecycleNotificationState = createWorkflowLifecycleNotificationState();
89
140
  const hilAnswerNotificationState = createWorkflowHilAnswerNotificationState();
141
+ const workflowHeartbeatSchedulerState = createWorkflowHeartbeatSchedulerState();
90
142
  const beforeRestoreCompleted = (snapshots: readonly RunSnapshot[]): void => {
91
143
  seedWorkflowLifecycleNotificationState(lifecycleNotificationState, {
92
144
  ...readGraphStoreSnapshot(store),
@@ -102,6 +154,7 @@ export function createWorkflowExtensionRuntimeState(
102
154
  : undefined;
103
155
  registerLifecycleNoticeRenderer({ rendererHost: pi, registerMessageRenderer });
104
156
  registerHilAnswerNoticeRenderer({ rendererHost: pi, registerMessageRenderer });
157
+ registerWorkflowHeartbeatRenderer({ rendererHost: pi, registerMessageRenderer });
105
158
  const sendWorkflowNotificationMessage: ExtensionAPI["sendMessage"] | undefined =
106
159
  typeof pi.sendMessage === "function" ? (message, options) => pi.sendMessage!(message, options) : undefined;
107
160
  const reinstallLifecycleNotifications = (): void => {
@@ -127,6 +180,86 @@ export function createWorkflowExtensionRuntimeState(
127
180
  sendMessage: sendWorkflowNotificationMessage,
128
181
  });
129
182
  };
183
+ // `message_end` is the host's injection signal: agent-core emits it at the
184
+ // moment a message enters the conversation. That is what releases a held
185
+ // heartbeat slot.
186
+ //
187
+ // The turn-settled event is deliberately not used. The host emits it from the
188
+ // prompt cycle's `finally` whether or not the queued messages were drained, so
189
+ // pausing the queue mid-turn would release a slot whose card is still parked
190
+ // and let the next boundary stack a second card behind it. `message_end` never
191
+ // fires for a parked card. The visible display card is published on the
192
+ // session-listener channel only, so an extension sees the hidden
193
+ // reconciliation at consumption and never the card at admission — there is no
194
+ // ambiguity to disambiguate. Proven end to end against a real `AgentSession`
195
+ // in test/unit/workflow-heartbeat-parent-pickup.test.ts.
196
+ //
197
+ // Registered exactly once, at construction: `pi.on` has no unsubscribe, so a
198
+ // per-install registration would accumulate a handler on every notification
199
+ // cycle. The mutable scheduler reference is what routes the signal to the
200
+ // current installation, and a `null` reference makes it a no-op.
201
+ //
202
+ // The same condition decides whether the scheduler holds a slot at all, so the
203
+ // registration and the option can never disagree: a host that reports no
204
+ // consumption would leave a held slot with nothing to release it.
205
+ const parentAvailabilityReported = typeof pi.on === "function";
206
+ if (parentAvailabilityReported) {
207
+ // This handler observes, and for one narrow case replaces.
208
+ //
209
+ // The last guard on the heartbeat path (issue #1975): the three before it
210
+ // all sit before `sendMessage`, so a heartbeat the host has already
211
+ // admitted is beyond them. Consumption is the final moment before its
212
+ // steer joins the model's context, and `message_end` may return a
213
+ // replacement of the same role, so a heartbeat whose run has finished,
214
+ // vanished, or no longer owns that exact scheduled identity is excluded
215
+ // from context rather than steering the parent about stale work.
216
+ //
217
+ // `absent` counts as much as `terminal`. A card recovered at the restart
218
+ // door is consumed just after `session_start` cleared the store, so its run
219
+ // is normally absent. A durable resume may instead reuse the run id, but its
220
+ // future pending boundary has a different `scheduledAt`. Both cases fail
221
+ // exact pending ownership and preserve the no-backfill rule.
222
+ pi.on?.("message_end", (event, ctx) => {
223
+ // The host hands the session manager to the handler on its context; the
224
+ // top-level `pi.sessionManager` is not populated in every host, so the
225
+ // per-call one is preferred and the other is the fallback. Verified
226
+ // against a real `AgentSession` in
227
+ // test/unit/workflow-heartbeat-parent-pickup.test.ts.
228
+ const entries = (ctx?.sessionManager ?? pi.sessionManager)?.getEntries?.();
229
+ const invalidation = workflowHeartbeatContextInvalidation(event, entries, isWorkflowHeartbeatIdentityOwned);
230
+ if (invalidation !== undefined) return invalidation;
231
+ const identity = workflowHeartbeatConsumedIdentity(event, entries);
232
+ if (identity !== undefined) workflowHeartbeatScheduler?.notifyHeartbeatConsumed(identity);
233
+ return undefined;
234
+ });
235
+ }
236
+
237
+ /** Whether this exact identity is still pending for a current nonterminal run. */
238
+ function isWorkflowHeartbeatIdentityOwned(identity: WorkflowHeartbeatIdentity): boolean {
239
+ const pending = workflowHeartbeatSchedulerState.pending.get(identity.runId);
240
+ const run = store.runs().find((candidate) => candidate.id === identity.runId);
241
+ return pending?.scheduledAt === identity.scheduledAt && run !== undefined && !isWorkflowHeartbeatTerminalRun(run);
242
+ }
243
+
244
+ /**
245
+ * Workflow heartbeats share the lifecycle-notice lifetime: they are armed
246
+ * while notifications are active and disposed with them. The cadence itself
247
+ * comes from the live registry, so a workflow reload is picked up without a
248
+ * reinstall.
249
+ */
250
+ const reinstallWorkflowHeartbeatScheduler = (): void => {
251
+ workflowHeartbeatScheduler?.dispose();
252
+ workflowHeartbeatScheduler = null;
253
+ if (!notificationsActive) return;
254
+ workflowHeartbeatScheduler = installWorkflowHeartbeatScheduler({
255
+ store,
256
+ state: workflowHeartbeatSchedulerState,
257
+ sendMessage: sendWorkflowNotificationMessage,
258
+ resolveIntervalMinutes: (workflowName) => runtimeProxy.registry.get(workflowName)?.heartbeatIntervalMinutes,
259
+ parentAvailabilityReported,
260
+ anchorStore: durableWorkflowHeartbeatAnchorStore(),
261
+ });
262
+ };
130
263
 
131
264
  const hostStageSessionDir: { current: string | undefined } = { current: undefined };
132
265
  const resolveDefaultStageSessionDir = (): string | undefined => hostStageSessionDir.current;
@@ -425,6 +558,7 @@ export function createWorkflowExtensionRuntimeState(
425
558
  discoveryRef,
426
559
  lifecycleNotificationState,
427
560
  hilAnswerNotificationState,
561
+ workflowHeartbeatSchedulerState,
428
562
  beforeRestoreCompleted,
429
563
  runtimeForContext,
430
564
  resetWorkflowDiscoveryForSession,
@@ -442,6 +576,10 @@ export function createWorkflowExtensionRuntimeState(
442
576
  notificationsActive = active;
443
577
  reinstallLifecycleNotifications();
444
578
  reinstallHilAnswerNotifications();
579
+ // A fresh host session restarts the cadence from each run's persisted
580
+ // start time; prior-session schedule and pending state cannot apply.
581
+ if (!active) resetWorkflowHeartbeatSchedulerState(workflowHeartbeatSchedulerState);
582
+ reinstallWorkflowHeartbeatScheduler();
445
583
  },
446
584
  updateHostStageSessionDir(sessionManager) {
447
585
  try {