@runfusion/fusion 0.0.5 → 0.0.6

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.
package/dist/bin.js CHANGED
@@ -134,6 +134,8 @@ var init_settings_schema = __esm({
134
134
  defaultPresetBySize: {},
135
135
  autoResolveConflicts: true,
136
136
  smartConflictResolution: true,
137
+ worktreeRebaseBeforeMerge: true,
138
+ worktreeRebaseRemote: "",
137
139
  strictScopeEnforcement: false,
138
140
  buildRetryCount: 0,
139
141
  verificationFixRetries: 1,
@@ -30549,6 +30551,7 @@ ${newTask.description}
30549
30551
  }
30550
30552
  }
30551
30553
  if (updates.steps !== void 0) task.steps = updates.steps;
30554
+ if (updates.currentStep !== void 0) task.currentStep = updates.currentStep;
30552
30555
  if (updates.status === null) {
30553
30556
  task.status = void 0;
30554
30557
  } else if (updates.status !== void 0) {
@@ -50790,12 +50793,59 @@ var require_lib = __commonJS({
50790
50793
  import { EventEmitter as EventEmitter17 } from "events";
50791
50794
  import * as os2 from "os";
50792
50795
  import * as path from "path";
50793
- import { existsSync as existsSync17 } from "node:fs";
50796
+ import { existsSync as existsSync17, chmodSync } from "node:fs";
50794
50797
  import { join as join22, dirname as dirname7 } from "node:path";
50795
- function findStagedNativeDir() {
50798
+ function getNativePrebuildName() {
50796
50799
  const platform3 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
50797
50800
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
50798
- const prebuildName = `${platform3}-${arch}`;
50801
+ return `${platform3}-${arch}`;
50802
+ }
50803
+ function findInstalledNodePtyNativeDir() {
50804
+ try {
50805
+ const packageJsonPath = __require.resolve("node-pty/package.json");
50806
+ const nativeDir = join22(dirname7(packageJsonPath), "prebuilds", getNativePrebuildName());
50807
+ return existsSync17(join22(nativeDir, "pty.node")) ? nativeDir : null;
50808
+ } catch {
50809
+ return null;
50810
+ }
50811
+ }
50812
+ function ensureNodePtyNativePermissions() {
50813
+ if (process.platform === "win32") {
50814
+ return;
50815
+ }
50816
+ const candidateDirs = /* @__PURE__ */ new Set();
50817
+ const envNativeDir = process.env.NODE_PTY_SPAWN_HELPER_DIR || process.env.FUSION_NATIVE_ASSETS_PATH;
50818
+ if (envNativeDir) {
50819
+ candidateDirs.add(envNativeDir);
50820
+ }
50821
+ const stagedNativeDir = findStagedNativeDir();
50822
+ if (stagedNativeDir) {
50823
+ candidateDirs.add(stagedNativeDir);
50824
+ }
50825
+ const installedNativeDir = findInstalledNodePtyNativeDir();
50826
+ if (installedNativeDir) {
50827
+ candidateDirs.add(installedNativeDir);
50828
+ }
50829
+ for (const nativeDir of candidateDirs) {
50830
+ const helperPath = join22(nativeDir, "spawn-helper");
50831
+ const nativeModulePath = join22(nativeDir, "pty.node");
50832
+ try {
50833
+ if (existsSync17(helperPath)) {
50834
+ chmodSync(helperPath, 493);
50835
+ }
50836
+ if (existsSync17(nativeModulePath)) {
50837
+ chmodSync(nativeModulePath, 493);
50838
+ }
50839
+ } catch (err) {
50840
+ console.warn("[terminal] Failed to repair node-pty native permissions:", {
50841
+ nativeDir,
50842
+ error: err instanceof Error ? err.message : String(err)
50843
+ });
50844
+ }
50845
+ }
50846
+ }
50847
+ function findStagedNativeDir() {
50848
+ const prebuildName = getNativePrebuildName();
50799
50849
  if (process.env.FUSION_RUNTIME_DIR) {
50800
50850
  const envPath = join22(process.env.FUSION_RUNTIME_DIR, prebuildName);
50801
50851
  if (existsSync17(join22(envPath, "pty.node"))) {
@@ -50836,6 +50886,7 @@ async function loadPtyModule() {
50836
50886
  }
50837
50887
  }
50838
50888
  try {
50889
+ ensureNodePtyNativePermissions();
50839
50890
  const mod = await Promise.resolve().then(() => __toESM(require_lib(), 1));
50840
50891
  ptyModule = mod;
50841
50892
  return ptyModule;
@@ -65072,6 +65123,71 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
65072
65123
  mergerLog.warn(`${taskId}: unable to verify/checkout main branch \u2014 proceeding on current HEAD`);
65073
65124
  }
65074
65125
  }
65126
+ if (settings.worktreeRebaseBeforeMerge !== false) {
65127
+ try {
65128
+ let remote = settings.worktreeRebaseRemote?.trim();
65129
+ if (!remote) {
65130
+ try {
65131
+ const { stdout: mainBranchOut } = await execAsync2(
65132
+ "git rev-parse --abbrev-ref HEAD",
65133
+ { cwd: rootDir, encoding: "utf-8" }
65134
+ );
65135
+ const mainBranch = mainBranchOut.trim();
65136
+ const { stdout: configuredRemote } = await execAsync2(
65137
+ `git config --get branch.${mainBranch}.remote`,
65138
+ { cwd: rootDir, encoding: "utf-8" }
65139
+ ).catch(() => ({ stdout: "" }));
65140
+ remote = configuredRemote.trim();
65141
+ } catch {
65142
+ }
65143
+ }
65144
+ if (!remote) {
65145
+ try {
65146
+ const { stdout: remotesOut } = await execAsync2("git remote", {
65147
+ cwd: rootDir,
65148
+ encoding: "utf-8"
65149
+ });
65150
+ const remotes = remotesOut.trim().split(/\s+/).filter(Boolean);
65151
+ if (remotes.length === 1) {
65152
+ remote = remotes[0];
65153
+ } else if (remotes.includes("origin")) {
65154
+ remote = "origin";
65155
+ }
65156
+ } catch {
65157
+ }
65158
+ }
65159
+ if (!remote) {
65160
+ mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping pre-merge rebase`);
65161
+ } else {
65162
+ mergerLog.log(`${taskId}: fetching ${remote} before merge`);
65163
+ await execAsync2(`git fetch "${remote}"`, { cwd: rootDir });
65164
+ try {
65165
+ const { stdout: mainBranchOut } = await execAsync2(
65166
+ "git rev-parse --abbrev-ref HEAD",
65167
+ { cwd: rootDir, encoding: "utf-8" }
65168
+ );
65169
+ const mainBranch = mainBranchOut.trim();
65170
+ const remoteRef = `${remote}/${mainBranch}`;
65171
+ if (worktreePath) {
65172
+ await execAsync2(`git rebase "${remoteRef}"`, { cwd: worktreePath });
65173
+ mergerLog.log(`${taskId}: rebased ${branch} onto ${remoteRef}`);
65174
+ } else {
65175
+ mergerLog.warn(`${taskId}: no worktreePath \u2014 skipping task branch rebase`);
65176
+ }
65177
+ } catch (rebaseErr) {
65178
+ const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
65179
+ mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through to smart/AI merge`);
65180
+ if (worktreePath) {
65181
+ await execAsync2("git rebase --abort", { cwd: worktreePath }).catch(() => {
65182
+ });
65183
+ }
65184
+ }
65185
+ }
65186
+ } catch (err) {
65187
+ const msg = err instanceof Error ? err.message : String(err);
65188
+ mergerLog.warn(`${taskId}: pre-merge rebase pipeline failed (${msg}) \u2014 proceeding without rebase`);
65189
+ }
65190
+ }
65075
65191
  let commitLog = "";
65076
65192
  let diffStat = "";
65077
65193
  try {
@@ -67588,19 +67704,6 @@ import { existsSync as existsSync26 } from "node:fs";
67588
67704
  import { readFile as readFile16, writeFile as writeFile12 } from "node:fs/promises";
67589
67705
  import { Type as Type4 } from "@mariozechner/pi-ai";
67590
67706
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
67591
- function determineRevisionResetStart(steps, feedback) {
67592
- const total = steps.length;
67593
- if (total === 0) return 0;
67594
- const skipPreflight = /preflight/i.test(steps[0].name);
67595
- const firstCandidate = skipPreflight ? 1 : 0;
67596
- if (firstCandidate >= total) return total;
67597
- const fb = feedback.toLowerCase();
67598
- for (let i = firstCandidate; i < total; i++) {
67599
- const tokens = steps[i].name.toLowerCase().match(/[a-z][a-z]{4,}/g) ?? [];
67600
- if (tokens.some((t) => fb.includes(t))) return i;
67601
- }
67602
- return firstCandidate;
67603
- }
67604
67707
  function truncateWorkflowScriptOutput2(output) {
67605
67708
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2) return output;
67606
67709
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2} characters ...
@@ -69917,35 +70020,23 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
69917
70020
  }
69918
70021
  /**
69919
70022
  * Handle a workflow step revision request.
69920
- *
69921
- * This method:
69922
- * 1. Updates PROMPT.md with "Workflow Revision Instructions" section
69923
- * 2. Resets task execution state (all steps reset to pending)
69924
- * 3. Schedules fresh execution to run after current guard unwinds
69925
- *
69926
- * The task stays in "in-progress" and is scheduled for a fresh executor pass.
70023
+ *
70024
+ * Re-opens ONLY the last step so the executor has exactly one pending slot
70025
+ * to re-enter through. All earlier done steps stay done — the agent reads
70026
+ * the injected feedback from PROMPT.md and applies an in-place fix rather
70027
+ * than redoing any completed step.
69927
70028
  */
69928
70029
  async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
69929
70030
  executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
69930
70031
  const updatedTask = await this.store.getTask(task.id);
69931
- const resetStart = determineRevisionResetStart(updatedTask.steps, feedback);
69932
- const targetStepName = resetStart < updatedTask.steps.length ? updatedTask.steps[resetStart].name : null;
69933
- const resetSummary = resetStart >= updatedTask.steps.length ? "no steps to reset" : `resetting steps ${resetStart + 1}\u2013${updatedTask.steps.length} (starting at "${targetStepName}")`;
70032
+ const reopen = await this.reopenLastStepForRevision(task.id, updatedTask);
70033
+ const reopenSummary = reopen ? `re-opening Step ${reopen.index + 1} ("${reopen.name}") for in-place fix` : "no step to re-open (none were completed)";
69934
70034
  await this.store.logEntry(
69935
70035
  task.id,
69936
- `Workflow step "${stepName}" requested revision \u2014 ${resetSummary}`,
70036
+ `Workflow step "${stepName}" requested revision \u2014 ${reopenSummary}`,
69937
70037
  feedback
69938
70038
  );
69939
- await this.injectWorkflowRevisionInstructions(task, feedback, {
69940
- resetStart,
69941
- targetStepName,
69942
- totalSteps: updatedTask.steps.length
69943
- });
69944
- for (let i = resetStart; i < updatedTask.steps.length; i++) {
69945
- if (updatedTask.steps[i].status !== "pending") {
69946
- await this.store.updateStep(task.id, i, "pending");
69947
- }
69948
- }
70039
+ await this.injectWorkflowRevisionInstructions(task, feedback);
69949
70040
  await this.store.updateTask(task.id, {
69950
70041
  status: null,
69951
70042
  sessionFile: null
@@ -69967,12 +70058,36 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
69967
70058
  }
69968
70059
  }, 0);
69969
70060
  }
