@runfusion/fusion 0.14.2 → 0.14.3

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 (34) hide show
  1. package/dist/bin.js +739 -67
  2. package/dist/client/assets/AgentDetailView-BBCnqhqI.js +18 -0
  3. package/dist/client/assets/{AgentsView-DkX0tzrN.js → AgentsView-BY-Yq-Te.js} +3 -3
  4. package/dist/client/assets/{ChatView-CEm2Hw6m.js → ChatView-DkoJNxFW.js} +1 -1
  5. package/dist/client/assets/{DevServerView-Bumvo_ge.js → DevServerView-qvs6pp6c.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-CXN11cBp.js → DirectoryPicker-BkAIXNrP.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-B71IqAxA.js → DocumentsView-BcaUGgaL.js} +1 -1
  8. package/dist/client/assets/{InsightsView-Bs4Rldu6.js → InsightsView-Dz9Ivclw.js} +1 -1
  9. package/dist/client/assets/{MemoryView-Bs7b_L2Q.js → MemoryView-BsweARBT.js} +1 -1
  10. package/dist/client/assets/{NodesView-BvAGTXbO.js → NodesView-bAU-v4bJ.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-3Kcc4uhA.js → PiExtensionsManager-C_U2g7y3.js} +1 -1
  12. package/dist/client/assets/{PluginManager-Ch-Xynlm.js → PluginManager-pIDsTk5v.js} +1 -1
  13. package/dist/client/assets/{ResearchView-Bj6Saqf6.js → ResearchView-D4Eib_uR.js} +1 -1
  14. package/dist/client/assets/{RoadmapsView-9qT8Vwd0.js → RoadmapsView-BaGwsUGS.js} +1 -1
  15. package/dist/client/assets/{SettingsModal-D4ERGQNQ.js → SettingsModal-BiZVi3cI.js} +1 -1
  16. package/dist/client/assets/SettingsModal-CRyg643t.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-Dv0rX2_o.js → SetupWizardModal-BcIGBBpA.js} +1 -1
  18. package/dist/client/assets/{SkillMultiselect-CSkXQzdv.js → SkillMultiselect-DPARHJeQ.js} +1 -1
  19. package/dist/client/assets/{SkillsView-2srXMOzj.js → SkillsView-Da_d_HPu.js} +1 -1
  20. package/dist/client/assets/{TodoView-CxPPIvw2.js → TodoView-5rAeqYtV.js} +1 -1
  21. package/dist/client/assets/{folder-open-FA1PwpXV.js → folder-open-CgjcFqww.js} +1 -1
  22. package/dist/client/assets/{index-CEavim6l.js → index-DoQ5ALYY.js} +25 -25
  23. package/dist/client/assets/{list-checks-6EktkUso.js → list-checks-C9YWtF7h.js} +1 -1
  24. package/dist/client/assets/{star-B6Th07jw.js → star-4nUh67-U.js} +1 -1
  25. package/dist/client/assets/{upload-BJwuErhV.js → upload-CEt5-Bnq.js} +1 -1
  26. package/dist/client/assets/{users-BrnPTF8H.js → users-4I0JDmgO.js} +1 -1
  27. package/dist/client/index.html +1 -1
  28. package/dist/client/version.json +1 -1
  29. package/dist/extension.js +518 -51
  30. package/dist/pi-claude-cli/index.ts +2 -2
  31. package/dist/pi-claude-cli/package.json +1 -1
  32. package/package.json +1 -1
  33. package/dist/client/assets/AgentDetailView-C2Iik3Qf.js +0 -18
  34. package/dist/client/assets/SettingsModal-Zo5qDGOq.js +0 -31
