@enderfga/claw-orchestrator 7.12.0 → 7.13.0

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 (76) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/cli.js +66 -6
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/src/autoloop/dispatcher.d.ts +19 -0
  5. package/dist/src/autoloop/dispatcher.js +57 -0
  6. package/dist/src/autoloop/dispatcher.js.map +1 -1
  7. package/dist/src/autoloop/runner.d.ts +102 -2
  8. package/dist/src/autoloop/runner.js +367 -44
  9. package/dist/src/autoloop/runner.js.map +1 -1
  10. package/dist/src/autoloop/types.d.ts +17 -0
  11. package/dist/src/autoloop/types.js.map +1 -1
  12. package/dist/src/dashboard/index.html +79 -73
  13. package/dist/src/embedded-server.js +28 -5
  14. package/dist/src/embedded-server.js.map +1 -1
  15. package/dist/src/engine-spawn.d.ts +8 -1
  16. package/dist/src/engine-spawn.js +13 -3
  17. package/dist/src/engine-spawn.js.map +1 -1
  18. package/dist/src/index.js +58 -69
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/kernel/agent-step.d.ts +3 -0
  21. package/dist/src/kernel/agent-step.js.map +1 -1
  22. package/dist/src/kernel/engine.d.ts +5 -2
  23. package/dist/src/kernel/engine.js +11 -12
  24. package/dist/src/kernel/engine.js.map +1 -1
  25. package/dist/src/kernel/nodes/autoloop.d.ts +32 -2
  26. package/dist/src/kernel/nodes/autoloop.js +122 -15
  27. package/dist/src/kernel/nodes/autoloop.js.map +1 -1
  28. package/dist/src/kernel/nodes/goal.d.ts +94 -0
  29. package/dist/src/kernel/nodes/goal.js +386 -0
  30. package/dist/src/kernel/nodes/goal.js.map +1 -0
  31. package/dist/src/kernel/nodes/index.d.ts +2 -1
  32. package/dist/src/kernel/nodes/index.js +3 -1
  33. package/dist/src/kernel/nodes/index.js.map +1 -1
  34. package/dist/src/kernel/templates/index.d.ts +27 -3
  35. package/dist/src/kernel/templates/index.js +78 -1
  36. package/dist/src/kernel/templates/index.js.map +1 -1
  37. package/dist/src/kernel/types.d.ts +41 -2
  38. package/dist/src/kernel/types.js.map +1 -1
  39. package/dist/src/models.js +10 -2
  40. package/dist/src/models.js.map +1 -1
  41. package/dist/src/persistent-agy-session.js +1 -0
  42. package/dist/src/persistent-agy-session.js.map +1 -1
  43. package/dist/src/persistent-codex-app-session.d.ts +55 -26
  44. package/dist/src/persistent-codex-app-session.js +109 -24
  45. package/dist/src/persistent-codex-app-session.js.map +1 -1
  46. package/dist/src/persistent-codex-session.js +1 -0
  47. package/dist/src/persistent-codex-session.js.map +1 -1
  48. package/dist/src/persistent-cursor-session.js +1 -0
  49. package/dist/src/persistent-cursor-session.js.map +1 -1
  50. package/dist/src/persistent-custom-session.js +2 -0
  51. package/dist/src/persistent-custom-session.js.map +1 -1
  52. package/dist/src/persistent-gemini-session.js +1 -0
  53. package/dist/src/persistent-gemini-session.js.map +1 -1
  54. package/dist/src/persistent-grok-session.d.ts +4 -0
  55. package/dist/src/persistent-grok-session.js +12 -2
  56. package/dist/src/persistent-grok-session.js.map +1 -1
  57. package/dist/src/persistent-opencode-session.js +1 -0
  58. package/dist/src/persistent-opencode-session.js.map +1 -1
  59. package/dist/src/persistent-session.js +1 -0
  60. package/dist/src/persistent-session.js.map +1 -1
  61. package/dist/src/sanitize.d.ts +10 -0
  62. package/dist/src/sanitize.js +26 -0
  63. package/dist/src/sanitize.js.map +1 -1
  64. package/dist/src/session-manager.d.ts +25 -20
  65. package/dist/src/session-manager.js +217 -44
  66. package/dist/src/session-manager.js.map +1 -1
  67. package/dist/src/types.d.ts +6 -0
  68. package/package.json +2 -2
  69. package/skills/SKILL.md +8 -5
  70. package/skills/references/autoloop.md +52 -8
  71. package/skills/references/claude-cli-tracking.md +3 -1
  72. package/skills/references/cli.md +32 -1
  73. package/skills/references/dashboard.md +5 -1
  74. package/skills/references/multi-engine.md +26 -15
  75. package/skills/references/tools.md +25 -14
  76. package/skills/references/workflow.md +87 -13
