@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/client/index.html
CHANGED
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
}
|
|
79
79
|
})();
|
|
80
80
|
</script>
|
|
81
|
-
<script type="module" crossorigin src="/assets/index-
|
|
81
|
+
<script type="module" crossorigin src="/assets/index-zTogbMzz.js"></script>
|
|
82
82
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-K0fH_qHe.js">
|
|
83
83
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DzcZoU0P.js">
|
|
84
84
|
<link rel="stylesheet" crossorigin href="/assets/vendor-xterm-LZoznX6r.css">
|
|
85
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
85
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CYkQfLYV.css">
|
|
86
86
|
</head>
|
|
87
87
|
<body>
|
|
88
88
|
<div id="root"></div>
|
package/dist/extension.js
CHANGED
|
@@ -133,6 +133,8 @@ var init_settings_schema = __esm({
|
|
|
133
133
|
defaultPresetBySize: {},
|
|
134
134
|
autoResolveConflicts: true,
|
|
135
135
|
smartConflictResolution: true,
|
|
136
|
+
worktreeRebaseBeforeMerge: true,
|
|
137
|
+
worktreeRebaseRemote: "",
|
|
136
138
|
strictScopeEnforcement: false,
|
|
137
139
|
buildRetryCount: 0,
|
|
138
140
|
verificationFixRetries: 1,
|
|
@@ -3509,13 +3511,25 @@ import { mkdir, readFile, writeFile, readdir, unlink, rename } from "node:fs/pro
|
|
|
3509
3511
|
import { basename, join as join2, resolve } from "node:path";
|
|
3510
3512
|
import { randomUUID, randomBytes, createHash } from "node:crypto";
|
|
3511
3513
|
import { EventEmitter } from "node:events";
|
|
3512
|
-
|
|
3514
|
+
function resolveCreationRuntimeConfig(incoming, metadata) {
|
|
3515
|
+
const isEphemeral = isEphemeralAgent({ metadata });
|
|
3516
|
+
if (isEphemeral) {
|
|
3517
|
+
return incoming;
|
|
3518
|
+
}
|
|
3519
|
+
const rc = { ...incoming ?? {} };
|
|
3520
|
+
if (typeof rc.heartbeatIntervalMs !== "number" || !Number.isFinite(rc.heartbeatIntervalMs)) {
|
|
3521
|
+
rc.heartbeatIntervalMs = DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS;
|
|
3522
|
+
}
|
|
3523
|
+
return rc;
|
|
3524
|
+
}
|
|
3525
|
+
var DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS, AgentStore;
|
|
3513
3526
|
var init_agent_store = __esm({
|
|
3514
3527
|
"../core/src/agent-store.ts"() {
|
|
3515
3528
|
"use strict";
|
|
3516
3529
|
init_types();
|
|
3517
3530
|
init_agent_permissions();
|
|
3518
3531
|
init_db();
|
|
3532
|
+
DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS = 36e5;
|
|
3519
3533
|
AgentStore = class extends EventEmitter {
|
|
3520
3534
|
rootDir;
|
|
3521
3535
|
agentsDir;
|
|
@@ -3654,6 +3668,16 @@ var init_agent_store = __esm({
|
|
|
3654
3668
|
}
|
|
3655
3669
|
/**
|
|
3656
3670
|
* Create a new agent with "idle" state.
|
|
3671
|
+
*
|
|
3672
|
+
* For non-ephemeral agents, ensures `runtimeConfig.heartbeatIntervalMs` is
|
|
3673
|
+
* persisted at creation time — previously it was only ever written when the
|
|
3674
|
+
* user interacted with the dashboard dropdown, so agents created and never
|
|
3675
|
+
* touched would end up with no interval on disk. That made the dashboard's
|
|
3676
|
+
* freshness check behave inconsistently between agents that had been
|
|
3677
|
+
* configured and agents that hadn't, even though the scheduler applied the
|
|
3678
|
+
* same default (1h) to both at runtime. Writing the default explicitly
|
|
3679
|
+
* removes that divergence and keeps the persisted config truthful.
|
|
3680
|
+
*
|
|
3657
3681
|
* @param input - Creation parameters
|
|
3658
3682
|
* @returns The created agent
|
|
3659
3683
|
* @throws Error if input is invalid
|
|
@@ -3667,6 +3691,8 @@ var init_agent_store = __esm({
|
|
|
3667
3691
|
}
|
|
3668
3692
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3669
3693
|
const agentId = `agent-${randomUUID().slice(0, 8)}`;
|
|
3694
|
+
const metadata = input.metadata ?? {};
|
|
3695
|
+
const runtimeConfig = resolveCreationRuntimeConfig(input.runtimeConfig, metadata);
|
|
3670
3696
|
const agent = {
|
|
3671
3697
|
id: agentId,
|
|
3672
3698
|
name: input.name.trim(),
|
|
@@ -3674,11 +3700,11 @@ var init_agent_store = __esm({
|
|
|
3674
3700
|
state: "idle",
|
|
3675
3701
|
createdAt: now,
|
|
3676
3702
|
updatedAt: now,
|
|
3677
|
-
metadata
|
|
3703
|
+
metadata,
|
|
3678
3704
|
...input.title && { title: input.title },
|
|
3679
3705
|
...input.icon && { icon: input.icon },
|
|
3680
3706
|
...input.reportsTo && { reportsTo: input.reportsTo },
|
|
3681
|
-
...
|
|
3707
|
+
...runtimeConfig && { runtimeConfig },
|
|
3682
3708
|
...input.permissions && { permissions: input.permissions },
|
|
3683
3709
|
...input.instructionsPath && { instructionsPath: input.instructionsPath },
|
|
3684
3710
|
...input.instructionsText && { instructionsText: input.instructionsText },
|
|
@@ -30524,6 +30550,7 @@ ${newTask.description}
|
|
|
30524
30550
|
}
|
|
30525
30551
|
}
|
|
30526
30552
|
if (updates.steps !== void 0) task.steps = updates.steps;
|
|
30553
|
+
if (updates.currentStep !== void 0) task.currentStep = updates.currentStep;
|
|
30527
30554
|
if (updates.status === null) {
|
|
30528
30555
|
task.status = void 0;
|
|
30529
30556
|
} else if (updates.status !== void 0) {
|
|
@@ -47048,6 +47075,7 @@ __export(src_exports, {
|
|
|
47048
47075
|
CheckoutConflictError: () => CheckoutConflictError,
|
|
47049
47076
|
DAEMON_TOKEN_HEX_LENGTH: () => DAEMON_TOKEN_HEX_LENGTH,
|
|
47050
47077
|
DAEMON_TOKEN_PREFIX: () => DAEMON_TOKEN_PREFIX,
|
|
47078
|
+
DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS: () => DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS,
|
|
47051
47079
|
DEFAULT_AUTO_SUMMARIZE_SCHEDULE: () => DEFAULT_AUTO_SUMMARIZE_SCHEDULE,
|
|
47052
47080
|
DEFAULT_EXECUTION_MODE: () => DEFAULT_EXECUTION_MODE,
|
|
47053
47081
|
DEFAULT_GLOBAL_SETTINGS: () => DEFAULT_GLOBAL_SETTINGS,
|
|
@@ -53395,6 +53423,71 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
53395
53423
|
mergerLog.warn(`${taskId}: unable to verify/checkout main branch \u2014 proceeding on current HEAD`);
|
|
53396
53424
|
}
|
|
53397
53425
|
}
|
|
53426
|
+
if (settings.worktreeRebaseBeforeMerge !== false) {
|
|
53427
|
+
try {
|
|
53428
|
+
let remote = settings.worktreeRebaseRemote?.trim();
|
|
53429
|
+
if (!remote) {
|
|
53430
|
+
try {
|
|
53431
|
+
const { stdout: mainBranchOut } = await execAsync2(
|
|
53432
|
+
"git rev-parse --abbrev-ref HEAD",
|
|
53433
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
53434
|
+
);
|
|
53435
|
+
const mainBranch = mainBranchOut.trim();
|
|
53436
|
+
const { stdout: configuredRemote } = await execAsync2(
|
|
53437
|
+
`git config --get branch.${mainBranch}.remote`,
|
|
53438
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
53439
|
+
).catch(() => ({ stdout: "" }));
|
|
53440
|
+
remote = configuredRemote.trim();
|
|
53441
|
+
} catch {
|
|
53442
|
+
}
|
|
53443
|
+
}
|
|
53444
|
+
if (!remote) {
|
|
53445
|
+
try {
|
|
53446
|
+
const { stdout: remotesOut } = await execAsync2("git remote", {
|
|
53447
|
+
cwd: rootDir,
|
|
53448
|
+
encoding: "utf-8"
|
|
53449
|
+
});
|
|
53450
|
+
const remotes = remotesOut.trim().split(/\s+/).filter(Boolean);
|
|
53451
|
+
if (remotes.length === 1) {
|
|
53452
|
+
remote = remotes[0];
|
|
53453
|
+
} else if (remotes.includes("origin")) {
|
|
53454
|
+
remote = "origin";
|
|
53455
|
+
}
|
|
53456
|
+
} catch {
|
|
53457
|
+
}
|
|
53458
|
+
}
|
|
53459
|
+
if (!remote) {
|
|
53460
|
+
mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping pre-merge rebase`);
|
|
53461
|
+
} else {
|
|
53462
|
+
mergerLog.log(`${taskId}: fetching ${remote} before merge`);
|
|
53463
|
+
await execAsync2(`git fetch "${remote}"`, { cwd: rootDir });
|
|
53464
|
+
try {
|
|
53465
|
+
const { stdout: mainBranchOut } = await execAsync2(
|
|
53466
|
+
"git rev-parse --abbrev-ref HEAD",
|
|
53467
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
53468
|
+
);
|
|
53469
|
+
const mainBranch = mainBranchOut.trim();
|
|
53470
|
+
const remoteRef = `${remote}/${mainBranch}`;
|
|
53471
|
+
if (worktreePath) {
|
|
53472
|
+
await execAsync2(`git rebase "${remoteRef}"`, { cwd: worktreePath });
|
|
53473
|
+
mergerLog.log(`${taskId}: rebased ${branch} onto ${remoteRef}`);
|
|
53474
|
+
} else {
|
|
53475
|
+
mergerLog.warn(`${taskId}: no worktreePath \u2014 skipping task branch rebase`);
|
|
53476
|
+
}
|
|
53477
|
+
} catch (rebaseErr) {
|
|
53478
|
+
const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
|
|
53479
|
+
mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through to smart/AI merge`);
|
|
53480
|
+
if (worktreePath) {
|
|
53481
|
+
await execAsync2("git rebase --abort", { cwd: worktreePath }).catch(() => {
|
|
53482
|
+
});
|
|
53483
|
+
}
|
|
53484
|
+
}
|
|
53485
|
+
}
|
|
53486
|
+
} catch (err) {
|
|
53487
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53488
|
+
mergerLog.warn(`${taskId}: pre-merge rebase pipeline failed (${msg}) \u2014 proceeding without rebase`);
|
|
53489
|
+
}
|
|
53490
|
+
}
|
|
53398
53491
|
let commitLog = "";
|
|
53399
53492
|
let diffStat = "";
|
|
53400
53493
|
try {
|
|
@@ -55911,19 +56004,6 @@ import { existsSync as existsSync25 } from "node:fs";
|
|
|
55911
56004
|
import { readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
|
|
55912
56005
|
import { Type as Type4 } from "@mariozechner/pi-ai";
|
|
55913
56006
|
import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
|
|
55914
|
-
function determineRevisionResetStart(steps, feedback) {
|
|
55915
|
-
const total = steps.length;
|
|
55916
|
-
if (total === 0) return 0;
|
|
55917
|
-
const skipPreflight = /preflight/i.test(steps[0].name);
|
|
55918
|
-
const firstCandidate = skipPreflight ? 1 : 0;
|
|
55919
|
-
if (firstCandidate >= total) return total;
|
|
55920
|
-
const fb = feedback.toLowerCase();
|
|
55921
|
-
for (let i = firstCandidate; i < total; i++) {
|
|
55922
|
-
const tokens = steps[i].name.toLowerCase().match(/[a-z][a-z]{4,}/g) ?? [];
|
|
55923
|
-
if (tokens.some((t) => fb.includes(t))) return i;
|
|
55924
|
-
}
|
|
55925
|
-
return firstCandidate;
|
|
55926
|
-
}
|
|
55927
56007
|
function truncateWorkflowScriptOutput2(output) {
|
|
55928
56008
|
if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2) return output;
|
|
55929
56009
|
return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2} characters ...
|
|
@@ -58240,35 +58320,23 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
|
|
|
58240
58320
|
}
|
|
58241
58321
|
/**
|
|
58242
58322
|
* Handle a workflow step revision request.
|
|
58243
|
-
*
|
|
58244
|
-
*
|
|
58245
|
-
*
|
|
58246
|
-
*
|
|
58247
|
-
*
|
|
58248
|
-
*
|
|
58249
|
-
* The task stays in "in-progress" and is scheduled for a fresh executor pass.
|
|
58323
|
+
*
|
|
58324
|
+
* Re-opens ONLY the last step so the executor has exactly one pending slot
|
|
58325
|
+
* to re-enter through. All earlier done steps stay done — the agent reads
|
|
58326
|
+
* the injected feedback from PROMPT.md and applies an in-place fix rather
|
|
58327
|
+
* than redoing any completed step.
|
|
58250
58328
|
*/
|
|
58251
58329
|
async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
|
|
58252
58330
|
executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
|
|
58253
58331
|
const updatedTask = await this.store.getTask(task.id);
|
|
58254
|
-
const
|
|
58255
|
-
const
|
|
58256
|
-
const resetSummary = resetStart >= updatedTask.steps.length ? "no steps to reset" : `resetting steps ${resetStart + 1}\u2013${updatedTask.steps.length} (starting at "${targetStepName}")`;
|
|
58332
|
+
const reopen = await this.reopenLastStepForRevision(task.id, updatedTask);
|
|
58333
|
+
const reopenSummary = reopen ? `re-opening Step ${reopen.index + 1} ("${reopen.name}") for in-place fix` : "no step to re-open (none were completed)";
|
|
58257
58334
|
await this.store.logEntry(
|
|
58258
58335
|
task.id,
|
|
58259
|
-
`Workflow step "${stepName}" requested revision \u2014 ${
|
|
58336
|
+
`Workflow step "${stepName}" requested revision \u2014 ${reopenSummary}`,
|
|
58260
58337
|
feedback
|
|
58261
58338
|
);
|
|
58262
|
-
await this.injectWorkflowRevisionInstructions(task, feedback
|
|
58263
|
-
resetStart,
|
|
58264
|
-
targetStepName,
|
|
58265
|
-
totalSteps: updatedTask.steps.length
|
|
58266
|
-
});
|
|
58267
|
-
for (let i = resetStart; i < updatedTask.steps.length; i++) {
|
|
58268
|
-
if (updatedTask.steps[i].status !== "pending") {
|
|
58269
|
-
await this.store.updateStep(task.id, i, "pending");
|
|
58270
|
-
}
|
|
58271
|
-
}
|
|
58339
|
+
await this.injectWorkflowRevisionInstructions(task, feedback);
|
|
58272
58340
|
await this.store.updateTask(task.id, {
|
|
58273
58341
|
status: null,
|
|
58274
58342
|
sessionFile: null
|
|
@@ -58290,12 +58358,36 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
|
|
|
58290
58358
|
}
|
|
58291
58359
|
}, 0);
|
|
58292
58360
|
}
|
|
58361
|
+
/**
|
|
58362
|
+
* Re-open the last non-pending step so a revision/failure handler gives the
|
|
58363
|
+
* executor exactly one pending slot to re-enter through. Returns the index
|
|
58364
|
+
* and name of the step that was flipped to `pending`, or null when there
|
|
58365
|
+
* was nothing to re-open.
|
|
58366
|
+
*/
|
|
58367
|
+
async reopenLastStepForRevision(taskId, task) {
|
|
58368
|
+
const steps = task.steps;
|
|
58369
|
+
if (steps.length === 0) return null;
|
|
58370
|
+
let targetIndex = -1;
|
|
58371
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
58372
|
+
if (steps[i].status !== "pending") {
|
|
58373
|
+
targetIndex = i;
|
|
58374
|
+
break;
|
|
58375
|
+
}
|
|
58376
|
+
}
|
|
58377
|
+
if (targetIndex === -1) {
|
|
58378
|
+
await this.store.updateTask(taskId, { currentStep: 0 });
|
|
58379
|
+
return null;
|
|
58380
|
+
}
|
|
58381
|
+
await this.store.updateStep(taskId, targetIndex, "pending");
|
|
58382
|
+
await this.store.updateTask(taskId, { currentStep: targetIndex });
|
|
58383
|
+
return { index: targetIndex, name: steps[targetIndex].name };
|
|
58384
|
+
}
|
|
58293
58385
|
/**
|
|
58294
58386
|
* Inject or update the "Workflow Revision Instructions" section in PROMPT.md.
|
|
58295
58387
|
* This section contains feedback from workflow steps that requested revisions.
|
|
58296
58388
|
* The section is replaced entirely to avoid accumulation of old feedback.
|
|
58297
58389
|
*/
|
|
58298
|
-
async injectWorkflowRevisionInstructions(task, feedback
|
|
58390
|
+
async injectWorkflowRevisionInstructions(task, feedback) {
|
|
58299
58391
|
const promptPath = join32(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
|
|
58300
58392
|
let content;
|
|
58301
58393
|
try {
|
|
@@ -58304,14 +58396,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
|
|
|
58304
58396
|
executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
|
|
58305
58397
|
return;
|
|
58306
58398
|
}
|
|
58307
|
-
|
|
58308
|
-
if (scope && scope.targetStepName && scope.resetStart < scope.totalSteps) {
|
|
58309
|
-
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.`;
|
|
58310
|
-
} else if (scope && scope.resetStart >= scope.totalSteps) {
|
|
58311
|
-
scopeLine = "No steps were reset; apply the feedback as an in-place fix and call task_done() when complete.";
|
|
58312
|
-
} else {
|
|
58313
|
-
scopeLine = "Address the feedback above by making the necessary code changes, then mark all affected steps as done and call task_done() when complete.";
|
|
58314
|
-
}
|
|
58399
|
+
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.";
|
|
58315
58400
|
const revisionSectionHeader = "## Workflow Revision Instructions";
|
|
58316
58401
|
const revisionSectionContent = `${revisionSectionHeader}
|
|
58317
58402
|
|
|
@@ -58370,11 +58455,7 @@ ${feedback}
|
|
|
58370
58455
|
});
|
|
58371
58456
|
await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount);
|
|
58372
58457
|
const updatedTask = await this.store.getTask(task.id);
|
|
58373
|
-
|
|
58374
|
-
if (updatedTask.steps[i].status !== "pending") {
|
|
58375
|
-
await this.store.updateStep(task.id, i, "pending");
|
|
58376
|
-
}
|
|
58377
|
-
}
|
|
58458
|
+
await this.reopenLastStepForRevision(task.id, updatedTask);
|
|
58378
58459
|
await this.store.updateTask(task.id, {
|
|
58379
58460
|
status: null,
|
|
58380
58461
|
sessionFile: null
|
|
@@ -58418,11 +58499,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
|
|
|
58418
58499
|
);
|
|
58419
58500
|
await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, MAX_WORKFLOW_STEP_RETRIES);
|
|
58420
58501
|
const updatedTask = await this.store.getTask(taskId);
|
|
58421
|
-
|
|
58422
|
-
if (updatedTask.steps[i].status !== "pending") {
|
|
58423
|
-
await this.store.updateStep(taskId, i, "pending");
|
|
58424
|
-
}
|
|
58425
|
-
}
|
|
58502
|
+
await this.reopenLastStepForRevision(taskId, updatedTask);
|
|
58426
58503
|
await this.store.updateTask(taskId, {
|
|
58427
58504
|
status: null,
|
|
58428
58505
|
error: null,
|
|
@@ -64221,6 +64298,12 @@ async function getHeartbeatMemorySettings(taskStore) {
|
|
|
64221
64298
|
}
|
|
64222
64299
|
return maybeGetSettings.call(taskStore);
|
|
64223
64300
|
}
|
|
64301
|
+
function isTickableState(state) {
|
|
64302
|
+
return state === "active" || state === "running";
|
|
64303
|
+
}
|
|
64304
|
+
function isHeartbeatManaged(agent) {
|
|
64305
|
+
return !isEphemeralAgent(agent);
|
|
64306
|
+
}
|
|
64224
64307
|
var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, heartbeatDoneParams, HeartbeatMonitor, HeartbeatTriggerScheduler;
|
|
64225
64308
|
var init_agent_heartbeat = __esm({
|
|
64226
64309
|
"../engine/src/agent-heartbeat.ts"() {
|
|
@@ -65457,10 +65540,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
65457
65540
|
* @param config - Per-agent heartbeat config
|
|
65458
65541
|
*/
|
|
65459
65542
|
registerAgent(agentId, config) {
|
|
65460
|
-
if (config.enabled === false) {
|
|
65461
|
-
heartbeatLog.log(`Skipping timer registration for ${agentId} (disabled)`);
|
|
65462
|
-
return;
|
|
65463
|
-
}
|
|
65464
65543
|
let rawIntervalMs = config.heartbeatIntervalMs;
|
|
65465
65544
|
let usingDefaultInterval = false;
|
|
65466
65545
|
if (!rawIntervalMs || typeof rawIntervalMs !== "number" || !Number.isFinite(rawIntervalMs) || rawIntervalMs <= 0) {
|
|
@@ -65550,8 +65629,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
65550
65629
|
this.assignedListener = async (agent, taskId) => {
|
|
65551
65630
|
if (!this.running) return;
|
|
65552
65631
|
try {
|
|
65553
|
-
if (agent
|
|
65554
|
-
heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (
|
|
65632
|
+
if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
|
|
65633
|
+
heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (state=${agent.state})`);
|
|
65555
65634
|
return;
|
|
65556
65635
|
}
|
|
65557
65636
|
const activeRun = await this.store.getActiveHeartbeatRun(agent.id);
|
|
@@ -65620,9 +65699,21 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
65620
65699
|
watchAgentLifecycle() {
|
|
65621
65700
|
if (this.updatedListener || this.deletedListener) return;
|
|
65622
65701
|
this.updatedListener = (agent) => {
|
|
65623
|
-
if (agent
|
|
65702
|
+
if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
|
|
65624
65703
|
this.unregisterAgent(agent.id);
|
|
65704
|
+
return;
|
|
65625
65705
|
}
|
|
65706
|
+
if (this.timers.has(agent.id)) {
|
|
65707
|
+
return;
|
|
65708
|
+
}
|
|
65709
|
+
const rc = agent.runtimeConfig ?? {};
|
|
65710
|
+
this.registerAgent(agent.id, {
|
|
65711
|
+
heartbeatIntervalMs: rc.heartbeatIntervalMs,
|
|
65712
|
+
maxConcurrentRuns: rc.maxConcurrentRuns
|
|
65713
|
+
});
|
|
65714
|
+
heartbeatLog.log(
|
|
65715
|
+
`State-driven registration: ${agent.id} is ${agent.state} \u2014 timer armed`
|
|
65716
|
+
);
|
|
65626
65717
|
};
|
|
65627
65718
|
this.deletedListener = (agentId) => {
|
|
65628
65719
|
this.unregisterAgent(agentId);
|
|
@@ -65653,8 +65744,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
65653
65744
|
this.unregisterAgent(agentId);
|
|
65654
65745
|
return;
|
|
65655
65746
|
}
|
|
65656
|
-
if (agent
|
|
65657
|
-
heartbeatLog.log(`Timer tick skipped for ${agentId} (
|
|
65747
|
+
if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
|
|
65748
|
+
heartbeatLog.log(`Timer tick skipped for ${agentId} (state=${agent.state})`);
|
|
65658
65749
|
this.unregisterAgent(agentId);
|
|
65659
65750
|
return;
|
|
65660
65751
|
}
|
|
@@ -67859,13 +67950,13 @@ var init_in_process_runtime = __esm({
|
|
|
67859
67950
|
this.taskStore
|
|
67860
67951
|
);
|
|
67861
67952
|
this.triggerScheduler.start();
|
|
67953
|
+
const isTickable = (agent) => !isEphemeralAgent(agent) && (agent.state === "active" || agent.state === "running");
|
|
67862
67954
|
this.agentCreatedListener = (agent) => {
|
|
67863
67955
|
if (!this.triggerScheduler) return;
|
|
67956
|
+
if (!isTickable(agent)) return;
|
|
67864
67957
|
const rc = agent.runtimeConfig;
|
|
67865
|
-
if (rc?.enabled === false) return;
|
|
67866
67958
|
this.triggerScheduler.registerAgent(agent.id, {
|
|
67867
67959
|
heartbeatIntervalMs: rc?.heartbeatIntervalMs,
|
|
67868
|
-
enabled: rc?.enabled,
|
|
67869
67960
|
maxConcurrentRuns: rc?.maxConcurrentRuns
|
|
67870
67961
|
});
|
|
67871
67962
|
runtimeLog.log(`Registered new agent ${agent.id} for heartbeat triggers`);
|
|
@@ -67873,18 +67964,17 @@ var init_in_process_runtime = __esm({
|
|
|
67873
67964
|
this.agentStore.on("agent:created", this.agentCreatedListener);
|
|
67874
67965
|
this.agentUpdatedListener = (agent) => {
|
|
67875
67966
|
if (!this.triggerScheduler) return;
|
|
67876
|
-
|
|
67877
|
-
if (rc?.enabled === false) {
|
|
67967
|
+
if (!isTickable(agent)) {
|
|
67878
67968
|
this.triggerScheduler.unregisterAgent(agent.id);
|
|
67879
|
-
runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (
|
|
67880
|
-
|
|
67881
|
-
this.triggerScheduler.registerAgent(agent.id, {
|
|
67882
|
-
heartbeatIntervalMs: rc?.heartbeatIntervalMs,
|
|
67883
|
-
enabled: rc?.enabled,
|
|
67884
|
-
maxConcurrentRuns: rc?.maxConcurrentRuns
|
|
67885
|
-
});
|
|
67886
|
-
runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers`);
|
|
67969
|
+
runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (state=${agent.state})`);
|
|
67970
|
+
return;
|
|
67887
67971
|
}
|
|
67972
|
+
const rc = agent.runtimeConfig;
|
|
67973
|
+
this.triggerScheduler.registerAgent(agent.id, {
|
|
67974
|
+
heartbeatIntervalMs: rc?.heartbeatIntervalMs,
|
|
67975
|
+
maxConcurrentRuns: rc?.maxConcurrentRuns
|
|
67976
|
+
});
|
|
67977
|
+
runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers (state=${agent.state})`);
|
|
67888
67978
|
};
|
|
67889
67979
|
this.agentStore.on("agent:updated", this.agentUpdatedListener);
|
|
67890
67980
|
this.ephemeralTerminationListener = (agentId, from, to) => {
|
|
@@ -67919,15 +68009,13 @@ var init_in_process_runtime = __esm({
|
|
|
67919
68009
|
const agents = await this.agentStore.listAgents();
|
|
67920
68010
|
let registeredCount = 0;
|
|
67921
68011
|
for (const agent of agents) {
|
|
68012
|
+
if (!isTickable(agent)) continue;
|
|
67922
68013
|
const rc = agent.runtimeConfig;
|
|
67923
|
-
|
|
67924
|
-
|
|
67925
|
-
|
|
67926
|
-
|
|
67927
|
-
|
|
67928
|
-
});
|
|
67929
|
-
registeredCount++;
|
|
67930
|
-
}
|
|
68014
|
+
this.triggerScheduler.registerAgent(agent.id, {
|
|
68015
|
+
heartbeatIntervalMs: rc?.heartbeatIntervalMs,
|
|
68016
|
+
maxConcurrentRuns: rc?.maxConcurrentRuns
|
|
68017
|
+
});
|
|
68018
|
+
registeredCount++;
|
|
67931
68019
|
}
|
|
67932
68020
|
if (agents.length > 0) {
|
|
67933
68021
|
runtimeLog.log(`Registered ${registeredCount} of ${agents.length} agents for heartbeat triggers`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runfusion/fusion",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
|
|
6
6
|
"homepage": "https://github.com/Runfusion/Fusion#readme",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"vitest": "^3.1.0",
|
|
70
70
|
"yaml": "^2.8.3",
|
|
71
71
|
"@fusion/core": "0.1.0",
|
|
72
|
-
"@fusion/
|
|
73
|
-
"@fusion/
|
|
72
|
+
"@fusion/dashboard": "0.1.0",
|
|
73
|
+
"@fusion/engine": "0.1.0"
|
|
74
74
|
},
|
|
75
75
|
"repository": {
|
|
76
76
|
"type": "git",
|