@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/bin.js CHANGED
@@ -77,6 +77,7 @@ var init_settings_schema = __esm({
77
77
  favoriteModels: void 0,
78
78
  openrouterModelSync: true,
79
79
  updateCheckEnabled: true,
80
+ fnBinaryCheckEnabled: true,
80
81
  updateCheckFrequency: "daily",
81
82
  showGitHubStarButton: true,
82
83
  modelOnboardingComplete: void 0,
@@ -3292,6 +3293,12 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3292
3293
  if (!inMemory && !isAbsolute(fusionDir)) {
3293
3294
  throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
3294
3295
  }
3296
+ if (!inMemory && /\.fusion[\\/]\.fusion(?:[\\/]|$)/.test(fusionDir)) {
3297
+ throw new Error(
3298
+ `[fusion] Refusing to open Database at nested .fusion/.fusion path: ${fusionDir}
3299
+ 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.`
3300
+ );
3301
+ }
3295
3302
  if (!inMemory && !existsSync(fusionDir)) {
3296
3303
  mkdirSync(fusionDir, { recursive: true });
3297
3304
  }
@@ -6018,6 +6025,7 @@ var init_agent_store = __esm({
6018
6025
  };
6019
6026
  const line = JSON.stringify(safeEntry) + "\n";
6020
6027
  await appendFile(this.runLogPath(agentId, runId), line, "utf-8");
6028
+ this.emit("run:log", agentId, runId, safeEntry);
6021
6029
  }
6022
6030
  /**
6023
6031
  * Read all log entries for a given run from its JSONL file.
@@ -36344,12 +36352,16 @@ var init_gh_cli = __esm({
36344
36352
 
36345
36353
  // ../core/src/fn-binary.ts
36346
36354
  import { spawn as spawn2 } from "node:child_process";
36347
- import { platform as platform2 } from "node:os";
36355
+ import { platform as platform2, tmpdir as tmpdir2 } from "node:os";
36348
36356
  function runProbe(command, args, timeoutMs) {
36349
36357
  return new Promise((resolve42) => {
36350
36358
  let stdout = "";
36351
36359
  let stderr = "";
36352
- const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: false });
36360
+ const child = spawn2(command, args, {
36361
+ stdio: ["ignore", "pipe", "pipe"],
36362
+ shell: false,
36363
+ cwd: tmpdir2()
36364
+ });
36353
36365
  const timer = setTimeout(() => {
36354
36366
  try {
36355
36367
  child.kill("SIGKILL");
@@ -37667,7 +37679,7 @@ async function syncBackupAutomation(automationStore, settings) {
37667
37679
  if (!AutomationStore2.isValidCron(schedule)) {
37668
37680
  throw new Error(`Invalid backup schedule: ${schedule}`);
37669
37681
  }
37670
- const command = "npx runfusion.ai backup --create";
37682
+ const command = "fn backup --create";
37671
37683
  if (existingSchedule) {
37672
37684
  return await automationStore.updateSchedule(existingSchedule.id, {
37673
37685
  scheduleType: "custom",
@@ -37700,7 +37712,7 @@ async function syncBackupRoutine(routineStore, settings) {
37700
37712
  if (!RoutineStore2.isValidCron(schedule)) {
37701
37713
  throw new Error(`Invalid backup schedule: ${schedule}`);
37702
37714
  }
37703
- const command = "npx runfusion.ai backup --create";
37715
+ const command = "fn backup --create";
37704
37716
  const input = {
37705
37717
  name: BACKUP_SCHEDULE_NAME,
37706
37718
  description: "Automatic database backup based on project settings",
@@ -49575,7 +49587,7 @@ var require_dist3 = __commonJS({
49575
49587
 
49576
49588
  // ../core/src/agent-companies-parser.ts
49577
49589
  import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync4 } from "node:fs";
49578
- import { tmpdir as tmpdir2 } from "node:os";
49590
+ import { tmpdir as tmpdir3 } from "node:os";
49579
49591
  import { join as join20, resolve as resolve9 } from "node:path";
49580
49592
  function slugifyAgentReference(value) {
49581
49593
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -49898,7 +49910,7 @@ async function extractTarArchive(archivePath, outputDir) {
49898
49910
  }
49899
49911
  async function parseCompanyArchive(archivePath) {
49900
49912
  const resolvedArchivePath = resolve9(archivePath);
49901
- const tempDir = mkdtempSync(join20(tmpdir2(), "agent-companies-"));
49913
+ const tempDir = mkdtempSync(join20(tmpdir3(), "agent-companies-"));
49902
49914
  try {
49903
49915
  if (resolvedArchivePath.endsWith(".tar.gz") || resolvedArchivePath.endsWith(".tgz")) {
49904
49916
  await extractTarArchive(resolvedArchivePath, tempDir);
@@ -65767,6 +65779,20 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65767
65779
  );
65768
65780
  this.activeStepExecutors.delete(task.id);
65769
65781
  }
65782
+ if (this.activeWorkflowStepSessions.has(task.id)) {
65783
+ executorLog.log(`${task.id} moved from in-progress to ${to} \u2014 terminating workflow step session`);
65784
+ this.pausedAborted.add(task.id);
65785
+ this.options.stuckTaskDetector?.untrackTask(task.id);
65786
+ const workflowSession = this.activeWorkflowStepSessions.get(task.id);
65787
+ const sessionWithAbort = workflowSession;
65788
+ if (typeof sessionWithAbort.abort === "function") {
65789
+ void sessionWithAbort.abort().catch((err) => {
65790
+ executorLog.warn(`Failed to abort workflow step session for ${task.id}: ${err}`);
65791
+ });
65792
+ }
65793
+ workflowSession.dispose();
65794
+ this.activeWorkflowStepSessions.delete(task.id);
65795
+ }
65770
65796
  this.disposeSubagentsForTask(task.id, `parent moved from in-progress to ${to}`);
65771
65797
  this.loopRecoveryState.delete(task.id);
65772
65798
  this.spawnedAgents.delete(task.id);
@@ -65799,8 +65825,42 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65799
65825
  this.disposeSubagentsForTask(task.id, "task paused");
65800
65826
  return;
65801
65827
  }
65802
- if (!task.paused && task.column === "in-progress" && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id)) {
65828
+ if (task.paused && this.activeWorkflowStepSessions.has(task.id)) {
65829
+ executorLog.log(`Pausing ${task.id} \u2014 terminating workflow step session`);
65830
+ this.pausedAborted.add(task.id);
65831
+ this.options.stuckTaskDetector?.untrackTask(task.id);
65832
+ const workflowSession = this.activeWorkflowStepSessions.get(task.id);
65833
+ const sessionWithAbort = workflowSession;
65834
+ if (typeof sessionWithAbort.abort === "function") {
65835
+ await sessionWithAbort.abort().catch(
65836
+ (err) => executorLog.warn(`Failed to abort workflow step session for pause ${task.id}: ${err}`)
65837
+ );
65838
+ }
65839
+ workflowSession.dispose();
65840
+ this.activeWorkflowStepSessions.delete(task.id);
65841
+ this.loopRecoveryState.delete(task.id);
65842
+ this.spawnedAgents.delete(task.id);
65843
+ this.stuckAborted.delete(task.id);
65844
+ this.disposeSubagentsForTask(task.id, "task paused");
65845
+ return;
65846
+ }
65847
+ if (!task.paused && task.column === "in-progress" && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id) && !this.activeWorkflowStepSessions.has(task.id)) {
65803
65848
  if (!this.executing.has(task.id) && !this.resumingUnpaused.has(task.id) && !this.recoveringCompleted.has(task.id)) {
65849
+ const pauseLabel = await this.getExecutionPauseLabel();
65850
+ if (pauseLabel) {
65851
+ executorLog.log(`Skipping unpause resume for ${task.id} \u2014 ${pauseLabel} active`);
65852
+ return;
65853
+ }
65854
+ if (this.isTaskWorkComplete(task) && !task.mergeDetails) {
65855
+ this.recoveringCompleted.add(task.id);
65856
+ executorLog.log(`${task.id} unpaused with completed work and no session \u2014 recovering directly to in-review`);
65857
+ void this.recoverCompletedTask(task).catch(
65858
+ (err) => executorLog.error(`Failed to recover completed unpaused task ${task.id}:`, err)
65859
+ ).finally(() => {
65860
+ this.recoveringCompleted.delete(task.id);
65861
+ });
65862
+ return;
65863
+ }
65804
65864
  this.resumingUnpaused.add(task.id);
65805
65865
  executorLog.log(`Unpaused ${task.id} in-progress with no session \u2014 resuming execution`);
65806
65866
  try {
@@ -65919,6 +65979,22 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65919
65979
  this.spawnedAgents.delete(taskId);
65920
65980
  this.stuckAborted.delete(taskId);
65921
65981
  }
65982
+ for (const [taskId, workflowSession] of this.activeWorkflowStepSessions) {
65983
+ executorLog.log(`Global pause \u2014 terminating workflow step session for ${taskId}`);
65984
+ this.pausedAborted.add(taskId);
65985
+ this.options.stuckTaskDetector?.untrackTask(taskId);
65986
+ const sessionWithAbort = workflowSession;
65987
+ if (typeof sessionWithAbort.abort === "function") {
65988
+ void sessionWithAbort.abort().catch((err) => {
65989
+ executorLog.warn(`Failed to abort workflow step session for ${taskId}: ${err}`);
65990
+ });
65991
+ }
65992
+ workflowSession.dispose();
65993
+ this.activeWorkflowStepSessions.delete(taskId);
65994
+ this.loopRecoveryState.delete(taskId);
65995
+ this.spawnedAgents.delete(taskId);
65996
+ this.stuckAborted.delete(taskId);
65997
+ }
65922
65998
  }
65923
65999
  });
65924
66000
  }
@@ -65936,6 +66012,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65936
66012
  activeSessions = /* @__PURE__ */ new Map();
65937
66013
  /** Active step-session executors per task (mutually exclusive with activeSessions). */
65938
66014
  activeStepExecutors = /* @__PURE__ */ new Map();
66015
+ /** Active pre-merge workflow step sessions per task. */
66016
+ activeWorkflowStepSessions = /* @__PURE__ */ new Map();
65939
66017
  /**
65940
66018
  * Reviewer subagent sessions per task. Reviewers (`reviewer.ts`) create their
65941
66019
  * own AgentSessions that aren't part of `activeSessions`/`activeStepExecutors`,
@@ -65982,6 +66060,69 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65982
66060
  await this.store.mergeTask(taskId);
65983
66061
  return "merged";
65984
66062
  }
66063
+ async getExecutionPauseLabel() {
66064
+ const settings = await this.store.getSettings();
66065
+ if (settings.globalPause) return "global pause";
66066
+ if (settings.enginePaused) return "engine pause";
66067
+ return null;
66068
+ }
66069
+ async shouldDeferCompletionForGlobalPause(taskId, context) {
66070
+ const settings = await this.store.getSettings();
66071
+ if (!settings.globalPause) {
66072
+ return false;
66073
+ }
66074
+ this.clearCompletedTaskWatchdog(taskId);
66075
+ executorLog.log(`${taskId}: completion handoff deferred \u2014 global pause active (${context})`);
66076
+ await this.store.logEntry(
66077
+ taskId,
66078
+ `Completion handoff deferred \u2014 global pause active (${context})`,
66079
+ void 0,
66080
+ this.currentRunContext
66081
+ ).catch(() => void 0);
66082
+ return true;
66083
+ }
66084
+ async shouldDeferWorkflowStepCompletion(taskId, context) {
66085
+ let latestTask = null;
66086
+ try {
66087
+ latestTask = await this.store.getTask(taskId);
66088
+ } catch {
66089
+ latestTask = null;
66090
+ }
66091
+ if (latestTask?.paused || this.pausedAborted.has(taskId)) {
66092
+ this.clearCompletedTaskWatchdog(taskId);
66093
+ executorLog.log(`${taskId}: completion handoff deferred \u2014 task paused (${context})`);
66094
+ await this.store.logEntry(
66095
+ taskId,
66096
+ `Completion handoff deferred \u2014 task paused (${context})`,
66097
+ void 0,
66098
+ this.currentRunContext
66099
+ ).catch(() => void 0);
66100
+ return true;
66101
+ }
66102
+ return this.shouldDeferCompletionForGlobalPause(taskId, context);
66103
+ }
66104
+ async parkTaskAfterWorkflowStepPause(taskId) {
66105
+ let latestTask = null;
66106
+ try {
66107
+ latestTask = await this.store.getTask(taskId);
66108
+ } catch {
66109
+ latestTask = null;
66110
+ }
66111
+ if (!latestTask?.paused) {
66112
+ return false;
66113
+ }
66114
+ executorLog.log(`${taskId}: workflow step interrupted by task pause \u2014 moving to todo`);
66115
+ await this.store.logEntry(
66116
+ taskId,
66117
+ "Execution paused during pre-merge workflow step \u2014 moved to todo",
66118
+ void 0,
66119
+ this.currentRunContext
66120
+ ).catch(() => void 0);
66121
+ if (latestTask.column === "in-progress") {
66122
+ await this.store.moveTask(taskId, "todo", { preserveResumeState: true });
66123
+ }
66124
+ return true;
66125
+ }
65985
66126
  /** Child agent sessions keyed by agent ID. Used for termination. */
65986
66127
  childSessions = /* @__PURE__ */ new Map();
65987
66128
  /** Total count of currently spawned agents (across all parents). */
@@ -66176,11 +66317,15 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66176
66317
  this.clearCompletedTaskWatchdog(taskId);
66177
66318
  const handle = setTimeout(async () => {
66178
66319
  this.completedTaskWatchdogs.delete(taskId);
66179
- if (this.recoveringCompleted.has(taskId) || this.executing.has(taskId) || this.activeSessions.has(taskId) || this.activeStepExecutors.has(taskId) || this.resumingUnpaused.has(taskId)) {
66320
+ 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)) {
66180
66321
  return;
66181
66322
  }
66182
66323
  this.recoveringCompleted.add(taskId);
66183
66324
  try {
66325
+ const pauseLabel = await this.getExecutionPauseLabel();
66326
+ if (pauseLabel) {
66327
+ return;
66328
+ }
66184
66329
  let currentTask = null;
66185
66330
  try {
66186
66331
  currentTask = await this.store.getTask(taskId);
@@ -66226,6 +66371,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66226
66371
  * stuck.
66227
66372
  */
66228
66373
  async performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState = true) {
66374
+ const pauseLabel = await this.getExecutionPauseLabel();
66375
+ if (pauseLabel) {
66376
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 ${pauseLabel} active`);
66377
+ return "deferred-paused";
66378
+ }
66229
66379
  if (this.workflowRerunPending.has(taskId)) {
66230
66380
  executorLog.warn(`${taskId}: workflow rerun bounce already in flight \u2014 skipping re-entry`);
66231
66381
  return "skipped-pending";
@@ -66236,6 +66386,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66236
66386
  if (!latestTask) {
66237
66387
  throw new Error("task missing during workflow rerun bounce");
66238
66388
  }
66389
+ if (latestTask.paused) {
66390
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 task is paused`);
66391
+ return "deferred-paused";
66392
+ }
66239
66393
  if (latestTask.column === "in-progress") {
66240
66394
  const originalExecutionStartedAt = latestTask.executionStartedAt;
66241
66395
  if (preserveResumeState) {
@@ -66247,11 +66401,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66247
66401
  worktree: worktreePath,
66248
66402
  executionStartedAt: originalExecutionStartedAt ?? null
66249
66403
  });
66404
+ const pauseLabelAfterTodo = await this.getExecutionPauseLabel();
66405
+ if (pauseLabelAfterTodo) {
66406
+ executorLog.log(`${taskId}: workflow rerun parked in todo \u2014 ${pauseLabelAfterTodo} became active during bounce`);
66407
+ return "deferred-paused";
66408
+ }
66250
66409
  await this.store.moveTask(taskId, "in-progress");
66251
66410
  return "bounced";
66252
66411
  }
66253
66412
  if (latestTask.column === "todo") {
66254
66413
  await this.store.updateTask(taskId, { worktree: worktreePath });
66414
+ const pauseLabelBeforeResume = await this.getExecutionPauseLabel();
66415
+ if (pauseLabelBeforeResume) {
66416
+ executorLog.log(`${taskId}: workflow rerun parked in todo \u2014 ${pauseLabelBeforeResume} became active before resume`);
66417
+ return "deferred-paused";
66418
+ }
66255
66419
  await this.store.moveTask(taskId, "in-progress");
66256
66420
  return "bounced";
66257
66421
  }
@@ -66267,8 +66431,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66267
66431
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
66268
66432
  if (outcome === "bounced") {
66269
66433
  executorLog.log(successMessage);
66270
- } else {
66434
+ } else if (outcome === "skipped-pending") {
66271
66435
  executorLog.warn(`${taskId}: rerun bounce skipped \u2014 another bounce already in flight`);
66436
+ } else {
66437
+ executorLog.log(`${taskId}: rerun bounce deferred while pause is active`);
66272
66438
  }
66273
66439
  } catch (err) {
66274
66440
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -66277,6 +66443,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66277
66443
  }, 0);
66278
66444
  const watchdog = setTimeout(async () => {
66279
66445
  this.workflowRerunWatchdogs.delete(taskId);
66446
+ const pauseLabel = await this.getExecutionPauseLabel();
66447
+ if (pauseLabel) {
66448
+ executorLog.log(`${taskId}: workflow rerun watchdog skipped \u2014 ${pauseLabel} active`);
66449
+ return;
66450
+ }
66280
66451
  let currentTask = null;
66281
66452
  try {
66282
66453
  currentTask = await this.store.getTask(taskId);
@@ -66299,7 +66470,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66299
66470
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
66300
66471
  if (outcome === "bounced") {
66301
66472
  executorLog.warn(`${taskId}: workflow rerun watchdog retry succeeded`);
66302
- } else {
66473
+ } else if (outcome === "skipped-pending") {
66303
66474
  executorLog.error(
66304
66475
  `${taskId}: workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
66305
66476
  );
@@ -66307,6 +66478,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66307
66478
  taskId,
66308
66479
  `Workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
66309
66480
  ).catch(() => void 0);
66481
+ } else {
66482
+ executorLog.log(`${taskId}: workflow rerun watchdog retry deferred while pause is active`);
66310
66483
  }
66311
66484
  } catch (err) {
66312
66485
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -66429,11 +66602,17 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66429
66602
  */
66430
66603
  async recoverCompletedTask(task) {
66431
66604
  try {
66432
- if (this.executing.has(task.id) || this.activeSessions.has(task.id) || this.activeStepExecutors.has(task.id) || this.resumingUnpaused.has(task.id)) {
66605
+ 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)) {
66433
66606
  executorLog.log(`${task.id}: skipping recoverCompletedTask \u2014 task has active execution in flight`);
66434
66607
  return false;
66435
66608
  }
66436
66609
  const settings = await this.store.getSettings();
66610
+ if (settings.globalPause || settings.enginePaused) {
66611
+ executorLog.log(
66612
+ `${task.id}: skipping recoverCompletedTask \u2014 ${settings.globalPause ? "global pause" : "engine pause"} active`
66613
+ );
66614
+ return false;
66615
+ }
66437
66616
  if (task.worktree && existsSync27(task.worktree)) {
66438
66617
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
66439
66618
  if (modifiedFiles.length > 0) {
@@ -66441,7 +66620,16 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66441
66620
  executorLog.log(`${task.id}: recovered ${modifiedFiles.length} modified files`);
66442
66621
  }
66443
66622
  if (task.executionMode !== "fast") {
66623
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps during completed-task recovery")) {
66624
+ return false;
66625
+ }
66444
66626
  const workflowResult = await this.runWorkflowSteps(task, task.worktree, settings);
66627
+ if (workflowResult === "deferred-paused") {
66628
+ if (this.pausedAborted.has(task.id)) {
66629
+ this.pausedAborted.delete(task.id);
66630
+ }
66631
+ return false;
66632
+ }
66445
66633
  if (!workflowResult.allPassed) {
66446
66634
  await this.sendTaskBackForFix(task, task.worktree, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed during recovery", false);
66447
66635
  return true;
@@ -66450,6 +66638,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66450
66638
  executorLog.log(`${task.id}: fast mode \u2014 skipping workflow steps on auto-recovery`);
66451
66639
  }
66452
66640
  }
66641
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition during completed-task recovery")) {
66642
+ return false;
66643
+ }
66453
66644
  await this.persistTokenUsage(task.id);
66454
66645
  await this.store.moveTask(task.id, "in-review");
66455
66646
  this.clearCompletedTaskWatchdog(task.id);
@@ -66515,6 +66706,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66515
66706
  * directly to in-review without spawning a new agent session.
66516
66707
  */
66517
66708
  async resumeOrphaned() {
66709
+ const settings = await this.store.getSettings();
66710
+ if (settings.globalPause || settings.enginePaused) {
66711
+ executorLog.log(
66712
+ `resumeOrphaned skipped \u2014 ${settings.globalPause ? "global pause" : "engine pause"} is active`
66713
+ );
66714
+ return;
66715
+ }
66518
66716
  const tasks = await this.store.listTasks({ slim: true, column: "in-progress" });
66519
66717
  const inProgress = tasks.filter(
66520
66718
  (t) => t.column === "in-progress" && !this.executing.has(t.id) && !t.paused
@@ -66953,8 +67151,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66953
67151
  await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
66954
67152
  }
66955
67153
  this.scheduleCompletedTaskWatchdog(task.id, "step-session completion");
67154
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
67155
+ return;
67156
+ }
66956
67157
  if (executionMode !== "fast") {
66957
67158
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
67159
+ if (workflowResult === "deferred-paused") {
67160
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
67161
+ this.pausedAborted.delete(task.id);
67162
+ return;
67163
+ }
67164
+ if (this.pausedAborted.has(task.id)) {
67165
+ this.pausedAborted.delete(task.id);
67166
+ }
67167
+ return;
67168
+ }
66958
67169
  if (!workflowResult.allPassed) {
66959
67170
  if (workflowResult.revisionRequested) {
66960
67171
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66972,6 +67183,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66972
67183
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66973
67184
  }
66974
67185
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
67186
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after step-session completion")) {
67187
+ return;
67188
+ }
66975
67189
  await this.store.moveTask(task.id, "in-review");
66976
67190
  this.clearCompletedTaskWatchdog(task.id);
66977
67191
  await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
@@ -67324,6 +67538,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67324
67538
  this.pausedAborted.delete(task.id);
67325
67539
  wasPaused = true;
67326
67540
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
67541
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
67542
+ return;
67543
+ }
67327
67544
  executorLog.log(`${task.id} paused after completion (graceful session exit) \u2014 finalizing to in-review`);
67328
67545
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review");
67329
67546
  await this.persistTokenUsage(task.id);
@@ -67360,8 +67577,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67360
67577
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
67361
67578
  }
67362
67579
  this.scheduleCompletedTaskWatchdog(task.id, "task completion");
67580
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion")) {
67581
+ return;
67582
+ }
67363
67583
  if (executionMode !== "fast") {
67364
67584
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
67585
+ if (workflowResult === "deferred-paused") {
67586
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
67587
+ this.pausedAborted.delete(task.id);
67588
+ wasPaused = true;
67589
+ return;
67590
+ }
67591
+ if (this.pausedAborted.has(task.id)) {
67592
+ this.pausedAborted.delete(task.id);
67593
+ wasPaused = true;
67594
+ }
67595
+ return;
67596
+ }
67365
67597
  if (!workflowResult.allPassed) {
67366
67598
  if (workflowResult.revisionRequested) {
67367
67599
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -67379,6 +67611,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67379
67611
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
67380
67612
  }
67381
67613
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
67614
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after task completion")) {
67615
+ return;
67616
+ }
67382
67617
  await this.persistTokenUsage(task.id);
67383
67618
  await this.store.moveTask(task.id, "in-review");
67384
67619
  this.clearCompletedTaskWatchdog(task.id);
@@ -67503,8 +67738,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67503
67738
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
67504
67739
  }
67505
67740
  this.scheduleCompletedTaskWatchdog(task.id, "task completion retry");
67741
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion retry")) {
67742
+ return;
67743
+ }
67506
67744
  if (executionMode !== "fast") {
67507
67745
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
67746
+ if (workflowResult === "deferred-paused") {
67747
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
67748
+ this.pausedAborted.delete(task.id);
67749
+ wasPaused = true;
67750
+ return;
67751
+ }
67752
+ if (this.pausedAborted.has(task.id)) {
67753
+ this.pausedAborted.delete(task.id);
67754
+ wasPaused = true;
67755
+ }
67756
+ return;
67757
+ }
67508
67758
  if (!workflowResult.allPassed) {
67509
67759
  if (workflowResult.revisionRequested) {
67510
67760
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -67518,6 +67768,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67518
67768
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
67519
67769
  }
67520
67770
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
67771
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after task completion retry")) {
67772
+ return;
67773
+ }
67521
67774
  await this.persistTokenUsage(task.id);