70061
+ /**
70062
+ * Re-open the last non-pending step so a revision/failure handler gives the
70063
+ * executor exactly one pending slot to re-enter through. Returns the index
70064
+ * and name of the step that was flipped to `pending`, or null when there
70065
+ * was nothing to re-open.
70066
+ */
70067
+ async reopenLastStepForRevision(taskId, task) {
70068
+ const steps = task.steps;
70069
+ if (steps.length === 0) return null;
70070
+ let targetIndex = -1;
70071
+ for (let i = steps.length - 1; i >= 0; i--) {
70072
+ if (steps[i].status !== "pending") {
70073
+ targetIndex = i;
70074
+ break;
70075
+ }
70076
+ }
70077
+ if (targetIndex === -1) {
70078
+ await this.store.updateTask(taskId, { currentStep: 0 });
70079
+ return null;
70080
+ }
70081
+ await this.store.updateStep(taskId, targetIndex, "pending");
70082
+ await this.store.updateTask(taskId, { currentStep: targetIndex });
70083
+ return { index: targetIndex, name: steps[targetIndex].name };
70084
+ }
69970
70085
  /**
69971
70086
  * Inject or update the "Workflow Revision Instructions" section in PROMPT.md.
69972
70087
  * This section contains feedback from workflow steps that requested revisions.
69973
70088
  * The section is replaced entirely to avoid accumulation of old feedback.
69974
70089
  */