@@ -142,9 +142,10 @@ import { resolveAlias, isClaudeModel, lookupModel } from './models.js';
142
142
  import { isAgyConversationId } from './agy-conversation.js';
143
143
  import { Council } from './council.js';
144
144
  import { AutoloopRunner } from './autoloop/runner.js';
145
+ import { boundedErrorReason } from './sanitize.js';
145
146
  import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
146
- import { DEFAULT_PUSH_POLICY, DEFAULT_SEND_TIMEOUT_MS, validateAutoloopTimeoutConfig } from './autoloop/types.js';
147
- import { Msg as AutoloopMsg } from './autoloop/messages.js';
147
+ import { DEFAULT_PUSH_POLICY, DEFAULT_SEND_TIMEOUT_MS, MAX_METRIC_HISTORY, validateAutoloopTimeoutConfig, } from './autoloop/types.js';
148
+ import { Msg as AutoloopMsg, } from './autoloop/messages.js';
148
149
  import { appendPushLog, notifyUserFallbackChain } from './autoloop/notify.js';
149
150
  import { UltraappManager } from './ultraapp/manager.js';
150
151
  import { UltraappStore, defaultStoreRoot } from './ultraapp/store.js';
@@ -646,6 +647,128 @@ function readPermissionDenials(evt) {
646
647
  }
647
648
  return out;
648
649
  }