67522
67775
  await this.store.moveTask(task.id, "in-review");
67523
67776
  this.clearCompletedTaskWatchdog(task.id);
@@ -67610,6 +67863,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67610
67863
  } else if (this.pausedAborted.has(task.id)) {
67611
67864
  this.pausedAborted.delete(task.id);
67612
67865
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
67866
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
67867
+ return;
67868
+ }
67613
67869
  executorLog.log(`${task.id} paused after completion \u2014 finalizing to in-review`);
67614
67870
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review", void 0, this.currentRunContext);
67615
67871
  await this.persistTokenUsage(task.id);
@@ -67968,22 +68224,28 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67968
68224
  if (params.summary) {
67969
68225
  await store.updateTask(taskId, { summary: params.summary });
67970
68226
  }
67971
- await store.updateTask(taskId, { paused: false, status: null });
68227
+ const settings = await store.getSettings();
68228
+ const hardPauseActive = Boolean(task.paused || settings.globalPause);
68229
+ if (hardPauseActive) {
68230
+ await store.updateTask(taskId, { status: null });
68231
+ } else {
68232
+ await store.updateTask(taskId, { paused: false, status: null });
68233
+ }
67972
68234
  await store.logEntry(taskId, "Task marked done by agent");
67973
68235
  const latestTask = await store.getTask(taskId);
