@runfusion/fusion 0.14.1 → 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 (40) hide show
  1. package/dist/bin.js +934 -166
  2. package/dist/client/assets/AgentDetailView-BBCnqhqI.js +18 -0
  3. package/dist/client/assets/{AgentsView-DoXb_amw.js → AgentsView-BY-Yq-Te.js} +3 -3
  4. package/dist/client/assets/ChatView-DkoJNxFW.js +1 -0
  5. package/dist/client/assets/{DevServerView-DbgM4tlT.js → DevServerView-qvs6pp6c.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-DfmtfMiu.js → DirectoryPicker-BkAIXNrP.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-_-Efkx_W.js → DocumentsView-BcaUGgaL.js} +1 -1
  8. package/dist/client/assets/{InsightsView-DUjcfW53.js → InsightsView-Dz9Ivclw.js} +1 -1
  9. package/dist/client/assets/{MemoryView-DxMPBb0q.js → MemoryView-BsweARBT.js} +1 -1
  10. package/dist/client/assets/{NodesView-BEBTI15s.js → NodesView-bAU-v4bJ.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-BpMYhHH_.js → PiExtensionsManager-C_U2g7y3.js} +2 -2
  12. package/dist/client/assets/{PluginManager-CPv7yQd3.js → PluginManager-pIDsTk5v.js} +1 -1
  13. package/dist/client/assets/{ResearchView-BrFvdyXT.js → ResearchView-D4Eib_uR.js} +1 -1
  14. package/dist/client/assets/{RoadmapsView-BDjLrtcj.js → RoadmapsView-BaGwsUGS.js} +1 -1
  15. package/dist/client/assets/{SettingsModal-CxDxiTRy.js → SettingsModal-BiZVi3cI.js} +1 -1
  16. package/dist/client/assets/SettingsModal-CRyg643t.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-DFUA4X3z.js → SetupWizardModal-BcIGBBpA.js} +1 -1
  18. package/dist/client/assets/{SkillMultiselect-BUWe5ujb.js → SkillMultiselect-DPARHJeQ.js} +1 -1
  19. package/dist/client/assets/{SkillsView-RAkqGX3y.js → SkillsView-Da_d_HPu.js} +1 -1
  20. package/dist/client/assets/{TodoView-Ceb0wrg1.js → TodoView-5rAeqYtV.js} +1 -1
  21. package/dist/client/assets/{folder-open-DcM-Vd6r.js → folder-open-CgjcFqww.js} +1 -1
  22. package/dist/client/assets/index-D1gTSlYB.css +1 -0
  23. package/dist/client/assets/{index-DH3aprf6.js → index-DoQ5ALYY.js} +150 -149
  24. package/dist/client/assets/{list-checks-ByGHVQpZ.js → list-checks-C9YWtF7h.js} +1 -1
  25. package/dist/client/assets/{star-DlEYI8GL.js → star-4nUh67-U.js} +1 -1
  26. package/dist/client/assets/{upload-DKshabz-.js → upload-CEt5-Bnq.js} +1 -1
  27. package/dist/client/assets/{users-X6tYPPBV.js → users-4I0JDmgO.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/extension.js +709 -146
  31. package/dist/pi-claude-cli/index.ts +2 -2
  32. package/dist/pi-claude-cli/package.json +1 -1
  33. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +107 -0
  34. package/dist/pi-claude-cli/src/event-bridge.ts +48 -4
  35. package/package.json +1 -1
  36. package/skill/fusion/references/engine-tools.md +0 -1
  37. package/dist/client/assets/AgentDetailView-B3KAsP2O.js +0 -18
  38. package/dist/client/assets/ChatView-BJ2c7wvd.js +0 -1
  39. package/dist/client/assets/SettingsModal-Cd-QGB0C.js +0 -31
  40. package/dist/client/assets/index-C1prPuSl.css +0 -1
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");
@@ -37493,6 +37505,60 @@ var init_plugin_loader = __esm({
37493
37505
  }
37494
37506
  return runtimes2;
37495
37507
  }
37508
+ /**
37509
+ * Get all skill contributions from loaded plugins.
37510
+ */
37511
+ getPluginSkills() {
37512
+ const skills = [];
37513
+ for (const [pluginId, plugin4] of this.plugins) {
37514
+ if (plugin4.skills) {
37515
+ for (const skill of plugin4.skills) {
37516
+ skills.push({ pluginId, skill });
37517
+ }
37518
+ }
37519
+ }
37520
+ return skills;
37521
+ }
37522
+ /**
37523
+ * Get all workflow step contributions from loaded plugins.
37524
+ */
37525
+ getPluginWorkflowSteps() {
37526
+ const steps = [];
37527
+ for (const [pluginId, plugin4] of this.plugins) {
37528
+ if (plugin4.workflowSteps) {
37529
+ for (const step of plugin4.workflowSteps) {
37530
+ steps.push({ pluginId, step });
37531
+ }
37532
+ }
37533
+ }
37534
+ return steps;
37535
+ }
37536
+ /**
37537
+ * Get all prompt contributions from loaded plugins.
37538
+ */
37539
+ getPluginPromptContributions() {
37540
+ const contributions = [];
37541
+ for (const [pluginId, plugin4] of this.plugins) {
37542
+ if (plugin4.promptContributions) {
37543
+ for (const contribution of plugin4.promptContributions.contributions) {
37544
+ contributions.push({ pluginId, contribution, config: plugin4.promptContributions });
37545
+ }
37546
+ }
37547
+ }
37548
+ return contributions;
37549
+ }
37550
+ /**
37551
+ * Get all setup metadata and hooks from loaded plugins.
37552
+ */
37553
+ getPluginSetupInfo() {
37554
+ const setups = [];
37555
+ for (const [pluginId, plugin4] of this.plugins) {
37556
+ if (plugin4.setup) {
37557
+ setups.push({ pluginId, manifest: plugin4.setup.manifest, hooks: plugin4.setup.hooks });
37558
+ }
37559
+ }
37560
+ return setups;
37561
+ }
37496
37562
  /**
37497
37563
  * Get all loaded plugin instances.
37498
37564
  */
@@ -37613,7 +37679,7 @@ async function syncBackupAutomation(automationStore, settings) {
37613
37679
  if (!AutomationStore2.isValidCron(schedule)) {
37614
37680
  throw new Error(`Invalid backup schedule: ${schedule}`);
37615
37681
  }
37616
- const command = "npx runfusion.ai backup --create";
37682
+ const command = "fn backup --create";
37617
37683
  if (existingSchedule) {
37618
37684
  return await automationStore.updateSchedule(existingSchedule.id, {
37619
37685
  scheduleType: "custom",
@@ -37646,7 +37712,7 @@ async function syncBackupRoutine(routineStore, settings) {
37646
37712
  if (!RoutineStore2.isValidCron(schedule)) {
37647
37713
  throw new Error(`Invalid backup schedule: ${schedule}`);
37648
37714
  }
37649
- const command = "npx runfusion.ai backup --create";
37715
+ const command = "fn backup --create";
37650
37716
  const input = {
37651
37717
  name: BACKUP_SCHEDULE_NAME,
37652
37718
  description: "Automatic database backup based on project settings",
@@ -49521,7 +49587,7 @@ var require_dist3 = __commonJS({
49521
49587
 
49522
49588
  // ../core/src/agent-companies-parser.ts
49523
49589
  import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync4 } from "node:fs";
49524
- import { tmpdir as tmpdir2 } from "node:os";
49590
+ import { tmpdir as tmpdir3 } from "node:os";
49525
49591
  import { join as join20, resolve as resolve9 } from "node:path";
49526
49592
  function slugifyAgentReference(value) {
49527
49593
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -49844,7 +49910,7 @@ async function extractTarArchive(archivePath, outputDir) {
49844
49910
  }
49845
49911
  async function parseCompanyArchive(archivePath) {
49846
49912
  const resolvedArchivePath = resolve9(archivePath);
49847
- const tempDir = mkdtempSync(join20(tmpdir2(), "agent-companies-"));
49913
+ const tempDir = mkdtempSync(join20(tmpdir3(), "agent-companies-"));
49848
49914
  try {
49849
49915
  if (resolvedArchivePath.endsWith(".tar.gz") || resolvedArchivePath.endsWith(".tgz")) {
49850
49916
  await extractTarArchive(resolvedArchivePath, tempDir);
@@ -56178,65 +56244,6 @@ ${lines.join("\n")}`
56178
56244
  }
56179
56245
  };
56180
56246
  }
56181
- function createIdentityTool({ agent, resolvedInstructions }) {
56182
- const identityParams = Type.Object({});
56183
- return {
56184
- name: "fn_identity",
56185
- label: "Identity Check",
56186
- description: "Return a structured summary of which soul, instructions, and memory are loaded for this heartbeat tick. Call this FIRST before any other tool.",
56187
- parameters: identityParams,
56188
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
56189
- execute: async (_id, _params, _signal, _onUpdate, _ctx) => {
56190
- const PREVIEW_CHARS = 500;
56191
- const INSTRUCTIONS_PREVIEW_CHARS = 1e3;
56192
- const MEMORY_PREVIEW_CHARS = 1e3;
56193
- const soulPresent = typeof agent.soul === "string" && agent.soul.trim().length > 0;
56194
- const instructionsPresent = resolvedInstructions.trim().length > 0;
56195
- const memoryPresent = typeof agent.memory === "string" && agent.memory.trim().length > 0;
56196
- const soulPreview = soulPresent ? agent.soul.slice(0, PREVIEW_CHARS) : "";
56197
- const instructionsPreview = instructionsPresent ? resolvedInstructions.slice(0, INSTRUCTIONS_PREVIEW_CHARS) : "";
56198
- const memoryPreview = memoryPresent ? agent.memory.slice(0, MEMORY_PREVIEW_CHARS) : "";
56199
- const result = {
56200
- agentId: agent.id,
56201
- name: agent.name,
56202
- role: agent.role,
56203
- soulPresent,
56204
- instructionsPresent,
56205
- memoryPresent,
56206
- soulPreview,
56207
- instructionsPreview,
56208
- memoryPreview
56209
- };
56210
- const lines = [
56211
- `agentId: ${result.agentId}`,
56212
- `name: ${result.name}`,
56213
- `role: ${result.role}`,
56214
- `soul: ${result.soulPresent ? "loaded" : "absent"}`,
56215
- `instructions: ${result.instructionsPresent ? "loaded" : "absent"}`,
56216
- `memory: ${result.memoryPresent ? "loaded" : "absent"}`
56217
- ];
56218
- if (result.soulPresent && result.soulPreview) {
56219
- lines.push(`
56220
- Soul preview (first ${PREVIEW_CHARS} chars):
56221
- ${result.soulPreview}`);
56222
- }
56223
- if (result.instructionsPresent && result.instructionsPreview) {
56224
- lines.push(`
56225
- Instructions preview (first ${INSTRUCTIONS_PREVIEW_CHARS} chars):
56226
- ${result.instructionsPreview}`);
56227
- }
56228
- if (result.memoryPresent && result.memoryPreview) {
56229
- lines.push(`
56230
- Memory preview (first ${MEMORY_PREVIEW_CHARS} chars):
56231
- ${result.memoryPreview}`);
56232
- }
56233
- return {
56234
- content: [{ type: "text", text: lines.join("\n") }],
56235
- details: result
56236
- };
56237
- }
56238
- };
56239
- }
56240
56247
  var taskCreateParams, taskLogParams, taskDocumentWriteParams, taskDocumentReadParams, reflectOnPerformanceParams, listAgentsParams, delegateTaskParams, sendMessageParams, readMessagesParams, memorySearchParams, memoryGetParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, memoryAppendParams, log10, AGENT_MEMORY_ROOT2, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2;
56241
56248
  var init_agent_tools = __esm({
56242
56249
  "../engine/src/agent-tools.ts"() {
@@ -65772,6 +65779,20 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65772
65779
  );
65773
65780
  this.activeStepExecutors.delete(task.id);
65774
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
+ }
65775
65796
  this.disposeSubagentsForTask(task.id, `parent moved from in-progress to ${to}`);
65776
65797
  this.loopRecoveryState.delete(task.id);
65777
65798
  this.spawnedAgents.delete(task.id);
@@ -65804,8 +65825,42 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65804
65825
  this.disposeSubagentsForTask(task.id, "task paused");
65805
65826
  return;
65806
65827
  }
65807
- 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)) {
65808
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
+ }
65809
65864
  this.resumingUnpaused.add(task.id);
65810
65865
  executorLog.log(`Unpaused ${task.id} in-progress with no session \u2014 resuming execution`);
65811
65866
  try {
@@ -65924,6 +65979,22 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65924
65979
  this.spawnedAgents.delete(taskId);
65925
65980
  this.stuckAborted.delete(taskId);
65926
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
+ }
65927
65998
  }
65928
65999
  });
65929
66000
  }
@@ -65941,6 +66012,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65941
66012
  activeSessions = /* @__PURE__ */ new Map();
65942
66013
  /** Active step-session executors per task (mutually exclusive with activeSessions). */
65943
66014
  activeStepExecutors = /* @__PURE__ */ new Map();
66015
+ /** Active pre-merge workflow step sessions per task. */
66016
+ activeWorkflowStepSessions = /* @__PURE__ */ new Map();
65944
66017
  /**
65945
66018
  * Reviewer subagent sessions per task. Reviewers (`reviewer.ts`) create their
65946
66019
  * own AgentSessions that aren't part of `activeSessions`/`activeStepExecutors`,
@@ -65987,6 +66060,69 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65987
66060
  await this.store.mergeTask(taskId);
65988
66061
  return "merged";
65989
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
+ }
65990
66126
  /** Child agent sessions keyed by agent ID. Used for termination. */
65991
66127
  childSessions = /* @__PURE__ */ new Map();
65992
66128
  /** Total count of currently spawned agents (across all parents). */
@@ -66181,11 +66317,15 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66181
66317
  this.clearCompletedTaskWatchdog(taskId);
66182
66318
  const handle = setTimeout(async () => {
66183
66319
  this.completedTaskWatchdogs.delete(taskId);
66184
- 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)) {
66185
66321
  return;
66186
66322
  }
66187
66323
  this.recoveringCompleted.add(taskId);
66188
66324
  try {
66325
+ const pauseLabel = await this.getExecutionPauseLabel();
66326
+ if (pauseLabel) {
66327
+ return;
66328
+ }
66189
66329
  let currentTask = null;
66190
66330
  try {
66191
66331
  currentTask = await this.store.getTask(taskId);
@@ -66231,6 +66371,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66231
66371
  * stuck.
66232
66372
  */
66233
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
+ }
66234
66379
  if (this.workflowRerunPending.has(taskId)) {
66235
66380
  executorLog.warn(`${taskId}: workflow rerun bounce already in flight \u2014 skipping re-entry`);
66236
66381
  return "skipped-pending";
@@ -66241,6 +66386,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66241
66386
  if (!latestTask) {
66242
66387
  throw new Error("task missing during workflow rerun bounce");
66243
66388
  }
66389
+ if (latestTask.paused) {
66390
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 task is paused`);
66391
+ return "deferred-paused";
66392
+ }
66244
66393
  if (latestTask.column === "in-progress") {
66245
66394
  const originalExecutionStartedAt = latestTask.executionStartedAt;
66246
66395
  if (preserveResumeState) {
@@ -66252,11 +66401,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66252
66401
  worktree: worktreePath,
66253
66402
  executionStartedAt: originalExecutionStartedAt ?? null
66254
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
+ }
66255
66409
  await this.store.moveTask(taskId, "in-progress");
66256
66410
  return "bounced";
66257
66411
  }
66258
66412
  if (latestTask.column === "todo") {
66259
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
+ }
66260
66419
  await this.store.moveTask(taskId, "in-progress");
66261
66420
  return "bounced";
66262
66421
  }
@@ -66272,8 +66431,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66272
66431
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
66273
66432
  if (outcome === "bounced") {
66274
66433
  executorLog.log(successMessage);
66275
- } else {
66434
+ } else if (outcome === "skipped-pending") {
66276
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`);
66277
66438
  }
66278
66439
  } catch (err) {
66279
66440
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -66282,6 +66443,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66282
66443
  }, 0);
66283
66444
  const watchdog = setTimeout(async () => {
66284
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
+ }
66285
66451
  let currentTask = null;
66286
66452
  try {
66287
66453
  currentTask = await this.store.getTask(taskId);
@@ -66304,7 +66470,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66304
66470
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
66305
66471
  if (outcome === "bounced") {
66306
66472
  executorLog.warn(`${taskId}: workflow rerun watchdog retry succeeded`);
66307
- } else {
66473
+ } else if (outcome === "skipped-pending") {
66308
66474
  executorLog.error(
66309
66475
  `${taskId}: workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
66310
66476
  );
@@ -66312,6 +66478,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66312
66478
  taskId,
66313
66479
  `Workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
66314
66480
  ).catch(() => void 0);
66481
+ } else {
66482
+ executorLog.log(`${taskId}: workflow rerun watchdog retry deferred while pause is active`);
66315
66483
  }
66316
66484
  } catch (err) {
66317
66485
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -66434,11 +66602,17 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66434
66602
  */
66435
66603
  async recoverCompletedTask(task) {
66436
66604
  try {
66437
- 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)) {
66438
66606
  executorLog.log(`${task.id}: skipping recoverCompletedTask \u2014 task has active execution in flight`);
66439
66607
  return false;
66440
66608
  }
66441
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
+ }
66442
66616
  if (task.worktree && existsSync27(task.worktree)) {
66443
66617
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
66444
66618
  if (modifiedFiles.length > 0) {
@@ -66446,7 +66620,16 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66446
66620
  executorLog.log(`${task.id}: recovered ${modifiedFiles.length} modified files`);
66447
66621
  }
66448
66622
  if (task.executionMode !== "fast") {
66623
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps during completed-task recovery")) {
66624
+ return false;
66625
+ }
66449
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
+ }
66450
66633
  if (!workflowResult.allPassed) {
66451
66634
  await this.sendTaskBackForFix(task, task.worktree, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed during recovery", false);
66452
66635
  return true;
@@ -66455,6 +66638,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66455
66638
  executorLog.log(`${task.id}: fast mode \u2014 skipping workflow steps on auto-recovery`);
66456
66639
  }
66457
66640
  }
66641
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition during completed-task recovery")) {
66642
+ return false;
66643
+ }
66458
66644
  await this.persistTokenUsage(task.id);
66459
66645
  await this.store.moveTask(task.id, "in-review");
66460
66646
  this.clearCompletedTaskWatchdog(task.id);
@@ -66520,6 +66706,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66520
66706
  * directly to in-review without spawning a new agent session.
66521
66707
  */
66522
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
+ }
66523
66716
  const tasks = await this.store.listTasks({ slim: true, column: "in-progress" });
66524
66717
  const inProgress = tasks.filter(
66525
66718
  (t) => t.column === "in-progress" && !this.executing.has(t.id) && !t.paused
@@ -66958,8 +67151,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66958
67151
  await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
66959
67152
  }
66960
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
+ }
66961
67157
  if (executionMode !== "fast") {
66962
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
+ }
66963
67169
  if (!workflowResult.allPassed) {
66964
67170
  if (workflowResult.revisionRequested) {
66965
67171
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66977,6 +67183,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66977
67183
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66978
67184
  }
66979
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
+ }
66980
67189
  await this.store.moveTask(task.id, "in-review");
66981
67190
  this.clearCompletedTaskWatchdog(task.id);
66982
67191
  await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
@@ -67329,6 +67538,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67329
67538
  this.pausedAborted.delete(task.id);
67330
67539
  wasPaused = true;
67331
67540
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
67541
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
67542
+ return;
67543
+ }
67332
67544
  executorLog.log(`${task.id} paused after completion (graceful session exit) \u2014 finalizing to in-review`);
67333
67545
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review");
67334
67546
  await this.persistTokenUsage(task.id);
@@ -67365,8 +67577,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67365
67577
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
67366
67578
  }
67367
67579
  this.scheduleCompletedTaskWatchdog(task.id, "task completion");
67580
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion")) {
67581
+ return;
67582
+ }
67368
67583
  if (executionMode !== "fast") {
67369
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
+ }
67370
67597
  if (!workflowResult.allPassed) {
67371
67598
  if (workflowResult.revisionRequested) {
67372
67599
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -67384,6 +67611,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67384
67611
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
67385
67612
  }
67386
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
+ }
67387
67617
  await this.persistTokenUsage(task.id);
67388
67618
  await this.store.moveTask(task.id, "in-review");
67389
67619
  this.clearCompletedTaskWatchdog(task.id);
@@ -67508,8 +67738,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67508
67738
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
67509
67739
  }
67510
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
+ }
67511
67744
  if (executionMode !== "fast") {
67512
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
+ }
67513
67758
  if (!workflowResult.allPassed) {
67514
67759
  if (workflowResult.revisionRequested) {
67515
67760
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -67523,6 +67768,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67523
67768
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
67524
67769
  }
67525
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
+ }
67526
67774
  await this.persistTokenUsage(task.id);
67527
67775
  await this.store.moveTask(task.id, "in-review");
67528
67776
  this.clearCompletedTaskWatchdog(task.id);
@@ -67615,6 +67863,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67615
67863
  } else if (this.pausedAborted.has(task.id)) {
67616
67864
  this.pausedAborted.delete(task.id);
67617
67865
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
67866
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
67867
+ return;
67868
+ }
67618
67869
  executorLog.log(`${task.id} paused after completion \u2014 finalizing to in-review`);
67619
67870
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review", void 0, this.currentRunContext);
67620
67871
  await this.persistTokenUsage(task.id);
@@ -67973,22 +68224,28 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67973
68224
  if (params.summary) {
67974
68225
  await store.updateTask(taskId, { summary: params.summary });
67975
68226
  }
67976
- 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
+ }
67977
68234
  await store.logEntry(taskId, "Task marked done by agent");
67978
68235
  const latestTask = await store.getTask(taskId);
67979
68236
  let latestColumn = latestTask.column;
67980
68237
  if (latestColumn === "todo") {
67981
68238
  await store.logEntry(
67982
68239
  taskId,
67983
- "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"
67984
68241
  );
67985
68242
  await store.moveTask(taskId, "in-progress");
67986
68243
  latestColumn = "in-progress";
67987
68244
  }
67988
- if (latestColumn === "in-progress") {
68245
+ if (latestColumn === "in-progress" && !hardPauseActive) {
67989
68246
  this.scheduleCompletedTaskWatchdog(taskId, "fn_task_done");
67990
68247
  }
67991
- 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.";
67992
68249
  return {
67993
68250
  content: [{ type: "text", text: successMessage }],
67994
68251
  details: {}
@@ -68563,6 +68820,9 @@ ${failureFeedback}
68563
68820
  await this.store.updateTask(task.id, { workflowStepResults: results });
68564
68821
  continue;
68565
68822
  }
68823
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `before workflow step '${ws.name}'`)) {
68824
+ return "deferred-paused";
68825
+ }
68566
68826
  await this.store.logEntry(task.id, `[pre-merge] Starting workflow step: ${ws.name} (${stepMode} mode)`);
68567
68827
  executorLog.log(`${task.id} \u2014 [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
68568
68828
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -68577,6 +68837,9 @@ ${failureFeedback}
68577
68837
  await this.store.updateTask(task.id, { workflowStepResults: results });
68578
68838
  try {
68579
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
+ }
68580
68843
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
68581
68844
  if (result.success) {
68582
68845
  await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
@@ -68642,6 +68905,9 @@ ${failureFeedback}
68642
68905
  };
68643
68906
  }
68644
68907
  } catch (err) {
68908
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68909
+ return "deferred-paused";
68910
+ }
68645
68911
  const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
68646
68912
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
68647
68913
  await this.store.logEntry(
@@ -68807,6 +69073,7 @@ and show an appropriate message to the user.\`
68807
69073
  task.id,
68808
69074
  `Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`
68809
69075
  );
69076
+ this.activeWorkflowStepSessions.set(task.id, session);
68810
69077
  let output = "";
68811
69078
  session.subscribe((event) => {
68812
69079
  if (event.type === "message_update") {
@@ -68879,6 +69146,10 @@ Review the work done in this worktree and evaluate it against the criteria in yo
68879
69146
  return { success: false, error: errorMessage };
68880
69147
  } finally {
68881
69148
  if (timeoutHandle) clearTimeout(timeoutHandle);
69149
+ const activeWorkflowStepSession = this.activeWorkflowStepSessions.get(task.id);
69150
+ if (activeWorkflowStepSession === session) {
69151
+ this.activeWorkflowStepSessions.delete(task.id);
69152
+ }
68882
69153
  void timedOut;
68883
69154
  }
68884
69155
  };
@@ -70622,6 +70893,15 @@ var init_scheduler = __esm({
70622
70893
  schedulerLog.log(`Task ${task.id} is paused \u2014 skipping dispatch`);
70623
70894
  continue;
70624
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
+ }
70625
70905
  let effectiveNode = resolveEffectiveNode(freshTask, settings);
70626
70906
  schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
70627
70907
  if (effectiveNode.nodeId !== void 0 && this.options.nodeHealthMonitor) {
@@ -72742,6 +73022,7 @@ Rules:
72742
73022
 
72743
73023
  // ../engine/src/agent-heartbeat.ts
72744
73024
  import { Type as Type6 } from "@mariozechner/pi-ai";
73025
+ import { createHash as createHash5 } from "node:crypto";
72745
73026
  function isBlockedStateDuplicate(current, previous) {
72746
73027
  return current.blockedBy === previous.blockedBy && current.contextHash === previous.contextHash;
72747
73028
  }
@@ -72751,39 +73032,30 @@ function truncatePrompt(text, maxChars) {
72751
73032
 
72752
73033
  ... (truncated, ${text.length} chars)`;
72753
73034
  }
73035
+ function shortContentHash(value) {
73036
+ return createHash5("sha256").update(value).digest("hex").slice(0, 8);
73037
+ }
72754
73038
  function buildIdentitySnapshot(args) {
72755
73039
  const { agent, resolvedInstructions } = args;
72756
- const SOUL_PREVIEW = 500;
72757
- const INSTR_PREVIEW = 1e3;
72758
- const MEM_PREVIEW = 1e3;
72759
- const soulPresent = typeof agent.soul === "string" && agent.soul.trim().length > 0;
72760
- const instrPresent = resolvedInstructions.trim().length > 0;
72761
- const memPresent = typeof agent.memory === "string" && agent.memory.trim().length > 0;
72762
- const lines = [
73040
+ const soulTrimmed = typeof agent.soul === "string" ? agent.soul.trim() : "";
73041
+ const instrTrimmed = resolvedInstructions.trim();
73042
+ const memTrimmed = typeof agent.memory === "string" ? agent.memory.trim() : "";
73043
+ const formatField = (trimmed) => {
73044
+ if (!trimmed) return "absent";
73045
+ return `loaded (${trimmed.length} chars, sha256:${shortContentHash(trimmed)})`;
73046
+ };
73047
+ return [
72763
73048
  "## Identity Snapshot",
72764
73049
  "",
72765
- "Verify these match what you expect. Surface any anomalies in your first text output before acting.",
73050
+ "Full content is in the Custom Instructions section of your system prompt. Surface anomalies in your first text output before acting.",
72766
73051
  "",
72767
73052
  `- agentId: ${agent.id}`,
72768
73053
  `- name: ${agent.name}`,
72769
73054
  `- role: ${agent.role}`,
72770
- `- soul: ${soulPresent ? "loaded" : "absent"}`,
72771
- `- instructions: ${instrPresent ? "loaded" : "absent"}`,
72772
- `- memory: ${memPresent ? "loaded" : "absent"}`
72773
- ];
72774
- if (soulPresent) {
72775
- const preview = agent.soul.trim().slice(0, SOUL_PREVIEW);
72776
- lines.push("", `### Soul (first ${SOUL_PREVIEW} chars)`, preview);
72777
- }
72778
- if (instrPresent) {
72779
- const preview = resolvedInstructions.trim().slice(0, INSTR_PREVIEW);
72780
- lines.push("", `### Instructions (first ${INSTR_PREVIEW} chars)`, preview);
72781
- }
72782
- if (memPresent) {
72783
- const preview = agent.memory.trim().slice(0, MEM_PREVIEW);
72784
- lines.push("", `### Memory (first ${MEM_PREVIEW} chars)`, preview);
72785
- }
72786
- return lines.join("\n");
73055
+ `- soul: ${formatField(soulTrimmed)}`,
73056
+ `- instructions: ${formatField(instrTrimmed)}`,
73057
+ `- memory: ${formatField(memTrimmed)}`
73058
+ ].join("\n");
72787
73059
  }
72788
73060
  async function getHeartbeatMemorySettings(taskStore) {
72789
73061
  const maybeGetSettings = taskStore.getSettings;
@@ -72965,9 +73237,8 @@ When sending messages:
72965
73237
  1. **Identity & context** \u2014 review the **Identity Snapshot** at the top of
72966
73238
  this prompt. Confirm your role, soul, instructions, and memory match what
72967
73239
  you expect, and surface any anomalies in your first text output before
72968
- doing anything else. (If fn_identity is available in your runtime you may
72969
- also call it for full structured detail; the snapshot above is the
72970
- authoritative source.)
73240
+ doing anything else. The full content is in the Custom Instructions
73241
+ section of your system prompt.
72971
73242
  2. **Inbox** \u2014 when fn_read_messages is available, call it. Process any pending
72972
73243
  messages first; reply with reply_to_message_id when answering.
72973
73244
  3. **Wake delta** \u2014 read the Wake Delta block above. The wake reason is the
@@ -72993,9 +73264,8 @@ a bug. Do not loop on the same plan across heartbeats without recording why.`;
72993
73264
  1. **Identity & context** \u2014 review the **Identity Snapshot** at the top of
72994
73265
  this prompt. Confirm your role, soul, instructions, and memory match what
72995
73266
  you expect, and surface any anomalies in your first text output before
72996
- doing anything else. (If fn_identity is available in your runtime you may
72997
- also call it for full structured detail; the snapshot above is the
72998
- authoritative source.)
73267
+ doing anything else. The full content is in the Custom Instructions
73268
+ section of your system prompt.
72999
73269
  2. **Inbox** \u2014 when fn_read_messages is available, call it. Process any pending
73000
73270
  messages first; reply with reply_to_message_id when answering.
73001
73271
  3. **Wake delta** \u2014 read the Wake Delta block above. The wake reason is the
@@ -73797,7 +74067,6 @@ not loop on the same plan across heartbeats without recording why.`;
73797
74067
  baseHeartbeatSystemPrompt,
73798
74068
  [resolvedInstructionsForIdentity, memoryInstructions].filter((part) => part.trim()).join("\n\n")
73799
74069
  );
73800
- heartbeatTools.push(createIdentityTool({ agent, resolvedInstructions: resolvedInstructionsForIdentity }));
73801
74070
  heartbeatTools.push(heartbeatDoneTool);
73802
74071
  if (isNoTaskRun) {
73803
74072
  agentLogger = new AgentLogger({
@@ -75770,6 +76039,36 @@ function execCommand(command, options) {
75770
76039
  });
75771
76040
  });
75772
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
+ }
75773
76072
  async function createAiPromptExecutor(cwd) {
75774
76073
  const disposeLog = createLogger2("cron-runner");
75775
76074
  return async (prompt, modelProvider, modelId) => {
@@ -75809,7 +76108,7 @@ function truncateOutput(stdout, stderr) {
75809
76108
  }
75810
76109
  return combined;
75811
76110
  }
75812
- 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;
75813
76112
  var init_cron_runner = __esm({
75814
76113
  "../engine/src/cron-runner.ts"() {
75815
76114
  "use strict";
@@ -75818,6 +76117,14 @@ var init_cron_runner = __esm({
75818
76117
  init_shell_utils();
75819
76118
  init_pi();
75820
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 = /[&|;<>`$()]/;
75821
76128
  DEFAULT_TIMEOUT_MS6 = 5 * 60 * 1e3;
75822
76129
  MAX_BUFFER = 1024 * 1024;
75823
76130
  MAX_OUTPUT_LENGTH = 10 * 1024;
@@ -75958,6 +76265,9 @@ var init_cron_runner = __esm({
75958
76265
  */
75959
76266
  async executeLegacyCommand(schedule, startedAt) {
75960
76267
  log14.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
76268
+ if (isInProcessBackupCommand(schedule.command)) {
76269
+ return this.executeBackupInProcess(schedule, startedAt);
76270
+ }
75961
76271
  try {
75962
76272
  const timeoutMs = schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
75963
76273
  const { stdout, stderr } = await execCommand(schedule.command, {
@@ -75988,6 +76298,47 @@ var init_cron_runner = __esm({
75988
76298
  };
75989
76299
  }
75990
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
+ }
75991
76342
  /**
75992
76343
  * Execute multiple steps sequentially.
75993
76344
  * Aggregates per-step results into an overall AutomationRunResult.
@@ -76076,6 +76427,19 @@ var init_cron_runner = __esm({
76076
76427
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
76077
76428
  };
76078
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
+ }
76079
76443
  try {
76080
76444
  const { stdout, stderr } = await execCommand(step.command, {
76081
76445
  timeout: timeoutMs,
@@ -76267,6 +76631,7 @@ var init_routine_runner = __esm({
76267
76631
  "../engine/src/routine-runner.ts"() {
76268
76632
  "use strict";
76269
76633
  import_cron_parser4 = __toESM(require_dist2(), 1);
76634
+ init_cron_runner();
76270
76635
  init_logger2();
76271
76636
  init_shell_utils();
76272
76637
  log15 = createLogger2("routine-runner");
@@ -76424,6 +76789,30 @@ var init_routine_runner = __esm({
76424
76789
  return this.executeCommand(routine.command ?? "", routine.timeoutMs, startedAt);
76425
76790
  }
76426
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
+ }
76427
76816
  try {
76428
76817
  const { stdout, stderr } = await execAsync6(command, {
76429
76818
  timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS7,
@@ -77215,6 +77604,13 @@ var init_self_healing = __esm({
77215
77604
  * stale in-progress/planning tasks that no longer have a live worker.
77216
77605
  */
77217
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
+ }
77218
77614
  const steps = [
77219
77615
  { name: "no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures().then(() => void 0) },
77220
77616
  { name: "completed-tasks", fn: () => this.recoverCompletedTasks().then(() => void 0) },
@@ -77565,27 +77961,34 @@ var init_self_healing = __esm({
77565
77961
  log16.error(`Maintenance batch 1 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77566
77962
  }
77567
77963
  }
77568
- const batch2Fns = [
77569
- { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
77570
- { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
77571
- { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
77572
- { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
77573
- { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
77574
- { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
77575
- { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
77576
- { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
77577
- { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
77578
- { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
77579
- { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
77580
- { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
77581
- { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
77582
- ];
77583
- for (const fn of batch2Fns) {
77584
- try {
77585
- await fn.fn();
77586
- log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
77587
- } catch (stepErr) {
77588
- 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
+ }
77589
77992
  }
77590
77993
  }
77591
77994
  const batch3Fns = [
@@ -78651,10 +79054,18 @@ var init_plugin_runner = __esm({
78651
79054
  cachedRoutes = null;
78652
79055
  cachedUiSlots = null;
78653
79056
  cachedRuntimes = null;
79057
+ cachedSkills = null;
79058
+ cachedWorkflowSteps = null;
79059
+ cachedPromptContributions = null;
79060
+ cachedSetupInfo = null;
78654
79061
  toolsCacheVersion = 0;
78655
79062
  routesCacheVersion = 0;
78656
79063
  uiSlotsCacheVersion = 0;
78657
79064
  runtimesCacheVersion = 0;
79065
+ skillsCacheVersion = 0;
79066
+ workflowStepsCacheVersion = 0;
79067
+ promptContributionsCacheVersion = 0;
79068
+ setupCacheVersion = 0;
78658
79069
  hookTimeoutMs;
78659
79070
  // Event handler references for cleanup
78660
79071
  handlePluginEnabled;
@@ -78686,6 +79097,10 @@ var init_plugin_runner = __esm({
78686
79097
  this.invalidateRoutesCache();
78687
79098
  this.invalidateUiSlotsCache();
78688
79099
  this.invalidateRuntimesCache();
79100
+ this.invalidateSkillsCache();
79101
+ this.invalidateWorkflowStepsCache();
79102
+ this.invalidatePromptContributionsCache();
79103
+ this.invalidateSetupCache();
78689
79104
  }
78690
79105
  /**
78691
79106
  * Shutdown the plugin runner.
@@ -78766,6 +79181,54 @@ var init_plugin_runner = __esm({
78766
79181
  }
78767
79182
  return this.cachedRuntimes.runtimes;
78768
79183
  }
79184
+ getPluginSkills() {
79185
+ if (!this.cachedSkills || this.cachedSkills.version !== this.skillsCacheVersion) {
79186
+ this.cachedSkills = {
79187
+ skills: this.options.pluginLoader.getPluginSkills(),
79188
+ version: this.skillsCacheVersion
79189
+ };
79190
+ }
79191
+ return this.cachedSkills.skills;
79192
+ }
79193
+ getPluginWorkflowSteps() {
79194
+ if (!this.cachedWorkflowSteps || this.cachedWorkflowSteps.version !== this.workflowStepsCacheVersion) {
79195
+ this.cachedWorkflowSteps = {
79196
+ steps: this.options.pluginLoader.getPluginWorkflowSteps(),
79197
+ version: this.workflowStepsCacheVersion
79198
+ };
79199
+ }
79200
+ return this.cachedWorkflowSteps.steps;
79201
+ }
79202
+ getPluginPromptContributions() {
79203
+ if (!this.cachedPromptContributions || this.cachedPromptContributions.version !== this.promptContributionsCacheVersion) {
79204
+ this.cachedPromptContributions = {
79205
+ contributions: this.options.pluginLoader.getPluginPromptContributions(),
79206
+ version: this.promptContributionsCacheVersion
79207
+ };
79208
+ }
79209
+ return this.cachedPromptContributions.contributions;
79210
+ }
79211
+ getPluginSetupInfo() {
79212
+ if (!this.cachedSetupInfo || this.cachedSetupInfo.version !== this.setupCacheVersion) {
79213
+ this.cachedSetupInfo = {
79214
+ setups: this.options.pluginLoader.getPluginSetupInfo(),
79215
+ version: this.setupCacheVersion
79216
+ };
79217
+ }
79218
+ return this.cachedSetupInfo.setups;
79219
+ }
79220
+ getPromptContributionsForSurface(surface) {
79221
+ return this.getPluginPromptContributions().filter(({ pluginId, contribution, config }) => {
79222
+ const plugin4 = this.options.pluginLoader.getPlugin(pluginId);
79223
+ if (!plugin4 || plugin4.state !== "started") {
79224
+ return false;
79225
+ }
79226
+ if (contribution.surface !== surface) {
79227
+ return false;
79228
+ }
79229
+ return config.enabledByDefault !== false;
79230
+ });
79231
+ }
78769
79232
  /**
78770
79233
  * Get a specific runtime registration by its runtimeId.
78771
79234
  *
@@ -78799,6 +79262,10 @@ var init_plugin_runner = __esm({
78799
79262
  this.invalidateRoutesCache();
78800
79263
  this.invalidateUiSlotsCache();
78801
79264
  this.invalidateRuntimesCache();
79265
+ this.invalidateSkillsCache();
79266
+ this.invalidateWorkflowStepsCache();
79267
+ this.invalidatePromptContributionsCache();
79268
+ this.invalidateSetupCache();
78802
79269
  executorLog.log(`Plugin ${pluginId} reloaded`);
78803
79270
  }
78804
79271
  // ── Event Handlers for Hot-Load/Unload ─────────────────────────
@@ -78810,6 +79277,10 @@ var init_plugin_runner = __esm({
78810
79277
  this.invalidateRoutesCache();
78811
79278
  this.invalidateUiSlotsCache();
78812
79279
  this.invalidateRuntimesCache();
79280
+ this.invalidateSkillsCache();
79281
+ this.invalidateWorkflowStepsCache();
79282
+ this.invalidatePromptContributionsCache();
79283
+ this.invalidateSetupCache();
78813
79284
  try {
78814
79285
  executorLog.log(`Auto-loading enabled plugin: ${plugin4.id}`);
78815
79286
  await this.options.pluginLoader.loadPlugin(plugin4.id);
@@ -78825,6 +79296,10 @@ var init_plugin_runner = __esm({
78825
79296
  this.invalidateRoutesCache();
78826
79297
  this.invalidateUiSlotsCache();
78827
79298
  this.invalidateRuntimesCache();
79299
+ this.invalidateSkillsCache();
79300
+ this.invalidateWorkflowStepsCache();
79301
+ this.invalidatePromptContributionsCache();
79302
+ this.invalidateSetupCache();
78828
79303
  try {
78829
79304
  executorLog.log(`Auto-stopping disabled plugin: ${plugin4.id}`);
78830
79305
  await this.options.pluginLoader.stopPlugin(plugin4.id);
@@ -78840,6 +79315,10 @@ var init_plugin_runner = __esm({
78840
79315
  this.invalidateRoutesCache();
78841
79316
  this.invalidateUiSlotsCache();
78842
79317
  this.invalidateRuntimesCache();
79318
+ this.invalidateSkillsCache();
79319
+ this.invalidateWorkflowStepsCache();
79320
+ this.invalidatePromptContributionsCache();
79321
+ this.invalidateSetupCache();
78843
79322
  try {
78844
79323
  executorLog.log(`Stopping unregistered plugin: ${plugin4.id}`);
78845
79324
  await this.options.pluginLoader.stopPlugin(plugin4.id);
@@ -78856,6 +79335,10 @@ var init_plugin_runner = __esm({
78856
79335
  this.invalidateRoutesCache();
78857
79336
  this.invalidateUiSlotsCache();
78858
79337
  this.invalidateRuntimesCache();
79338
+ this.invalidateSkillsCache();
79339
+ this.invalidateWorkflowStepsCache();
79340
+ this.invalidatePromptContributionsCache();
79341
+ this.invalidateSetupCache();
78859
79342
  }
78860
79343
  /**
78861
79344
  * Handle plugin updates - invalidate caches.
@@ -78865,6 +79348,10 @@ var init_plugin_runner = __esm({
78865
79348
  this.invalidateRoutesCache();
78866
79349
  this.invalidateUiSlotsCache();
78867
79350
  this.invalidateRuntimesCache();
79351
+ this.invalidateSkillsCache();
79352
+ this.invalidateWorkflowStepsCache();
79353
+ this.invalidatePromptContributionsCache();
79354
+ this.invalidateSetupCache();
78868
79355
  }
78869
79356
  /**
78870
79357
  * Handle plugin:loaded event from loader - invalidate caches.
@@ -78874,6 +79361,10 @@ var init_plugin_runner = __esm({
78874
79361
  this.invalidateRoutesCache();
78875
79362
  this.invalidateUiSlotsCache();
78876
79363
  this.invalidateRuntimesCache();
79364
+ this.invalidateSkillsCache();
79365
+ this.invalidateWorkflowStepsCache();
79366
+ this.invalidatePromptContributionsCache();
79367
+ this.invalidateSetupCache();
78877
79368
  }
78878
79369
  /**
78879
79370
  * Handle plugin:unloaded event from loader - invalidate caches.
@@ -78883,6 +79374,10 @@ var init_plugin_runner = __esm({
78883
79374
  this.invalidateRoutesCache();
78884
79375
  this.invalidateUiSlotsCache();
78885
79376
  this.invalidateRuntimesCache();
79377
+ this.invalidateSkillsCache();
79378
+ this.invalidateWorkflowStepsCache();
79379
+ this.invalidatePromptContributionsCache();
79380
+ this.invalidateSetupCache();
78886
79381
  }
78887
79382
  /**
78888
79383
  * Handle plugin:reloaded event from loader - invalidate caches.
@@ -78892,6 +79387,10 @@ var init_plugin_runner = __esm({
78892
79387
  this.invalidateRoutesCache();
78893
79388
  this.invalidateUiSlotsCache();
78894
79389
  this.invalidateRuntimesCache();
79390
+ this.invalidateSkillsCache();
79391
+ this.invalidateWorkflowStepsCache();
79392
+ this.invalidatePromptContributionsCache();
79393
+ this.invalidateSetupCache();
78895
79394
  }
78896
79395
  // ── Tool Conversion ───────────────────────────────────────────────
78897
79396
  /**
@@ -79063,6 +79562,22 @@ var init_plugin_runner = __esm({
79063
79562
  this.runtimesCacheVersion++;
79064
79563
  this.log.log(`Runtimes cache invalidated (version: ${this.runtimesCacheVersion})`);
79065
79564
  }
79565
+ invalidateSkillsCache() {
79566
+ this.skillsCacheVersion++;
79567
+ this.log.log(`Skills cache invalidated (version: ${this.skillsCacheVersion})`);
79568
+ }
79569
+ invalidateWorkflowStepsCache() {
79570
+ this.workflowStepsCacheVersion++;
79571
+ this.log.log(`Workflow steps cache invalidated (version: ${this.workflowStepsCacheVersion})`);
79572
+ }
79573
+ invalidatePromptContributionsCache() {
79574
+ this.promptContributionsCacheVersion++;
79575
+ this.log.log(`Prompt contributions cache invalidated (version: ${this.promptContributionsCacheVersion})`);
79576
+ }
79577
+ invalidateSetupCache() {
79578
+ this.setupCacheVersion++;
79579
+ this.log.log(`Setup cache invalidated (version: ${this.setupCacheVersion})`);
79580
+ }
79066
79581
  // ── Store Event Subscriptions ────────────────────────────────────
79067
79582
  /**
79068
79583
  * Subscribe to TaskStore events for task lifecycle hooks.
@@ -79206,6 +79721,10 @@ var init_in_process_runtime = __esm({
79206
79721
  * before `start()` via `setMergeEnqueuer`.
79207
79722
  */
79208
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;
79209
79728
  /**
79210
79729
  * Start the runtime and initialize all subsystems.
79211
79730
  *
@@ -79239,7 +79758,7 @@ var init_in_process_runtime = __esm({
79239
79758
  runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
79240
79759
  }
79241
79760
  this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
79242
- this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
79761
+ this.pluginStore = new PluginStoreClass(this.config.workingDirectory);
79243
79762
  await this.pluginStore.init();
79244
79763
  this.pluginLoader = new PluginLoaderClass({
79245
79764
  pluginStore: this.pluginStore,
@@ -79631,11 +80150,16 @@ var init_in_process_runtime = __esm({
79631
80150
  this.selfHealingManager.start();
79632
80151
  this.stuckTaskDetector.start();
79633
80152
  this.setupEventForwarding();
79634
- await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
79635
- await this.executor.resumeOrphaned();
79636
- void this.selfHealingManager.runStartupRecovery().catch((err) => {
79637
- runtimeLog.error("Self-healing startup recovery failed:", err);
79638
- });
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
+ }
79639
80163
  this.scheduler.start();
79640
80164
  this.triageProcessor?.start();
79641
80165
  this.missionExecutionLoop = missionExecutionLoop;
@@ -79801,6 +80325,45 @@ var init_in_process_runtime = __esm({
79801
80325
  setMergeEnqueuer(enqueueMerge) {
79802
80326
  this.mergeEnqueuer = enqueueMerge;
79803
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
+ }
79804
80367
  /**
79805
80368
  * Get the project's TaskStore instance.
79806
80369
  * @throws Error if runtime has not been started
@@ -83640,13 +84203,13 @@ ${detail}`
83640
84203
  if (prev.globalPause && !s.globalPause) {
83641
84204
  runtimeLog.log("Global unpause \u2014 resuming agentic activity");
83642
84205
  try {
83643
- const executor = this.runtime.executor;
83644
- executor?.resumeOrphaned?.().catch(
83645
- (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)
83646
84209
  );
83647
84210
  } catch (err) {
83648
84211
  runtimeLog.warn(
83649
- `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)}`
83650
84213
  );
83651
84214
  }
83652
84215
  if (s.autoMerge) {
@@ -83670,13 +84233,13 @@ ${detail}`
83670
84233
  if (prev.enginePaused && !s.enginePaused) {
83671
84234
  runtimeLog.log("Engine unpaused \u2014 resuming agentic activity");
83672
84235
  try {
83673
- const executor = this.runtime.executor;
83674
- executor?.resumeOrphaned?.().catch(
83675
- (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)
83676
84239
  );
83677
84240
  } catch (err) {
83678
84241
  runtimeLog.warn(
83679
- `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)}`
83680
84243
  );
83681
84244
  }
83682
84245
  if (s.autoMerge) {
@@ -133416,8 +133979,8 @@ var init_register_settings_sync_routes = __esm({
133416
133979
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
133417
133980
  version: 1
133418
133981
  };
133419
- const { createHash: createHash5 } = await import("node:crypto");
133420
- const checksum = createHash5("sha256").update(JSON.stringify(payload)).digest("hex");
133982
+ const { createHash: createHash6 } = await import("node:crypto");
133983
+ const checksum = createHash6("sha256").update(JSON.stringify(payload)).digest("hex");
133421
133984
  await fetchFromRemoteNode(node, "/api/settings/sync-receive", {
133422
133985
  method: "POST",
133423
133986
  body: { ...payload, checksum }
@@ -133476,7 +134039,7 @@ var init_register_settings_sync_routes = __esm({
133476
134039
  });
133477
134040
  return;
133478
134041
  }
133479
- const { createHash: createHash5 } = await import("node:crypto");
134042
+ const { createHash: createHash6 } = await import("node:crypto");
133480
134043
  const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
133481
134044
  const payloadWithoutChecksum = {
133482
134045
  global: remoteSettings.global,
@@ -133484,7 +134047,7 @@ var init_register_settings_sync_routes = __esm({
133484
134047
  exportedAt,
133485
134048
  version: 1
133486
134049
  };
133487
- const checksum = createHash5("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
134050
+ const checksum = createHash6("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
133488
134051
  const result = await central.applyRemoteSettings({
133489
134052
  ...payloadWithoutChecksum,
133490
134053
  checksum
@@ -136574,7 +137137,7 @@ Rules:
136574
137137
  // ../dashboard/src/routes/register-agent-import-export-generation-routes.ts
136575
137138
  import { createWriteStream } from "node:fs";
136576
137139
  import * as fsPromises2 from "node:fs/promises";
136577
- import { tmpdir as tmpdir3 } from "node:os";
137140
+ import { tmpdir as tmpdir4 } from "node:os";
136578
137141
  import { join as join45, resolve as resolve22 } from "node:path";
136579
137142
  import { Readable } from "node:stream";
136580
137143
  import { pipeline as streamPipeline } from "node:stream/promises";
@@ -136616,7 +137179,7 @@ function registerAgentImportExportRoutes(ctx) {
136616
137179
  } else if (typeof outputDir === "string") {
136617
137180
  throw badRequest("outputDir cannot be empty");
136618
137181
  } else {
136619
- resolvedOutputDir = await mkdtemp(join45(tmpdir3(), "fusion-agent-export-"));
137182
+ resolvedOutputDir = await mkdtemp(join45(tmpdir4(), "fusion-agent-export-"));
136620
137183
  }
136621
137184
  const result = await exportAgentsToDirectory2(agentsToExport, resolvedOutputDir, {
136622
137185
  companyName: typeof companyName === "string" ? companyName : void 0,
@@ -136931,7 +137494,7 @@ ${body}`;
136931
137494
  const archiveUrl = `https://github.com/${repoOwner}/${repoName}/archive/refs/heads/main.tar.gz`;
136932
137495
  let tempDir = null;
136933
137496
  try {
136934
- tempDir = await mkdtemp(join45(tmpdir3(), `fn-agent-import-${importCompanySlug}-`));
137497
+ tempDir = await mkdtemp(join45(tmpdir4(), `fn-agent-import-${importCompanySlug}-`));
136935
137498
  const archivePath = join45(tempDir, "archive.tar.gz");
136936
137499
  const downloadController = new AbortController();
136937
137500
  const downloadTimeout = setTimeout(() => downloadController.abort(), 3e4);
@@ -141458,6 +142021,21 @@ ${stderr}`;
141458
142021
  });
141459
142022
  });
141460
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
+ }
141461
142039
  var INSTALL_TIMEOUT_MS, MAX_OUTPUT_BYTES2, registerFnBinaryRoutes;
141462
142040
  var init_register_fn_binary_routes = __esm({
141463
142041
  "../dashboard/src/routes/register-fn-binary-routes.ts"() {
@@ -141468,11 +142046,23 @@ var init_register_fn_binary_routes = __esm({
141468
142046
  INSTALL_TIMEOUT_MS = 18e4;
141469
142047
  MAX_OUTPUT_BYTES2 = 64 * 1024;
141470
142048
  registerFnBinaryRoutes = (ctx) => {
141471
- 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
+ }
141472
142058
  router.get("/system/fn-binary/status", async (_req, res) => {
141473
142059
  try {
141474
- const binary = await detectFnBinary();
141475
142060
  const expectedVersion = getCliPackageVersion();
142061
+ if (!await isCheckEnabled()) {
142062
+ res.json(buildSkippedStatusPayload(expectedVersion));
142063
+ return;
142064
+ }
142065
+ const binary = await detectFnBinary();
141476
142066
  res.json(buildStatusPayload(binary, expectedVersion));
141477
142067
  } catch (err) {
141478
142068
  if (err instanceof ApiError) throw err;
@@ -141481,6 +142071,13 @@ var init_register_fn_binary_routes = __esm({
141481
142071
  });
141482
142072
  router.post("/system/fn-binary/install", async (_req, res) => {
141483
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
+ }
141484
142081
  const installResult = await runNpmInstall();
141485
142082
  const binary = await detectFnBinary();
141486
142083
  const expectedVersion = getCliPackageVersion();
@@ -153257,7 +153854,7 @@ var require_websocket = __commonJS({
153257
153854
  var http = __require("http");
153258
153855
  var net = __require("net");
153259
153856
  var tls = __require("tls");
153260
- var { randomBytes: randomBytes4, createHash: createHash5 } = __require("crypto");
153857
+ var { randomBytes: randomBytes4, createHash: createHash6 } = __require("crypto");
153261
153858
  var { Duplex, Readable: Readable2 } = __require("stream");
153262
153859
  var { URL: URL2 } = __require("url");
153263
153860
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -153917,7 +154514,7 @@ var require_websocket = __commonJS({
153917
154514
  abortHandshake(websocket, socket, "Invalid Upgrade header");
153918
154515
  return;
153919
154516
  }
153920
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
154517
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
153921
154518
  if (res.headers["sec-websocket-accept"] !== digest) {
153922
154519
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
153923
154520
  return;
@@ -154284,7 +154881,7 @@ var require_websocket_server = __commonJS({
154284
154881
  var EventEmitter37 = __require("events");
154285
154882
  var http = __require("http");
154286
154883
  var { Duplex } = __require("stream");
154287
- var { createHash: createHash5 } = __require("crypto");
154884
+ var { createHash: createHash6 } = __require("crypto");
154288
154885
  var extension2 = require_extension();
154289
154886
  var PerMessageDeflate2 = require_permessage_deflate();
154290
154887
  var subprotocol2 = require_subprotocol();
@@ -154585,7 +155182,7 @@ var require_websocket_server = __commonJS({
154585
155182
  );
154586
155183
  }
154587
155184
  if (this._state > RUNNING) return abortHandshake(socket, 503);
154588
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
155185
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
154589
155186
  const headers = [
154590
155187
  "HTTP/1.1 101 Switching Protocols",
154591
155188
  "Upgrade: websocket",
@@ -155627,6 +156224,43 @@ data: ${JSON.stringify(entry)}
155627
156224
  scopedStore.off("agent:log", onAgentLog);
155628
156225
  });
155629
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
+ });
155630
156264
  app.get("/api/terminal/sessions/:id/stream", rateLimit(RATE_LIMITS.sse), (req, res) => {
155631
156265
  const sessionId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
155632
156266
  res.setHeader("Content-Type", "text/event-stream");
@@ -158252,7 +158886,7 @@ var app_exports = {};
158252
158886
  __export(app_exports, {
158253
158887
  DashboardApp: () => DashboardApp
158254
158888
  });
158255
- 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";
158256
158890
  import { Box, Text, useInput, useApp, useStdout } from "ink";
158257
158891
  import Spinner from "ink-spinner";
158258
158892
  import TextInput from "ink-text-input";
@@ -159034,7 +159668,8 @@ function formatLogTime(iso) {
159034
159668
  function TaskDetailScreen({
159035
159669
  task,
159036
159670
  projectPath,
159037
- interactiveData
159671
+ interactiveData,
159672
+ controller
159038
159673
  }) {
159039
159674
  const { stdout } = useStdout();
159040
159675
  const cols = stdout?.columns ?? 80;
@@ -159099,6 +159734,27 @@ function TaskDetailScreen({
159099
159734
  useEffect2(() => {
159100
159735
  if (autoFollow) setLogScrollOffset(0);
159101
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]);
159102
159758
  useInput((input, key) => {
159103
159759
  if (detail && detail !== "unavailable" && detail.recentLogs.length > 0) {
159104
159760
  const maxOffset = Math.max(0, detail.recentLogs.length - logPaneRows);
@@ -159469,7 +160125,8 @@ function BoardView({ state, controller }) {
159469
160125
  {
159470
160126
  task: selectedTask,
159471
160127
  projectPath: selectedProject?.path ?? null,
159472
- interactiveData: state.interactiveData
160128
+ interactiveData: state.interactiveData,
160129
+ controller
159473
160130
  }
159474
160131
  ) }) : tasksState.loading ? /* @__PURE__ */ jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, gap: 1, children: [
159475
160132
  /* @__PURE__ */ jsx(Text, { color: "white", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
@@ -160291,7 +160948,7 @@ function PushModal({
160291
160948
  }
160292
160949
  );
160293
160950
  }
160294
- function GitView({ state }) {
160951
+ function GitView({ state, controller }) {
160295
160952
  const { stdout } = useStdout();
160296
160953
  const cols = stdout?.columns ?? 80;
160297
160954
  const data = state.interactiveData;
@@ -160479,6 +161136,23 @@ function GitView({ state }) {
160479
161136
  }
160480
161137
  }
160481
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]);
160482
161156
  const isNarrow = cols < NARROW_THRESHOLD;
160483
161157
  const leftWidth = Math.max(24, Math.floor(cols * 0.35));
160484
161158
  const rightWidth = cols - leftWidth - 1;
@@ -160820,7 +161494,7 @@ function entriesToNodes(entries, depth) {
160820
161494
  const files = filtered.filter((e) => !e.isDirectory).sort((a, b) => a.name.localeCompare(b.name));
160821
161495
  return [...dirs, ...files].map((e) => ({ entry: e, depth, expanded: false, children: void 0 }));
160822
161496
  }
160823
- function FilesView({ state }) {
161497
+ function FilesView({ state, controller }) {
160824
161498
  const { stdout } = useStdout();
160825
161499
  const cols = stdout?.columns ?? 80;
160826
161500
  const data = state.interactiveData;
@@ -161028,6 +161702,23 @@ function FilesView({ state }) {
161028
161702
  }
161029
161703
  }
161030
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]);
161031
161722
  const isNarrow = cols < NARROW_THRESHOLD;
161032
161723
  const treeWidth = isNarrow ? Math.max(20, cols - 2) : Math.max(20, Math.floor(cols * 0.38));
161033
161724
  const previewEntry = selectedNode && !selectedNode.entry.isDirectory ? selectedNode.entry : null;
@@ -161191,8 +161882,8 @@ function InteractiveMode({ state, controller }) {
161191
161882
  state.interactiveView === "board" && /* @__PURE__ */ jsx(BoardView, { state, controller }),
161192
161883
  state.interactiveView === "agents" && /* @__PURE__ */ jsx(AgentsView, { state }),
161193
161884
  state.interactiveView === "settings" && /* @__PURE__ */ jsx(SettingsInteractiveView, { state, controller }),
161194
- state.interactiveView === "git" && /* @__PURE__ */ jsx(GitView, { state }),
161195
- 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 })
161196
161887
  ] })
161197
161888
  ] });
161198
161889
  }
@@ -161223,6 +161914,23 @@ function DashboardApp({ controller }) {
161223
161914
  useCallback2((cb) => controller.subscribe(cb), [controller]),
161224
161915
  useCallback2(() => controller.getSnapshot(), [controller])
161225
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]);
161226
161934
  const [qrOverlay, setQrOverlay] = useState2(null);
161227
161935
  useInput((input, key) => {
161228
161936
  if ((input === "q" || input === "Q") && !key.ctrl || key.ctrl && input === "c") {
@@ -161605,6 +162313,15 @@ var init_controller = __esm({
161605
162313
  // no remote API is wired up).
161606
162314
  remoteStatus = null;
161607
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;
161608
162325
  constructor() {
161609
162326
  this.logBuffer = new LogRingBuffer();
161610
162327
  }
@@ -161613,6 +162330,15 @@ var init_controller = __esm({
161613
162330
  this.subscribers.add(callback);
161614
162331
  return () => this.subscribers.delete(callback);
161615
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
+ }
161616
162342
  getSnapshot() {
161617
162343
  if (this.cachedSnapshot) return this.cachedSnapshot;
161618
162344
  this.cachedSnapshot = {
@@ -161997,6 +162723,10 @@ var init_controller = __esm({
161997
162723
  this.inkInstance = render(
161998
162724
  createElement(DashboardApp2, { controller: this })
161999
162725
  );
162726
+ if (process.stdin?.isTTY) {
162727
+ process.stdout.write("\x1B[?1000h\x1B[?1006h");
162728
+ this.installMouseListener();
162729
+ }
162000
162730
  this.resizeListener = () => {
162001
162731
  if (this.resizeDebounceTimer) clearTimeout(this.resizeDebounceTimer);
162002
162732
  this.resizeDebounceTimer = setTimeout(() => {
@@ -162103,10 +162833,48 @@ var init_controller = __esm({
162103
162833
  this.inkInstance = null;
162104
162834
  }
162105
162835
  if (process.stdout?.isTTY && typeof process.stdout.write === "function") {
162836
+ this.uninstallMouseListener();
162837
+ process.stdout.write("\x1B[?1006l\x1B[?1000l");
162106
162838
  process.stdout.write("\x1B[?1049l");
162107
162839
  }
162108
162840
  }
162109
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
+ }
162110
162878
  clampSelectedLogIndex(entries) {
162111
162879
  if (entries.length === 0) {
162112
162880
  this.selectedLogIndex = 0;
@@ -170710,7 +171478,7 @@ __export(native_patch_exports, {
170710
171478
  });
170711
171479
  import { join as join68, basename as basename21, dirname as dirname28 } from "node:path";
170712
171480
  import { existsSync as existsSync51, copyFileSync, mkdirSync as mkdirSync12, symlinkSync as symlinkSync2, rmSync as rmSync5, lstatSync as lstatSync3, readlinkSync as readlinkSync2 } from "node:fs";
170713
- import { tmpdir as tmpdir4 } from "node:os";
171481
+ import { tmpdir as tmpdir5 } from "node:os";
170714
171482
  function findStagedNativeDir2() {
170715
171483
  const platform4 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
170716
171484
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
@@ -170755,7 +171523,7 @@ function setupNativeResolution() {
170755
171523
  process.env.NODE_PTY_SPAWN_HELPER_DIR = nativeDir;
170756
171524
  }
170757
171525
  process.env.FUSION_NATIVE_ASSETS_PATH = nativeDir;
170758
- const tmpRoot = join68(tmpdir4(), `fn-bunfs-${process.pid}`);
171526
+ const tmpRoot = join68(tmpdir5(), `fn-bunfs-${process.pid}`);
170759
171527
  const fnDir = join68(tmpRoot, "fn");
170760
171528
  const prebuildsDir = join68(fnDir, "prebuilds");
170761
171529
  const platformDir = join68(prebuildsDir, basename21(nativeDir));
@@ -170843,7 +171611,7 @@ var init_native_patch = __esm({
170843
171611
  import { existsSync as existsSync52, mkdtempSync as mkdtempSync2, readFileSync as readFileSync24, symlinkSync as symlinkSync3, writeFileSync as writeFileSync6 } from "node:fs";
170844
171612
  import { createRequire as createRequire7 } from "node:module";
170845
171613
  import { join as join69, dirname as dirname29, resolve as resolve41 } from "node:path";
170846
- import { tmpdir as tmpdir5 } from "node:os";
171614
+ import { tmpdir as tmpdir6 } from "node:os";
170847
171615
  import { performance as performance3 } from "node:perf_hooks";
170848
171616
  import { fileURLToPath as fileURLToPath11 } from "node:url";
170849
171617
  var isBunBinary3 = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
@@ -170851,7 +171619,7 @@ function configurePiPackage() {
170851
171619
  if (process.env.PI_PACKAGE_DIR) {
170852
171620
  return;
170853
171621
  }
170854
- const tmp = mkdtempSync2(join69(tmpdir5(), "fn-pkg-"));
171622
+ const tmp = mkdtempSync2(join69(tmpdir6(), "fn-pkg-"));
170855
171623
  let packageJson = {
170856
171624
  name: "pi",
170857
171625
  version: "0.1.0",