@runfusion/fusion 0.0.4 → 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,
@@ -3510,13 +3512,25 @@ import { mkdir, readFile, writeFile, readdir, unlink, rename } from "node:fs/pro
3510
3512
  import { basename, join as join2, resolve } from "node:path";
3511
3513
  import { randomUUID, randomBytes, createHash } from "node:crypto";
3512
3514
  import { EventEmitter } from "node:events";
3513
- var AgentStore;
3515
+ function resolveCreationRuntimeConfig(incoming, metadata) {
3516
+ const isEphemeral = isEphemeralAgent({ metadata });
3517
+ if (isEphemeral) {
3518
+ return incoming;
3519
+ }
3520
+ const rc = { ...incoming ?? {} };
3521
+ if (typeof rc.heartbeatIntervalMs !== "number" || !Number.isFinite(rc.heartbeatIntervalMs)) {
3522
+ rc.heartbeatIntervalMs = DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS;
3523
+ }
3524
+ return rc;
3525
+ }
3526
+ var DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS, AgentStore;
3514
3527
  var init_agent_store = __esm({
3515
3528
  "../core/src/agent-store.ts"() {
3516
3529
  "use strict";
3517
3530
  init_types();
3518
3531
  init_agent_permissions();
3519
3532
  init_db();
3533
+ DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS = 36e5;
3520
3534
  AgentStore = class extends EventEmitter {
3521
3535
  rootDir;
3522
3536
  agentsDir;
@@ -3655,6 +3669,16 @@ var init_agent_store = __esm({
3655
3669
  }
3656
3670
  /**
3657
3671
  * Create a new agent with "idle" state.
3672
+ *
3673
+ * For non-ephemeral agents, ensures `runtimeConfig.heartbeatIntervalMs` is
3674
+ * persisted at creation time — previously it was only ever written when the
3675
+ * user interacted with the dashboard dropdown, so agents created and never
3676
+ * touched would end up with no interval on disk. That made the dashboard's
3677
+ * freshness check behave inconsistently between agents that had been
3678
+ * configured and agents that hadn't, even though the scheduler applied the
3679
+ * same default (1h) to both at runtime. Writing the default explicitly
3680
+ * removes that divergence and keeps the persisted config truthful.
3681
+ *
3658
3682
  * @param input - Creation parameters
3659
3683
  * @returns The created agent
3660
3684
  * @throws Error if input is invalid
@@ -3668,6 +3692,8 @@ var init_agent_store = __esm({
3668
3692
  }
3669
3693
  const now = (/* @__PURE__ */ new Date()).toISOString();
3670
3694
  const agentId = `agent-${randomUUID().slice(0, 8)}`;
3695
+ const metadata = input.metadata ?? {};
3696
+ const runtimeConfig = resolveCreationRuntimeConfig(input.runtimeConfig, metadata);
3671
3697
  const agent = {
3672
3698
  id: agentId,
3673
3699
  name: input.name.trim(),
@@ -3675,11 +3701,11 @@ var init_agent_store = __esm({
3675
3701
  state: "idle",
3676
3702
  createdAt: now,
3677
3703
  updatedAt: now,
3678
- metadata: input.metadata ?? {},
3704
+ metadata,
3679
3705
  ...input.title && { title: input.title },
3680
3706
  ...input.icon && { icon: input.icon },
3681
3707
  ...input.reportsTo && { reportsTo: input.reportsTo },
3682
- ...input.runtimeConfig && { runtimeConfig: input.runtimeConfig },
3708
+ ...runtimeConfig && { runtimeConfig },
3683
3709
  ...input.permissions && { permissions: input.permissions },
3684
3710
  ...input.instructionsPath && { instructionsPath: input.instructionsPath },
3685
3711
  ...input.instructionsText && { instructionsText: input.instructionsText },
@@ -30525,6 +30551,7 @@ ${newTask.description}
30525
30551
  }
30526
30552
  }
30527
30553
  if (updates.steps !== void 0) task.steps = updates.steps;
30554
+ if (updates.currentStep !== void 0) task.currentStep = updates.currentStep;
30528
30555
  if (updates.status === null) {
30529
30556
  task.status = void 0;
30530
30557
  } else if (updates.status !== void 0) {
@@ -47049,6 +47076,7 @@ __export(src_exports, {
47049
47076
  CheckoutConflictError: () => CheckoutConflictError,
47050
47077
  DAEMON_TOKEN_HEX_LENGTH: () => DAEMON_TOKEN_HEX_LENGTH,
47051
47078
  DAEMON_TOKEN_PREFIX: () => DAEMON_TOKEN_PREFIX,
47079
+ DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS: () => DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS,
47052
47080
  DEFAULT_AUTO_SUMMARIZE_SCHEDULE: () => DEFAULT_AUTO_SUMMARIZE_SCHEDULE,
47053
47081
  DEFAULT_EXECUTION_MODE: () => DEFAULT_EXECUTION_MODE,
47054
47082
  DEFAULT_GLOBAL_SETTINGS: () => DEFAULT_GLOBAL_SETTINGS,
@@ -50765,12 +50793,59 @@ var require_lib = __commonJS({
50765
50793
  import { EventEmitter as EventEmitter17 } from "events";
50766
50794
  import * as os2 from "os";
50767
50795
  import * as path from "path";
50768
- import { existsSync as existsSync17 } from "node:fs";
50796
+ import { existsSync as existsSync17, chmodSync } from "node:fs";
50769
50797
  import { join as join22, dirname as dirname7 } from "node:path";
50770
- function findStagedNativeDir() {
50798
+ function getNativePrebuildName() {
50771
50799
  const platform3 = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : process.platform === "win32" ? "win32" : "unknown";
50772
50800
  const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : "unknown";
50773
- 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();
50774
50849
  if (process.env.FUSION_RUNTIME_DIR) {
50775
50850
  const envPath = join22(process.env.FUSION_RUNTIME_DIR, prebuildName);
50776
50851
  if (existsSync17(join22(envPath, "pty.node"))) {
@@ -50811,6 +50886,7 @@ async function loadPtyModule() {
50811
50886
  }
50812
50887
  }
50813
50888
  try {
50889
+ ensureNodePtyNativePermissions();
50814
50890
  const mod = await Promise.resolve().then(() => __toESM(require_lib(), 1));
50815
50891
  ptyModule = mod;
50816
50892
  return ptyModule;
@@ -65047,6 +65123,71 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
65047
65123
  mergerLog.warn(`${taskId}: unable to verify/checkout main branch \u2014 proceeding on current HEAD`);
65048
65124
  }
65049
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
+ }
65050
65191
  let commitLog = "";
65051
65192
  let diffStat = "";
65052
65193
  try {
@@ -67563,19 +67704,6 @@ import { existsSync as existsSync26 } from "node:fs";
67563
67704
  import { readFile as readFile16, writeFile as writeFile12 } from "node:fs/promises";
67564
67705
  import { Type as Type4 } from "@mariozechner/pi-ai";
67565
67706
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
67566
- function determineRevisionResetStart(steps, feedback) {
67567
- const total = steps.length;
67568
- if (total === 0) return 0;
67569
- const skipPreflight = /preflight/i.test(steps[0].name);
67570
- const firstCandidate = skipPreflight ? 1 : 0;
67571
- if (firstCandidate >= total) return total;
67572
- const fb = feedback.toLowerCase();
67573
- for (let i = firstCandidate; i < total; i++) {
67574
- const tokens = steps[i].name.toLowerCase().match(/[a-z][a-z]{4,}/g) ?? [];
67575
- if (tokens.some((t) => fb.includes(t))) return i;
67576
- }
67577
- return firstCandidate;
67578
- }
67579
67707
  function truncateWorkflowScriptOutput2(output) {
67580
67708
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2) return output;
67581
67709
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2} characters ...
@@ -69892,35 +70020,23 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
69892
70020
  }
69893
70021
  /**
69894
70022
  * Handle a workflow step revision request.
69895
- *
69896
- * This method:
69897
- * 1. Updates PROMPT.md with "Workflow Revision Instructions" section
69898
- * 2. Resets task execution state (all steps reset to pending)
69899
- * 3. Schedules fresh execution to run after current guard unwinds
69900
- *
69901
- * 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.
69902
70028
  */
69903
70029
  async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
69904
70030
  executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
69905
70031
  const updatedTask = await this.store.getTask(task.id);
69906
- const resetStart = determineRevisionResetStart(updatedTask.steps, feedback);
69907
- const targetStepName = resetStart < updatedTask.steps.length ? updatedTask.steps[resetStart].name : null;
69908
- 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)";
69909
70034
  await this.store.logEntry(
69910
70035
  task.id,
69911
- `Workflow step "${stepName}" requested revision \u2014 ${resetSummary}`,
70036
+ `Workflow step "${stepName}" requested revision \u2014 ${reopenSummary}`,
69912
70037
  feedback
69913
70038
  );
69914
- await this.injectWorkflowRevisionInstructions(task, feedback, {
69915
- resetStart,
69916
- targetStepName,
69917
- totalSteps: updatedTask.steps.length
69918
- });
69919
- for (let i = resetStart; i < updatedTask.steps.length; i++) {
69920
- if (updatedTask.steps[i].status !== "pending") {
69921
- await this.store.updateStep(task.id, i, "pending");
69922
- }
69923
- }
70039
+ await this.injectWorkflowRevisionInstructions(task, feedback);
69924
70040
  await this.store.updateTask(task.id, {
69925
70041
  status: null,
69926
70042
  sessionFile: null
@@ -69942,12 +70058,36 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
69942
70058
  }
69943
70059
  }, 0);
69944
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
+ }
69945
70085
  /**
69946
70086
  * Inject or update the "Workflow Revision Instructions" section in PROMPT.md.
69947
70087
  * This section contains feedback from workflow steps that requested revisions.
69948
70088
  * The section is replaced entirely to avoid accumulation of old feedback.
69949
70089
  */
69950
- async injectWorkflowRevisionInstructions(task, feedback, scope) {
70090
+ async injectWorkflowRevisionInstructions(task, feedback) {
69951
70091
  const promptPath = join35(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
69952
70092
  let content;
69953
70093
  try {
@@ -69956,14 +70096,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
69956
70096
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
69957
70097
  return;
69958
70098
  }
69959
- let scopeLine;
69960
- if (scope && scope.targetStepName && scope.resetStart < scope.totalSteps) {
69961
- 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.`;
69962
- } else if (scope && scope.resetStart >= scope.totalSteps) {
69963
- scopeLine = "No steps were reset; apply the feedback as an in-place fix and call task_done() when complete.";
69964
- } else {
69965
- scopeLine = "Address the feedback above by making the necessary code changes, then mark all affected steps as done and call task_done() when complete.";
69966
- }
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.";
69967
70100
  const revisionSectionHeader = "## Workflow Revision Instructions";
69968
70101
  const revisionSectionContent = `${revisionSectionHeader}
69969
70102
 
@@ -70022,11 +70155,7 @@ ${feedback}
70022
70155
  });
70023
70156
  await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount);
70024
70157
  const updatedTask = await this.store.getTask(task.id);
70025
- for (let i = 0; i < updatedTask.steps.length; i++) {
70026
- if (updatedTask.steps[i].status !== "pending") {
70027
- await this.store.updateStep(task.id, i, "pending");
70028
- }
70029
- }
70158
+ await this.reopenLastStepForRevision(task.id, updatedTask);
70030
70159
  await this.store.updateTask(task.id, {
70031
70160
  status: null,
70032
70161
  sessionFile: null
@@ -70070,11 +70199,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
70070
70199
  );
70071
70200
  await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, MAX_WORKFLOW_STEP_RETRIES);
70072
70201
  const updatedTask = await this.store.getTask(taskId);
70073
- for (let i = 0; i < updatedTask.steps.length; i++) {
70074
- if (updatedTask.steps[i].status !== "pending") {
70075
- await this.store.updateStep(taskId, i, "pending");
70076
- }
70077
- }
70202
+ await this.reopenLastStepForRevision(taskId, updatedTask);
70078
70203
  await this.store.updateTask(taskId, {
70079
70204
  status: null,
70080
70205
  error: null,
@@ -75873,6 +75998,12 @@ async function getHeartbeatMemorySettings(taskStore) {
75873
75998
  }
75874
75999
  return maybeGetSettings.call(taskStore);
75875
76000
  }
76001
+ function isTickableState(state) {
76002
+ return state === "active" || state === "running";
76003
+ }
76004
+ function isHeartbeatManaged(agent) {
76005
+ return !isEphemeralAgent(agent);
76006
+ }
75876
76007
  var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, heartbeatDoneParams, HeartbeatMonitor, HeartbeatTriggerScheduler;
75877
76008
  var init_agent_heartbeat = __esm({
75878
76009
  "../engine/src/agent-heartbeat.ts"() {
@@ -77109,10 +77240,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77109
77240
  * @param config - Per-agent heartbeat config
77110
77241
  */
77111
77242
  registerAgent(agentId, config) {
77112
- if (config.enabled === false) {
77113
- heartbeatLog.log(`Skipping timer registration for ${agentId} (disabled)`);
77114
- return;
77115
- }
77116
77243
  let rawIntervalMs = config.heartbeatIntervalMs;
77117
77244
  let usingDefaultInterval = false;
77118
77245
  if (!rawIntervalMs || typeof rawIntervalMs !== "number" || !Number.isFinite(rawIntervalMs) || rawIntervalMs <= 0) {
@@ -77202,8 +77329,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77202
77329
  this.assignedListener = async (agent, taskId) => {
77203
77330
  if (!this.running) return;
77204
77331
  try {
77205
- if (agent.runtimeConfig?.enabled === false) {
77206
- 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})`);
77207
77334
  return;
77208
77335
  }
77209
77336
  const activeRun = await this.store.getActiveHeartbeatRun(agent.id);
@@ -77272,9 +77399,21 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77272
77399
  watchAgentLifecycle() {
77273
77400
  if (this.updatedListener || this.deletedListener) return;
77274
77401
  this.updatedListener = (agent) => {
77275
- if (agent.state === "terminated" || agent.runtimeConfig?.enabled === false) {
77402
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
77276
77403
  this.unregisterAgent(agent.id);
77404
+ return;
77405
+ }
77406
+ if (this.timers.has(agent.id)) {
77407
+ return;
77277
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
+ );
77278
77417
  };
77279
77418
  this.deletedListener = (agentId) => {
77280
77419
  this.unregisterAgent(agentId);
@@ -77305,8 +77444,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
77305
77444
  this.unregisterAgent(agentId);
77306
77445
  return;
77307
77446
  }
77308
- if (agent.state === "terminated" || agent.runtimeConfig?.enabled === false) {
77309
- 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})`);
77310
77449
  this.unregisterAgent(agentId);
77311
77450
  return;
77312
77451
  }
@@ -79511,13 +79650,13 @@ var init_in_process_runtime = __esm({
79511
79650
  this.taskStore
79512
79651
  );
79513
79652
  this.triggerScheduler.start();
79653
+ const isTickable = (agent) => !isEphemeralAgent(agent) && (agent.state === "active" || agent.state === "running");
79514
79654
  this.agentCreatedListener = (agent) => {
79515
79655
  if (!this.triggerScheduler) return;
79656
+ if (!isTickable(agent)) return;
79516
79657
  const rc = agent.runtimeConfig;
79517
- if (rc?.enabled === false) return;
79518
79658
  this.triggerScheduler.registerAgent(agent.id, {
79519
79659
  heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79520
- enabled: rc?.enabled,
79521
79660
  maxConcurrentRuns: rc?.maxConcurrentRuns
79522
79661
  });
79523
79662
  runtimeLog.log(`Registered new agent ${agent.id} for heartbeat triggers`);
@@ -79525,18 +79664,17 @@ var init_in_process_runtime = __esm({
79525
79664
  this.agentStore.on("agent:created", this.agentCreatedListener);
79526
79665
  this.agentUpdatedListener = (agent) => {
79527
79666
  if (!this.triggerScheduler) return;
79528
- const rc = agent.runtimeConfig;
79529
- if (rc?.enabled === false) {
79667
+ if (!isTickable(agent)) {
79530
79668
  this.triggerScheduler.unregisterAgent(agent.id);
79531
- runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (disabled)`);
79532
- } else {
79533
- this.triggerScheduler.registerAgent(agent.id, {
79534
- heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79535
- enabled: rc?.enabled,
79536
- maxConcurrentRuns: rc?.maxConcurrentRuns
79537
- });
79538
- 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;
79539
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})`);
79540
79678
  };
79541
79679
  this.agentStore.on("agent:updated", this.agentUpdatedListener);
79542
79680
  this.ephemeralTerminationListener = (agentId, from, to) => {
@@ -79571,15 +79709,13 @@ var init_in_process_runtime = __esm({
79571
79709
  const agents = await this.agentStore.listAgents();
79572
79710
  let registeredCount = 0;
79573
79711
  for (const agent of agents) {
79712
+ if (!isTickable(agent)) continue;
79574
79713
  const rc = agent.runtimeConfig;
79575
- if (rc?.enabled !== false) {
79576
- this.triggerScheduler.registerAgent(agent.id, {
79577
- heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79578
- enabled: rc?.enabled,
79579
- maxConcurrentRuns: rc?.maxConcurrentRuns
79580
- });
79581
- registeredCount++;
79582
- }
79714
+ this.triggerScheduler.registerAgent(agent.id, {
79715
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
79716
+ maxConcurrentRuns: rc?.maxConcurrentRuns
79717
+ });
79718
+ registeredCount++;
79583
79719
  }
79584
79720
  if (agents.length > 0) {
79585
79721
  runtimeLog.log(`Registered ${registeredCount} of ${agents.length} agents for heartbeat triggers`);
@@ -121443,6 +121579,34 @@ Description: ${step.description}`
121443
121579
  rethrowAsApiError3(err);
121444
121580
  }
121445
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
+ }
121446
121610
  router.get("/agents", async (req, res) => {
121447
121611
  try {
121448
121612
  const filter2 = {};
@@ -121460,7 +121624,8 @@ Description: ${step.description}`
121460
121624
  const agentStore = new AgentStore2({ rootDir: scopedStore.getFusionDir() });
121461
121625
  await agentStore.init();
121462
121626
  const agents = await agentStore.listAgents(filter2);
121463
- res.json(agents);
121627
+ const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
121628
+ res.json(sanitizedAgents);
121464
121629
  } catch (err) {
121465
121630
  if (err instanceof ApiError) {
121466
121631
  throw err;
@@ -122155,7 +122320,8 @@ ${body}`;
122155
122320
  await agentStore.init();
122156
122321
  const agents = await agentStore.listAgents();
122157
122322
  const activeCount = agents.filter((a) => a.state === "active" || a.state === "running").length;
122158
- const assignedTaskCount = agents.filter((a) => a.taskId).length;
122323
+ const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
122324
+ const assignedTaskCount = sanitizedAgents.filter((a) => a.taskId).length;
122159
122325
  let completedRuns = 0;
122160
122326
  let failedRuns = 0;
122161
122327
  for (const agent of agents) {
@@ -122217,7 +122383,8 @@ ${body}`;
122217
122383
  if (!agent) {
122218
122384
  throw notFound("Agent not found");
122219
122385
  }
122220
- res.json(agent);
122386
+ const [sanitizedAgent] = await sanitizeAgentTaskLinks([agent], scopedStore);
122387
+ res.json(sanitizedAgent);
122221
122388
  } catch (err) {
122222
122389
  if (err instanceof ApiError) {
122223
122390
  throw err;
@@ -131552,6 +131719,9 @@ function isApiPath(path4) {
131552
131719
  return path4 === "/api" || path4.startsWith("/api/");
131553
131720
  }
131554
131721
  function getDaemonToken(options) {
131722
+ if (options?.noAuth) {
131723
+ return void 0;
131724
+ }
131555
131725
  if (options?.daemon?.token) {
131556
131726
  return options.daemon.token;
131557
131727
  }
@@ -131775,7 +131945,7 @@ function createServer(store, options) {
131775
131945
  }
131776
131946
  }
131777
131947
  }));
131778
- 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;
131779
131949
  if (daemonToken) {
131780
131950
  app.use(createAuthMiddleware(daemonToken));
131781
131951
  }
@@ -133928,7 +134098,8 @@ async function runDashboard(port, opts = {}) {
133928
134098
  onProjectFirstAccessed: (projectId) => engineManager.onProjectAccessed(projectId),
133929
134099
  skillsAdapter,
133930
134100
  https: loadTlsCredentialsFromEnv(),
133931
- daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0
134101
+ daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0,
134102
+ noAuth: opts.noAuth
133932
134103
  });
133933
134104
  const shutdown = async (signal) => {
133934
134105
  if (shutdownInProgress) return;
@@ -134023,14 +134194,13 @@ async function runDashboard(port, opts = {}) {
134023
134194
  triggerScheduler.start();
134024
134195
  const agents = await agentStore.listAgents();
134025
134196
  for (const agent of agents) {
134197
+ if (isEphemeralAgent(agent)) continue;
134198
+ if (agent.state !== "active" && agent.state !== "running") continue;
134026
134199
  const rc = agent.runtimeConfig;
134027
- if (rc && (rc.heartbeatIntervalMs || rc.enabled !== void 0 || rc.maxConcurrentRuns)) {
134028
- triggerScheduler.registerAgent(agent.id, {
134029
- heartbeatIntervalMs: rc.heartbeatIntervalMs,
134030
- enabled: rc.enabled,
134031
- maxConcurrentRuns: rc.maxConcurrentRuns
134032
- });
134033
- }
134200
+ triggerScheduler.registerAgent(agent.id, {
134201
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
134202
+ maxConcurrentRuns: rc?.maxConcurrentRuns
134203
+ });
134034
134204
  }
134035
134205
  if (agents.length > 0) {
134036
134206
  console.log(`[engine] Registered ${triggerScheduler.getRegisteredAgents().length} agents for heartbeat triggers`);
@@ -134069,7 +134239,8 @@ async function runDashboard(port, opts = {}) {
134069
134239
  pluginRunner: pluginLoader,
134070
134240
  skillsAdapter,
134071
134241
  https: loadTlsCredentialsFromEnv(),
134072
- daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0
134242
+ daemon: dashboardAuthToken ? { token: dashboardAuthToken } : void 0,
134243
+ noAuth: opts.noAuth
134073
134244
  });
134074
134245
  }
134075
134246
  if (opts.dev) {