67974
68236
  let latestColumn = latestTask.column;
67975
68237
  if (latestColumn === "todo") {
67976
68238
  await store.logEntry(
67977
68239
  taskId,
67978
- "fn_task_done called while task was in todo \u2014 promoting to in-progress before completion handoff"
68240
+ 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"
67979
68241
  );
67980
68242
  await store.moveTask(taskId, "in-progress");
67981
68243
  latestColumn = "in-progress";
67982
68244
  }
67983
- if (latestColumn === "in-progress") {
68245
+ if (latestColumn === "in-progress" && !hardPauseActive) {
67984
68246
  this.scheduleCompletedTaskWatchdog(taskId, "fn_task_done");
67985
68247
  }
67986
- 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.";
68248
+ 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.";
67987
68249
  return {
67988
68250
  content: [{ type: "text", text: successMessage }],
67989
68251
  details: {}
@@ -68558,6 +68820,9 @@ ${failureFeedback}
68558
68820
  await this.store.updateTask(task.id, { workflowStepResults: results });
68559
68821
  continue;
68560
68822
  }
68823
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `before workflow step '${ws.name}'`)) {
68824
+ return "deferred-paused";
68825
+ }
68561
68826
  await this.store.logEntry(task.id, `[pre-merge] Starting workflow step: ${ws.name} (${stepMode} mode)`);