69975
- async injectWorkflowRevisionInstructions(task, feedback, scope) {
70090
+ async injectWorkflowRevisionInstructions(task, feedback) {
69976
70091
  const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
69977
70092
  let content;
69978
70093
  try {
@@ -69981,14 +70096,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
69981
70096
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
69982
70097
  return;
69983
70098
  }
69984
- let scopeLine;
69985
- if (scope && scope.targetStepName && scope.resetStart < scope.totalSteps) {
69986
- scopeLine = `Re-execution starts at **Step ${scope.resetStart + 1} ("${scope.targetStepName}")**. Earlier steps remain done \u2014 do not re-run them unless the feedback explicitly calls them out.`;
69987
- } else if (scope && scope.resetStart >= scope.totalSteps) {
69988
- scopeLine = "No steps were reset; apply the feedback as an in-place fix and call task_done() when complete.";
69989
- } else {
69990
- scopeLine = "Address the feedback above by making the necessary code changes, then mark all affected steps as done and call task_done() when complete.";
69991
- }
70099
+ const scopeLine = "All prior steps remain **done**. Apply the feedback above as an in-place fix (make the necessary code changes, commit, and call `task_done()` when complete). Do **not** re-run or re-plan any earlier step unless the feedback explicitly calls it out.";
69992
70100
  const revisionSectionHeader = "## Workflow Revision Instructions";
69993
70101
  const revisionSectionContent = `${revisionSectionHeader}
69994
70102
 
@@ -70047,11 +70155,7 @@ ${feedback}
70047
70155
  });
70048
70156
  await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount);
70049
70157
  const updatedTask = await this.store.getTask(task.id);
70050
- for (let i = 0; i < updatedTask.steps.length; i++) {
70051
- if (updatedTask.steps[i].status !== "pending") {
70052
- await this.store.updateStep(task.id, i, "pending");
70053
- }
70054
- }
70158
+ await this.reopenLastStepForRevision(task.id, updatedTask);
70055
70159
  await this.store.updateTask(task.id, {
70056
70160
  status: null,
70057
70161
  sessionFile: null
@@ -70095,11 +70199,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
70095
70199
  );
70096
70200
  await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, MAX_WORKFLOW_STEP_RETRIES);
70097
70201
  const updatedTask = await this.store.getTask(taskId);
70098
- for (let i = 0; i < updatedTask.steps.length; i++) {
70099
- if (updatedTask.steps[i].status !== "pending") {
70100
- await this.store.updateStep(taskId, i, "pending");
70101
- }
70102
- }
70202
+ await this.reopenLastStepForRevision(taskId, updatedTask);
70103
70203
  await this.store.updateTask(taskId, {
70104
70204
  status: null,
70105
70205
  error: null,
@@ -75898,6 +75998,12 @@ async function getHeartbeatMemorySettings(taskStore) {
75898
75998
  }
75899
75999
  return maybeGetSettings.call(taskStore);
75900
76000
  }
76001
+ function isTickableState(state) {
76002
+ return state === "active" || state === "running";
76003
+ }
76004
+ function isHeartbeatManaged(agent) {
76005
+ return !isEphemeralAgent(agent);
76006
+ }
75901
76007
  var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, heartbeatDoneParams, HeartbeatMonitor, HeartbeatTriggerScheduler;
75902
76008
  var init_agent_heartbeat = __esm({
75903
76009
  "../engine/src/agent-heartbeat.ts"() {
@@ -77134,10 +77240,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77134
77240
  * @param config - Per-agent heartbeat config
77135
77241
  */
77136
77242
  registerAgent(agentId, config) {
77137
- if (config.enabled === false) {
77138
- heartbeatLog.log(`Skipping timer registration for ${agentId} (disabled)`);
77139
- return;
77140
- }
77141
77243
  let rawIntervalMs = config.heartbeatIntervalMs;
77142
77244
  let usingDefaultInterval = false;
77143
77245
  if (!rawIntervalMs || typeof rawIntervalMs !== "number" || !Number.isFinite(rawIntervalMs) || rawIntervalMs <= 0) {
@@ -77227,8 +77329,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77227
77329
  this.assignedListener = async (agent, taskId) => {
77228
77330
  if (!this.running) return;
77229
77331
  try {
77230
- if (agent.runtimeConfig?.enabled === false) {
77231
- heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (heartbeat disabled)`);
77332
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
77333
+ heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (state=${agent.state})`);
77232
77334
  return;
77233
77335
  }
77234
77336
  const activeRun = await this.store.getActiveHeartbeatRun(agent.id);
@@ -77297,9 +77399,21 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77297
77399
  watchAgentLifecycle() {
77298
77400
  if (this.updatedListener || this.deletedListener) return;
77299
77401
  this.updatedListener = (agent) => {
77300
- if (agent.state === "terminated" || agent.runtimeConfig?.enabled === false) {
77402
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
77301
77403
  this.unregisterAgent(agent.id);
77404
+ return;
77405
+ }
77406
+ if (this.timers.has(agent.id)) {
77407
+ return;
77302
77408
  }
77409
+ const rc = agent.runtimeConfig ?? {};
77410
+ this.registerAgent(agent.id, {
77411
+ heartbeatIntervalMs: rc.heartbeatIntervalMs,
77412
+ maxConcurrentRuns: rc.maxConcurrentRuns
77413
+ });
77414
+ heartbeatLog.log(
77415
+ `State-driven registration: ${agent.id} is ${agent.state} \u2014 timer armed`
77416
+ );
77303
77417
  };
77304
77418
  this.deletedListener = (agentId) => {
77305
77419
  this.unregisterAgent(agentId);
@@ -77330,8 +77444,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77330
77444
  this.unregisterAgent(agentId);
77331
77445
  return;
77332
77446
  }
77333
- if (agent.state === "terminated" || agent.runtimeConfig?.enabled === false) {
77334
- heartbeatLog.log(`Timer tick skipped for ${agentId} (disabled or terminated)`);
77447
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
77448
+ heartbeatLog.log(`Timer tick skipped for ${agentId} (state=${agent.state})`);
77335
77449
  this.unregisterAgent(agentId);
77336
77450
  return;
77337
77451
  }
@@ -79536,13 +79650,13 @@ var init_in_process_runtime = __esm({
79536
79650
  this.taskStore
79537
79651
  );
79538
79652
  this.triggerScheduler.start();
79653
+ const isTickable = (agent) => !isEphemeralAgent(agent) && (agent.state === "active" || agent.state === "running");
79539
79654
  this.agentCreatedListener = (agent) => {
79540
79655
  if (!this.triggerScheduler) return;
79656
+ if (!isTickable(agent)) return;
79541
79657
  const rc = agent.runtimeConfig;
79542
- if (rc?.enabled === false) return;
79543
79658
  this.triggerScheduler.registerAgent(agent.id, {
79544
79659
  heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79545
- enabled: rc?.enabled,
79546
79660
  maxConcurrentRuns: rc?.maxConcurrentRuns
79547
79661
  });
79548
79662
  runtimeLog.log(`Registered new agent ${agent.id} for heartbeat triggers`);
@@ -79550,18 +79664,17 @@ var init_in_process_runtime = __esm({
79550
79664
  this.agentStore.on("agent:created", this.agentCreatedListener);
79551
79665
  this.agentUpdatedListener = (agent) => {
79552
79666
  if (!this.triggerScheduler) return;
79553
- const rc = agent.runtimeConfig;
79554
- if (rc?.enabled === false) {
79667
+ if (!isTickable(agent)) {
79555
79668
  this.triggerScheduler.unregisterAgent(agent.id);
79556
- runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (disabled)`);
79557
- } else {
79558
- this.triggerScheduler.registerAgent(agent.id, {
79559
- heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79560
- enabled: rc?.enabled,
79561
- maxConcurrentRuns: rc?.maxConcurrentRuns
79562
- });
79563
- runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers`);
79669
+ runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (state=${agent.state})`);
79670
+ return;
79564
79671
  }
