@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
@@ -53376,6 +53376,51 @@ function registerWorkflowLifecycleHandlers(pi, deps) {
53376
53376
  });
53377
53377
  }
53378
53378
 
53379
+ // dist/builtin/workflows/src/durable/workflow-heartbeat-anchor.ts
53380
+ var WORKFLOW_HEARTBEAT_ANCHOR_CHECKPOINT_NAME = "workflow-heartbeat-anchor";
53381
+ function asPersistedAnchorOutput(output) {
53382
+ if (typeof output !== "object" || output === null || Array.isArray(output))
53383
+ return;
53384
+ const { anchorAt, intervalMinutes } = output;
53385
+ return {
53386
+ ...typeof anchorAt === "number" ? { anchorAt } : {},
53387
+ ...typeof intervalMinutes === "number" ? { intervalMinutes } : {}
53388
+ };
53389
+ }
53390
+ function readWorkflowHeartbeatAnchor(backend, workflowId) {
53391
+ const record = asPersistedAnchorOutput(backend.getToolOutput(workflowId, WORKFLOW_HEARTBEAT_ANCHOR_CHECKPOINT_NAME));
53392
+ if (record === undefined)
53393
+ return;
53394
+ const { anchorAt, intervalMinutes } = record;
53395
+ if (anchorAt === undefined || !Number.isFinite(anchorAt))
53396
+ return;
53397
+ return intervalMinutes !== undefined && Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? { anchorAt, intervalMinutes } : { anchorAt };
53398
+ }
53399
+ async function recordWorkflowHeartbeatAnchor(backend, record) {
53400
+ if (!Number.isFinite(record.anchorAt))
53401
+ return false;
53402
+ if (!Number.isFinite(record.intervalMinutes) || record.intervalMinutes <= 0)
53403
+ return false;
53404
+ const existing = readWorkflowHeartbeatAnchor(backend, record.runId);
53405
+ if (existing !== undefined)
53406
+ return true;
53407
+ if (backend.listCheckpoints(record.runId).length === 0)
53408
+ return false;
53409
+ const checkpoint = {
53410
+ kind: "tool",
53411
+ workflowId: record.runId,
53412
+ checkpointId: WORKFLOW_HEARTBEAT_ANCHOR_CHECKPOINT_NAME,
53413
+ name: WORKFLOW_HEARTBEAT_ANCHOR_CHECKPOINT_NAME,
53414
+ argsHash: WORKFLOW_HEARTBEAT_ANCHOR_CHECKPOINT_NAME,
53415
+ output: { anchorAt: record.anchorAt, intervalMinutes: record.intervalMinutes },
53416
+ completedAt: record.now
53417
+ };
53418
+ const stored = await backend.recordAdditiveCheckpointBestEffort(checkpoint);
53419
+ if (!stored)
53420
+ return false;
53421
+ return readWorkflowHeartbeatAnchor(backend, record.runId) !== undefined;
53422
+ }
53423
+
53379
53424
  // dist/builtin/workflows/src/extension/config-loader.ts
53380
53425
  import { isAbsolute, join as join3 } from "node:path";
53381
53426
  import { CONFIG_DIR_NAME, CONFIG_DIR_NAMES, getAgentDir, getAgentDirs, getProjectConfigPaths } from "@bastani/atomic";
@@ -53684,6 +53729,10 @@ import { Type as Type2 } from "typebox";
53684
53729
  import { basename } from "node:path";
53685
53730
  import { fileURLToPath } from "node:url";
53686
53731
 
53732
+ // dist/builtin/workflows/src/shared/workflow-heartbeat-contract.ts
53733
+ var DEFAULT_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES = 15;
53734
+ var WORKFLOW_HEARTBEAT_CUSTOM_TYPE = "workflows:workflow-heartbeat";
53735
+
53687
53736
  // dist/builtin/workflows/src/workflows/identity.ts