package/dist/extension.js CHANGED
@@ -75,6 +75,7 @@ var init_settings_schema = __esm({
75
75
  favoriteModels: void 0,
76
76
  openrouterModelSync: true,
77
77
  updateCheckEnabled: true,
78
+ fnBinaryCheckEnabled: true,
78
79
  updateCheckFrequency: "daily",
79
80
  showGitHubStarButton: true,
80
81
  modelOnboardingComplete: void 0,
@@ -3290,6 +3291,12 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3290
3291
  if (!inMemory && !isAbsolute(fusionDir)) {
3291
3292
  throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
3292
3293
  }
3294
+ if (!inMemory && /\.fusion[\\/]\.fusion(?:[\\/]|$)/.test(fusionDir)) {
3295
+ throw new Error(
3296
+ `[fusion] Refusing to open Database at nested .fusion/.fusion path: ${fusionDir}
3297
+ This means a caller passed a .fusion directory where a project root was expected. Audit the call site for an extra \`join(rootDir, '.fusion')\` step.`
3298
+ );
3299
+ }
3293
3300
  if (!inMemory && !existsSync(fusionDir)) {
3294
3301
  mkdirSync(fusionDir, { recursive: true });
3295
3302
  }
@@ -6016,6 +6023,7 @@ var init_agent_store = __esm({
6016
6023
  };
6017
6024
  const line = JSON.stringify(safeEntry) + "\n";
6018
6025
  await appendFile(this.runLogPath(agentId, runId), line, "utf-8");
6026
+ this.emit("run:log", agentId, runId, safeEntry);
6019
6027
  }
6020
6028
  /**
6021
6029
  * Read all log entries for a given run from its JSONL file.
@@ -36342,12 +36350,16 @@ var init_gh_cli = __esm({
36342
36350
 
36343
36351
  // ../core/src/fn-binary.ts
36344
36352
  import { spawn as spawn2 } from "node:child_process";
36345
- import { platform as platform2 } from "node:os";
36353
+ import { platform as platform2, tmpdir as tmpdir2 } from "node:os";
36346
36354
  function runProbe(command, args, timeoutMs) {
36347
36355
  return new Promise((resolve19) => {
36348
36356
  let stdout = "";
36349
36357
  let stderr = "";
36350
- const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: false });
36358
+ const child = spawn2(command, args, {
36359
+ stdio: ["ignore", "pipe", "pipe"],
36360
+ shell: false,
36361
+ cwd: tmpdir2()
36362
+ });
36351
36363
  const timer = setTimeout(() => {
36352
36364
  try {
36353
36365
  child.kill("SIGKILL");
@@ -37665,7 +37677,7 @@ async function syncBackupAutomation(automationStore, settings) {
37665
37677
  if (!AutomationStore2.isValidCron(schedule)) {
37666
37678
  throw new Error(`Invalid backup schedule: ${schedule}`);
37667
37679
  }
37668
- const command = "npx runfusion.ai backup --create";
37680
+ const command = "fn backup --create";
37669
37681
  if (existingSchedule) {
37670
37682
  return await automationStore.updateSchedule(existingSchedule.id, {
37671
37683
  scheduleType: "custom",
@@ -37698,7 +37710,7 @@ async function syncBackupRoutine(routineStore, settings) {
37698
37710
  if (!RoutineStore2.isValidCron(schedule)) {
37699
37711
  throw new Error(`Invalid backup schedule: ${schedule}`);
37700
37712
  }
37701
- const command = "npx runfusion.ai backup --create";
37713
+ const command = "fn backup --create";
37702
37714
  const input = {
37703
37715
  name: BACKUP_SCHEDULE_NAME,
37704
37716
  description: "Automatic database backup based on project settings",
@@ -49573,7 +49585,7 @@ var require_dist3 = __commonJS({
49573
49585
 
49574
49586
  // ../core/src/agent-companies-parser.ts
49575
49587
  import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync4 } from "node:fs";
49576
- import { tmpdir as tmpdir2 } from "node:os";
49588
+ import { tmpdir as tmpdir3 } from "node:os";
49577
49589
  import { join as join20, resolve as resolve9 } from "node:path";
49578
49590
  function slugifyAgentReference(value) {
49579
49591
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -49896,7 +49908,7 @@ async function extractTarArchive(archivePath, outputDir) {
49896
49908
  }
49897
49909
  async function parseCompanyArchive(archivePath) {
49898
49910
  const resolvedArchivePath = resolve9(archivePath);
49899
- const tempDir = mkdtempSync(join20(tmpdir2(), "agent-companies-"));
49911
+ const tempDir = mkdtempSync(join20(tmpdir3(), "agent-companies-"));
49900
49912
  try {
49901
49913
  if (resolvedArchivePath.endsWith(".tar.gz") || resolvedArchivePath.endsWith(".tgz")) {
49902
49914
  await extractTarArchive(resolvedArchivePath, tempDir);
@@ -65008,6 +65020,20 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65008
65020
  );
65009
65021
  this.activeStepExecutors.delete(task.id);
65010
65022
  }
65023
+ if (this.activeWorkflowStepSessions.has(task.id)) {
65024
+ executorLog.log(`${task.id} moved from in-progress to ${to} \u2014 terminating workflow step session`);
65025
+ this.pausedAborted.add(task.id);
65026
+ this.options.stuckTaskDetector?.untrackTask(task.id);
65027
+ const workflowSession = this.activeWorkflowStepSessions.get(task.id);
65028
+ const sessionWithAbort = workflowSession;
65029
+ if (typeof sessionWithAbort.abort === "function") {
65030
+ void sessionWithAbort.abort().catch((err) => {
65031
+ executorLog.warn(`Failed to abort workflow step session for ${task.id}: ${err}`);
65032
+ });
65033
+ }
65034
+ workflowSession.dispose();
65035
+ this.activeWorkflowStepSessions.delete(task.id);
65036
+ }
65011
65037
  this.disposeSubagentsForTask(task.id, `parent moved from in-progress to ${to}`);
65012
65038
  this.loopRecoveryState.delete(task.id);
65013
65039
  this.spawnedAgents.delete(task.id);
@@ -65040,8 +65066,42 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65040
65066
  this.disposeSubagentsForTask(task.id, "task paused");
65041
65067
  return;
65042
65068
  }
65043
- if (!task.paused && task.column === "in-progress" && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id)) {
65069
+ if (task.paused && this.activeWorkflowStepSessions.has(task.id)) {
65070
+ executorLog.log(`Pausing ${task.id} \u2014 terminating workflow step session`);
65071
+ this.pausedAborted.add(task.id);
65072
+ this.options.stuckTaskDetector?.untrackTask(task.id);
65073
+ const workflowSession = this.activeWorkflowStepSessions.get(task.id);
65074
+ const sessionWithAbort = workflowSession;
65075
+ if (typeof sessionWithAbort.abort === "function") {
65076
+ await sessionWithAbort.abort().catch(
65077
+ (err) => executorLog.warn(`Failed to abort workflow step session for pause ${task.id}: ${err}`)
65078
+ );
65079
+ }
65080
+ workflowSession.dispose();
65081
+ this.activeWorkflowStepSessions.delete(task.id);
65082
+ this.loopRecoveryState.delete(task.id);
65083
+ this.spawnedAgents.delete(task.id);
65084
+ this.stuckAborted.delete(task.id);
65085
+ this.disposeSubagentsForTask(task.id, "task paused");
65086
+ return;
65087
+ }
65088
+ if (!task.paused && task.column === "in-progress" && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id) && !this.activeWorkflowStepSessions.has(task.id)) {
65044
65089
  if (!this.executing.has(task.id) && !this.resumingUnpaused.has(task.id) && !this.recoveringCompleted.has(task.id)) {
65090
+ const pauseLabel = await this.getExecutionPauseLabel();
65091
+ if (pauseLabel) {
65092
+ executorLog.log(`Skipping unpause resume for ${task.id} \u2014 ${pauseLabel} active`);
65093
+ return;
65094
+ }
65095
+ if (this.isTaskWorkComplete(task) && !task.mergeDetails) {
65096
+ this.recoveringCompleted.add(task.id);
65097
+ executorLog.log(`${task.id} unpaused with completed work and no session \u2014 recovering directly to in-review`);
65098
+ void this.recoverCompletedTask(task).catch(
65099
+ (err) => executorLog.error(`Failed to recover completed unpaused task ${task.id}:`, err)
65100
+ ).finally(() => {
65101
+ this.recoveringCompleted.delete(task.id);
65102
+ });
65103
+ return;
65104
+ }
65045
65105
  this.resumingUnpaused.add(task.id);
65046
65106
  executorLog.log(`Unpaused ${task.id} in-progress with no session \u2014 resuming execution`);
65047
65107
  try {
@@ -65160,6 +65220,22 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65160
65220
  this.spawnedAgents.delete(taskId);
65161
65221
  this.stuckAborted.delete(taskId);
65162
65222
  }
65223
+ for (const [taskId, workflowSession] of this.activeWorkflowStepSessions) {
65224
+ executorLog.log(`Global pause \u2014 terminating workflow step session for ${taskId}`);
65225
+ this.pausedAborted.add(taskId);
65226
+ this.options.stuckTaskDetector?.untrackTask(taskId);
65227
+ const sessionWithAbort = workflowSession;
65228
+ if (typeof sessionWithAbort.abort === "function") {
65229
+ void sessionWithAbort.abort().catch((err) => {
65230
+ executorLog.warn(`Failed to abort workflow step session for ${taskId}: ${err}`);
65231
+ });
65232
+ }
65233
+ workflowSession.dispose();
65234
+ this.activeWorkflowStepSessions.delete(taskId);
65235
+ this.loopRecoveryState.delete(taskId);
65236
+ this.spawnedAgents.delete(taskId);
65237
+ this.stuckAborted.delete(taskId);
65238
+ }
65163
65239
  }
65164
65240
  });
65165
65241
  }
@@ -65177,6 +65253,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65177
65253
  activeSessions = /* @__PURE__ */ new Map();
65178
65254
  /** Active step-session executors per task (mutually exclusive with activeSessions). */
65179
65255
  activeStepExecutors = /* @__PURE__ */ new Map();
65256
+ /** Active pre-merge workflow step sessions per task. */
65257
+ activeWorkflowStepSessions = /* @__PURE__ */ new Map();
65180
65258
  /**
65181
65259
  * Reviewer subagent sessions per task. Reviewers (`reviewer.ts`) create their
65182
65260
  * own AgentSessions that aren't part of `activeSessions`/`activeStepExecutors`,
@@ -65223,6 +65301,69 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65223
65301
  await this.store.mergeTask(taskId);
65224
65302
  return "merged";
65225
65303
  }
65304
+ async getExecutionPauseLabel() {
65305
+ const settings = await this.store.getSettings();
65306
+ if (settings.globalPause) return "global pause";
65307
+ if (settings.enginePaused) return "engine pause";
65308
+ return null;
65309
+ }
65310
+ async shouldDeferCompletionForGlobalPause(taskId, context) {
65311
+ const settings = await this.store.getSettings();
65312
+ if (!settings.globalPause) {
65313
+ return false;
65314
+ }
65315
+ this.clearCompletedTaskWatchdog(taskId);
65316
+ executorLog.log(`${taskId}: completion handoff deferred \u2014 global pause active (${context})`);
65317
+ await this.store.logEntry(
65318
+ taskId,
65319
+ `Completion handoff deferred \u2014 global pause active (${context})`,
65320
+ void 0,
65321
+ this.currentRunContext
65322
+ ).catch(() => void 0);
65323
+ return true;
65324
+ }
65325
+ async shouldDeferWorkflowStepCompletion(taskId, context) {
65326
+ let latestTask = null;
65327
+ try {
65328
+ latestTask = await this.store.getTask(taskId);
65329
+ } catch {
65330
+ latestTask = null;
65331
+ }
65332
+ if (latestTask?.paused || this.pausedAborted.has(taskId)) {
65333
+ this.clearCompletedTaskWatchdog(taskId);
65334
+ executorLog.log(`${taskId}: completion handoff deferred \u2014 task paused (${context})`);
65335
+ await this.store.logEntry(
65336
+ taskId,
65337
+ `Completion handoff deferred \u2014 task paused (${context})`,
65338
+ void 0,
65339
+ this.currentRunContext
65340
+ ).catch(() => void 0);
65341
+ return true;
65342
+ }
65343
+ return this.shouldDeferCompletionForGlobalPause(taskId, context);
65344
+ }
65345
+ async parkTaskAfterWorkflowStepPause(taskId) {
65346
+ let latestTask = null;
65347
+ try {
65348
+ latestTask = await this.store.getTask(taskId);
65349
+ } catch {
65350
+ latestTask = null;
65351
+ }
65352
+ if (!latestTask?.paused) {
65353
+ return false;
65354
+ }
65355
+ executorLog.log(`${taskId}: workflow step interrupted by task pause \u2014 moving to todo`);
65356
+ await this.store.logEntry(
65357
+ taskId,
65358
+ "Execution paused during pre-merge workflow step \u2014 moved to todo",
65359
+ void 0,
65360
+ this.currentRunContext
65361
+ ).catch(() => void 0);
65362
+ if (latestTask.column === "in-progress") {
65363
+ await this.store.moveTask(taskId, "todo", { preserveResumeState: true });
65364
+ }
65365
+ return true;
65366
+ }
65226
65367
  /** Child agent sessions keyed by agent ID. Used for termination. */
65227
65368
  childSessions = /* @__PURE__ */ new Map();
65228
65369
  /** Total count of currently spawned agents (across all parents). */
@@ -65417,11 +65558,15 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65417
65558
  this.clearCompletedTaskWatchdog(taskId);
65418
65559
  const handle = setTimeout(async () => {
65419
65560
  this.completedTaskWatchdogs.delete(taskId);
65420
- if (this.recoveringCompleted.has(taskId) || this.executing.has(taskId) || this.activeSessions.has(taskId) || this.activeStepExecutors.has(taskId) || this.resumingUnpaused.has(taskId)) {
65561
+ if (this.recoveringCompleted.has(taskId) || this.executing.has(taskId) || this.activeSessions.has(taskId) || this.activeStepExecutors.has(taskId) || this.activeWorkflowStepSessions.has(taskId) || this.resumingUnpaused.has(taskId)) {
65421
65562
  return;
65422
65563
  }
65423
65564
  this.recoveringCompleted.add(taskId);
65424
65565
  try {
65566
+ const pauseLabel = await this.getExecutionPauseLabel();
65567
+ if (pauseLabel) {
65568
+ return;
65569
+ }
65425
65570
  let currentTask = null;
65426
65571
  try {
65427
65572
  currentTask = await this.store.getTask(taskId);
@@ -65467,6 +65612,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65467
65612
  * stuck.
65468
65613
  */
65469
65614
  async performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState = true) {
65615
+ const pauseLabel = await this.getExecutionPauseLabel();
65616
+ if (pauseLabel) {
65617
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 ${pauseLabel} active`);
65618
+ return "deferred-paused";
65619
+ }
65470
65620
  if (this.workflowRerunPending.has(taskId)) {
65471
65621
  executorLog.warn(`${taskId}: workflow rerun bounce already in flight \u2014 skipping re-entry`);
65472
65622
  return "skipped-pending";
@@ -65477,6 +65627,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65477
65627
  if (!latestTask) {
65478
65628
  throw new Error("task missing during workflow rerun bounce");
65479
65629
  }
65630
+ if (latestTask.paused) {
65631
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 task is paused`);
65632
+ return "deferred-paused";
65633
+ }
65480
65634
  if (latestTask.column === "in-progress") {
65481
65635
  const originalExecutionStartedAt = latestTask.executionStartedAt;
65482
65636
  if (preserveResumeState) {
@@ -65488,11 +65642,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65488
65642
  worktree: worktreePath,
65489
65643
  executionStartedAt: originalExecutionStartedAt ?? null
65490
65644
  });
65645
+ const pauseLabelAfterTodo = await this.getExecutionPauseLabel();
65646
+ if (pauseLabelAfterTodo) {
65647
+ executorLog.log(`${taskId}: workflow rerun parked in todo \u2014 ${pauseLabelAfterTodo} became active during bounce`);
65648
+ return "deferred-paused";
65649
+ }
65491
65650
  await this.store.moveTask(taskId, "in-progress");
65492
65651
  return "bounced";
65493
65652
  }
65494
65653
  if (latestTask.column === "todo") {
65495
65654
  await this.store.updateTask(taskId, { worktree: worktreePath });
65655
+ const pauseLabelBeforeResume = await this.getExecutionPauseLabel();
65656
+ if (pauseLabelBeforeResume) {
65657
+ executorLog.log(`${taskId}: workflow rerun parked in todo \u2014 ${pauseLabelBeforeResume} became active before resume`);
65658
+ return "deferred-paused";
65659
+ }
65496
65660
  await this.store.moveTask(taskId, "in-progress");
65497
65661
  return "bounced";
65498
65662
  }
@@ -65508,8 +65672,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65508
65672
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
65509
65673
  if (outcome === "bounced") {
65510
65674
  executorLog.log(successMessage);
65511
- } else {
65675
+ } else if (outcome === "skipped-pending") {
65512
65676
  executorLog.warn(`${taskId}: rerun bounce skipped \u2014 another bounce already in flight`);
65677
+ } else {
65678
+ executorLog.log(`${taskId}: rerun bounce deferred while pause is active`);
65513
65679
  }
65514
65680
  } catch (err) {
65515
65681
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -65518,6 +65684,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65518
65684
  }, 0);
65519
65685
  const watchdog = setTimeout(async () => {
65520
65686
  this.workflowRerunWatchdogs.delete(taskId);
65687
+ const pauseLabel = await this.getExecutionPauseLabel();
65688
+ if (pauseLabel) {
65689
+ executorLog.log(`${taskId}: workflow rerun watchdog skipped \u2014 ${pauseLabel} active`);
65690
+ return;
65691
+ }
65521
65692
  let currentTask = null;
65522
65693
  try {
65523
65694
  currentTask = await this.store.getTask(taskId);
@@ -65540,7 +65711,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65540
65711
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
65541
65712
  if (outcome === "bounced") {
65542
65713
  executorLog.warn(`${taskId}: workflow rerun watchdog retry succeeded`);
65543
- } else {
65714
+ } else if (outcome === "skipped-pending") {
65544
65715
  executorLog.error(
65545
65716
  `${taskId}: workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
65546
65717
  );
@@ -65548,6 +65719,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65548
65719
  taskId,
65549
65720
  `Workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
65550
65721
  ).catch(() => void 0);
65722
+ } else {
65723
+ executorLog.log(`${taskId}: workflow rerun watchdog retry deferred while pause is active`);
65551
65724
  }
65552
65725
  } catch (err) {
65553
65726
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -65670,11 +65843,17 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65670
65843
  */
65671
65844
  async recoverCompletedTask(task) {
65672
65845
  try {
65673
- if (this.executing.has(task.id) || this.activeSessions.has(task.id) || this.activeStepExecutors.has(task.id) || this.resumingUnpaused.has(task.id)) {
65846
+ if (this.executing.has(task.id) || this.activeSessions.has(task.id) || this.activeStepExecutors.has(task.id) || this.activeWorkflowStepSessions.has(task.id) || this.resumingUnpaused.has(task.id)) {
65674
65847
  executorLog.log(`${task.id}: skipping recoverCompletedTask \u2014 task has active execution in flight`);
65675
65848
  return false;
65676
65849
  }
65677
65850
  const settings = await this.store.getSettings();
65851
+ if (settings.globalPause || settings.enginePaused) {
65852
+ executorLog.log(
65853
+ `${task.id}: skipping recoverCompletedTask \u2014 ${settings.globalPause ? "global pause" : "engine pause"} active`
65854
+ );
65855
+ return false;
65856
+ }
65678
65857
  if (task.worktree && existsSync27(task.worktree)) {
65679
65858
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
65680
65859
  if (modifiedFiles.length > 0) {
@@ -65682,7 +65861,16 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65682
65861
  executorLog.log(`${task.id}: recovered ${modifiedFiles.length} modified files`);
65683
65862
  }
65684
65863
  if (task.executionMode !== "fast") {
65864
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps during completed-task recovery")) {
65865
+ return false;
65866
+ }
65685
65867
  const workflowResult = await this.runWorkflowSteps(task, task.worktree, settings);
65868
+ if (workflowResult === "deferred-paused") {
65869
+ if (this.pausedAborted.has(task.id)) {
65870
+ this.pausedAborted.delete(task.id);
65871
+ }
65872
+ return false;
65873
+ }
65686
65874
  if (!workflowResult.allPassed) {
65687
65875
  await this.sendTaskBackForFix(task, task.worktree, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed during recovery", false);
65688
65876
  return true;
@@ -65691,6 +65879,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65691
65879
  executorLog.log(`${task.id}: fast mode \u2014 skipping workflow steps on auto-recovery`);
65692
65880
  }
65693
65881
  }
65882
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition during completed-task recovery")) {
65883
+ return false;
65884
+ }
65694
65885
  await this.persistTokenUsage(task.id);
65695
65886
  await this.store.moveTask(task.id, "in-review");
65696
65887
  this.clearCompletedTaskWatchdog(task.id);
@@ -65756,6 +65947,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65756
65947
  * directly to in-review without spawning a new agent session.
65757
65948
  */
65758
65949
  async resumeOrphaned() {
65950
+ const settings = await this.store.getSettings();
65951
+ if (settings.globalPause || settings.enginePaused) {
65952
+ executorLog.log(
65953
+ `resumeOrphaned skipped \u2014 ${settings.globalPause ? "global pause" : "engine pause"} is active`
65954
+ );
65955
+ return;
65956
+ }
65759
65957
  const tasks = await this.store.listTasks({ slim: true, column: "in-progress" });
65760
65958
  const inProgress = tasks.filter(
65761
65959
  (t) => t.column === "in-progress" && !this.executing.has(t.id) && !t.paused
@@ -66194,8 +66392,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66194
66392
  await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
66195
66393
  }
66196
66394
  this.scheduleCompletedTaskWatchdog(task.id, "step-session completion");
66395
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
66396
+ return;
66397
+ }
66197
66398
  if (executionMode !== "fast") {
66198
66399
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
66400
+ if (workflowResult === "deferred-paused") {
66401
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
66402
+ this.pausedAborted.delete(task.id);
66403
+ return;
66404
+ }
66405
+ if (this.pausedAborted.has(task.id)) {
66406
+ this.pausedAborted.delete(task.id);
66407
+ }
66408
+ return;
66409
+ }
66199
66410
  if (!workflowResult.allPassed) {
66200
66411
  if (workflowResult.revisionRequested) {
66201
66412
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66213,6 +66424,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66213
66424
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66214
66425
  }
66215
66426
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
66427
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after step-session completion")) {
66428
+ return;
66429
+ }
66216
66430
  await this.store.moveTask(task.id, "in-review");
66217
66431
  this.clearCompletedTaskWatchdog(task.id);
66218
66432
  await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
@@ -66565,6 +66779,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66565
66779
  this.pausedAborted.delete(task.id);
66566
66780
  wasPaused = true;
66567
66781
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
66782
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
66783
+ return;
66784
+ }
66568
66785
  executorLog.log(`${task.id} paused after completion (graceful session exit) \u2014 finalizing to in-review`);
66569
66786
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review");
66570
66787
  await this.persistTokenUsage(task.id);
@@ -66601,8 +66818,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66601
66818
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
66602
66819
  }
66603
66820
  this.scheduleCompletedTaskWatchdog(task.id, "task completion");
66821
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion")) {
66822
+ return;
66823
+ }
66604
66824
  if (executionMode !== "fast") {
66605
66825
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
66826
+ if (workflowResult === "deferred-paused") {
66827
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
66828
+ this.pausedAborted.delete(task.id);
66829
+ wasPaused = true;
66830
+ return;
66831
+ }
66832
+ if (this.pausedAborted.has(task.id)) {
66833
+ this.pausedAborted.delete(task.id);
66834
+ wasPaused = true;
66835
+ }
66836
+ return;
66837
+ }
66606
66838
  if (!workflowResult.allPassed) {
66607
66839
  if (workflowResult.revisionRequested) {
66608
66840
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66620,6 +66852,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66620
66852
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66621
66853
  }
66622
66854
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
66855
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after task completion")) {
66856
+ return;
66857
+ }
66623
66858
  await this.persistTokenUsage(task.id);
66624
66859
  await this.store.moveTask(task.id, "in-review");
66625
66860
  this.clearCompletedTaskWatchdog(task.id);
@@ -66744,8 +66979,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66744
66979
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
66745
66980
  }
66746
66981
  this.scheduleCompletedTaskWatchdog(task.id, "task completion retry");
66982
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion retry")) {
66983
+ return;
66984
+ }
66747
66985
  if (executionMode !== "fast") {
66748
66986
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
66987
+ if (workflowResult === "deferred-paused") {
66988
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
66989
+ this.pausedAborted.delete(task.id);
66990
+ wasPaused = true;
66991
+ return;
66992
+ }
66993
+ if (this.pausedAborted.has(task.id)) {
66994
+ this.pausedAborted.delete(task.id);
66995
+ wasPaused = true;
66996
+ }
66997
+ return;
66998
+ }
66749
66999
  if (!workflowResult.allPassed) {
66750
67000
  if (workflowResult.revisionRequested) {
66751
67001
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66759,6 +67009,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66759
67009
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66760
67010
  }
66761
67011
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
67012
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after task completion retry")) {
67013
+ return;
67014
+ }
66762
67015
  await this.persistTokenUsage(task.id);
66763
67016
  await this.store.moveTask(task.id, "in-review");
66764
67017
  this.clearCompletedTaskWatchdog(task.id);
@@ -66851,6 +67104,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66851
67104
  } else if (this.pausedAborted.has(task.id)) {
66852
67105
  this.pausedAborted.delete(task.id);
66853
67106
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
67107
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
67108
+ return;
67109
+ }
66854
67110
  executorLog.log(`${task.id} paused after completion \u2014 finalizing to in-review`);
66855
67111
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review", void 0, this.currentRunContext);
66856
67112
  await this.persistTokenUsage(task.id);
@@ -67209,22 +67465,28 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67209
67465
  if (params.summary) {
67210
67466
  await store.updateTask(taskId, { summary: params.summary });
67211
67467
  }
67212
- await store.updateTask(taskId, { paused: false, status: null });
67468
+ const settings = await store.getSettings();
67469
+ const hardPauseActive = Boolean(task.paused || settings.globalPause);
67470
+ if (hardPauseActive) {
67471
+ await store.updateTask(taskId, { status: null });
67472
+ } else {
67473
+ await store.updateTask(taskId, { paused: false, status: null });
67474
+ }
67213
67475
  await store.logEntry(taskId, "Task marked done by agent");
67214
67476
  const latestTask = await store.getTask(taskId);
67215
67477
  let latestColumn = latestTask.column;
67216
67478
  if (latestColumn === "todo") {
67217
67479
  await store.logEntry(
67218
67480
  taskId,
67219
- "fn_task_done called while task was in todo \u2014 promoting to in-progress before completion handoff"
67481
+ hardPauseActive ? "fn_task_done called while task was in todo during pause \u2014 promoting to in-progress for deferred completion handoff" : "fn_task_done called while task was in todo \u2014 promoting to in-progress before completion handoff"
67220
67482
  );
67221
67483
  await store.moveTask(taskId, "in-progress");
67222
67484
  latestColumn = "in-progress";
67223
67485
  }
67224
- if (latestColumn === "in-progress") {
67486
+ if (latestColumn === "in-progress" && !hardPauseActive) {
67225
67487
  this.scheduleCompletedTaskWatchdog(taskId, "fn_task_done");
67226
67488
  }
67227
- const successMessage = params.summary ? "Task marked complete with summary. All steps done. Moving to in-review." : "Task marked complete. All steps done. Moving to in-review.";
67489
+ const successMessage = hardPauseActive ? "Task marked complete. Completion handoff deferred until pause is cleared." : params.summary ? "Task marked complete with summary. All steps done. Moving to in-review." : "Task marked complete. All steps done. Moving to in-review.";
67228
67490
  return {
67229
67491
  content: [{ type: "text", text: successMessage }],
67230
67492
  details: {}
@@ -67799,6 +68061,9 @@ ${failureFeedback}
67799
68061
  await this.store.updateTask(task.id, { workflowStepResults: results });
67800
68062
  continue;
67801
68063
  }
68064
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `before workflow step '${ws.name}'`)) {
68065
+ return "deferred-paused";
68066
+ }
67802
68067
  await this.store.logEntry(task.id, `[pre-merge] Starting workflow step: ${ws.name} (${stepMode} mode)`);
67803
68068
  executorLog.log(`${task.id} \u2014 [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
67804
68069
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -67813,6 +68078,9 @@ ${failureFeedback}
67813
68078
  await this.store.updateTask(task.id, { workflowStepResults: results });
67814
68079
  try {
67815
68080
  const result = stepMode === "script" ? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings) : await this.executeWorkflowStep(task, ws, worktreePath, settings);
68081
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68082
+ return "deferred-paused";
68083
+ }
67816
68084
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
67817
68085
  if (result.success) {
67818
68086
  await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
@@ -67878,6 +68146,9 @@ ${failureFeedback}
67878
68146
  };
67879
68147
  }
67880
68148
  } catch (err) {
68149
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68150
+ return "deferred-paused";
68151
+ }
67881
68152
  const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
67882
68153
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
67883
68154
  await this.store.logEntry(
@@ -68043,6 +68314,7 @@ and show an appropriate message to the user.\`
68043
68314
  task.id,
68044
68315
  `Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`
68045
68316
  );
68317
+ this.activeWorkflowStepSessions.set(task.id, session);
68046
68318
  let output = "";
68047
68319
  session.subscribe((event) => {
68048
68320
  if (event.type === "message_update") {
@@ -68115,6 +68387,10 @@ Review the work done in this worktree and evaluate it against the criteria in yo
68115
68387
  return { success: false, error: errorMessage };
68116
68388
  } finally {
68117
68389
  if (timeoutHandle) clearTimeout(timeoutHandle);
68390
+ const activeWorkflowStepSession = this.activeWorkflowStepSessions.get(task.id);
68391
+ if (activeWorkflowStepSession === session) {
68392
+ this.activeWorkflowStepSessions.delete(task.id);
68393
+ }
68118
68394
  void timedOut;
68119
68395
  }
68120
68396
  };
@@ -69858,6 +70134,15 @@ var init_scheduler = __esm({
69858
70134
  schedulerLog.log(`Task ${task.id} is paused \u2014 skipping dispatch`);
69859
70135
  continue;
69860
70136
  }
70137
+ const latestSettings = await this.store.getSettings();
70138
+ if (latestSettings.globalPause) {
70139
+ schedulerLog.log(`Task ${task.id} dispatch aborted \u2014 globalPause became active mid-pass`);
70140
+ continue;
70141
+ }
70142
+ if (latestSettings.enginePaused) {
70143
+ schedulerLog.log(`Task ${task.id} dispatch aborted \u2014 enginePaused became active mid-pass`);
70144
+ continue;
70145
+ }
69861
70146
  let effectiveNode = resolveEffectiveNode(freshTask, settings);
69862
70147
  schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
69863
70148
  if (effectiveNode.nodeId !== void 0 && this.options.nodeHealthMonitor) {
@@ -74995,6 +75280,36 @@ function execCommand(command, options) {
74995
75280
  });
74996
75281
  });
74997
75282
  }
75283
+ function isInProcessBackupCommand(command) {
75284
+ if (!command) return false;
75285
+ const trimmed = command.trim();
75286
+ if (!trimmed) return false;
75287
+ if (SHELL_METACHARACTERS_REGEX.test(trimmed)) return false;
75288
+ const tokens = trimmed.split(/\s+/).map((tok) => tok.toLowerCase());
75289
+ let cursor = 0;
75290
+ if (tokens[cursor] === "npx") {
75291
+ cursor += 1;
75292
+ while (cursor < tokens.length) {
75293
+ const tok = tokens[cursor];
75294
+ if (tok === void 0 || !tok.startsWith("-")) break;
75295
+ const takesValue = (tok === "-p" || tok === "--package") && cursor + 1 < tokens.length && tokens[cursor + 1] !== void 0 && !tokens[cursor + 1].startsWith("-");
75296
+ cursor += takesValue ? 2 : 1;
75297
+ }
75298
+ }
75299
+ const binary = tokens[cursor];
75300
+ if (!binary || !FUSION_BINARY_TOKENS.has(binary)) return false;
75301
+ cursor += 1;
75302
+ if (tokens[cursor] !== "backup") return false;
75303
+ cursor += 1;
75304
+ if (tokens[cursor] !== "--create") return false;
75305
+ cursor += 1;
75306
+ for (; cursor < tokens.length; cursor += 1) {
75307
+ const tok = tokens[cursor];
75308
+ if (!tok) continue;
75309
+ if (!tok.startsWith("-")) return false;
75310
+ }
75311
+ return true;
75312
+ }
74998
75313
  async function createAiPromptExecutor(cwd) {
74999
75314
  const disposeLog = createLogger2("cron-runner");
75000
75315
  return async (prompt, modelProvider, modelId) => {
@@ -75034,7 +75349,7 @@ function truncateOutput(stdout, stderr) {
75034
75349
  }
75035
75350
  return combined;
75036
75351
  }
75037
- var log14, DEFAULT_TIMEOUT_MS6, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
75352
+ var log14, FUSION_BINARY_TOKENS, SHELL_METACHARACTERS_REGEX, DEFAULT_TIMEOUT_MS6, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
75038
75353
  var init_cron_runner = __esm({
75039
75354
  "../engine/src/cron-runner.ts"() {
75040
75355
  "use strict";
@@ -75043,6 +75358,14 @@ var init_cron_runner = __esm({
75043
75358
  init_shell_utils();
75044
75359
  init_pi();
75045
75360
  log14 = createLogger2("cron-runner");
75361
+ FUSION_BINARY_TOKENS = /* @__PURE__ */ new Set([
75362
+ "fn",
75363
+ "fusion",
75364
+ "runfusion",
75365
+ "runfusion.ai",
75366
+ "@runfusion/fusion"
75367
+ ]);
75368
+ SHELL_METACHARACTERS_REGEX = /[&|;<>`$()]/;
75046
75369
  DEFAULT_TIMEOUT_MS6 = 5 * 60 * 1e3;
75047
75370
  MAX_BUFFER = 1024 * 1024;
75048
75371
  MAX_OUTPUT_LENGTH = 10 * 1024;
@@ -75183,6 +75506,9 @@ var init_cron_runner = __esm({
75183
75506
  */
75184
75507
  async executeLegacyCommand(schedule, startedAt) {
75185
75508
  log14.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
75509
+ if (isInProcessBackupCommand(schedule.command)) {
75510
+ return this.executeBackupInProcess(schedule, startedAt);
75511
+ }
75186
75512
  try {
75187
75513
  const timeoutMs = schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
75188
75514
  const { stdout, stderr } = await execCommand(schedule.command, {
@@ -75213,6 +75539,47 @@ var init_cron_runner = __esm({
75213
75539
  };
75214
75540
  }
75215
75541
  }
75542
+ /**
75543
+ * Run an auto-backup schedule in-process via the engine's open TaskStore,
75544
+ * bypassing the shell-out that would otherwise invoke an outdated fusion
75545
+ * binary on PATH. See `isInProcessBackupCommand` for the matching contract.
75546
+ */
75547
+ async executeBackupInProcess(schedule, startedAt) {
75548
+ const action = await this.runBackupActionInProcess();
75549
+ if (action.success) {
75550
+ log14.log(`\u2713 ${schedule.name} completed in-process`);
75551
+ } else {
75552
+ log14.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
75553
+ }
75554
+ return {
75555
+ success: action.success,
75556
+ output: action.output,
75557
+ error: action.error,
75558
+ startedAt,
75559
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
75560
+ };
75561
+ }
75562
+ /**
75563
+ * Shared in-process backup execution used by both the legacy-command path
75564
+ * and the command-step path. Returns the success/output/error tuple in
75565
+ * a shape that callers can wrap into either a run or a step result.
75566
+ */
75567
+ async runBackupActionInProcess() {
75568
+ try {
75569
+ const { runBackupCommand: runBackupCommand2 } = await Promise.resolve().then(() => (init_src(), src_exports));
75570
+ const fusionDir = this.store.getFusionDir();
75571
+ const settings = await this.store.getSettings();
75572
+ const result = await runBackupCommand2(fusionDir, settings);
75573
+ return {
75574
+ success: result.success,
75575
+ output: truncateOutput(result.output ?? "", ""),
75576
+ error: result.success ? void 0 : result.output
75577
+ };
75578
+ } catch (err) {
75579
+ const message = err instanceof Error ? err.message : String(err);
75580
+ return { success: false, output: "", error: message };
75581
+ }
75582
+ }
75216
75583
  /**
75217
75584
  * Execute multiple steps sequentially.
75218
75585
  * Aggregates per-step results into an overall AutomationRunResult.
@@ -75301,6 +75668,19 @@ var init_cron_runner = __esm({
75301
75668
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
75302
75669
  };
75303
75670
  }
75671
+ if (isInProcessBackupCommand(step.command)) {
75672
+ const action = await this.runBackupActionInProcess();
75673
+ return {
75674
+ stepId: step.id,
75675
+ stepName: step.name,
75676
+ stepIndex,
75677
+ success: action.success,
75678
+ output: action.output,
75679
+ error: action.error,
75680
+ startedAt,
75681
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
75682
+ };
75683
+ }
75304
75684
  try {
75305
75685
  const { stdout, stderr } = await execCommand(step.command, {
75306
75686
  timeout: timeoutMs,
@@ -75492,6 +75872,7 @@ var init_routine_runner = __esm({
75492
75872
  "../engine/src/routine-runner.ts"() {
75493
75873
  "use strict";
75494
75874
  import_cron_parser4 = __toESM(require_dist2(), 1);
75875
+ init_cron_runner();
75495
75876
  init_logger2();
75496
75877
  init_shell_utils();
75497
75878
  log15 = createLogger2("routine-runner");
@@ -75649,6 +76030,30 @@ var init_routine_runner = __esm({
75649
76030
  return this.executeCommand(routine.command ?? "", routine.timeoutMs, startedAt);
75650
76031
  }
75651
76032
  async executeCommand(command, timeoutMs, startedAt) {
76033
+ if (isInProcessBackupCommand(command) && this.options.taskStore) {
76034
+ try {
76035
+ const { runBackupCommand: runBackupCommand2 } = await Promise.resolve().then(() => (init_src(), src_exports));
76036
+ const fusionDir = this.options.taskStore.getFusionDir();
76037
+ const settings = await this.options.taskStore.getSettings();
76038
+ const result = await runBackupCommand2(fusionDir, settings);
76039
+ return {
76040
+ success: result.success,
76041
+ output: truncateOutput2(result.output ?? "", ""),
76042
+ error: result.success ? void 0 : result.output,
76043
+ startedAt,
76044
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76045
+ };
76046
+ } catch (err) {
76047
+ const message = err instanceof Error ? err.message : String(err);
76048
+ return {
76049
+ success: false,
76050
+ output: "",
76051
+ error: message,
76052
+ startedAt,
76053
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76054
+ };
76055
+ }
76056
+ }
75652
76057
  try {
75653
76058
  const { stdout, stderr } = await execAsync6(command, {
75654
76059
  timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS7,
@@ -76440,6 +76845,13 @@ var init_self_healing = __esm({
76440
76845
  * stale in-progress/planning tasks that no longer have a live worker.
76441
76846
  */
76442
76847
  async runStartupRecovery() {
76848
+ const settings = await this.store.getSettings();
76849
+ if (settings.globalPause || settings.enginePaused) {
76850
+ log16.log(
76851
+ `Startup recovery skipped \u2014 ${settings.globalPause ? "global pause" : "engine pause"} is active`
76852
+ );
76853
+ return;
76854
+ }
76443
76855
  const steps = [
76444
76856
  { name: "no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures().then(() => void 0) },
76445
76857
  { name: "completed-tasks", fn: () => this.recoverCompletedTasks().then(() => void 0) },
@@ -76790,27 +77202,34 @@ var init_self_healing = __esm({
76790
77202
  log16.error(`Maintenance batch 1 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
76791
77203
  }
76792
77204
  }
76793
- const batch2Fns = [
76794
- { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
76795
- { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
76796
- { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
76797
- { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
76798
- { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
76799
- { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
76800
- { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
76801
- { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
76802
- { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
76803
- { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
76804
- { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
76805
- { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
76806
- { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
76807
- ];
76808
- for (const fn of batch2Fns) {
76809
- try {
76810
- await fn.fn();
76811
- log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
76812
- } catch (stepErr) {
76813
- log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77205
+ const recoverySettings = await this.store.getSettings();
77206
+ if (recoverySettings.globalPause || recoverySettings.enginePaused) {
77207
+ log16.log(
77208
+ `Maintenance batch 2 skipped \u2014 ${recoverySettings.globalPause ? "global pause" : "engine pause"} is active`
77209
+ );
77210
+ } else {
77211
+ const batch2Fns = [
77212
+ { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
77213
+ { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
77214
+ { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
77215
+ { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
77216
+ { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
77217
+ { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
77218
+ { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
77219
+ { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
77220
+ { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
77221
+ { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
77222
+ { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
77223
+ { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
77224
+ { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
77225
+ ];
77226
+ for (const fn of batch2Fns) {
77227
+ try {
77228
+ await fn.fn();
77229
+ log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
77230
+ } catch (stepErr) {
77231
+ log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77232
+ }
76814
77233
  }
76815
77234
  }
76816
77235
  const batch3Fns = [
@@ -78543,6 +78962,10 @@ var init_in_process_runtime = __esm({
78543
78962
  * before `start()` via `setMergeEnqueuer`.
78544
78963
  */
78545
78964
  mergeEnqueuer;
78965
+ /** Tracks whether startup recovery was intentionally deferred due to pause state. */
78966
+ startupRecoveryDeferred = false;
78967
+ /** Prevent duplicate unpause recovery dispatches from racing each other. */
78968
+ resumeAfterUnpauseRunning = false;
78546
78969
  /**
78547
78970
  * Start the runtime and initialize all subsystems.
78548
78971
  *
@@ -78576,7 +78999,7 @@ var init_in_process_runtime = __esm({
78576
78999
  runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
78577
79000
  }
78578
79001
  this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
78579
- this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
79002
+ this.pluginStore = new PluginStoreClass(this.config.workingDirectory);
78580
79003
  await this.pluginStore.init();
78581
79004
  this.pluginLoader = new PluginLoaderClass({
78582
79005
  pluginStore: this.pluginStore,
@@ -78968,11 +79391,16 @@ var init_in_process_runtime = __esm({
78968
79391
  this.selfHealingManager.start();
78969
79392
  this.stuckTaskDetector.start();
78970
79393
  this.setupEventForwarding();
78971
- await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
78972
- await this.executor.resumeOrphaned();
78973
- void this.selfHealingManager.runStartupRecovery().catch((err) => {
78974
- runtimeLog.error("Self-healing startup recovery failed:", err);
78975
- });
79394
+ const startupSettings = await this.taskStore.getSettings();
79395
+ if (startupSettings.globalPause || startupSettings.enginePaused) {
79396
+ this.startupRecoveryDeferred = true;
79397
+ runtimeLog.log(
79398
+ `Startup recovery deferred \u2014 ${startupSettings.globalPause ? "global pause" : "engine pause"} is active`
79399
+ );
79400
+ } else {
79401
+ this.startupRecoveryDeferred = false;
79402
+ await this.resumeStartupRecoverySequence();
79403
+ }
78976
79404
  this.scheduler.start();
78977
79405
  this.triageProcessor?.start();
78978
79406
  this.missionExecutionLoop = missionExecutionLoop;
@@ -79138,6 +79566,45 @@ var init_in_process_runtime = __esm({
79138
79566
  setMergeEnqueuer(enqueueMerge) {
79139
79567
  this.mergeEnqueuer = enqueueMerge;
79140
79568
  }
79569
+ /**
79570
+ * Resume executor/self-healing activity after an unpause transition.
79571
+ *
79572
+ * When startup recovery had been deferred, this replays the original startup
79573
+ * ordering so orphan resume and self-healing cannot race each other.
79574
+ */
79575
+ async resumeAfterUnpause() {
79576
+ if (!this.taskStore || !this.executor || !this.selfHealingManager) {
79577
+ return;
79578
+ }
79579
+ if (this.resumeAfterUnpauseRunning) {
79580
+ return;
79581
+ }
79582
+ this.resumeAfterUnpauseRunning = true;
79583
+ try {
79584
+ const settings = await this.taskStore.getSettings();
79585
+ if (settings.globalPause || settings.enginePaused) {
79586
+ runtimeLog.log(
79587
+ `Unpause recovery still blocked \u2014 ${settings.globalPause ? "global pause" : "engine pause"} remains active`
79588
+ );
79589
+ return;
79590
+ }
79591
+ if (this.startupRecoveryDeferred) {
79592
+ await this.resumeStartupRecoverySequence();
79593
+ this.startupRecoveryDeferred = false;
79594
+ return;
79595
+ }
79596
+ await this.executor.resumeOrphaned();
79597
+ } finally {
79598
+ this.resumeAfterUnpauseRunning = false;
79599
+ }
79600
+ }
79601
+ async resumeStartupRecoverySequence() {
79602
+ await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
79603
+ await this.executor.resumeOrphaned();
79604
+ void this.selfHealingManager.runStartupRecovery().catch((err) => {
79605
+ runtimeLog.error("Self-healing startup recovery failed:", err);
79606
+ });
79607
+ }
79141
79608
  /**
79142
79609
  * Get the project's TaskStore instance.
79143
79610
  * @throws Error if runtime has not been started
@@ -82977,13 +83444,13 @@ ${detail}`
82977
83444
  if (prev.globalPause && !s.globalPause) {
82978
83445
  runtimeLog.log("Global unpause \u2014 resuming agentic activity");
82979
83446
  try {
82980
- const executor = this.runtime.executor;
82981
- executor?.resumeOrphaned?.().catch(
82982
- (err) => runtimeLog.error("Failed to resume orphaned tasks on unpause:", err)
83447
+ const runtime = this.runtime;
83448
+ runtime.resumeAfterUnpause?.().catch(
83449
+ (err) => runtimeLog.error("Failed to resume agentic activity on unpause:", err)
82983
83450
  );
82984
83451
  } catch (err) {
82985
83452
  runtimeLog.warn(
82986
- `Global unpause: failed to dispatch resumeOrphaned: ${err instanceof Error ? err.message : String(err)}`
83453
+ `Global unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`
82987
83454
  );
82988
83455
  }
82989
83456
  if (s.autoMerge) {
@@ -83007,13 +83474,13 @@ ${detail}`
83007
83474
  if (prev.enginePaused && !s.enginePaused) {
83008
83475
  runtimeLog.log("Engine unpaused \u2014 resuming agentic activity");
83009
83476
  try {
83010
- const executor = this.runtime.executor;
83011
- executor?.resumeOrphaned?.().catch(
83012
- (err) => runtimeLog.error("Failed to resume orphaned tasks on engine unpause:", err)
83477
+ const runtime = this.runtime;
83478
+ runtime.resumeAfterUnpause?.().catch(
83479
+ (err) => runtimeLog.error("Failed to resume agentic activity on engine unpause:", err)
83013
83480
  );
83014
83481
  } catch (err) {
83015
83482
  runtimeLog.warn(
83016
- `Engine unpause: failed to dispatch resumeOrphaned: ${err instanceof Error ? err.message : String(err)}`
83483
+ `Engine unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`
83017
83484
  );
83018
83485
  }
83019
83486
  if (s.autoMerge) {