79672
+ const rc = agent.runtimeConfig;
79673
+ this.triggerScheduler.registerAgent(agent.id, {
79674
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79675
+ maxConcurrentRuns: rc?.maxConcurrentRuns
79676
+ });
79677
+ runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers (state=${agent.state})`);
79565
79678
  };
79566
79679
  this.agentStore.on("agent:updated", this.agentUpdatedListener);
79567
79680
  this.ephemeralTerminationListener = (agentId, from, to) => {
@@ -79596,15 +79709,13 @@ var init_in_process_runtime = __esm({
79596
79709
  const agents = await this.agentStore.listAgents();
79597
79710
  let registeredCount = 0;
79598
79711
  for (const agent of agents) {
79712
+ if (!isTickable(agent)) continue;
79599
79713
  const rc = agent.runtimeConfig;
79600
- if (rc?.enabled !== false) {
79601
- this.triggerScheduler.registerAgent(agent.id, {
79602
- heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79603
- enabled: rc?.enabled,
79604
- maxConcurrentRuns: rc?.maxConcurrentRuns
79605
- });
79606
- registeredCount++;
79607
- }
79714
+ this.triggerScheduler.registerAgent(agent.id, {
79715
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79716
+ maxConcurrentRuns: rc?.maxConcurrentRuns
79717
+ });
79718
+ registeredCount++;
79608
79719
  }
79609
79720
  if (agents.length > 0) {
79610
79721
  runtimeLog.log(`Registered ${registeredCount} of ${agents.length} agents for heartbeat triggers`);
@@ -121468,6 +121579,34 @@ Description: ${step.description}`
121468
121579
  rethrowAsApiError3(err);