53688
53737
  function normalizeWorkflowName(name) {
53689
53738
  if (!name || typeof name !== "string") {
@@ -53777,6 +53826,10 @@ function workflow(spec) {
53777
53826
  if (spec.inputs !== undefined && (spec.inputs === null || typeof spec.inputs !== "object" || Array.isArray(spec.inputs))) {
53778
53827
  throw new TypeError("workflow: inputs must be a schema map");
53779
53828
  }
53829
+ const heartbeatIntervalMinutes = spec.heartbeatIntervalMinutes === undefined ? DEFAULT_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES : spec.heartbeatIntervalMinutes;
53830
+ if (!Number.isFinite(heartbeatIntervalMinutes) || heartbeatIntervalMinutes < 0) {
53831
+ throw new TypeError("workflow: heartbeatIntervalMinutes must be a non-negative finite number");
53832
+ }
53780
53833
  const name = resolveWorkflowName(spec.name);
53781
53834
  const normalizedName = normalizeWorkflowName(name);
53782
53835
  requireNonEmptyString(normalizedName, "normalized name");
@@ -53790,6 +53843,7 @@ function workflow(spec) {
53790
53843
  normalizedName,
53791
53844
  description: spec.description,
53792
53845
  ...spec.autoAttach === true ? { autoAttach: true } : {},
53846
+ heartbeatIntervalMinutes,
53793
53847
  inputs: frozenInputs,
53794
53848
  outputs: frozenOutputs,
53795
53849
  ...inputBindings !== undefined ? { inputBindings } : {},
@@ -54184,6 +54238,7 @@ async function runAdversarialVerification(ctx) {
54184
54238
  var adversarial_verification_default = workflow({
54185
54239
  name: "adversarial-verification",
54186
54240
  description: "Produce a candidate, challenge it with fresh-context rubric-based verifiers, and reduce their evidence through a bounded repair loop.",
54241
+ heartbeatIntervalMinutes: 15,
54187
54242
  inputs: {
54188
54243
  task: Type2.String({ description: "Task whose candidate result must be independently verified." }),
54189
54244
  verifier_count: Type2.Integer({ minimum: 1, maximum: 5, default: 3, description: "Number of independent verifiers per review round." }),
@@ -54337,6 +54392,7 @@ var DEFAULT_ACTION_CATEGORIES = ["analysis", "implementation", "research"];
54337
54392
  var classify_and_act_default = workflow({
54338
54393
  name: "classify-and-act",
54339
54394
  description: "Classify a task with structured confidence, route deterministically to an isolated category action, and ask for human selection when confidence is low.",
54395
+ heartbeatIntervalMinutes: 15,
54340
54396
  inputs: {
54341
54397
  prompt: Type4.String({ description: "Task to classify and execute." }),
54342
54398
  categories: Type4.Array(Type4.String({ minLength: 1 }), {
@@ -54524,6 +54580,7 @@ async function runFanOutAndSynthesize(ctx) {
54524
54580
  var fan_out_and_synthesize_default = workflow({
54525
54581
  name: "fan-out-and-synthesize",
54526
54582
  description: "Partition a task, run bounded independent artifact branches, then synthesize all evidence at an explicit barrier.",
54583
+ heartbeatIntervalMinutes: 15,
54527
54584
  inputs: {
54528
54585
  prompt: Type6.String({ description: "Task to partition, investigate, and synthesize." }),
54529
54586
  max_branches: Type6.Integer({
@@ -54745,6 +54802,7 @@ async function runGenerateAndFilter(ctx) {
54745
54802
  var generate_and_filter_default = workflow({
54746
54803
  name: "generate-and-filter",
54747
54804
  description: "Generate more independent candidates than needed, deduplicate and filter them by rubric, optionally judge them, and return a parent-consumable shortlist.",
54805
+ heartbeatIntervalMinutes: 15,
54748
54806
  inputs: {
54749
54807
  prompt: Type8.String({ description: "Prompt for candidate generation and selection." }),
54750
54808
  num_candidates: Type8.Integer({ minimum: 2, maximum: 20, default: 8, description: "Number of independent candidates to generate." }),
@@ -56039,6 +56097,7 @@ async function runGoalWorkflow(ctx, options) {
56039
56097
  var goal_default = workflow({
56040
56098
  name: "goal",
56041
56099
  description: "Goal Runner workflow with bounded sub-agent orchestration turns, immutable acceptance criteria, ledger artifacts, parallel reviewers, and reducer-gated completion. When launching follow-up goal runs from review findings, pass the ORIGINAL task text as acceptance_criteria so deltas cannot drift from the literal contract. If the task includes submitting a pull request (or MR/review), remove that final action from the objective text and set create_pr=true instead when preparing the workflow inputs.",
56100
+ heartbeatIntervalMinutes: 15,
56042
56101
  inputs: {
56043
56102
  objective: Type10.String({ description: "The objective or delta for this Goal Runner workflow run. Do not include PR/MR submission instructions here; strip them from the task text and request them via create_pr=true instead." }),
56044
56103
  acceptance_criteria: Type10.Optional(Type10.String({ description: "Original immutable task contract this run must remain consistent with. Defaults to objective. Orchestrators launching follow-up runs from reviewer findings should pass the ORIGINAL task text here." })),
@@ -57269,6 +57328,7 @@ async function runOpenClaudeDesignWorkflow(ctx) {
57269
57328
  var open_claude_design_default = workflow({
57270
57329
  name: "open-claude-design",
57271
57330
  description: "AI-powered design workflow: combined discovery/init → design-system/reference research → curated reference discovery → HTML generation → live-driven refinement → rich HTML handoff. The discovery stage asks what to build, the output type, and which references to emulate, then runs impeccable init for PRODUCT.md/DESIGN.md (references take precedence over project context). The user iteratively reviews the generated HTML.",
57331
+ heartbeatIntervalMinutes: 0,
57272
57332
  inputs: {
57273
57333
  prompt: Type12.String({
57274
57334
  description: "What to design (for example, a dashboard, page, component, or prototype). The discovery stage refines this into a confirmed brief and asks for the output type and references."
@@ -57522,6 +57582,7 @@ async function runLoopUntilDone(ctx) {
57522
57582
  var loop_until_done_default = workflow({
57523
57583
  name: "loop-until-done",
57524
57584
  description: "Repeat evidence-producing work and independent completion evaluation against a durable ledger until done or an inspectable iteration-limit failure.",
57585
+ heartbeatIntervalMinutes: 15,
57525
57586
  inputs: {
57526
57587
  prompt: Type14.String({ description: "Objective whose explicit completion condition controls the bounded loop." }),
57527
57588
  max_iterations: Type14.Integer({
@@ -58477,6 +58538,7 @@ Read it before implementation or delegation because it is the primary current im
58477
58538
  var ralph_default = workflow({
58478
58539
  name: "ralph",
58479
58540
  description: "Raw prompt → research-prompt-refinement → research → orchestrate → multi-model parallel review loop with bounded iteration and immutable acceptance criteria. When launching follow-up ralph runs from review findings, pass the ORIGINAL task text as acceptance_criteria so deltas cannot drift from the literal contract. If the task includes submitting a pull request (or MR/review), remove that final action from the prompt text and set create_pr=true instead when preparing the workflow inputs.",
58541
+ heartbeatIntervalMinutes: 15,
58480
58542
  inputs: {
58481
58543
  prompt: Type16.String({ description: "The task or goal to research, execute, and refine. Do not include PR/MR submission instructions here; strip them from the task text and request them via create_pr=true instead." }),
58482
58544
  acceptance_criteria: Type16.Optional(Type16.String({ description: "Original immutable task contract this run must remain consistent with. Defaults to prompt. Orchestrators launching follow-up runs from reviewer findings should pass the ORIGINAL task text here." })),
@@ -58769,6 +58831,7 @@ async function runTournament(ctx) {
58769
58831
  var tournament_default = workflow({
58770
58832
  name: "tournament",
58771
58833
  description: "Run several independent whole-task attempts through a balanced pairwise judging bracket and return an auditable winner.",
58834
+ heartbeatIntervalMinutes: 15,
58772
58835
  inputs: {
58773
58836
  prompt: Type18.String({ description: "Task every competing agent must attempt independently." }),
58774
58837
  num_attempts: Type18.Integer({
@@ -58880,11 +58943,13 @@ __export(exports_sdk_surface, {
58880
58943
  createRegistry: () => createRegistry,
58881
58944
  createCancellationRegistry: () => createCancellationRegistry,
58882
58945
  cancellationRegistry: () => cancellationRegistry,
58946
+ WORKFLOW_HEARTBEAT_CUSTOM_TYPE: () => WORKFLOW_HEARTBEAT_CUSTOM_TYPE,
58883
58947
  NON_INTERACTIVE_WORKFLOW_POLICY: () => NON_INTERACTIVE_WORKFLOW_POLICY,
58884
58948
  KEEP_CONTEXT_OPEN_TAG: () => KEEP_CONTEXT_OPEN_TAG,
58885
58949
  KEEP_CONTEXT_CLOSE_TAG: () => KEEP_CONTEXT_CLOSE_TAG,
58886
58950
  INTERACTIVE_WORKFLOW_POLICY: () => INTERACTIVE_WORKFLOW_POLICY,
58887
- GraphFrontierTracker: () => GraphFrontierTracker
58951
+ GraphFrontierTracker: () => GraphFrontierTracker,
58952
+ DEFAULT_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES: () => DEFAULT_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES
58888
58953
  });
58889
58954
 
58890
58955
  // dist/builtin/workflows/src/engine/graph-inference.ts
@@ -64889,6 +64954,15 @@ function createDurableStageSessionRecorder(input) {
64889
64954
  await recordRunTimingCheckpointAsync(input.deps.backend, input.runSnapshot, {
64890
64955
  debounce: options?.forceDurable !== true
64891
64956
  });
64957
+ const intervalMinutes = input.heartbeatIntervalMinutes;
64958
+ if (Number.isFinite(intervalMinutes * 60000)) {
64959
+ await recordWorkflowHeartbeatAnchor(input.deps.backend, {
64960
+ runId: input.runId,
64961
+ anchorAt: input.runSnapshot.startedAt,
64962
+ intervalMinutes,
64963
+ now: input.deps.now?.() ?? Date.now()
64964
+ });
64965
+ }
64892
64966
  }
64893
64967
  }
64894
64968
  await input.onStageSession?.(stageRunId, snapshot, options);
@@ -65509,6 +65583,9 @@ async function prepareRuntimeDurableResumable(getBackend, workflowId) {
65509
65583
  }
65510
65584
 
65511
65585
  // dist/builtin/workflows/src/durable/completed-catalog.ts
65586
+ function isReservedRunCheckpointHash(argsHash) {
65587
+ return argsHash === RUN_TIMING_CHECKPOINT_NAME || argsHash === WORKFLOW_HEARTBEAT_ANCHOR_CHECKPOINT_NAME;
65588
+ }
65512
65589
  function listCompletedFromBackend(backend) {
65513
65590
  return backend.listCompletedWorkflows();
65514
65591
  }
@@ -65625,7 +65702,7 @@ function checkpointDrafts(checkpoints) {
65625
65702
  stageByReplayKey.set(checkpoint.replayKey, mergeStageDraft(existing2, checkpoint, sequence));
65626
65703
  return;
65627
65704
  }
65628
- if (checkpoint.kind !== "tool" || checkpoint.argsHash === RUN_TIMING_CHECKPOINT_NAME)
65705
+ if (checkpoint.kind !== "tool" || isReservedRunCheckpointHash(checkpoint.argsHash))
65629
65706
  return;
65630
65707
  if (!firstToolSequenceByHash.has(checkpoint.argsHash)) {
65631
65708
  firstToolSequenceByHash.set(checkpoint.argsHash, sequence);
@@ -65736,7 +65813,7 @@ function runSnapshotsFromCheckpoints(checkpoints, rootRunId, rootRunName, fallba
65736
65813
  }
65737
65814
  }
65738
65815
  if (candidates.length === 0 && toolCheckpoints.length === 0) {
65739
- const supportedNonGraphState = checkpoints.every((checkpoint) => checkpoint.kind === "ui" || checkpoint.kind === "tool" && checkpoint.argsHash === RUN_TIMING_CHECKPOINT_NAME);
65816
+ const supportedNonGraphState = checkpoints.every((checkpoint) => checkpoint.kind === "ui" || checkpoint.kind === "tool" && isReservedRunCheckpointHash(checkpoint.argsHash));
65740
65817
  if (policy.failClosed && supportedNonGraphState) {
65741
65818
  return [emptyGraphRun(rootRunId, rootRunName, fallbackCompletedAt)];
65742
65819
  }
@@ -70634,7 +70711,8 @@ async function run(def, inputs, opts = {}) {
70634
70711
  runId,
70635
70712
  deps: durableStageDeps,
70636
70713
  onStageSession: opts.onStageSession,
70637
- ...opts.parentRun === undefined ? { runSnapshot } : {}
70714
+ ...opts.parentRun === undefined ? { runSnapshot } : {},
70715
+ heartbeatIntervalMinutes: def.heartbeatIntervalMinutes
70638
70716
  });
70639
70717
  const stageOptions = {
70640
70718
  continuation: opts.continuation,
@@ -72299,6 +72377,628 @@ function createStatusWriter(store2, config, opts = {}) {
72299
72377
  };
72300
72378
  }
72301
72379
 
72380
+ // dist/builtin/workflows/src/extension/workflow-heartbeat-notice.ts
72381
+ var WORKFLOW_HEARTBEAT_TITLE = "WORKFLOW HEARTBEAT";
72382
+ var WORKFLOW_HEARTBEAT_GLYPH = "♥";
72383
+ var rendererRegisteredHosts5 = new WeakSet;
72384
+ function formatWorkflowHeartbeatNoticeText(details) {
72385
+ const workflowName = escapeQuotedText3(details.workflowName);
72386
+ const elapsed = formatWorkflowHeartbeatElapsed(details);
72387
+ const cadence = formatWorkflowHeartbeatCadence(details.intervalMinutes);
72388
+ return `${WORKFLOW_HEARTBEAT_GLYPH} Workflow "${workflowName}" heartbeat (run ${details.runId}) — still running` + `${elapsed === undefined ? "" : ` after ${elapsed}`}, on a ${cadence} cadence. ` + "This is a periodic alignment check, not a failure. Review whether the run is still on goal, then " + "continue, steer it, stop and replace it, or ask the user. " + `Inspect: /workflow status ${details.runId}`;
72389
+ }
72390
+ function registerWorkflowHeartbeatRenderer(options) {
72391
+ const register = options.registerMessageRenderer;
72392
+ if (typeof register !== "function")
72393
+ return;
72394
+ const host = options.rendererHost ?? register;
72395
+ if (rendererRegisteredHosts5.has(host))
72396
+ return;
72397
+ const renderer = (raw, _options, piTheme) => {
72398
+ const message = raw;
72399
+ if (!message.details)
72400
+ return;
72401
+ return makeWorkflowHeartbeatComponent(message.details, themeFromRenderer3(piTheme));
72402
+ };
72403
+ register(WORKFLOW_HEARTBEAT_CUSTOM_TYPE, renderer);
72404
+ rendererRegisteredHosts5.add(host);
72405
+ }
72406
+ function makeWorkflowHeartbeatComponent(details, theme) {
72407
+ const text = formatWorkflowHeartbeatNoticeText(details);
72408
+ return {
72409
+ render(width) {
72410
+ return renderWorkflowHeartbeatCard(details, { width, theme, fallbackText: text });
72411
+ },
72412
+ invalidate() {}
72413
+ };
72414
+ }
72415
+ function renderWorkflowHeartbeatCard(details, opts) {
72416
+ return renderWorkflowNoticeCard({
72417
+ title: WORKFLOW_HEARTBEAT_TITLE,
72418
+ glyph: WORKFLOW_HEARTBEAT_GLYPH,
72419
+ headline: `Workflow "${details.workflowName}" is still running`,
72420
+ tone: "mauve",
72421
+ fields: [
72422
+ { label: "workflow", value: details.workflowName },
72423
+ { label: "run", value: details.runId },
72424
+ { label: "cadence", value: formatWorkflowHeartbeatCadence(details.intervalMinutes), tone: "muted" },
72425
+ { label: "elapsed", value: formatWorkflowHeartbeatElapsed(details), tone: "muted" }
72426
+ ],
72427
+ hints: [`/workflow status ${details.runId}`],
72428
+ fallbackText: opts.fallbackText,
72429
+ width: opts.width,
72430
+ ...opts.theme ? { theme: opts.theme } : {}
72431
+ });
72432
+ }
72433
+ function formatWorkflowHeartbeatCadence(intervalMinutes) {
72434
+ return `${intervalMinutes}-minute`;
72435
+ }
72436
+ function formatWorkflowHeartbeatElapsed(details) {
72437
+ const elapsedMs = details.scheduledAt - details.startedAt;
72438
+ if (!Number.isFinite(elapsedMs) || elapsedMs < 0)
72439
+ return;
72440
+ const totalMinutes = Math.floor(elapsedMs / 60000);
72441
+ if (totalMinutes < 1)
72442
+ return `${Math.max(0, Math.floor(elapsedMs / 1000))}s`;
72443
+ if (totalMinutes < 60)
72444
+ return `${totalMinutes}m`;
72445
+ const hours = Math.floor(totalMinutes / 60);
72446
+ const minutes = totalMinutes % 60;
72447
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
72448
+ }
72449
+ function escapeQuotedText3(value2) {
72450
+ return value2.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
72451
+ }
72452
+ function themeFromRenderer3(piTheme) {
72453
+ return piTheme === undefined ? undefined : deriveGraphThemeFromPiTheme(piTheme);
72454
+ }
72455
+
72456
+ // dist/builtin/workflows/src/extension/workflow-heartbeat-delivery.ts
72457
+ var defaultWorkflowHeartbeatTimerApi = {
72458
+ setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
72459
+ clearTimeout: (handle) => clearTimeout(handle)
72460
+ };
72461
+ var WORKFLOW_HEARTBEAT_MAX_DELIVERY_ATTEMPTS = 5;
72462
+ function createWorkflowHeartbeatDelivery(options) {
72463
+ const retryTimers = new Set;
72464
+ const queue = [];
72465
+ let attempt = 0;
72466
+ let running = false;
72467
+ let active2 = true;
72468
+ let headDiscarded = false;
72469
+ const settleHead = (details, delivered) => {
72470
+ queue.shift();
72471
+ attempt = 0;
72472
+ running = false;
72473
+ headDiscarded = false;
72474
+ options.onSettled(details, delivered);
72475
+ runHead();
72476
+ };
72477
+ const runHead = () => {
72478
+ if (!active2 || running || retryTimers.size > 0)
72479
+ return;
72480
+ const details = queue[0];
72481
+ if (details === undefined)
72482
+ return;
72483
+ if (options.canDeliver !== undefined && !options.canDeliver(details)) {
72484
+ settleHead(details, false);
72485
+ return;
72486
+ }
72487
+ running = true;
72488
+ attempt += 1;
72489
+ const thisAttempt = attempt;
72490
+ const settle = (delivered) => {
72491
+ if (delivered || headDiscarded || thisAttempt >= WORKFLOW_HEARTBEAT_MAX_DELIVERY_ATTEMPTS || !active2) {
72492
+ settleHead(details, delivered);
72493
+ return;
72494
+ }
72495
+ running = false;
72496
+ const handle = options.timers.setTimeout(() => {
72497
+ retryTimers.delete(handle);
72498
+ runHead();
72499
+ }, Math.min(20 * 2 ** (thisAttempt - 1), 1000));
72500
+ handle.unref?.();
72501
+ retryTimers.add(handle);
72502
+ };
72503
+ const result = options.emit(details);
72504
+ if (typeof result === "boolean")
72505
+ settle(result);
72506
+ else
72507
+ result.then(settle);
72508
+ };
72509
+ const deliver = (details) => {
72510
+ if (!active2)
72511
+ return;
72512
+ queue.push(details);
72513
+ runHead();
72514
+ };
72515
+ const discard = (runId) => {
72516
+ const head = queue[0];
72517
+ const headInFlight = running && head !== undefined && head.runId === runId;
72518
+ let discarded = false;
72519
+ for (let index = queue.length - 1;index >= 0; index -= 1) {
72520
+ if (queue[index]?.runId !== runId)
72521
+ continue;
72522
+ if (index === 0 && headInFlight) {
72523
+ headDiscarded = true;
72524
+ continue;
72525
+ }
72526
+ queue.splice(index, 1);
72527
+ discarded = true;
72528
+ if (index > 0)
72529
+ continue;
72530
+ attempt = 0;
72531
+ running = false;
72532
+ for (const handle of retryTimers)
72533
+ options.timers.clearTimeout(handle);
72534
+ retryTimers.clear();
72535
+ }
72536
+ if (discarded)
72537
+ runHead();
72538
+ return discarded;
72539
+ };
72540
+ return {
72541
+ deliver,
72542
+ discard,
72543
+ dispose() {
72544
+ active2 = false;
72545
+ for (const handle of retryTimers)
72546
+ options.timers.clearTimeout(handle);
72547
+ retryTimers.clear();
72548
+ queue.length = 0;
72549
+ }
72550
+ };
72551
+ }
72552
+
72553
+ // dist/builtin/workflows/src/extension/workflow-heartbeat-scheduler.ts
72554
+ var MAX_TIMER_DELAY_MS = 2147483647;
72555
+ var MS_PER_MINUTE = 60000;
72556
+ function createWorkflowHeartbeatSchedulerState() {
72557
+ return {
72558
+ scheduled: new Map,
72559
+ pending: new Map,
72560
+ awaitingParentPickup: new Map,
72561
+ lastEnqueuedAt: new Map,
72562
+ anchorAt: new Map,
72563
+ anchorPersisted: new Set,
72564
+ anchorWritesPending: new Set,
72565
+ intervalMinutes: new Map
72566
+ };
72567
+ }
72568
+ function resetWorkflowHeartbeatSchedulerState(state2) {
72569
+ state2.scheduled.clear();
72570
+ state2.pending.clear();
72571
+ state2.awaitingParentPickup.clear();
72572
+ state2.lastEnqueuedAt.clear();
72573
+ state2.anchorAt.clear();
72574
+ state2.anchorPersisted.clear();
72575
+ state2.anchorWritesPending.clear();
72576
+ state2.intervalMinutes.clear();
72577
+ }
72578
+ var MAX_REPRESENTABLE_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES = Number.MAX_VALUE / MS_PER_MINUTE;
72579
+ function nextWorkflowHeartbeatBoundary(startedAt, intervalMinutes, after) {
72580
+ if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0)
72581
+ return;
72582
+ if (!Number.isFinite(startedAt) || !Number.isFinite(after))
72583
+ return;
72584
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
72585
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
72586
+ warnWorkflowHeartbeatUnschedulableInterval(intervalMinutes);
72587
+ return;
72588
+ }
72589
+ const boundaryAt = (n) => startedAt + n * intervalMs;
72590
+ const elapsed = after - startedAt;
72591
+ const estimate = elapsed < 0 ? 1 : Math.floor(elapsed / intervalMs) + 1;
72592
+ if (!Number.isFinite(estimate))
72593
+ return;
72594
+ if (boundaryAt(estimate) > after)
72595
+ return finiteBoundaryAfter(boundaryAt(estimate), after);
72596
+ if (boundaryAt(estimate + 1) > after)
72597
+ return finiteBoundaryAfter(boundaryAt(estimate + 1), after);
72598
+ let low = estimate + 1;
72599
+ let high = low;
72600
+ while (boundaryAt(high) <= after) {
72601
+ low = high;
72602
+ high *= 2;
72603
+ if (!Number.isFinite(high))
72604
+ return nextRepresentableAfter(after);
72605
+ }
72606
+ while (high - low > 1) {
72607
+ const mid = Math.floor(low / 2 + high / 2);
72608
+ if (mid <= low || mid >= high)
72609
+ break;
72610
+ if (boundaryAt(mid) > after)
72611
+ high = mid;
72612
+ else
72613
+ low = mid;
72614
+ }
72615
+ return finiteBoundaryAfter(boundaryAt(high), after);
72616
+ }
72617
+ function finiteBoundaryAfter(scheduledAt, after) {
72618
+ return Number.isFinite(scheduledAt) && scheduledAt > after ? scheduledAt : undefined;
72619
+ }
72620
+ function nextRepresentableAfter(at) {
72621
+ if (!Number.isFinite(at))
72622
+ return;
72623
+ const view = new DataView(new ArrayBuffer(8));
72624
+ view.setFloat64(0, at);
72625
+ const bits = view.getBigUint64(0);
72626
+ view.setBigUint64(0, at >= 0 ? bits + 1n : bits - 1n);
72627
+ const next = view.getFloat64(0);
72628
+ return Number.isFinite(next) && next > at ? next : undefined;
72629
+ }
72630
+ function isWorkflowHeartbeatEligibleRun(run2) {
72631
+ if (!isTopLevelWorkflowRun(run2))
72632
+ return false;
72633
+ if (run2.status !== "running")
72634
+ return false;
72635
+ if (run2.pausedAt !== undefined)
72636
+ return false;
72637
+ return !isTerminalRunStatus(effectiveRunStatus(run2));
72638
+ }
72639
+ function isWorkflowHeartbeatTerminalRun(run2) {
72640
+ return isTerminalRunStatus(run2.status);
72641
+ }
72642
+ function installWorkflowHeartbeatScheduler(options) {
72643
+ const state2 = options.state ?? createWorkflowHeartbeatSchedulerState();
72644
+ const now = options.now ?? Date.now;
72645
+ const timers = options.timers ?? defaultWorkflowHeartbeatTimerApi;
72646
+ const send = options.sendMessage;
72647
+ const parentAvailabilityReported = options.parentAvailabilityReported === true;
72648
+ const anchorStore = options.anchorStore;
72649
+ let active2 = true;
72650
+ let timerHandle;
72651
+ const emit = (details) => {
72652
+ if (typeof send !== "function")
72653
+ return false;
72654
+ try {
72655
+ const result = send({
72656
+ customType: WORKFLOW_HEARTBEAT_CUSTOM_TYPE,
72657
+ content: formatWorkflowHeartbeatNoticeText(details),
72658
+ display: true,
72659
+ details
72660
+ }, { triggerTurn: true, deliverAs: "steer", persistWhenStreaming: true });
72661
+ if (result === undefined)
72662
+ return true;
72663
+ return Promise.resolve(result).then(() => true, (error) => {
72664
+ warnWorkflowHeartbeatSendFailure(error);
72665
+ return false;
72666
+ });
72667
+ } catch (error) {
72668
+ warnWorkflowHeartbeatSendFailure(error);
72669
+ return false;
72670
+ }
72671
+ };
72672
+ const canDeliverWorkflowHeartbeat = (details) => {
72673
+ const run2 = findRun(readGraphStoreSnapshot(options.store), details.runId);
72674
+ return run2 !== undefined && isWorkflowHeartbeatEligibleRun(run2);
72675
+ };
72676
+ const delivery = createWorkflowHeartbeatDelivery({
72677
+ emit,
72678
+ timers,
72679
+ canDeliver: canDeliverWorkflowHeartbeat,
72680
+ onSettled: (details, delivered) => {
72681
+ const identity = state2.pending.get(details.runId);
72682
+ if (identity?.scheduledAt !== details.scheduledAt)
72683
+ return;
72684
+ if (!delivered) {
72685
+ state2.pending.delete(details.runId);
72686
+ state2.awaitingParentPickup.delete(details.runId);
72687
+ refresh();
72688
+ return;
72689
+ }
72690
+ if (!parentAvailabilityReported) {
72691
+ state2.pending.delete(details.runId);
72692
+ refresh();
72693
+ return;
72694
+ }
72695
+ state2.awaitingParentPickup.set(details.runId, identity);
72696
+ refresh();
72697
+ }
72698
+ });
72699
+ const clearRunHeartbeatState = (runId) => {
72700
+ let cleared = state2.scheduled.delete(runId);
72701
+ cleared = state2.pending.delete(runId) || cleared;
72702
+ cleared = state2.awaitingParentPickup.delete(runId) || cleared;
72703
+ cleared = state2.lastEnqueuedAt.delete(runId) || cleared;
72704
+ cleared = state2.anchorAt.delete(runId) || cleared;
72705
+ cleared = state2.anchorPersisted.delete(runId) || cleared;
72706
+ cleared = state2.anchorWritesPending.delete(runId) || cleared;
72707
+ cleared = state2.intervalMinutes.delete(runId) || cleared;
72708
+ return delivery.discard(runId) || cleared;
72709
+ };
72710
+ const clearWorkflowHeartbeats = (runId) => {
72711
+ if (!clearRunHeartbeatState(runId))
72712
+ return { kind: "already-clear" };
72713
+ arm();
72714
+ return { kind: "cleared" };
72715
+ };
72716
+ const notifyHeartbeatConsumed = (identity) => {
72717
+ if (!active2)
72718
+ return;
72719
+ const held = state2.awaitingParentPickup.get(identity.runId);
72720
+ const pending = state2.pending.get(identity.runId);
72721
+ if (held?.scheduledAt !== identity.scheduledAt || pending?.scheduledAt !== identity.scheduledAt)
72722
+ return;
72723
+ state2.awaitingParentPickup.delete(identity.runId);
72724
+ state2.pending.delete(identity.runId);
72725
+ refresh();
72726
+ };
72727
+ const scheduleWorkflowHeartbeats = (run2, intervalMinutes) => {
72728
+ state2.scheduled.delete(run2.id);
72729
+ if (intervalMinutes <= 0)
72730
+ return { kind: "disabled" };
72731
+ if (run2.status !== "running")
72732
+ return { kind: "disabled" };
72733
+ const floor = Math.max(now(), state2.lastEnqueuedAt.get(run2.id) ?? Number.NEGATIVE_INFINITY);
72734
+ const anchorAt = resolveAnchorAt(run2);
72735
+ const scheduledAt = nextWorkflowHeartbeatBoundary(anchorAt, intervalMinutes, floor);
72736
+ if (scheduledAt === undefined)
72737
+ return { kind: "disabled" };
72738
+ persistLaunchRecordOnce(run2.id, anchorAt, intervalMinutes);
72739
+ state2.scheduled.set(run2.id, {
72740
+ runId: run2.id,
72741
+ scheduledAt,
72742
+ workflowName: run2.name,
72743
+ startedAt: anchorAt,
72744
+ intervalMinutes
72745
+ });
72746
+ return { kind: "scheduled", scheduledAt };
72747
+ };
72748
+ const enqueueWorkflowHeartbeat = (payload) => {
72749
+ if (!active2 || typeof send !== "function")
72750
+ return { kind: "suppressed", reason: "unavailable" };
72751
+ const run2 = findRun(readGraphStoreSnapshot(options.store), payload.runId);
72752
+ if (run2 === undefined)
72753
+ return { kind: "suppressed", reason: "missing" };
72754
+ if (isTerminalRunStatus(effectiveRunStatus(run2)))
72755
+ return { kind: "suppressed", reason: "terminal" };
72756
+ if (!isWorkflowHeartbeatEligibleRun(run2))
72757
+ return { kind: "suppressed", reason: "paused" };
72758
+ if (state2.pending.has(payload.runId))
72759
+ return { kind: "suppressed", reason: "duplicate" };
72760
+ if ((state2.lastEnqueuedAt.get(payload.runId) ?? Number.NEGATIVE_INFINITY) >= payload.scheduledAt) {
72761
+ return { kind: "suppressed", reason: "duplicate" };
72762
+ }
72763
+ const identity = { runId: payload.runId, scheduledAt: payload.scheduledAt };
72764
+ state2.pending.set(payload.runId, identity);
72765
+ state2.lastEnqueuedAt.set(payload.runId, payload.scheduledAt);
72766
+ delivery.deliver(payload);
72767
+ return { kind: "enqueued", identity };
72768
+ };
72769
+ const arm = () => {
72770
+ if (timerHandle !== undefined) {
72771
+ timers.clearTimeout(timerHandle);
72772
+ timerHandle = undefined;
72773
+ }
72774
+ if (!active2)
72775
+ return;
72776
+ const next = earliestScheduled(state2);
72777
+ if (next === undefined)
72778
+ return;
72779
+ const nextAt = next.scheduledAt;
72780
+ const delay2 = Math.min(Math.max(1, Math.ceil(nextAt - now())), MAX_TIMER_DELAY_MS);
72781
+ const handle = timers.setTimeout(() => {
72782
+ timerHandle = undefined;
72783
+ processDue();
72784
+ }, delay2);
72785
+ handle.unref?.();
72786
+ timerHandle = handle;
72787
+ };
72788
+ const refresh = () => {
72789
+ if (!active2 || typeof send !== "function")
72790
+ return;
72791
+ const snapshot = readGraphStoreSnapshot(options.store);
72792
+ const observed = new Set;
72793
+ for (const run2 of snapshot.runs) {
72794
+ if (!isTopLevelWorkflowRun(run2))
72795
+ continue;
72796
+ if (isWorkflowHeartbeatTerminalRun(run2)) {
72797
+ clearWorkflowHeartbeats(run2.id);
72798
+ continue;
72799
+ }
72800
+ observed.add(run2.id);
72801
+ if (state2.pending.has(run2.id) || !isWorkflowHeartbeatEligibleRun(run2)) {
72802
+ state2.scheduled.delete(run2.id);
72803
+ continue;
72804
+ }
72805
+ const owed = state2.scheduled.get(run2.id);
72806
+ if (owed !== undefined && owed.scheduledAt <= now())
72807
+ continue;
72808
+ const launched = readLaunchRecord(run2.id);
72809
+ const intervalMinutes = resolveRunIntervalMinutes(state2, options.resolveIntervalMinutes, run2, launched?.intervalMinutes);
72810
+ if (intervalMinutes === undefined) {
72811
+ state2.scheduled.delete(run2.id);
72812
+ continue;
72813
+ }
72814
+ scheduleWorkflowHeartbeats(run2, intervalMinutes);
72815
+ }
72816
+ for (const runId of trackedRunIds(state2)) {
72817
+ if (!observed.has(runId))
72818
+ clearWorkflowHeartbeats(runId);
72819
+ }
72820
+ arm();
72821
+ };
72822
+ function readLaunchRecord(runId) {
72823
+ try {
72824
+ return anchorStore?.readAnchorAt(runId);
72825
+ } catch {
72826
+ return;
72827
+ }
72828
+ }
72829
+ function resolveAnchorAt(run2) {
72830
+ const cached = state2.anchorAt.get(run2.id);
72831
+ if (cached !== undefined)
72832
+ return Math.min(cached, run2.startedAt);
72833
+ const stored = readLaunchRecord(run2.id)?.anchorAt;
72834
+ const anchorAt = stored === undefined ? run2.startedAt : Math.min(stored, run2.startedAt);
72835
+ state2.anchorAt.set(run2.id, anchorAt);
72836
+ return anchorAt;
72837
+ }
72838
+ function persistLaunchRecordOnce(runId, anchorAt, intervalMinutes) {
72839
+ if (anchorStore === undefined)
72840
+ return;
72841
+ if (state2.anchorPersisted.has(runId) || state2.anchorWritesPending.has(runId))
72842
+ return;
72843
+ state2.anchorWritesPending.add(runId);
72844
+ let write;
72845
+ try {
72846
+ write = Promise.resolve(anchorStore.recordAnchorAt(runId, { anchorAt, intervalMinutes }));
72847
+ } catch {
72848
+ state2.anchorWritesPending.delete(runId);
72849
+ return;
72850
+ }
72851
+ write.then((acknowledged) => {
72852
+ const tracked2 = state2.anchorWritesPending.delete(runId);
72853
+ if (acknowledged && tracked2)
72854
+ state2.anchorPersisted.add(runId);
72855
+ }, () => {
72856
+ state2.anchorWritesPending.delete(runId);
72857
+ });
72858
+ }
72859
+ const processDue = () => {
72860
+ if (!active2)
72861
+ return;
72862
+ const at = now();
72863
+ const due = [...state2.scheduled.values()].filter((details) => details.scheduledAt <= at).sort(compareWorkflowHeartbeatOrder);
72864
+ const snapshot = readGraphStoreSnapshot(options.store);
72865
+ for (const details of due) {
72866
+ state2.scheduled.delete(details.runId);
72867
+ const run2 = findRun(snapshot, details.runId);
72868
+ if (run2 === undefined || !isWorkflowHeartbeatEligibleRun(run2))
72869
+ continue;
72870
+ enqueueWorkflowHeartbeat(details);
72871
+ }
72872
+ refresh();
72873
+ };
72874
+ const unsubscribe = subscribeStoreInvalidation(options.store, refresh);
72875
+ refresh();
72876
+ return {
72877
+ scheduleWorkflowHeartbeats,
72878
+ enqueueWorkflowHeartbeat,
72879
+ clearWorkflowHeartbeats,
72880
+ notifyHeartbeatConsumed,
72881
+ state: state2,
72882
+ dispose() {
72883
+ active2 = false;
72884
+ unsubscribe();
72885
+ if (timerHandle !== undefined) {
72886
+ timers.clearTimeout(timerHandle);
72887
+ timerHandle = undefined;
72888
+ }
72889
+ state2.scheduled.clear();
72890
+ delivery.dispose();
72891
+ }
72892
+ };
72893
+ }
72894
+ function compareWorkflowHeartbeatOrder(a, b) {
72895
+ if (a.scheduledAt !== b.scheduledAt)
72896
+ return a.scheduledAt - b.scheduledAt;
72897
+ if (a.runId === b.runId)
72898
+ return 0;
72899
+ return a.runId < b.runId ? -1 : 1;
72900
+ }
72901
+ function earliestScheduled(state2) {
72902
+ let earliest;
72903
+ for (const details of state2.scheduled.values()) {
72904
+ if (earliest === undefined || compareWorkflowHeartbeatOrder(details, earliest) < 0)
72905
+ earliest = details;
72906
+ }
72907
+ return earliest;
72908
+ }
72909
+ function trackedRunIds(state2) {
72910
+ const ids = new Set([
72911
+ ...state2.scheduled.keys(),
72912
+ ...state2.pending.keys(),
72913
+ ...state2.awaitingParentPickup.keys(),
72914
+ ...state2.lastEnqueuedAt.keys(),
72915
+ ...state2.anchorAt.keys(),
72916
+ ...state2.anchorPersisted,
72917
+ ...state2.anchorWritesPending,
72918
+ ...state2.intervalMinutes.keys()
72919
+ ]);
72920
+ return [...ids];
72921
+ }
72922
+ function resolveRunIntervalMinutes(state2, resolve14, run2, persistedIntervalMinutes) {
72923
+ const memoized = state2.intervalMinutes.get(run2.id);
72924
+ if (memoized !== undefined)
72925
+ return memoized > 0 ? memoized : undefined;
72926
+ const resolved = persistedIntervalMinutes ?? resolve14(run2.name);
72927
+ if (resolved === undefined || !Number.isFinite(resolved))
72928
+ return;
72929
+ state2.intervalMinutes.set(run2.id, resolved);
72930
+ return resolved > 0 ? resolved : undefined;
72931
+ }
72932
+ function findRun(snapshot, runId) {
72933
+ return snapshot.runs.find((run2) => run2.id === runId);
72934
+ }
72935
+ function warnWorkflowHeartbeatSendFailure(error) {
72936
+ if (process.env.ATOMIC_WORKFLOW_DEBUG !== "1")
72937
+ return;
72938
+ const message = error instanceof Error ? error.message : String(error);
72939
+ console.warn("[workflows] workflow heartbeat send failed", message);
72940
+ }
72941
+ function warnWorkflowHeartbeatUnschedulableInterval(intervalMinutes) {
72942
+ if (process.env.ATOMIC_WORKFLOW_DEBUG !== "1")
72943
+ return;
72944
+ console.warn(`[workflows] heartbeatIntervalMinutes ${intervalMinutes} exceeds the largest representable cadence ` + `(${MAX_REPRESENTABLE_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES} minutes); no boundary can be scheduled for it`);
72945
+ }
72946
+ function isRecord4(value2) {
72947
+ return typeof value2 === "object" && value2 !== null;
72948
+ }
72949
+ function asHostMessageEndEvent(event) {
72950
+ if (!isRecord4(event))
72951
+ return;
72952
+ const message = event.message;
72953
+ if (!isRecord4(message))
72954
+ return;
72955
+ const { role, customType, details } = message;
72956
+ if (typeof role !== "string")
72957
+ return;
72958
+ return {
72959
+ message: {
72960
+ role,
72961
+ ...typeof customType === "string" ? { customType } : {},
72962
+ ...isRecord4(details) ? { details } : {},
72963
+ raw: message
72964
+ }
72965
+ };
72966
+ }
72967
+ var PROTECTED_RECONCILIATION_CUSTOM_TYPE = "atomic:protected-streaming-reconciliation";
72968
+ function workflowHeartbeatConsumedIdentity(event, entries) {
72969
+ if (entries === undefined)
72970
+ return;
72971
+ const parsed = asHostMessageEndEvent(event);
72972
+ if (parsed === undefined)
72973
+ return;
72974
+ const { role, customType, details } = parsed.message;
72975
+ if (role !== "custom" || customType !== PROTECTED_RECONCILIATION_CUSTOM_TYPE)
72976
+ return;
72977
+ if (details === undefined)
72978
+ return;
72979
+ const intentEntryId = details.protectedReconciliationOf;
72980
+ if (typeof intentEntryId !== "string")
72981
+ return;
72982
+ const intent = entries.find((entry) => entry.id === intentEntryId);
72983
+ if (intent?.customType !== WORKFLOW_HEARTBEAT_CUSTOM_TYPE)
72984
+ return;
72985
+ if (!isRecord4(intent.details))
72986
+ return;
72987
+ const { runId, scheduledAt } = intent.details;
72988
+ if (typeof runId !== "string" || typeof scheduledAt !== "number")
72989
+ return;
72990
+ return { runId, scheduledAt };
72991
+ }
72992
+ function workflowHeartbeatContextInvalidation(event, entries, isIdentityOwned) {
72993
+ const identity = workflowHeartbeatConsumedIdentity(event, entries);
72994
+ if (identity === undefined || isIdentityOwned(identity))
72995
+ return;
72996
+ const parsed = asHostMessageEndEvent(event);
72997
+ if (parsed === undefined)
72998
+ return;
72999
+ return { message: { ...parsed.message.raw, excludeFromContext: true } };
73000
+ }
73001
+
72302
73002
  // dist/builtin/workflows/src/extension/workflow-model-catalog.ts
72303
73003
  function workflowModelCatalogFromContext(ctx) {
72304
73004
  if (ctx?.modelRegistry === undefined && ctx?.model === undefined)
@@ -72463,6 +73163,29 @@ function workflowReloadDiagnostics(configDiagnostics, discoveryDiagnostics) {
72463
73163
  }
72464
73164
 
72465
73165
  // dist/builtin/workflows/src/extension/extension-runtime-state.ts
73166
+ function durableWorkflowHeartbeatAnchorStore() {
73167
+ return {
73168
+ readAnchorAt(runId) {
73169
+ try {
73170
+ return readWorkflowHeartbeatAnchor(getDurableBackend(), runId);
73171
+ } catch {
73172
+ return;
73173
+ }
73174
+ },
73175
+ async recordAnchorAt(runId, record) {
73176
+ try {
73177
+ return await recordWorkflowHeartbeatAnchor(getDurableBackend(), {
73178
+ runId,
73179
+ anchorAt: record.anchorAt,
73180
+ intervalMinutes: record.intervalMinutes ?? 0,
73181
+ now: Date.now()
73182
+ });
73183
+ } catch {
73184
+ return false;
73185
+ }
73186
+ }
73187
+ };
73188
+ }
72466
73189
  function createWorkflowExtensionRuntimeState(pi, adapters, resolveCwd = () => pi.sessionManager?.getCwd?.() ?? process.cwd()) {
72467
73190
  const persistenceRef = { current: makePersistencePort(pi, WORKFLOW_CONFIG_DEFAULTS.persistRuns) };
72468
73191
  const mcpPort = makeMcpPort(pi);
@@ -72479,10 +73202,12 @@ function createWorkflowExtensionRuntimeState(pi, adapters, resolveCwd = () => pi
72479
73202
  let statusWriterRef = createStatusWriter(store, runtimeConfigRef.current);
72480
73203
  let lifecycleNotificationsUnsubscribe = null;
72481
73204
  let hilAnswerNotificationsUnsubscribe = null;
73205
+ let workflowHeartbeatScheduler = null;
72482
73206
  let notificationsActive = false;
72483
73207
  let notificationGeneration = 0;
72484
73208
  const lifecycleNotificationState = createWorkflowLifecycleNotificationState();
72485
73209
  const hilAnswerNotificationState = createWorkflowHilAnswerNotificationState();
73210
+ const workflowHeartbeatSchedulerState = createWorkflowHeartbeatSchedulerState();
72486
73211
  const beforeRestoreCompleted = (snapshots) => {
72487
73212
  seedWorkflowLifecycleNotificationState(lifecycleNotificationState, {
72488
73213
  ...readGraphStoreSnapshot(store),
@@ -72495,6 +73220,7 @@ function createWorkflowExtensionRuntimeState(pi, adapters, resolveCwd = () => pi
72495
73220
  const registerMessageRenderer = typeof pi.registerMessageRenderer === "function" ? (event, renderer) => pi.registerMessageRenderer(event, renderer) : undefined;
72496
73221
  registerLifecycleNoticeRenderer({ rendererHost: pi, registerMessageRenderer });
72497
73222
  registerHilAnswerNoticeRenderer({ rendererHost: pi, registerMessageRenderer });
73223
+ registerWorkflowHeartbeatRenderer({ rendererHost: pi, registerMessageRenderer });
72498
73224
  const sendWorkflowNotificationMessage = typeof pi.sendMessage === "function" ? (message, options) => pi.sendMessage(message, options) : undefined;
72499
73225
  const reinstallLifecycleNotifications = () => {
72500
73226
  lifecycleNotificationsUnsubscribe?.();
@@ -72521,6 +73247,38 @@ function createWorkflowExtensionRuntimeState(pi, adapters, resolveCwd = () => pi
72521
73247
  sendMessage: sendWorkflowNotificationMessage
72522
73248
  });
72523
73249
  };
73250
+ const parentAvailabilityReported = typeof pi.on === "function";
73251
+ if (parentAvailabilityReported) {
73252
+ pi.on?.("message_end", (event, ctx) => {
73253
+ const entries = (ctx?.sessionManager ?? pi.sessionManager)?.getEntries?.();
73254
+ const invalidation = workflowHeartbeatContextInvalidation(event, entries, isWorkflowHeartbeatIdentityOwned);
73255
+ if (invalidation !== undefined)
73256
+ return invalidation;
73257
+ const identity = workflowHeartbeatConsumedIdentity(event, entries);
73258
+ if (identity !== undefined)
73259
+ workflowHeartbeatScheduler?.notifyHeartbeatConsumed(identity);
73260
+ return;
73261
+ });
73262
+ }
73263
+ function isWorkflowHeartbeatIdentityOwned(identity) {
73264
+ const pending = workflowHeartbeatSchedulerState.pending.get(identity.runId);
73265
+ const run2 = store.runs().find((candidate) => candidate.id === identity.runId);
73266
+ return pending?.scheduledAt === identity.scheduledAt && run2 !== undefined && !isWorkflowHeartbeatTerminalRun(run2);
73267
+ }
73268
+ const reinstallWorkflowHeartbeatScheduler = () => {
73269
+ workflowHeartbeatScheduler?.dispose();
73270
+ workflowHeartbeatScheduler = null;
73271
+ if (!notificationsActive)
73272
+ return;
73273
+ workflowHeartbeatScheduler = installWorkflowHeartbeatScheduler({
73274
+ store,
73275
+ state: workflowHeartbeatSchedulerState,
73276
+ sendMessage: sendWorkflowNotificationMessage,
73277
+ resolveIntervalMinutes: (workflowName) => runtimeProxy.registry.get(workflowName)?.heartbeatIntervalMinutes,
73278
+ parentAvailabilityReported,
73279
+ anchorStore: durableWorkflowHeartbeatAnchorStore()
73280
+ });
73281
+ };
72524
73282
  const hostStageSessionDir = { current: undefined };
72525
73283
  const resolveDefaultStageSessionDir = () => hostStageSessionDir.current;
72526
73284
  const startupDiscovery = discoverStartupWorkflowsSync();
@@ -72789,6 +73547,7 @@ function createWorkflowExtensionRuntimeState(pi, adapters, resolveCwd = () => pi
72789
73547
  discoveryRef,
72790
73548
  lifecycleNotificationState,
72791
73549
  hilAnswerNotificationState,
73550
+ workflowHeartbeatSchedulerState,
72792
73551
  beforeRestoreCompleted,
72793
73552
  runtimeForContext,
72794
73553
  resetWorkflowDiscoveryForSession,
@@ -72804,6 +73563,9 @@ function createWorkflowExtensionRuntimeState(pi, adapters, resolveCwd = () => pi
72804
73563
  notificationsActive = active2;
72805
73564
  reinstallLifecycleNotifications();
72806
73565
  reinstallHilAnswerNotifications();
73566
+ if (!active2)
73567
+ resetWorkflowHeartbeatSchedulerState(workflowHeartbeatSchedulerState);
73568
+ reinstallWorkflowHeartbeatScheduler();
72807
73569
  },
72808
73570
  updateHostStageSessionDir(sessionManager) {
72809
73571
  try {