68562
68827
  executorLog.log(`${task.id} \u2014 [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
68563
68828
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -68572,6 +68837,9 @@ ${failureFeedback}
68572
68837
  await this.store.updateTask(task.id, { workflowStepResults: results });
68573
68838
  try {
68574
68839
  const result = stepMode === "script" ? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings) : await this.executeWorkflowStep(task, ws, worktreePath, settings);
68840
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68841
+ return "deferred-paused";
68842
+ }
68575
68843
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
68576
68844
  if (result.success) {
68577
68845
  await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
@@ -68637,6 +68905,9 @@ ${failureFeedback}
68637
68905
  };
68638
68906
  }
68639
68907
  } catch (err) {
68908
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68909
+ return "deferred-paused";
68910
+ }
68640
68911
  const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
68641
68912
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
68642
68913
  await this.store.logEntry(
@@ -68802,6 +69073,7 @@ and show an appropriate message to the user.\`
68802
69073
  task.id,
68803
69074
  `Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`
68804
69075
  );
69076
+ this.activeWorkflowStepSessions.set(task.id, session);
68805
69077
  let output = "";
68806
69078
  session.subscribe((event) => {
68807
69079
  if (event.type === "message_update") {
@@ -68874,6 +69146,10 @@ Review the work done in this worktree and evaluate it against the criteria in yo
68874
69146
  return { success: false, error: errorMessage };
68875
69147
  } finally {
68876
69148
  if (timeoutHandle) clearTimeout(timeoutHandle);
69149
+ const activeWorkflowStepSession = this.activeWorkflowStepSessions.get(task.id);
69150
+ if (activeWorkflowStepSession === session) {
69151
+ this.activeWorkflowStepSessions.delete(task.id);
69152
+ }
68877
69153
  void timedOut;
68878
69154
  }
68879
69155
  };
@@ -70617,6 +70893,15 @@ var init_scheduler = __esm({
70617
70893
  schedulerLog.log(`Task ${task.id} is paused \u2014 skipping dispatch`);
70618
70894
  continue;
70619
70895
  }
70896
+ const latestSettings = await this.store.getSettings();
70897
+ if (latestSettings.globalPause) {
70898
+ schedulerLog.log(`Task ${task.id} dispatch aborted \u2014 globalPause became active mid-pass`);
70899
+ continue;
70900
+ }
70901
+ if (latestSettings.enginePaused) {
70902
+ schedulerLog.log(`Task ${task.id} dispatch aborted \u2014 enginePaused became active mid-pass`);
70903
+ continue;
70904
+ }
70620
70905
  let effectiveNode = resolveEffectiveNode(freshTask, settings);
70621
70906
  schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
70622
70907
  if (effectiveNode.nodeId !== void 0 && this.options.nodeHealthMonitor) {
@@ -75754,6 +76039,36 @@ function execCommand(command, options) {
75754
76039
  });
75755
76040
  });
75756
76041
  }
76042
+ function isInProcessBackupCommand(command) {
76043
+ if (!command) return false;
76044
+ const trimmed = command.trim();
76045
+ if (!trimmed) return false;
76046
+ if (SHELL_METACHARACTERS_REGEX.test(trimmed)) return false;
76047
+ const tokens = trimmed.split(/\s+/).map((tok) => tok.toLowerCase());
76048
+ let cursor = 0;
76049
+ if (tokens[cursor] === "npx") {
76050
+ cursor += 1;
76051
+ while (cursor < tokens.length) {
76052
+ const tok = tokens[cursor];
76053
+ if (tok === void 0 || !tok.startsWith("-")) break;
76054
+ const takesValue = (tok === "-p" || tok === "--package") && cursor + 1 < tokens.length && tokens[cursor + 1] !== void 0 && !tokens[cursor + 1].startsWith("-");
76055
+ cursor += takesValue ? 2 : 1;
76056
+ }
76057
+ }
76058
+ const binary = tokens[cursor];
76059
+ if (!binary || !FUSION_BINARY_TOKENS.has(binary)) return false;
76060
+ cursor += 1;
76061
+ if (tokens[cursor] !== "backup") return false;
76062
+ cursor += 1;
76063
+ if (tokens[cursor] !== "--create") return false;
76064
+ cursor += 1;
76065
+ for (; cursor < tokens.length; cursor += 1) {
76066
+ const tok = tokens[cursor];
76067
+ if (!tok) continue;
76068
+ if (!tok.startsWith("-")) return false;
76069
+ }
76070
+ return true;
76071
+ }
75757
76072
  async function createAiPromptExecutor(cwd) {
75758
76073
  const disposeLog = createLogger2("cron-runner");
75759
76074
  return async (prompt, modelProvider, modelId) => {
@@ -75793,7 +76108,7 @@ function truncateOutput(stdout, stderr) {
75793
76108
  }
75794
76109
  return combined;
75795
76110
  }
75796
- var log14, DEFAULT_TIMEOUT_MS6, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
76111
+ 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;
75797
76112
  var init_cron_runner = __esm({
75798
76113
  "../engine/src/cron-runner.ts"() {
75799
76114
  "use strict";
@@ -75802,6 +76117,14 @@ var init_cron_runner = __esm({
75802
76117
  init_shell_utils();
75803
76118
  init_pi();
75804
76119
  log14 = createLogger2("cron-runner");
76120
+ FUSION_BINARY_TOKENS = /* @__PURE__ */ new Set([
76121
+ "fn",
76122
+ "fusion",
76123
+ "runfusion",
76124
+ "runfusion.ai",
76125
+ "@runfusion/fusion"
76126
+ ]);
76127
+ SHELL_METACHARACTERS_REGEX = /[&|;<>`$()]/;
75805
76128
  DEFAULT_TIMEOUT_MS6 = 5 * 60 * 1e3;
75806
76129
  MAX_BUFFER = 1024 * 1024;
75807
76130
  MAX_OUTPUT_LENGTH = 10 * 1024;
