@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 +272 -101
- package/dist/client/assets/{index-9GdD09x7.css → index-CYkQfLYV.css} +1 -1
- package/dist/client/assets/index-zTogbMzz.js +1241 -0
- package/dist/client/index.html +2 -2
- package/dist/extension.js +173 -85
- package/package.json +3 -3
- package/dist/client/assets/index-wtVkS5Qz.js +0 -1241
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
|
-
|
|
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
|
|
3704
|
+
metadata,
|
|
3679
3705
|
...input.title && { title: input.title },
|
|
3680
3706
|
...input.icon && { icon: input.icon },
|
|
3681
3707
|
...input.reportsTo && { reportsTo: input.reportsTo },
|
|
3682
|
-
...
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
69897
|
-
*
|
|
69898
|
-
*
|
|
69899
|
-
*
|
|
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
|
|
69907
|
-
const
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
77206
|
-
heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (
|
|
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
|
|
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
|
|
77309
|
-
heartbeatLog.log(`Timer tick skipped for ${agentId} (
|
|
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
|
-
|
|
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 (
|
|
79532
|
-
|
|
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
|
-
|
|
79576
|
-
|
|
79577
|
-
|
|
79578
|
-
|
|
79579
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
134028
|
-
|
|
134029
|
-
|
|
134030
|
-
|
|
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) {
|