650
+ /** Fixed refusal when a non-fresh boot cannot trust the closed-ledger prefix. */
651
+ const AUTOLOOP_CLOSED_LEDGER_REFUSAL = 'Autoloop resume refused: ledger closed-prefix is incomplete or malformed';
652
+ const REVIEWER_DECISIONS = new Set(['advance', 'hold', 'rollback']);
653
+ /**
654
+ * One-reader closed-ledger restore for non-fresh `_bootAutoloop`.
655
+ *
656
+ * Derives typed `iter` / `metric_history` from the contiguous valid closed
657
+ * prefix starting at 0. Checkpoint bytes are never progress authority.
658
+ * Returns null when there is no valid prefix (legacy lone verdict, empty).
659
+ * Throws {@link AUTOLOOP_CLOSED_LEDGER_REFUSAL} on gap / malformed tail.
660
+ */
661
+ /** Canonical numeric iter directory names — no leading zeros. */
662
+ const CLOSED_LEDGER_ITER_DIR = /^(0|[1-9][0-9]*)$/;
663
+ function readClosedLedgerRestore(ledgerDir) {
664
+ const iterRoot = path.join(ledgerDir, 'iter');
665
+ if (!fs.existsSync(iterRoot))
666
+ return null;
667
+ let numericDirs;
668
+ try {
669
+ numericDirs = fs
670
+ .readdirSync(iterRoot)
671
+ .filter((name) => CLOSED_LEDGER_ITER_DIR.test(name))
672
+ .map((name) => Number(name))
673
+ .sort((a, b) => a - b);
674
+ }
675
+ catch {
676
+ return null;
677
+ }
678
+ if (numericDirs.length === 0)
679
+ return null;
680
+ const present = new Set(numericDirs);
681
+ const metrics = [];
682
+ let i = 0;
683
+ for (;;) {
684
+ const iterDir = path.join(iterRoot, String(i));
685
+ if (!fs.existsSync(iterDir)) {
686
+ // K=-1 / no valid closed prefix: never refuse on higher dirs (base Planner-only).
687
+ if (i === 0)
688
+ return null;
689
+ for (const j of present) {
690
+ if (j > i)
691
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
692
+ }
693
+ break;
694
+ }
695
+ const directivePath = path.join(iterDir, 'directive.json');
696
+ const verdictPath = path.join(iterDir, 'verdict.json');
697
+ const hasDirective = fs.existsSync(directivePath);
698
+ const hasVerdict = fs.existsSync(verdictPath);
699
+ if (!hasDirective) {
700
+ if (hasVerdict) {
701
+ // Legacy lone verdict (or directive-less "closed" appearance).
702
+ if (i === 0)
703
+ return null;
704
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
705
+ }
706
+ // Empty/non-closed at 0: prefixless compat before any gap check.
707
+ if (i === 0)
708
+ return null;
709
+ for (const j of present) {
710
+ if (j > i)
711
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
712
+ }
713
+ break;
714
+ }
715
+ if (!hasVerdict) {
716
+ // Open / directive-only / committed-without-review tail at i.
717
+ // No closed prefix yet: return null before gap checks (open0+lateriter1 compat).
718
+ if (i === 0)
719
+ return null;
720
+ for (const j of present) {
721
+ if (j > i)
722
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
723
+ }
724
+ const history = metrics.length > MAX_METRIC_HISTORY ? metrics.slice(metrics.length - MAX_METRIC_HISTORY) : metrics.slice();
725
+ return { iter: i, metric_history: history };
726
+ }
727
+ let raw;
728
+ try {
729
+ raw = fs.readFileSync(verdictPath, 'utf8');
730
+ }
731
+ catch {
732
+ // Prefixless unreadable verdict at 0: no-restore / no-refusal (base compat).
733
+ if (i === 0)
734
+ return null;
735
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
736
+ }
737
+ let parsed;
738
+ try {
739
+ parsed = JSON.parse(raw);
740
+ }
741
+ catch {
742
+ if (i === 0)
743
+ return null;
744
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
745
+ }
746
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
747
+ if (i === 0)
748
+ return null;
749
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
750
+ }
751
+ const verdict = parsed;
752
+ if (verdict.iter !== i) {
753
+ if (i === 0)
754
+ return null;
755
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
756
+ }
757
+ if (typeof verdict.decision !== 'string' || !REVIEWER_DECISIONS.has(verdict.decision)) {
758
+ if (i === 0)
759
+ return null;
760
+ throw new Error(AUTOLOOP_CLOSED_LEDGER_REFUSAL);
761
+ }
762
+ if (typeof verdict.metric === 'number' && Number.isFinite(verdict.metric)) {
763
+ metrics.push(verdict.metric);
764
+ }
765
+ i += 1;
766
+ }
767
+ if (i === 0)
768
+ return null;
769
+ const history = metrics.length > MAX_METRIC_HISTORY ? metrics.slice(metrics.length - MAX_METRIC_HISTORY) : metrics.slice();
770
+ return { iter: i, metric_history: history };
771
+ }
649
772
  export class SessionManager {
650
773
  sessions = new Map();
651
774
  _pendingSessions = new Map();
@@ -1299,7 +1422,7 @@ export class SessionManager {
1299
1422
  // its executor is registered here with a builder closed over `this`
1300
1423
  // rather than living in the kernel.
1301
1424
  kernel.setExecutor('autoloop', makeAutoloopExecutor({
1302
- boot: (config, secrets, { freshStart }) => this._bootAutoloop({
1425
+ boot: (config, secrets, { freshStart, restoredInput }) => this._bootAutoloop({
1303
1426
  ...config,
1304
1427
  // Custom-engine configs never reach the spec, so they come from
1305
1428
  // the run's in-memory secret bag — supplied at start, and
@@ -1310,6 +1433,7 @@ export class SessionManager {
1310
1433
  // Always set (including false on resume) — omitting false left a
1311
1434
  // `_freshStart: true` from config/secrets intact.
1312
1435
  _freshStart: freshStart,
1436
+ _restoredInput: restoredInput,
1313
1437
  }),
1314
1438
  ready: (key, value) => {
1315
1439
  const deferred = this._autoloopReady.get(key);
@@ -1846,46 +1970,35 @@ export class SessionManager {
1846
1970
  // Persist final state (TTL-expired sessions already removed by cleanup)
1847
1971
  this._savePersisted();
1848
1972
  }
1849
- // ─── Codex /goal helpers (codex-app engine only) ─────────────────────
1850
- /**
1851
- * Send a `/goal <args>` slash command to a `codex-app` session. Used by
1852
- * the `codex_goal_*` tools. The server-side parser interprets the slash
1853
- * command and emits goal-related notifications which the session class
1854
- * caches.
1855
- *
1856
- * Errors clearly when called against a non-`codex-app` session — those
1857
- * sessions cannot interpret `/goal` (the `codex exec` path has no slash
1858
- * command surface).
1859
- */
1860
- async codexGoalCommand(name, slashArgs, timeoutMs) {
1861
- const managed = this.sessions.get(name);
1862
- if (!managed)
1863
- throw new Error(`Session not found: ${name}`);
1864
- const session = managed.session;
1865
- if (typeof session.sendGoalCommand !== 'function') {
1866
- const engine = managed.config.engine || 'claude';
1867
- throw new Error(`Session "${name}" uses engine "${engine}" which does not support /goal. ` +
1868
- `Start a session with engine: "codex-app" to use the goal tools.`);
1869
- }
1870
- const result = await session.sendGoalCommand(slashArgs, timeoutMs);
1871
- return { ok: true, text: result.text, goal: result.goal };
1973
+ // ─── Codex goals (codex-app engine only) ─────────────────────────────
1974
+ /** The live `codex-app` session behind `name`, for the kernel's `goal` node. */
1975
+ codexGoalSession(name) {
1976
+ const session = this.sessions.get(name)?.session;
1977
+ return session instanceof PersistentCodexAppServerSession ? session : undefined;
1872
1978
  }
1873
1979
  /**
1874
- * Read the cached goal state from a `codex-app` session without sending
1875
- * any command. Returns null if no goal is set or the session has not yet
1876
- * received a `thread/goal/updated` notification.
1980
+ * Act on a `codex-app` session's goal through the app-server's
1981
+ * `thread/goal/*` RPCs. Used by the `codex_goal_*` tools; `get` asks the
1982
+ * server rather than a cache.
1983
+ *
1984
+ * Errors clearly when called against a non-`codex-app` session — `codex
1985
+ * exec` has no goal surface.
1877
1986
  */
1878
- codexGoalGet(name) {
1879
- const managed = this.sessions.get(name);
1880
- if (!managed)
1881
- throw new Error(`Session not found: ${name}`);
1882
- const session = managed.session;
1883
- if (!('goal' in session)) {
1884
- const engine = managed.config.engine || 'claude';
1885
- throw new Error(`Session "${name}" uses engine "${engine}" which does not track goal state. ` +
1886
- `Start a session with engine: "codex-app" to use the goal tools.`);
1987
+ async codexGoal(name, op) {
1988
+ const session = this._getCodexAppSession(name, '/goal');
1989
+ switch (op.action) {
1990
+ case 'set':
1991
+ return { ok: true, goal: await session.setGoal({ objective: op.objective, tokenBudget: op.tokenBudget }) };
1992
+ case 'pause':
1993
+ return { ok: true, goal: await session.setGoal({ status: 'paused' }) };
1994
+ case 'resume':
1995
+ return { ok: true, goal: await session.setGoal({ status: 'active' }) };
1996
+ case 'clear':
1997
+ await session.clearGoal();
1998
+ return { ok: true, goal: null };
1999
+ case 'get':
2000
+ return { ok: true, goal: await session.getGoal() };
1887
2001
  }
1888
- return { ok: true, goal: session.goal ?? null };
1889
2002
  }
1890
2003
  // ─── Codex app-server v2 RPCs (codex-app engine only, Codex 0.137) ────────
1891
2004
  //
@@ -2557,6 +2670,12 @@ export class SessionManager {
2557
2670
  if (engine === 'codex') {
2558
2671
  return session.threadId;
2559
2672
  }
2673
+ // `sessionId` on a codex-app session is a label of ours; `thread/resume`
2674
+ // needs the app-server's own thread id, and silently opened a new thread
2675
+ // when handed the label.
2676
+ if (engine === 'codex-app') {
2677
+ return session.codexThreadId;
2678
+ }
2560
2679
  return session.sessionId;
2561
2680
  }
2562
2681
  _storedResumeId(engine, id) {
@@ -2566,6 +2685,8 @@ export class SessionManager {
2566
2685
  return id && !/^codex-\d+-/.test(id) ? id : undefined;
2567
2686
  if (engine === 'grok')
2568
2687
  return id && !/^grok-\d+-/.test(id) ? id : undefined;
2688
+ if (engine === 'codex-app')
2689
+ return id && !id.startsWith('codex-app-') ? id : undefined;
2569
2690
  return id;
2570
2691
  }
2571
2692
  _listMdFiles(dir) {
@@ -3086,6 +3207,13 @@ export class SessionManager {
3086
3207
  if (!fs.existsSync(ledgerDir)) {
3087
3208
  fs.mkdirSync(ledgerDir, { recursive: true });
3088
3209
  }
3210
+ // Non-fresh boots: restore typed progress from the contiguous closed ledger
3211
+ // prefix before any dispatcher/runner/session work. Refusal throws here so
3212
+ // no Planner start or checkpoint publish occurs.
3213
+ let closedLedgerRestore = null;
3214
+ if (opts._freshStart !== true) {
3215
+ closedLedgerRestore = readClosedLedgerRestore(ledgerDir);
3216
+ }
3089
3217
  // Per-run policy object — mutable so Planner's update_push_policy is visible
3090
3218
  // to the runner without re-wiring.
3091
3219
  const pushPolicy = JSON.parse(JSON.stringify(DEFAULT_PUSH_POLICY));
@@ -3132,6 +3260,10 @@ export class SessionManager {
3132
3260
  run_id: opts.runId,
3133
3261
  workspace: opts.workspace,
3134
3262
  ledger_dir: ledgerDir,
3263
+ persistCheckpoint: () => {
3264
+ const publish = this._autoloopPublishers.get(runId);
3265
+ return publish ? publish() : false;
3266
+ },
3135
3267
  push_policy: pushPolicy,
3136
3268
  notifyUser: async (level, summary, detail, channel) => {
3137
3269
  const result = await notifyUserFallbackChain({
@@ -3156,7 +3288,18 @@ export class SessionManager {
3156
3288
  activityLeaseMs: opts.activityLeaseMs,
3157
3289
  autoloopHardTimeoutMs: opts.autoloopHardTimeoutMs,
3158
3290
  });
3291
+ // Background resume/deadline/stall paths emit 'error' after a void catch.
3292
+ // Without a listener, Node turns that into an unhandled rejection.
3293
+ runner.on('error', (err) => {
3294
+ this.logger.error?.(`[autoloop/${runId}] background runner task failed: ${boundedErrorReason(err)}`);
3295
+ });
3159
3296
  runnerRef = runner;
3297
+ if (closedLedgerRestore) {
3298
+ runner.state.iter = closedLedgerRestore.iter;
3299
+ runner.state.metric_history = closedLedgerRestore.metric_history.slice();
3300
+ }
3301
+ if (opts._restoredInput)
3302
+ runner.loadRestoredInput(opts._restoredInput);
3160
3303
  try {
3161
3304
  await runner.start();
3162
3305
  // A reconstructed migration becomes externally visible only after its
@@ -3164,7 +3307,6 @@ export class SessionManager {
3164
3307
  // this inside the startup try makes an append failure follow the same
3165
3308
  // cleanup path as any other failed resume.
3166
3309
  opts._commitTimeoutMigration?.();
3167
- await this._recoverCommittedCoderIteration(opts.workspace, ledgerDir, runId, runner, dispatcher);
3168
3310
  }
3169
3311
  catch (err) {
3170
3312
  try {
@@ -3178,7 +3320,17 @@ export class SessionManager {
3178
3320
  runner.stop();
3179
3321
  throw err;
3180
3322
  }
3181
- return { runner, dispatcher, ledgerDir, pushPolicy };
3323
+ // Committed-iteration recovery runs after publisher registration, the
3324
+ // initial-publish check and notice reconciliation (see afterPublishReconcile
3325
+ // in makeAutoloopExecutor) — never inside boot before that gate.
3326
+ return {
3327
+ runner,
3328
+ dispatcher,
3329
+ ledgerDir,
3330
+ pushPolicy,
3331
+ abort: () => runner.markCancelledByKernel(),
3332
+ afterPublishReconcile: () => this._recoverCommittedCoderIteration(opts.workspace, ledgerDir, runId, runner, dispatcher),
3333
+ };
3182
3334
  }
3183
3335
  /**
3184
3336
  * If the durable ledger has a committed Coder iteration with no Reviewer
@@ -3219,8 +3371,12 @@ export class SessionManager {
3219
3371
  return;
3220
3372
  this.logger.info?.(`[autoloop/${runId}] recovering committed iter ${n} into Reviewer`);
3221
3373
  await dispatcher.ensureReviewerForCommittedRecovery();
3222
- runner.markSubagentsSpawned();
3374
+ // Keep a reconciled chat_notice_pending pause so Planner-bound follow-ups
3375
+ // park until the notice is recorded; live spawn_subagents stays unchanged.
3376
+ runner.markSubagentsSpawned({ preservePause: true });
3223
3377
  runner.state.iter = n;
3378
+ // Queue only — never await the physical Reviewer turn. Readiness and the
3379
+ // cancellation observer must install before any model transport settles.
3224
3380
  void runner
3225
3381
  .send(AutoloopMsg.reviewRequest(n, {
3226
3382
  iter: n,
@@ -3308,12 +3464,21 @@ export class SessionManager {
3308
3464
  this._autoloopStarting.delete(tag);
3309
3465
  }
3310
3466
  }
3467
+ /**
3468
+ * Durably accept a chat message for a live run's Planner and return the
3469
+ * envelope to deliver. Throws, with nothing accepted, when the run cannot
3470
+ * checkpoint it. Internal: callers pass the result to `runner.send`.
3471
+ */
3472
+ acceptAutoloopChat(runId, text) {
3473
+ return this._liveAutoloop(runId).runner.acceptChat(text);
3474
+ }
3311
3475
  /**
3312
3476
  * Inject a user chat message into a v2 run's Planner. Returns the Planner's
3313
3477
  * natural-language reply.
3314
3478
  */
3315
3479
  async autoloopChat(runId, text) {
3316
3480
  const ctx = this._liveAutoloop(runId);
3481
+ const env = this.acceptAutoloopChat(runId, text);
3317
3482
  let reply = '';
3318
3483
  const onReply = (...args) => {
3319
3484
  const t = args[0];
@@ -3322,7 +3487,7 @@ export class SessionManager {
3322
3487
  };
3323
3488
  ctx.dispatcher.on('planner_reply', onReply);
3324
3489
  try {
3325
- await ctx.runner.send(AutoloopMsg.chat(ctx.runner.state.iter, { text }));
3490
+ await ctx.runner.send(env);
3326
3491
  }
3327
3492
  finally {
3328
3493
  ctx.dispatcher.off('planner_reply', onReply);
@@ -3433,8 +3598,16 @@ export class SessionManager {
3433
3598
  }
3434
3599
  const live = this.kernel.handle(runId, LEGACY_NODE);
3435
3600
  if (live) {
3436
- if (!hasTimeoutIncrease)
3601
+ if (!hasTimeoutIncrease) {
3602
+ // Ordinary and activity-lease pauses have no uncertain in-flight
3603
+ // dispatch. A pending send timeout stays the historical no-op until
3604
+ // an explicit timeout increase names that dispatch.
3605
+ if (live.runner.state.status === 'paused' && live.runner.state.pending_dispatch === null) {
3606
+ live.runner.resumePaused();
3607
+ this._autoloopPublishers.get(runId)?.();
3608
+ }
3437
3609
  return live.runner.state;
3610
+ }
3438
3611
  const pending = live.runner.state.pending_dispatch;
3439
3612
  if (live.runner.state.status !== 'paused' || !pending) {
3440
3613
  throw new Error(`Autoloop run '${runId}' is not awaiting a recoverable send timeout`);