121469
121580
  }
121470
121581
  });
121582
+ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["done", "archived"]);
121583
+ function isTerminalTaskStatus(status) {
121584
+ return status !== void 0 && TERMINAL_TASK_STATUSES.has(status);
121585
+ }
121586
+ async function sanitizeAgentTaskLinks(agents, scopedStore) {
121587
+ const taskIds = [...new Set(agents.map((a) => a.taskId).filter((id) => id !== void 0))];
121588
+ const taskStatusMap = /* @__PURE__ */ new Map();
121589
+ await Promise.all(
121590
+ taskIds.map(async (taskId) => {
121591
+ try {
121592
+ const task = await scopedStore.getTask(taskId);
121593
+ if (task) {
121594
+ taskStatusMap.set(taskId, task.column);
121595
+ }
121596
+ } catch {
121597
+ }
121598
+ })
121599
+ );
121600
+ return agents.map((agent) => {
121601
+ if (!agent.taskId) return agent;
121602
+ const taskStatus = taskStatusMap.get(agent.taskId);
121603
+ if (isTerminalTaskStatus(taskStatus)) {
121604
+ const { taskId: _omitted, ...sanitized } = agent;
121605
+ return sanitized;
121606
+ }
121607
+ return agent;
121608
+ });
121609
+ }
121471
121610
  router.get("/agents", async (req, res) => {
121472
121611
  try {
121473
121612
  const filter2 = {};
@@ -121485,7 +121624,8 @@ Description: ${step.description}`
121485
121624
  const agentStore = new AgentStore2({ rootDir: scopedStore.getFusionDir() });
121486
121625
  await agentStore.init();
121487
121626
  const agents = await agentStore.listAgents(filter2);
121488
- res.json(agents);
121627
+ const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
121628
+ res.json(sanitizedAgents);
121489
121629
  } catch (err) {
121490
121630
  if (err instanceof ApiError) {
121491
121631
  throw err;
@@ -122180,7 +122320,8 @@ ${body}`;
122180
122320
  await agentStore.init();
122181
122321
  const agents = await agentStore.listAgents();
122182
122322
  const activeCount = agents.filter((a) => a.state === "active" || a.state === "running").length;
122183
- const assignedTaskCount = agents.filter((a) => a.taskId).length;
122323
+ const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
122324
+ const assignedTaskCount = sanitizedAgents.filter((a) => a.taskId).length;
122184
122325
  let completedRuns = 0;
122185
122326
  let failedRuns = 0;
122186
122327
  for (const agent of agents) {
@@ -122242,7 +122383,8 @@ ${body}`;
122242
122383
  if (!agent) {
122243
122384
  throw notFound("Agent not found");
122244
122385
  }
122245
- res.json(agent);
122386
+ const [sanitizedAgent] = await sanitizeAgentTaskLinks([agent], scopedStore);
122387
+ res.json(sanitizedAgent);
122246
122388
  } catch (err) {
122247
122389
  if (err instanceof ApiError) {
122248
122390
  throw err;
@@ -131577,6 +131719,9 @@ function isApiPath(path4) {
131577
131719
  return path4 === "/api" || path4.startsWith("/api/");
131578
131720
  }
131579
131721
  function getDaemonToken(options) {
131722
+ if (options?.noAuth) {
131723
+ return void 0;
131724
+ }
131580
131725
  if (options?.daemon?.token) {
131581
131726
  return options.daemon.token;
131582
131727
  }
@@ -131800,7 +131945,7 @@ function createServer(store, options) {
131800
131945
  }
131801
131946
  }
131802
131947
  }));
131803
- const daemonToken = options?.daemon?.token ?? process.env.FUSION_DAEMON_TOKEN;
131948
+ const daemonToken = options?.noAuth ? void 0 : options?.daemon?.token ?? process.env.FUSION_DAEMON_TOKEN;
131804
131949
  if (daemonToken) {
131805
131950
  app.use(createAuthMiddleware(daemonToken));
131806
131951
  }
@@ -133953,7 +134098,8 @@ async function runDashboard(port, opts = {}) {
133953
134098
  onProjectFirstAccessed: (projectId) => engineManager.onProjectAccessed(projectId),
133954
134099
  skillsAdapter,
133955
134100
  https: loadTlsCredentialsFromEnv(),
133956
- daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0
134101
+ daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0,
134102
+ noAuth: opts.noAuth
133957
134103
  });
133958
134104
  const shutdown = async (signal) => {
133959
134105
  if (shutdownInProgress) return;
@@ -134048,14 +134194,13 @@ async function runDashboard(port, opts = {}) {
134048
134194
  triggerScheduler.start();
134049
134195
  const agents = await agentStore.listAgents();
134050
134196
  for (const agent of agents) {
134197
+ if (isEphemeralAgent(agent)) continue;
134198
+ if (agent.state !== "active" && agent.state !== "running") continue;
134051
134199
  const rc = agent.runtimeConfig;
134052
- if (rc && (rc.heartbeatIntervalMs || rc.enabled !== void 0 || rc.maxConcurrentRuns)) {
134053
- triggerScheduler.registerAgent(agent.id, {
134054
- heartbeatIntervalMs: rc.heartbeatIntervalMs,
134055
- enabled: rc.enabled,
134056
- maxConcurrentRuns: rc.maxConcurrentRuns
134057
- });
134058
- }
134200
+ triggerScheduler.registerAgent(agent.id, {
134201
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
134202
+ maxConcurrentRuns: rc?.maxConcurrentRuns
134203
+ });
134059
134204
  }
134060
134205
  if (agents.length > 0) {
134061
134206
  console.log(`[engine] Registered ${triggerScheduler.getRegisteredAgents().length} agents for heartbeat triggers`);
@@ -134094,7 +134239,8 @@ async function runDashboard(port, opts = {}) {
134094
134239
  pluginRunner: pluginLoader,
134095
134240
  skillsAdapter,
134096
134241
  https: loadTlsCredentialsFromEnv(),
134097
- daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0
134242
+ daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0,
134243
+ noAuth: opts.noAuth
134098
134244
  });
134099
134245
  }
134100
134246
  if (opts.dev) {