@@ -75942,6 +76265,9 @@ var init_cron_runner = __esm({
75942
76265
  */
75943
76266
  async executeLegacyCommand(schedule, startedAt) {
75944
76267
  log14.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
76268
+ if (isInProcessBackupCommand(schedule.command)) {
76269
+ return this.executeBackupInProcess(schedule, startedAt);
76270
+ }
75945
76271
  try {
75946
76272
  const timeoutMs = schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
75947
76273
  const { stdout, stderr } = await execCommand(schedule.command, {
@@ -75972,6 +76298,47 @@ var init_cron_runner = __esm({
75972
76298
  };
75973
76299
  }
75974
76300
  }
76301
+ /**
76302
+ * Run an auto-backup schedule in-process via the engine's open TaskStore,
76303
+ * bypassing the shell-out that would otherwise invoke an outdated fusion
76304
+ * binary on PATH. See `isInProcessBackupCommand` for the matching contract.
76305
+ */
76306
+ async executeBackupInProcess(schedule, startedAt) {
76307
+ const action = await this.runBackupActionInProcess();
76308
+ if (action.success) {
76309
+ log14.log(`\u2713 ${schedule.name} completed in-process`);
76310
+ } else {
76311
+ log14.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
76312
+ }
76313
+ return {
76314
+ success: action.success,
76315
+ output: action.output,
76316
+ error: action.error,
76317
+ startedAt,
76318
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76319
+ };
76320
+ }
76321
+ /**
76322
+ * Shared in-process backup execution used by both the legacy-command path
76323
+ * and the command-step path. Returns the success/output/error tuple in
76324
+ * a shape that callers can wrap into either a run or a step result.
76325
+ */
76326
+ async runBackupActionInProcess() {
76327
+ try {
76328
+ const { runBackupCommand: runBackupCommand2 } = await Promise.resolve().then(() => (init_src(), src_exports));
76329
+ const fusionDir = this.store.getFusionDir();
76330
+ const settings = await this.store.getSettings();
76331
+ const result = await runBackupCommand2(fusionDir, settings);
76332
+ return {
76333
+ success: result.success,
76334
+ output: truncateOutput(result.output ?? "", ""),
76335
+ error: result.success ? void 0 : result.output
76336
+ };
76337
+ } catch (err) {
76338
+ const message = err instanceof Error ? err.message : String(err);
76339
+ return { success: false, output: "", error: message };
76340
+ }
76341
+ }
75975
76342
  /**
75976
76343
  * Execute multiple steps sequentially.
75977
76344
  * Aggregates per-step results into an overall AutomationRunResult.
@@ -76060,6 +76427,19 @@ var init_cron_runner = __esm({
76060
76427
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
76061
76428
  };
76062
76429
  }
76430
+ if (isInProcessBackupCommand(step.command)) {
76431
+ const action = await this.runBackupActionInProcess();
76432
+ return {
76433
+ stepId: step.id,
76434
+ stepName: step.name,
76435
+ stepIndex,
76436
+ success: action.success,
76437
+ output: action.output,
76438
+ error: action.error,
76439
+ startedAt,
76440
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76441
+ };
76442
+ }
76063
76443
  try {
76064
76444
  const { stdout, stderr } = await execCommand(step.command, {
76065
76445
  timeout: timeoutMs,
@@ -76251,6 +76631,7 @@ var init_routine_runner = __esm({
76251
76631
  "../engine/src/routine-runner.ts"() {
76252
76632
  "use strict";
76253
76633
  import_cron_parser4 = __toESM(require_dist2(), 1);
76634
+ init_cron_runner();
76254
76635
  init_logger2();
76255
76636
  init_shell_utils();
76256
76637
  log15 = createLogger2("routine-runner");
@@ -76408,6 +76789,30 @@ var init_routine_runner = __esm({
76408
76789
  return this.executeCommand(routine.command ?? "", routine.timeoutMs, startedAt);
76409
76790
  }
76410
76791
  async executeCommand(command, timeoutMs, startedAt) {
76792
+ if (isInProcessBackupCommand(command) && this.options.taskStore) {
76793
+ try {
76794
+ const { runBackupCommand: runBackupCommand2 } = await Promise.resolve().then(() => (init_src(), src_exports));
76795
+ const fusionDir = this.options.taskStore.getFusionDir();
76796
+ const settings = await this.options.taskStore.getSettings();
76797
+ const result = await runBackupCommand2(fusionDir, settings);
76798
+ return {
76799
+ success: result.success,
76800
+ output: truncateOutput2(result.output ?? "", ""),
76801
+ error: result.success ? void 0 : result.output,
76802
+ startedAt,
76803
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76804
+ };
76805
+ } catch (err) {
76806
+ const message = err instanceof Error ? err.message : String(err);
76807
+ return {
76808
+ success: false,
76809
+ output: "",
76810
+ error: message,
76811
+ startedAt,
76812
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76813
+ };
76814
+ }
76815
+ }
76411
76816
  try {
76412
76817
  const { stdout, stderr } = await execAsync6(command, {
76413
76818
  timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS7,
@@ -77199,6 +77604,13 @@ var init_self_healing = __esm({
77199
77604
  * stale in-progress/planning tasks that no longer have a live worker.
77200
77605
  */
77201
77606
  async runStartupRecovery() {
77607
+ const settings = await this.store.getSettings();
77608
+ if (settings.globalPause || settings.enginePaused) {
77609
+ log16.log(
77610
+ `Startup recovery skipped \u2014 ${settings.globalPause ? "global pause" : "engine pause"} is active`
77611
+ );
77612
+ return;
77613
+ }
77202
77614
  const steps = [
77203
77615
  { name: "no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures().then(() => void 0) },
77204
77616
  { name: "completed-tasks", fn: () => this.recoverCompletedTasks().then(() => void 0) },
@@ -77549,27 +77961,34 @@ var init_self_healing = __esm({
77549
77961
  log16.error(`Maintenance batch 1 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77550
77962
  }
77551
77963
  }
77552
- const batch2Fns = [
77553
- { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
77554
- { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
77555
- { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
77556
- { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
77557
- { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
77558
- { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
77559
- { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
77560
- { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
77561
- { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
77562
- { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
77563
- { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
77564
- { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
77565
- { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
77566
- ];
77567
- for (const fn of batch2Fns) {
77568
- try {
77569
- await fn.fn();
77570
- log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
77571
- } catch (stepErr) {
77572
- log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77964
+ const recoverySettings = await this.store.getSettings();
77965
+ if (recoverySettings.globalPause || recoverySettings.enginePaused) {
77966
+ log16.log(
77967
+ `Maintenance batch 2 skipped \u2014 ${recoverySettings.globalPause ? "global pause" : "engine pause"} is active`
77968
+ );
77969
+ } else {
77970
+ const batch2Fns = [
77971
+ { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
77972
+ { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
77973
+ { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
77974
+ { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
77975
+ { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
77976
+ { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
77977
+ { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
77978
+ { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
77979
+ { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
77980
+ { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
77981
+ { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
77982
+ { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
77983
+ { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
77984
+ ];
77985
+ for (const fn of batch2Fns) {
77986
+ try {
77987
+ await fn.fn();
77988
+ log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
77989
+ } catch (stepErr) {
77990
+ log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77991
+ }
77573
77992
  }
77574
77993
  }
77575
77994
  const batch3Fns = [
@@ -79302,6 +79721,10 @@ var init_in_process_runtime = __esm({
79302
79721
  * before `start()` via `setMergeEnqueuer`.
79303
79722
  */
79304
79723
  mergeEnqueuer;
79724
+ /** Tracks whether startup recovery was intentionally deferred due to pause state. */
79725
+ startupRecoveryDeferred = false;
79726
+ /** Prevent duplicate unpause recovery dispatches from racing each other. */
79727
+ resumeAfterUnpauseRunning = false;
79305
79728
  /**
79306
79729
  * Start the runtime and initialize all subsystems.
79307
79730
  *
@@ -79335,7 +79758,7 @@ var init_in_process_runtime = __esm({
79335
79758
  runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
79336
79759
  }
79337
79760
  this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
79338
- this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
79761
+ this.pluginStore = new PluginStoreClass(this.config.workingDirectory);
79339
79762
  await this.pluginStore.init();
79340
79763
  this.pluginLoader = new PluginLoaderClass({
79341
79764
  pluginStore: this.pluginStore,
@@ -79727,11 +80150,16 @@ var init_in_process_runtime = __esm({
79727
80150
  this.selfHealingManager.start();
79728
80151
  this.stuckTaskDetector.start();
79729
80152
  this.setupEventForwarding();
79730
- await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
79731
- await this.executor.resumeOrphaned();
79732
- void this.selfHealingManager.runStartupRecovery().catch((err) => {
79733
- runtimeLog.error("Self-healing startup recovery failed:", err);
79734
- });
80153
+ const startupSettings = await this.taskStore.getSettings();
80154
+ if (startupSettings.globalPause || startupSettings.enginePaused) {
80155
+ this.startupRecoveryDeferred = true;
80156
+ runtimeLog.log(
80157
+ `Startup recovery deferred \u2014 ${startupSettings.globalPause ? "global pause" : "engine pause"} is active`
80158
+ );
80159
+ } else {
80160
+ this.startupRecoveryDeferred = false;
80161
+ await this.resumeStartupRecoverySequence();
80162
+ }
79735
80163
  this.scheduler.start();
79736
80164
  this.triageProcessor?.start();
79737
80165
  this.missionExecutionLoop = missionExecutionLoop;
@@ -79897,6 +80325,45 @@ var init_in_process_runtime = __esm({
79897
80325
  setMergeEnqueuer(enqueueMerge) {
79898
80326
  this.mergeEnqueuer = enqueueMerge;
79899
80327
  }
80328
+ /**
80329
+ * Resume executor/self-healing activity after an unpause transition.
80330
+ *
80331
+ * When startup recovery had been deferred, this replays the original startup
80332
+ * ordering so orphan resume and self-healing cannot race each other.
80333
+ */
80334
+ async resumeAfterUnpause() {
80335
+ if (!this.taskStore || !this.executor || !this.selfHealingManager) {
80336
+ return;
80337
+ }
80338
+ if (this.resumeAfterUnpauseRunning) {
80339
+ return;
80340
+ }
80341
+ this.resumeAfterUnpauseRunning = true;
80342
+ try {
80343
+ const settings = await this.taskStore.getSettings();
80344
+ if (settings.globalPause || settings.enginePaused) {
80345
+ runtimeLog.log(
80346
+ `Unpause recovery still blocked \u2014 ${settings.globalPause ? "global pause" : "engine pause"} remains active`
80347
+ );
80348
+ return;
80349
+ }
80350
+ if (this.startupRecoveryDeferred) {
80351
+ await this.resumeStartupRecoverySequence();
80352
+ this.startupRecoveryDeferred = false;
80353
+ return;
80354
+ }
80355
+ await this.executor.resumeOrphaned();
80356
+ } finally {
80357
+ this.resumeAfterUnpauseRunning = false;
80358
+ }
80359
+ }
80360
+ async resumeStartupRecoverySequence() {
80361
+ await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
80362
+ await this.executor.resumeOrphaned();
80363
+ void this.selfHealingManager.runStartupRecovery().catch((err) => {
80364
+ runtimeLog.error("Self-healing startup recovery failed:", err);
80365
+ });
80366
+ }
79900
80367
  /**
79901
80368
  * Get the project's TaskStore instance.
79902
80369
  * @throws Error if runtime has not been started
@@ -83736,13 +84203,13 @@ ${detail}`
83736
84203
  if (prev.globalPause && !s.globalPause) {
83737
84204
  runtimeLog.log("Global unpause \u2014 resuming agentic activity");
83738
84205
  try {
83739
- const executor = this.runtime.executor;
83740
- executor?.resumeOrphaned?.().catch(
83741
- (err) => runtimeLog.error("Failed to resume orphaned tasks on unpause:", err)
84206
+ const runtime = this.runtime;
84207
+ runtime.resumeAfterUnpause?.().catch(
84208
+ (err) => runtimeLog.error("Failed to resume agentic activity on unpause:", err)
83742
84209
  );
83743
84210
  } catch (err) {
83744
84211
  runtimeLog.warn(
83745
- `Global unpause: failed to dispatch resumeOrphaned: ${err instanceof Error ? err.message : String(err)}`
84212
+ `Global unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`
83746
84213
  );
83747
84214
  }
83748
84215
  if (s.autoMerge) {
@@ -83766,13 +84233,13 @@ ${detail}`
83766
84233
  if (prev.enginePaused && !s.enginePaused) {
83767
84234
  runtimeLog.log("Engine unpaused \u2014 resuming agentic activity");
83768
84235
  try {
83769
- const executor = this.runtime.executor;
83770
- executor?.resumeOrphaned?.().catch(
83771
- (err) => runtimeLog.error("Failed to resume orphaned tasks on engine unpause:", err)
84236
+ const runtime = this.runtime;
84237
+ runtime.resumeAfterUnpause?.().catch(
84238
+ (err) => runtimeLog.error("Failed to resume agentic activity on engine unpause:", err)
83772
84239
  );
83773
84240
  } catch (err) {
83774
84241
  runtimeLog.warn(
83775
- `Engine unpause: failed to dispatch resumeOrphaned: ${err instanceof Error ? err.message : String(err)}`
84242
+ `Engine unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`
83776
84243
  );
83777
84244
  }
83778
84245
  if (s.autoMerge) {
@@ -136670,7 +137137,7 @@ Rules:
136670
137137
  // ../dashboard/src/routes/register-agent-import-export-generation-routes.ts
136671
137138
  import { createWriteStream } from "node:fs";
136672
137139
  import * as fsPromises2 from "node:fs/promises";
136673
- import { tmpdir as tmpdir3 } from "node:os";
137140
+ import { tmpdir as tmpdir4 } from "node:os";
136674
137141
  import { join as join45, resolve as resolve22 } from "node:path";
136675
137142
  import { Readable } from "node:stream";
136676
137143
  import { pipeline as streamPipeline } from "node:stream/promises";
@@ -136712,7 +137179,7 @@ function registerAgentImportExportRoutes(ctx) {
136712
137179
  } else if (typeof outputDir === "string") {
136713
137180
  throw badRequest("outputDir cannot be empty");
136714
137181
  } else {
136715
- resolvedOutputDir = await mkdtemp(join45(tmpdir3(), "fusion-agent-export-"));
137182
+ resolvedOutputDir = await mkdtemp(join45(tmpdir4(), "fusion-agent-export-"));
136716
137183
  }
136717
137184
  const result = await exportAgentsToDirectory2(agentsToExport, resolvedOutputDir, {
136718
137185
  companyName: typeof companyName === "string" ? companyName : void 0,
@@ -137027,7 +137494,7 @@ ${body}`;
137027
137494
  const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/main.tar.gz`;
137028
137495
  let tempDir = null;
137029
137496
  try {
137030
- tempDir = await mkdtemp(join45(tmpdir3(), `fn-agent-import-${importCompanySlug}-`));
137497
+ tempDir = await mkdtemp(join45(tmpdir4(), `fn-agent-import-${importCompanySlug}-`));
137031
137498
  const archivePath = join45(tempDir, "archive.tar.gz");
137032
137499
  const downloadController = new AbortController();
137033
137500
  const downloadTimeout = setTimeout(() => downloadController.abort(), 3e4);
@@ -141554,6 +142021,21 @@ ${stderr}`;
141554
142021
  });
141555
142022
  });
141556
142023
  }
142024
+ function buildSkippedStatusPayload(expectedVersion) {
142025
+ return {
142026
+ binary: {
142027
+ installed: false,
142028
+ invocation: FN_INSTALL_NPM
142029
+ },
142030
+ expectedVersion,
142031
+ state: "skipped",
142032
+ install: {
142033
+ npm: FN_INSTALL_NPM,
142034
+ curl: FN_INSTALL_CURL,
142035
+ package: FN_NPM_PACKAGE
142036
+ }
142037
+ };
142038
+ }
141557
142039
  var INSTALL_TIMEOUT_MS, MAX_OUTPUT_BYTES2, registerFnBinaryRoutes;
141558
142040
  var init_register_fn_binary_routes = __esm({
141559
142041
  "../dashboard/src/routes/register-fn-binary-routes.ts"() {
@@ -141564,11 +142046,23 @@ var init_register_fn_binary_routes = __esm({
141564
142046
  INSTALL_TIMEOUT_MS = 18e4;
141565
142047
  MAX_OUTPUT_BYTES2 = 64 * 1024;
141566
142048
  registerFnBinaryRoutes = (ctx) => {
141567
- const { router, rethrowAsApiError: rethrowAsApiError8 } = ctx;
142049
+ const { router, rethrowAsApiError: rethrowAsApiError8, store } = ctx;
142050
+ async function isCheckEnabled() {
142051
+ try {
142052
+ const settings = await store.getSettings();
142053
+ return settings.fnBinaryCheckEnabled !== false;
142054
+ } catch {
142055
+ return true;
142056
+ }
142057
+ }
141568
142058
  router.get("/system/fn-binary/status", async (_req, res) => {
141569
142059
  try {
141570
- const binary = await detectFnBinary();
141571
142060
  const expectedVersion = getCliPackageVersion();
142061
+ if (!await isCheckEnabled()) {
142062
+ res.json(buildSkippedStatusPayload(expectedVersion));
142063
+ return;
142064
+ }
142065
+ const binary = await detectFnBinary();
141572
142066
  res.json(buildStatusPayload(binary, expectedVersion));
141573
142067
  } catch (err) {
141574
142068
  if (err instanceof ApiError) throw err;
@@ -141577,6 +142071,13 @@ var init_register_fn_binary_routes = __esm({
141577
142071
  });
141578
142072
  router.post("/system/fn-binary/install", async (_req, res) => {
141579
142073
  try {
142074
+ if (!await isCheckEnabled()) {
142075
+ throw new ApiError(
142076
+ 409,
142077
+ "fn-binary checks are disabled in global settings (fnBinaryCheckEnabled=false). Re-enable them to install via the dashboard.",
142078
+ { code: "FN_BINARY_CHECK_DISABLED" }
142079
+ );
142080
+ }
141580
142081
  const installResult = await runNpmInstall();
141581
142082
  const binary = await detectFnBinary();
141582
142083
  const expectedVersion = getCliPackageVersion();
@@ -155723,6 +156224,43 @@ data: ${JSON.stringify(entry)}
155723
156224
  scopedStore.off("agent:log", onAgentLog);
155724
156225
  });
155725
156226
  });
156227
+ app.get("/api/agents/:id/runs/:runId/logs/stream", async (req, res) => {
156228
+ const agentId = req.params.id;
156229
+ const runId = req.params.runId;
156230
+ const projectId = typeof req.query.projectId === "string" ? req.query.projectId : void 0;
156231
+ res.setHeader("Content-Type", "text/event-stream");
156232
+ res.setHeader("Cache-Control", "no-cache");
156233
+ res.setHeader("Connection", "keep-alive");
156234
+ res.setHeader("X-Accel-Buffering", "no");
156235
+ res.flushHeaders();
156236
+ res.write(": connected\n\n");
156237
+ const engineManager = options?.engineManager;
156238
+ const engine2 = engineManager && projectId ? engineManager.getEngine(projectId) : options?.engine;
156239
+ const agentStore = engine2?.getAgentStore();
156240
+ if (!agentStore) {
156241
+ res.write(`event: error
156242
+ data: ${JSON.stringify({ message: "No active engine for project" })}
156243
+
156244
+ `);
156245
+ res.end();
156246
+ return;
156247
+ }
156248
+ const onRunLog = (eventAgentId, eventRunId, entry) => {
156249
+ if (eventAgentId !== agentId || eventRunId !== runId) return;
156250
+ res.write(`event: agent:log
156251
+ data: ${JSON.stringify(entry)}
156252
+
156253
+ `);
156254
+ };
156255
+ agentStore.on("run:log", onRunLog);
156256
+ const heartbeat = setInterval(() => {
156257
+ res.write(": heartbeat\n\n");
156258
+ }, 3e4);
156259
+ req.on("close", () => {
156260
+ clearInterval(heartbeat);
156261
+ agentStore.off("run:log", onRunLog);
156262
+ });
156263
+ });
155726
156264
  app.get("/api/terminal/sessions/:id/stream", rateLimit(RATE_LIMITS.sse), (req, res) => {
155727
156265
  const sessionId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
155728
156266
  res.setHeader("Content-Type", "text/event-stream");
@@ -158348,7 +158886,7 @@ var app_exports = {};
158348
158886
  __export(app_exports, {
158349
158887
  DashboardApp: () => DashboardApp
158350
158888
  });
158351
- import { useState as useState2, useSyncExternalStore, useCallback as useCallback2, useEffect as useEffect2 } from "react";
158889
+ import { useState as useState2, useSyncExternalStore, useCallback as useCallback2, useEffect as useEffect2, useRef } from "react";
158352
158890
  import { Box, Text, useInput, useApp, useStdout } from "ink";
158353
158891
  import Spinner from "ink-spinner";
158354
158892
  import TextInput from "ink-text-input";
@@ -159130,7 +159668,8 @@ function formatLogTime(iso) {
159130
159668
  function TaskDetailScreen({
159131
159669
  task,
159132
159670
  projectPath,
159133
- interactiveData
159671
+ interactiveData,
159672
+ controller
159134
159673
  }) {
159135
159674
  const { stdout } = useStdout();
159136
159675
  const cols = stdout?.columns ?? 80;
@@ -159195,6 +159734,27 @@ function TaskDetailScreen({
159195
159734
  useEffect2(() => {
159196
159735
  if (autoFollow) setLogScrollOffset(0);
159197
159736
  }, [autoFollow, logCount]);
159737
+ const WHEEL_STEP = 3;
159738
+ const logCountRef = useRef(logCount);
159739
+ const logPaneRowsRef = useRef(logPaneRows);
159740
+ logCountRef.current = logCount;
159741
+ logPaneRowsRef.current = logPaneRows;
159742
+ useEffect2(() => {
159743
+ return controller.onWheel((dir2) => {
159744
+ const maxOffset = Math.max(0, logCountRef.current - logPaneRowsRef.current);
159745
+ if (maxOffset === 0) return;
159746
+ if (dir2 === "up") {
159747
+ setAutoFollow(false);
159748
+ setLogScrollOffset((o) => Math.min(maxOffset, o + WHEEL_STEP));
159749
+ } else {
159750
+ setLogScrollOffset((o) => {
159751
+ const next = Math.max(0, o - WHEEL_STEP);
159752
+ if (next === 0) setAutoFollow(true);
159753
+ return next;
159754
+ });
159755
+ }
159756
+ });
159757
+ }, [controller]);
159198
159758
  useInput((input, key) => {
159199
159759
  if (detail && detail !== "unavailable" && detail.recentLogs.length > 0) {
159200
159760
  const maxOffset = Math.max(0, detail.recentLogs.length - logPaneRows);
@@ -159565,7 +160125,8 @@ function BoardView({ state, controller }) {
159565
160125
  {
159566
160126
  task: selectedTask,
159567
160127
  projectPath: selectedProject?.path ?? null,
159568
- interactiveData: state.interactiveData
160128
+ interactiveData: state.interactiveData,
160129
+ controller
159569
160130
  }
159570
160131
  ) }) : tasksState.loading ? /* @__PURE__ */ jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, gap: 1, children: [
159571
160132
  /* @__PURE__ */ jsx(Text, { color: "white", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
@@ -160387,7 +160948,7 @@ function PushModal({
160387
160948
  }
160388
160949
  );
160389
160950
  }
160390
- function GitView({ state }) {
160951
+ function GitView({ state, controller }) {
160391
160952
  const { stdout } = useStdout();
160392
160953
  const cols = stdout?.columns ?? 80;
160393
160954
  const data = state.interactiveData;
@@ -160575,6 +161136,23 @@ function GitView({ state }) {
160575
161136
  }
160576
161137
  }
160577
161138
  });
161139
+ const gitWheelRef = useRef({ activePane, commits, branches, worktrees });
161140
+ gitWheelRef.current = { activePane, commits, branches, worktrees };
161141
+ useEffect2(() => {
161142
+ if (state.interactiveView !== "git") return;
161143
+ return controller.onWheel((dir2) => {
161144
+ const { activePane: pane, commits: cs, branches: bs, worktrees: ws } = gitWheelRef.current;
161145
+ const STEP = 3;
161146
+ const delta = dir2 === "up" ? -STEP : STEP;
161147
+ if (pane === "commits") {
161148
+ setCommitIndex((i) => Math.max(0, Math.min(cs.length - 1, i + delta)));
161149
+ } else if (pane === "branches") {
161150
+ setBranchIndex((i) => Math.max(0, Math.min(bs.length - 1, i + delta)));
161151
+ } else if (pane === "worktrees") {
161152
+ setWorktreeIndex((i) => Math.max(0, Math.min(ws.length - 1, i + delta)));
161153
+ }
161154
+ });
161155
+ }, [controller, state.interactiveView]);
160578
161156
  const isNarrow = cols < NARROW_THRESHOLD;
160579
161157
  const leftWidth = Math.max(24, Math.floor(cols * 0.35));
160580
161158
  const rightWidth = cols - leftWidth - 1;
@@ -160916,7 +161494,7 @@ function entriesToNodes(entries, depth) {
160916
161494
  const files = filtered.filter((e) => !e.isDirectory).sort((a, b) => a.name.localeCompare(b.name));
160917
161495
  return [...dirs, ...files].map((e) => ({ entry: e, depth, expanded: false, children: void 0 }));
160918
161496
  }
160919
- function FilesView({ state }) {
161497
+ function FilesView({ state, controller }) {
160920
161498
  const { stdout } = useStdout();
160921
161499
  const cols = stdout?.columns ?? 80;
160922
161500
  const data = state.interactiveData;
@@ -161124,6 +161702,23 @@ function FilesView({ state }) {
161124
161702
  }
161125
161703
  }
161126
161704
  }, { isActive: state.interactiveView === "files" });
161705
+ const filesWheelRef = useRef({ focusedPane, flatNodes, previewResult, previewHeight });
161706
+ filesWheelRef.current = { focusedPane, flatNodes, previewResult, previewHeight };
161707
+ useEffect2(() => {
161708
+ if (state.interactiveView !== "files") return;
161709
+ return controller.onWheel((dir2) => {
161710
+ const { focusedPane: pane, flatNodes: nodes, previewResult: pr, previewHeight: ph } = filesWheelRef.current;
161711
+ const STEP = 3;
161712
+ const delta = dir2 === "up" ? -STEP : STEP;
161713
+ if (pane === "tree") {
161714
+ setSelectedIndex((i) => Math.max(0, Math.min(nodes.length - 1, i + delta)));
161715
+ } else {
161716
+ const lineCount = pr?.lineCount ?? 0;
161717
+ const maxScroll = Math.max(0, lineCount - ph);
161718
+ setPreviewScroll((s) => Math.max(0, Math.min(maxScroll, s + delta)));
161719
+ }
161720
+ });
161721
+ }, [controller, state.interactiveView]);
161127
161722
  const isNarrow = cols < NARROW_THRESHOLD;
161128
161723
  const treeWidth = isNarrow ? Math.max(20, cols - 2) : Math.max(20, Math.floor(cols * 0.38));
161129
161724
  const previewEntry = selectedNode && !selectedNode.entry.isDirectory ? selectedNode.entry : null;
@@ -161287,8 +161882,8 @@ function InteractiveMode({ state, controller }) {
161287
161882
  state.interactiveView === "board" && /* @__PURE__ */ jsx(BoardView, { state, controller }),
161288
161883
  state.interactiveView === "agents" && /* @__PURE__ */ jsx(AgentsView, { state }),
161289
161884
  state.interactiveView === "settings" && /* @__PURE__ */ jsx(SettingsInteractiveView, { state, controller }),
161290
- state.interactiveView === "git" && /* @__PURE__ */ jsx(GitView, { state }),
161291
- state.interactiveView === "files" && /* @__PURE__ */ jsx(FilesView, { state })
161885
+ state.interactiveView === "git" && /* @__PURE__ */ jsx(GitView, { state, controller }),
161886
+ state.interactiveView === "files" && /* @__PURE__ */ jsx(FilesView, { state, controller })
161292
161887
  ] })
161293
161888
  ] });
161294
161889
  }
@@ -161319,6 +161914,23 @@ function DashboardApp({ controller }) {
161319
161914
  useCallback2((cb) => controller.subscribe(cb), [controller]),
161320
161915
  useCallback2(() => controller.getSnapshot(), [controller])
161321
161916
  );
161917
+ const wheelStateRef = useRef(state);
161918
+ wheelStateRef.current = state;
161919
+ useEffect2(() => {
161920
+ return controller.onWheel((dir2) => {
161921
+ const s = wheelStateRef.current;
161922
+ if (s.activeSection !== "logs") return;
161923
+ const filtered = controller.getFilteredLogEntries();
161924
+ if (filtered.length === 0) return;
161925
+ const WHEEL_STEP = 3;
161926
+ const cur = s.selectedLogIndex;
161927
+ if (dir2 === "up") {
161928
+ controller.setSelectedLogIndex(Math.max(0, cur - WHEEL_STEP));
161929
+ } else {
161930
+ controller.setSelectedLogIndex(Math.min(filtered.length - 1, cur + WHEEL_STEP));
161931
+ }
161932
+ });
161933
+ }, [controller]);
161322
161934
  const [qrOverlay, setQrOverlay] = useState2(null);
161323
161935
  useInput((input, key) => {
161324
161936
  if ((input === "q" || input === "Q") && !key.ctrl || key.ctrl && input === "c") {
@@ -161701,6 +162313,15 @@ var init_controller = __esm({
161701
162313
  // no remote API is wired up).
161702
162314
  remoteStatus = null;
161703
162315
  remoteStatusTimer = null;
162316
+ // Mouse-wheel handling. We enable xterm SGR mouse mode in start() so the
162317
+ // terminal sends button reports for wheel up/down (buttons 64/65). A
162318
+ // parallel `data` listener parses those reports and dispatches to wheel
162319
+ // handlers. Ink's own keypress parser ignores SGR mouse sequences so
162320
+ // long as the full sequence (including the leading ESC) arrives in one
162321
+ // chunk — which it does once raw mode is enabled before mouse mode is
162322
+ // requested. (See ink#222 / @zenobius/ink-mouse for prior art.)
162323
+ wheelHandlers = /* @__PURE__ */ new Set();
162324
+ mouseStdinListener = null;
161704
162325
  constructor() {
161705
162326
  this.logBuffer = new LogRingBuffer();
161706
162327
  }
@@ -161709,6 +162330,15 @@ var init_controller = __esm({
161709
162330
  this.subscribers.add(callback);
161710
162331
  return () => this.subscribers.delete(callback);
161711
162332
  }
162333
+ /**
162334
+ * Subscribe to mouse-wheel events. Direction is "up" (scroll back/older
162335
+ * content) or "down" (scroll forward/newer content). Only fires while the
162336
+ * dashboard is running and the terminal supports xterm mouse reporting.
162337
+ */
162338
+ onWheel(handler) {
162339
+ this.wheelHandlers.add(handler);
162340
+ return () => this.wheelHandlers.delete(handler);
162341
+ }
161712
162342
  getSnapshot() {
161713
162343
  if (this.cachedSnapshot) return this.cachedSnapshot;
161714
162344
  this.cachedSnapshot = {
@@ -162093,6 +162723,10 @@ var init_controller = __esm({
162093
162723
  this.inkInstance = render(
162094
162724
  createElement(DashboardApp2, { controller: this })
162095
162725
  );
162726
+ if (process.stdin?.isTTY) {
162727
+ process.stdout.write("\x1B[?1000h\x1B[?1006h");
162728
+ this.installMouseListener();
162729
+ }
162096
162730
  this.resizeListener = () => {
162097
162731
  if (this.resizeDebounceTimer) clearTimeout(this.resizeDebounceTimer);
162098
162732
  this.resizeDebounceTimer = setTimeout(() => {
@@ -162199,10 +162833,48 @@ var init_controller = __esm({
162199
162833
  this.inkInstance = null;
162200
162834
  }
162201
162835
  if (process.stdout?.isTTY && typeof process.stdout.write === "function") {
162836
+ this.uninstallMouseListener();
162837
+ process.stdout.write("\x1B[?1006l\x1B[?1000l");
162202
162838
  process.stdout.write("\x1B[?1049l");
162203
162839
  }
162204
162840
  }
162205
162841
  // ── Private helpers ────────────────────────────────────────────────────────
162842
+ // Attach a parallel `data` listener that decodes xterm SGR mouse
162843
+ // sequences and dispatches wheel events. Ink's own listener is also
162844
+ // attached; SGR sequences arrive as a single chunk that Ink's keypress
162845
+ // parser silently ignores, so we don't need to (and shouldn't) strip
162846
+ // them from the stream.
162847
+ installMouseListener() {
162848
+ if (this.mouseStdinListener) return;
162849
+ const mouseRe = /\x1b\[<(\d+);\d+;\d+[Mm]/g;
162850
+ const listener = (chunk) => {
162851
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
162852
+ if (text.indexOf("\x1B[<") === -1) return;
162853
+ mouseRe.lastIndex = 0;
162854
+ let m;
162855
+ while ((m = mouseRe.exec(text)) !== null) {
162856
+ const btn = Number.parseInt(m[1] ?? "", 10);
162857
+ if (btn === 64) this.dispatchWheel("up");
162858
+ else if (btn === 65) this.dispatchWheel("down");
162859
+ }
162860
+ };
162861
+ this.mouseStdinListener = listener;
162862
+ process.stdin.on("data", listener);
162863
+ }
162864
+ uninstallMouseListener() {
162865
+ if (!this.mouseStdinListener) return;
162866
+ process.stdin.off("data", this.mouseStdinListener);
162867
+ this.mouseStdinListener = null;
162868
+ }
162869
+ dispatchWheel(direction) {
162870
+ for (const handler of this.wheelHandlers) {
162871
+ try {
162872
+ handler(direction);
162873
+ } catch (err) {
162874
+ tuiDebug2("wheel-handler-error", { err: String(err) });
162875
+ }
162876
+ }
162877
+ }
162206
162878
  clampSelectedLogIndex(entries) {
162207
162879
  if (entries.length === 0) {
162208
162880
  this.selectedLogIndex = 0;
@@ -170806,7 +171478,7 @@ __export(native_patch_exports, {
170806
171478
  });
170807
171479
  import { join as join68, basename as basename21, dirname as dirname28 } from "node:path";
170808
171480
  import { existsSync as existsSync51, copyFileSync, mkdirSync as mkdirSync12, symlinkSync as symlinkSync2, rmSync as rmSync5, lstatSync as lstatSync3, readlinkSync as readlinkSync2 } from "node:fs";
170809
- import { tmpdir as tmpdir4 } from "node:os";
171481
+ import { tmpdir as tmpdir5 } from "node:os";
170810
171482
  function findStagedNativeDir2() {
170811
171483
  const platform4 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
170812
171484
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
@@ -170851,7 +171523,7 @@ function setupNativeResolution() {
170851
171523
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
170852
171524
  }
170853
171525
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
170854
- const tmpRoot = join68(tmpdir4(), `fn-bunfs-${process.pid}`);
171526
+ const tmpRoot = join68(tmpdir5(), `fn-bunfs-${process.pid}`);
170855
171527
  const fnDir = join68(tmpRoot, "fn");
170856
171528
  const prebuildsDir = join68(fnDir, "prebuilds");
170857
171529
  const platformDir = join68(prebuildsDir, basename21(nativeDir));
@@ -170939,7 +171611,7 @@ var init_native_patch = __esm({
170939
171611
  import { existsSync as existsSync52, mkdtempSync as mkdtempSync2, readFileSync as readFileSync24, symlinkSync as symlinkSync3, writeFileSync as writeFileSync6 } from "node:fs";
170940
171612
  import { createRequire as createRequire7 } from "node:module";
170941
171613
  import { join as join69, dirname as dirname29, resolve as resolve41 } from "node:path";
170942
- import { tmpdir as tmpdir5 } from "node:os";
171614
+ import { tmpdir as tmpdir6 } from "node:os";
170943
171615
  import { performance as performance3 } from "node:perf_hooks";
170944
171616
  import { fileURLToPath as fileURLToPath11 } from "node:url";
170945
171617
  var isBunBinary3 = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
@@ -170947,7 +171619,7 @@ function configurePiPackage() {
170947
171619
  if (process.env.PI_PACKAGE_DIR) {
170948
171620
  return;
170949
171621
  }
170950
- const tmp = mkdtempSync2(join69(tmpdir5(), "fn-pkg-"));
171622
+ const tmp = mkdtempSync2(join69(tmpdir6(), "fn-pkg-"));
170951
171623
  let packageJson = {
170952
171624
  name: "pi",
170953
171625
  version: "0.1.0",