@runfusion/fusion 0.21.0 → 0.22.0
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 +1991 -993
- package/dist/client/assets/AgentDetailView-BKKpbp1S.js +18 -0
- package/dist/client/assets/AgentDetailView-CeO_1MK7.css +1 -0
- package/dist/client/assets/AgentsView-BRXFmrcJ.js +527 -0
- package/dist/client/assets/AgentsView-Bs03ptrd.css +1 -0
- package/dist/client/assets/ChatView-D7L2e_qu.js +1 -0
- package/dist/client/assets/DevServerView-l8RCyL2k.js +1 -0
- package/dist/client/assets/DirectoryPicker-CS1dwqcC.js +1 -0
- package/dist/client/assets/DocumentsView-DmthQWDZ.js +1 -0
- package/dist/client/assets/{InsightsView-CqDethVs.js → InsightsView-DvXpMKmH.js} +2 -2
- package/dist/client/assets/{MemoryView-BLIm9Vr7.js → MemoryView-CPwlKnUI.js} +2 -2
- package/dist/client/assets/{NodesView-DEXvp3WT.js → NodesView-BLlfUfsy.js} +3 -3
- package/dist/client/assets/{PiExtensionsManager-C2YjI9o2.js → PiExtensionsManager-j8rPXqmB.js} +2 -2
- package/dist/client/assets/PluginManager-pW6RMz5z.js +1 -0
- package/dist/client/assets/ResearchView-D9DNJYDq.js +1 -0
- package/dist/client/assets/{RoadmapsView-DPcfX5MS.js → RoadmapsView-Djc_X35v.js} +2 -2
- package/dist/client/assets/SettingsModal-WGCF_pk8.js +31 -0
- package/dist/client/assets/{SettingsModal-BRNAPR1u.js → SettingsModal-fxvTFLtR.js} +1 -1
- package/dist/client/assets/SetupWizardModal-tG_MF_nA.js +1 -0
- package/dist/client/assets/SkillsView-Ddf0YL8z.js +1 -0
- package/dist/client/assets/agentSkills-DDHJnrkn.css +1 -0
- package/dist/client/assets/agentSkills-EwIwBlG8.js +1 -0
- package/dist/client/assets/folder-open-BiJpmnaT.js +6 -0
- package/dist/client/assets/index-D6ebxTPF.css +1 -0
- package/dist/client/assets/index-DYDLmOcK.js +694 -0
- package/dist/client/assets/{star-B314SwLA.js → star-BwRZmiuZ.js} +2 -2
- package/dist/client/assets/upload-D4NwZhPp.js +6 -0
- package/dist/client/assets/{users-Bu_ltePs.js → users-DNISDtI1.js} +2 -2
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/extension.js +1154 -401
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
- package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +480 -0
- package/dist/plugins/fusion-plugin-hermes-runtime/manifest.json +14 -0
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +369 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/manifest.json +14 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +966 -0
- package/dist/plugins/fusion-plugin-paperclip-runtime/manifest.json +15 -0
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +11 -0
- package/package.json +2 -1
- package/skill/fusion/references/engine-tools.md +1 -1
- package/dist/client/assets/AgentDetailView-CUtWvXBn.css +0 -1
- package/dist/client/assets/AgentDetailView-Dg7Qa1rG.js +0 -18
- package/dist/client/assets/ChatView-ODq-kBk6.js +0 -1
- package/dist/client/assets/DevServerView-6PS9Lvl7.js +0 -1
- package/dist/client/assets/DirectoryPicker-B3dza2Dq.js +0 -1
- package/dist/client/assets/DocumentsView-Bu9YYlki.js +0 -1
- package/dist/client/assets/PluginManager-Dnf-LhYw.js +0 -1
- package/dist/client/assets/ResearchView-Z0TZ7WGo.js +0 -1
- package/dist/client/assets/SettingsModal-B6RN9VYe.js +0 -31
- package/dist/client/assets/SetupWizardModal-BFc3xID2.js +0 -1
- package/dist/client/assets/SkillsView-CipGahOR.js +0 -1
- package/dist/client/assets/index-Df1bHDY4.css +0 -1
- package/dist/client/assets/index-NFptaeUQ.js +0 -1222
package/dist/extension.js
CHANGED
|
@@ -1183,6 +1183,7 @@ function agentToConfigSnapshot(agent) {
|
|
|
1183
1183
|
role: agent.role,
|
|
1184
1184
|
title: agent.title,
|
|
1185
1185
|
icon: agent.icon,
|
|
1186
|
+
imageUrl: agent.imageUrl,
|
|
1186
1187
|
reportsTo: agent.reportsTo,
|
|
1187
1188
|
runtimeConfig: agent.runtimeConfig ? { ...agent.runtimeConfig } : void 0,
|
|
1188
1189
|
permissions: agent.permissions ? { ...agent.permissions } : void 0,
|
|
@@ -1204,6 +1205,7 @@ function diffConfigSnapshots(before, after) {
|
|
|
1204
1205
|
"role",
|
|
1205
1206
|
"title",
|
|
1206
1207
|
"icon",
|
|
1208
|
+
"imageUrl",
|
|
1207
1209
|
"reportsTo",
|
|
1208
1210
|
"runtimeConfig",
|
|
1209
1211
|
"permissions",
|
|
@@ -1574,12 +1576,10 @@ Output Requirements:
|
|
|
1574
1576
|
};
|
|
1575
1577
|
AGENT_VALID_TRANSITIONS = {
|
|
1576
1578
|
idle: ["active"],
|
|
1577
|
-
active: ["running", "paused", "
|
|
1578
|
-
running: ["
|
|
1579
|
-
paused: ["
|
|
1580
|
-
error: ["
|
|
1581
|
-
terminated: ["idle", "active", "running"]
|
|
1582
|
-
// Can be restarted or reset
|
|
1579
|
+
active: ["idle", "running", "paused", "error"],
|
|
1580
|
+
running: ["idle", "active", "paused", "error"],
|
|
1581
|
+
paused: ["idle", "active"],
|
|
1582
|
+
error: ["idle", "active"]
|
|
1583
1583
|
};
|
|
1584
1584
|
AGENT_PERMISSIONS = [
|
|
1585
1585
|
"tasks:assign",
|
|
@@ -5555,6 +5555,7 @@ var init_agent_store = __esm({
|
|
|
5555
5555
|
metadata,
|
|
5556
5556
|
...input.title && { title: input.title },
|
|
5557
5557
|
...input.icon && { icon: input.icon },
|
|
5558
|
+
...input.imageUrl && { imageUrl: input.imageUrl },
|
|
5558
5559
|
...input.reportsTo && { reportsTo: input.reportsTo },
|
|
5559
5560
|
...runtimeConfig && { runtimeConfig },
|
|
5560
5561
|
...input.permissions && { permissions: input.permissions },
|
|
@@ -5938,6 +5939,7 @@ var init_agent_store = __esm({
|
|
|
5938
5939
|
updatedAt,
|
|
5939
5940
|
..."title" in updates && { title: updates.title },
|
|
5940
5941
|
..."icon" in updates && { icon: updates.icon },
|
|
5942
|
+
..."imageUrl" in updates && { imageUrl: updates.imageUrl },
|
|
5941
5943
|
..."reportsTo" in updates && { reportsTo: updates.reportsTo },
|
|
5942
5944
|
..."runtimeConfig" in updates && { runtimeConfig: updates.runtimeConfig },
|
|
5943
5945
|
..."pauseReason" in updates && { pauseReason: updates.pauseReason },
|
|
@@ -6063,7 +6065,9 @@ var init_agent_store = __esm({
|
|
|
6063
6065
|
state: newState,
|
|
6064
6066
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6065
6067
|
// Clear lastError when transitioning away from terminated
|
|
6066
|
-
|
|
6068
|
+
// Clear lastError when an agent re-enters an actionable state so
|
|
6069
|
+
// a resumed agent does not carry stale "Error" badges.
|
|
6070
|
+
...(newState === "active" || newState === "running") && { lastError: void 0 }
|
|
6067
6071
|
};
|
|
6068
6072
|
await this.writeAgent(updated);
|
|
6069
6073
|
this.emit("agent:stateChanged", agentId, currentState, newState);
|
|
@@ -6278,9 +6282,6 @@ var init_agent_store = __esm({
|
|
|
6278
6282
|
if (activeRun) {
|
|
6279
6283
|
await this.endHeartbeatRun(activeRun.id, "terminated");
|
|
6280
6284
|
}
|
|
6281
|
-
if (agent.state !== "idle" && agent.state !== "terminated") {
|
|
6282
|
-
agent = await this.updateAgentState(agentId, "terminated");
|
|
6283
|
-
}
|
|
6284
6285
|
if (agent.state !== "idle") {
|
|
6285
6286
|
agent = await this.updateAgentState(agentId, "idle");
|
|
6286
6287
|
}
|
|
@@ -6867,6 +6868,7 @@ var init_agent_store = __esm({
|
|
|
6867
6868
|
role: snapshot.role,
|
|
6868
6869
|
title: snapshot.title,
|
|
6869
6870
|
icon: snapshot.icon,
|
|
6871
|
+
imageUrl: snapshot.imageUrl,
|
|
6870
6872
|
reportsTo: snapshot.reportsTo,
|
|
6871
6873
|
runtimeConfig: snapshot.runtimeConfig ? { ...snapshot.runtimeConfig } : void 0,
|
|
6872
6874
|
permissions: snapshot.permissions ? { ...snapshot.permissions } : void 0,
|
|
@@ -7090,6 +7092,7 @@ var init_agent_store = __esm({
|
|
|
7090
7092
|
metadata: data.metadata ?? {},
|
|
7091
7093
|
title: data.title,
|
|
7092
7094
|
icon: data.icon,
|
|
7095
|
+
imageUrl: data.imageUrl,
|
|
7093
7096
|
reportsTo: data.reportsTo,
|
|
7094
7097
|
runtimeConfig: data.runtimeConfig,
|
|
7095
7098
|
pauseReason: data.pauseReason,
|
|
@@ -7118,6 +7121,7 @@ var init_agent_store = __esm({
|
|
|
7118
7121
|
metadata: agent.metadata,
|
|
7119
7122
|
title: agent.title,
|
|
7120
7123
|
icon: agent.icon,
|
|
7124
|
+
imageUrl: agent.imageUrl,
|
|
7121
7125
|
reportsTo: agent.reportsTo,
|
|
7122
7126
|
runtimeConfig: agent.runtimeConfig,
|
|
7123
7127
|
pauseReason: agent.pauseReason,
|
|
@@ -32546,20 +32550,17 @@ This project has OpenClaw-style memory files:
|
|
|
32546
32550
|
|
|
32547
32551
|
**At the end of execution (before calling \`fn_task_done()\`):**
|
|
32548
32552
|
1. Review what you learned during this task that would genuinely benefit future runs
|
|
32549
|
-
2.
|
|
32550
|
-
|
|
32551
|
-
|
|
32552
|
-
|
|
32553
|
-
-
|
|
32554
|
-
-
|
|
32555
|
-
|
|
32556
|
-
|
|
32557
|
-
6. **
|
|
32558
|
-
|
|
32559
|
-
|
|
32560
|
-
- One-off file paths, variable names, or minor code changes
|
|
32561
|
-
- Notes about what you did rather than what future agents should know
|
|
32562
|
-
7. **Consolidate when possible**: If an existing entry already covers a concept, update or refine it rather than adding a duplicate. Delete entries that are no longer accurate.
|
|
32553
|
+
2. Choose scope intentionally:
|
|
32554
|
+
- Use \`fn_memory_append(scope="agent")\` for your private operating context (personal checklists, delegation habits, temporary playbooks, self-improvement notes)
|
|
32555
|
+
- Use \`fn_memory_append(scope="project")\` for repository-wide durable knowledge any future agent should know
|
|
32556
|
+
3. Choose layer intentionally:
|
|
32557
|
+
- \`layer="long-term"\` for durable conventions/decisions/pitfalls
|
|
32558
|
+
- \`layer="daily"\` for running observations, unresolved context, and open loops
|
|
32559
|
+
4. If using project scope with file backend, write long-term memory to \`.fusion/memory/MEMORY.md\` and daily notes to today's \`.fusion/memory/YYYY-MM-DD.md\`
|
|
32560
|
+
5. **If nothing durable was learned, skip the memory update entirely** \u2014 do not append trivial or task-specific notes
|
|
32561
|
+
6. Only write to **project** memory when the insight is genuinely reusable across the workspace (architecture patterns, shared conventions, durable pitfalls, cross-task constraints)
|
|
32562
|
+
7. **Do not** write private/ephemeral items to project memory, such as personal TODOs, one-off scratch notes, or preferences that only help you as an individual agent
|
|
32563
|
+
8. **Consolidate when possible**: If an existing entry already covers a concept, update or refine it rather than adding a duplicate. Delete entries that are no longer accurate.
|
|
32563
32564
|
|
|
32564
32565
|
**Format for additions:** Add bullet points under the relevant section heading:
|
|
32565
32566
|
- Use \`- \` prefix for list items
|
|
@@ -32579,18 +32580,15 @@ This project has a memory system that stores durable project learnings accumulat
|
|
|
32579
32580
|
|
|
32580
32581
|
**At the end of execution (before calling \`fn_task_done()\`):**
|
|
32581
32582
|
1. Review what you learned during this task that would genuinely benefit future runs
|
|
32582
|
-
2.
|
|
32583
|
-
|
|
32584
|
-
-
|
|
32585
|
-
|
|
32586
|
-
-
|
|
32587
|
-
-
|
|
32588
|
-
4. **
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
- One-off file paths, variable names, or minor code changes
|
|
32592
|
-
- Notes about what you did rather than what future agents should know
|
|
32593
|
-
5. Consolidate when possible: refine an existing memory entry instead of adding duplicates.
|
|
32583
|
+
2. Choose scope intentionally:
|
|
32584
|
+
- Use \`fn_memory_append(scope="agent")\` for your private operating context
|
|
32585
|
+
- Use \`fn_memory_append(scope="project")\` only for repo-wide durable knowledge
|
|
32586
|
+
3. Choose layer intentionally:
|
|
32587
|
+
- \`layer="long-term"\` for durable conventions/decisions/pitfalls
|
|
32588
|
+
- \`layer="daily"\` for running observations and open loops
|
|
32589
|
+
4. **If nothing durable was learned, skip the memory update entirely** \u2014 do not append trivial or task-specific notes
|
|
32590
|
+
5. **Avoid task-specific trivia** in project scope (for example: personal reminders, one-off scratch thoughts, individual communication preferences)
|
|
32591
|
+
6. Consolidate when possible: refine an existing memory entry instead of adding duplicates.
|
|
32594
32592
|
`;
|
|
32595
32593
|
}
|
|
32596
32594
|
function buildReviewerMemoryInstructions(rootDir, settings) {
|
|
@@ -56340,8 +56338,12 @@ function createFusionAuthStorage() {
|
|
|
56340
56338
|
const primary = AuthStorage.create(getFusionAuthPath());
|
|
56341
56339
|
let supplementalCredentials = readSupplementalCredentials();
|
|
56342
56340
|
let modelsJsonApiKeys = readModelsJsonApiKeys();
|
|
56341
|
+
const loggedOutProviders = /* @__PURE__ */ new Set();
|
|
56343
56342
|
const syncSupplementalOauthCredentials = () => {
|
|
56344
56343
|
for (const [provider, credential] of Object.entries(supplementalCredentials)) {
|
|
56344
|
+
if (loggedOutProviders.has(provider)) {
|
|
56345
|
+
continue;
|
|
56346
|
+
}
|
|
56345
56347
|
const current = primary.get(provider);
|
|
56346
56348
|
if (!shouldHydrateStoredCredential(current, credential)) {
|
|
56347
56349
|
continue;
|
|
@@ -56362,6 +56364,24 @@ function createFusionAuthStorage() {
|
|
|
56362
56364
|
return true;
|
|
56363
56365
|
},
|
|
56364
56366
|
get(target, prop, receiver) {
|
|
56367
|
+
if (prop === "logout") {
|
|
56368
|
+
return (provider) => {
|
|
56369
|
+
target.logout(provider);
|
|
56370
|
+
loggedOutProviders.add(provider);
|
|
56371
|
+
};
|
|
56372
|
+
}
|
|
56373
|
+
if (prop === "remove") {
|
|
56374
|
+
return (provider) => {
|
|
56375
|
+
target.remove(provider);
|
|
56376
|
+
loggedOutProviders.add(provider);
|
|
56377
|
+
};
|
|
56378
|
+
}
|
|
56379
|
+
if (prop === "set") {
|
|
56380
|
+
return (provider, credential) => {
|
|
56381
|
+
target.set(provider, credential);
|
|
56382
|
+
loggedOutProviders.delete(provider);
|
|
56383
|
+
};
|
|
56384
|
+
}
|
|
56365
56385
|
if (prop === "reload") {
|
|
56366
56386
|
return () => {
|
|
56367
56387
|
target.reload();
|
|
@@ -56371,25 +56391,43 @@ function createFusionAuthStorage() {
|
|
|
56371
56391
|
};
|
|
56372
56392
|
}
|
|
56373
56393
|
if (prop === "get") {
|
|
56374
|
-
return (provider) =>
|
|
56375
|
-
|
|
56376
|
-
|
|
56377
|
-
|
|
56394
|
+
return (provider) => {
|
|
56395
|
+
if (loggedOutProviders.has(provider)) {
|
|
56396
|
+
return void 0;
|
|
56397
|
+
}
|
|
56398
|
+
return choosePreferredStoredCredential(
|
|
56399
|
+
target.get(provider),
|
|
56400
|
+
supplementalCredentials[provider]
|
|
56401
|
+
);
|
|
56402
|
+
};
|
|
56378
56403
|
}
|
|
56379
56404
|
if (prop === "has") {
|
|
56380
|
-
return (provider) =>
|
|
56405
|
+
return (provider) => {
|
|
56406
|
+
if (loggedOutProviders.has(provider)) {
|
|
56407
|
+
return false;
|
|
56408
|
+
}
|
|
56409
|
+
return target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
|
|
56410
|
+
};
|
|
56381
56411
|
}
|
|
56382
56412
|
if (prop === "hasAuth") {
|
|
56383
|
-
return (provider) =>
|
|
56413
|
+
return (provider) => {
|
|
56414
|
+
if (loggedOutProviders.has(provider)) {
|
|
56415
|
+
return false;
|
|
56416
|
+
}
|
|
56417
|
+
return target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
|
|
56418
|
+
};
|
|
56384
56419
|
}
|
|
56385
56420
|
if (prop === "getAll") {
|
|
56386
56421
|
return () => {
|
|
56387
56422
|
const providerIds = /* @__PURE__ */ new Set([
|
|
56388
|
-
...Object.keys(
|
|
56389
|
-
...Object.keys(
|
|
56423
|
+
...Object.keys(target.getAll()),
|
|
56424
|
+
...loggedOutProviders.size > 0 ? Object.keys(supplementalCredentials).filter((p) => !loggedOutProviders.has(p)) : Object.keys(supplementalCredentials)
|
|
56390
56425
|
]);
|
|
56391
56426
|
const merged = {};
|
|
56392
56427
|
for (const providerId of providerIds) {
|
|
56428
|
+
if (loggedOutProviders.has(providerId)) {
|
|
56429
|
+
continue;
|
|
56430
|
+
}
|
|
56393
56431
|
const credential = choosePreferredStoredCredential(
|
|
56394
56432
|
target.get(providerId),
|
|
56395
56433
|
supplementalCredentials[providerId]
|
|
@@ -56402,10 +56440,26 @@ function createFusionAuthStorage() {
|
|
|
56402
56440
|
};
|
|
56403
56441
|
}
|
|
56404
56442
|
if (prop === "list") {
|
|
56405
|
-
return () =>
|
|
56443
|
+
return () => {
|
|
56444
|
+
const providers = /* @__PURE__ */ new Set([...target.list()]);
|
|
56445
|
+
for (const p of modelsJsonApiKeys.keys()) {
|
|
56446
|
+
if (!loggedOutProviders.has(p)) {
|
|
56447
|
+
providers.add(p);
|
|
56448
|
+
}
|
|
56449
|
+
}
|
|
56450
|
+
for (const p of Object.keys(supplementalCredentials)) {
|
|
56451
|
+
if (!loggedOutProviders.has(p)) {
|
|
56452
|
+
providers.add(p);
|
|
56453
|
+
}
|
|
56454
|
+
}
|
|
56455
|
+
return Array.from(providers).filter((p) => !loggedOutProviders.has(p));
|
|
56456
|
+
};
|
|
56406
56457
|
}
|
|
56407
56458
|
if (prop === "getApiKey") {
|
|
56408
56459
|
return async (provider) => {
|
|
56460
|
+
if (loggedOutProviders.has(provider)) {
|
|
56461
|
+
return void 0;
|
|
56462
|
+
}
|
|
56409
56463
|
const primaryKey = await target.getApiKey(provider);
|
|
56410
56464
|
if (primaryKey) return primaryKey;
|
|
56411
56465
|
const supplementalKey = resolveStoredCredentialApiKey(provider, supplementalCredentials[provider]);
|
|
@@ -56659,7 +56713,8 @@ async function flushMemoryBeforeSessionCompaction(session) {
|
|
|
56659
56713
|
}
|
|
56660
56714
|
const flushPrompt = [
|
|
56661
56715
|
"Before context compaction, preserve only unresolved durable memory if needed.",
|
|
56662
|
-
"If fn_memory_append is available and you learned reusable project decisions
|
|
56716
|
+
"If fn_memory_append is available and you learned reusable project decisions/conventions/pitfalls/open loops or private operating context that is not already saved, append it now.",
|
|
56717
|
+
'Use scope="project" for shared workspace knowledge and scope="agent" for private operating context.',
|
|
56663
56718
|
'Use layer="long-term" for durable facts and layer="daily" for running notes/open loops.',
|
|
56664
56719
|
"If there is nothing durable to save, reply exactly: NONE."
|
|
56665
56720
|
].join("\n");
|
|
@@ -57153,7 +57208,10 @@ async function createFnAgent2(options) {
|
|
|
57153
57208
|
if (selectionResult.diagnostics.length > 0) {
|
|
57154
57209
|
const purpose = effectiveSkillSelection.sessionPurpose ?? "skills";
|
|
57155
57210
|
for (const diag of selectionResult.diagnostics) {
|
|
57156
|
-
|
|
57211
|
+
const msg = `[skills] [${purpose}] ${diag.type}: ${diag.message}`;
|
|
57212
|
+
if (diag.type === "error") piLog.error(msg);
|
|
57213
|
+
else if (diag.type === "warning") piLog.warn(msg);
|
|
57214
|
+
else piLog.log(msg);
|
|
57157
57215
|
}
|
|
57158
57216
|
}
|
|
57159
57217
|
skillsOverrideFn = createSkillsOverrideFromSelection(selectionResult, {
|
|
@@ -58619,6 +58677,33 @@ function buildSystemPromptWithInstructions(basePrompt, instructions) {
|
|
|
58619
58677
|
|
|
58620
58678
|
${instructions}`;
|
|
58621
58679
|
}
|
|
58680
|
+
function buildPluginPromptSection(surface, pluginRunner) {
|
|
58681
|
+
if (!pluginRunner) {
|
|
58682
|
+
return "";
|
|
58683
|
+
}
|
|
58684
|
+
const contributions = pluginRunner.getPromptContributionsForSurface(surface);
|
|
58685
|
+
if (contributions.length === 0) {
|
|
58686
|
+
return "";
|
|
58687
|
+
}
|
|
58688
|
+
const prependByPlugin = /* @__PURE__ */ new Map();
|
|
58689
|
+
const appendByPlugin = /* @__PURE__ */ new Map();
|
|
58690
|
+
for (const { pluginId, contribution } of contributions) {
|
|
58691
|
+
const target = contribution.position === "prepend" ? prependByPlugin : appendByPlugin;
|
|
58692
|
+
const existing = target.get(pluginId) ?? [];
|
|
58693
|
+
existing.push(contribution.content);
|
|
58694
|
+
target.set(pluginId, existing);
|
|
58695
|
+
}
|
|
58696
|
+
const toSections = (group) => {
|
|
58697
|
+
return Array.from(group.entries()).map(([pluginId, contents]) => {
|
|
58698
|
+
return `## Plugin: ${pluginId}
|
|
58699
|
+
|
|
58700
|
+
${contents.join("\n\n")}`;
|
|
58701
|
+
});
|
|
58702
|
+
};
|
|
58703
|
+
const sections = [...toSections(prependByPlugin), ...toSections(appendByPlugin)];
|
|
58704
|
+
log11.log(`Applied ${contributions.length} prompt contributions for surface '${surface}'`);
|
|
58705
|
+
return sections.join("\n\n");
|
|
58706
|
+
}
|
|
58622
58707
|
var log11, MAX_INSTRUCTIONS_PATH_LENGTH, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_SOUL_LENGTH, MAX_MEMORY_LENGTH;
|
|
58623
58708
|
var init_agent_instructions = __esm({
|
|
58624
58709
|
"../engine/src/agent-instructions.ts"() {
|
|
@@ -59173,7 +59258,7 @@ function createMemoryAppendTool(rootDir, settings, options) {
|
|
|
59173
59258
|
return {
|
|
59174
59259
|
name: "fn_memory_append",
|
|
59175
59260
|
label: "Append Memory",
|
|
59176
|
-
description:
|
|
59261
|
+
description: 'Append concise Markdown to memory. Use scope="agent" for private operating context and scope="project" for workspace-wide durable knowledge. Use layer="long-term" for durable conventions/decisions/pitfalls and layer="daily" for running observations/open loops.',
|
|
59177
59262
|
parameters: memoryAppendParams,
|
|
59178
59263
|
execute: async (_id, params) => {
|
|
59179
59264
|
const content = params.content.trim();
|
|
@@ -61332,6 +61417,17 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
|
|
|
61332
61417
|
reviewerBasePrompt + memorySection,
|
|
61333
61418
|
reviewerInstructions
|
|
61334
61419
|
);
|
|
61420
|
+
const reviewerContributions = options.pluginRunner?.getPromptContributionsForSurface("reviewer") ?? [];
|
|
61421
|
+
if (reviewerContributions.length > 0) {
|
|
61422
|
+
reviewerLog.log(`applied ${reviewerContributions.length} plugin prompt contributions for reviewer surface`);
|
|
61423
|
+
}
|
|
61424
|
+
const reviewerPluginContributions = buildPluginPromptSection(
|
|
61425
|
+
"reviewer",
|
|
61426
|
+
options.pluginRunner
|
|
61427
|
+
);
|
|
61428
|
+
const reviewerSystemPromptFinal = reviewerPluginContributions ? `${reviewerSystemPrompt}
|
|
61429
|
+
|
|
61430
|
+
${reviewerPluginContributions}` : reviewerSystemPrompt;
|
|
61335
61431
|
let skillContext = void 0;
|
|
61336
61432
|
if (options.agentStore && options.rootDir) {
|
|
61337
61433
|
try {
|
|
@@ -61377,7 +61473,7 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
|
|
|
61377
61473
|
runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig),
|
|
61378
61474
|
pluginRunner: options.pluginRunner,
|
|
61379
61475
|
cwd,
|
|
61380
|
-
systemPrompt:
|
|
61476
|
+
systemPrompt: reviewerSystemPromptFinal,
|
|
61381
61477
|
tools: "readonly",
|
|
61382
61478
|
customTools: memoryTools,
|
|
61383
61479
|
onText: agentLogger ? agentLogger.onText : (delta) => options.onText?.(delta),
|
|
@@ -62637,23 +62733,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
|
|
|
62637
62733
|
this.options = options;
|
|
62638
62734
|
store.on("settings:updated", ({ settings, previous }) => {
|
|
62639
62735
|
if (settings.globalPause && !previous.globalPause) {
|
|
62640
|
-
|
|
62641
|
-
this.disposeSubagentsForTask(taskId, "global pause");
|
|
62642
|
-
}
|
|
62643
|
-
for (const [taskId, session] of this.activeSessions) {
|
|
62644
|
-
planLog.log(
|
|
62645
|
-
`Global pause \u2014 terminating triage session for ${taskId}`
|
|
62646
|
-
);
|
|
62647
|
-
this.pauseAborted.add(taskId);
|
|
62648
|
-
this.options.stuckTaskDetector?.untrackTask(taskId);
|
|
62649
|
-
const sessionWithAbort = session;
|
|
62650
|
-
if (typeof sessionWithAbort.abort === "function") {
|
|
62651
|
-
void sessionWithAbort.abort().catch((err) => {
|
|
62652
|
-
planLog.warn(`Failed to abort triage session for ${taskId}: ${err}`);
|
|
62653
|
-
});
|
|
62654
|
-
}
|
|
62655
|
-
session.dispose();
|
|
62656
|
-
}
|
|
62736
|
+
this.abortAndDisposeActiveSessions("global pause");
|
|
62657
62737
|
}
|
|
62658
62738
|
});
|
|
62659
62739
|
store.on("settings:updated", ({ settings, previous }) => {
|
|
@@ -62722,8 +62802,35 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
|
|
|
62722
62802
|
this.pollInterval = null;
|
|
62723
62803
|
this.activePollMs = null;
|
|
62724
62804
|
}
|
|
62805
|
+
this.abortAndDisposeActiveSessions("engine stop");
|
|
62725
62806
|
planLog.log("Processor stopped");
|
|
62726
62807
|
}
|
|
62808
|
+
/**
|
|
62809
|
+
* Abort and dispose every active specify session and reviewer subagent.
|
|
62810
|
+
* Used by the global-pause handler and by `stop()`.
|
|
62811
|
+
*
|
|
62812
|
+
* Reviewer subagents are torn down first so they don't keep streaming
|
|
62813
|
+
* verdicts while the main triage session is being disposed. abort()
|
|
62814
|
+
* interrupts any in-flight LLM stream / tool call; dispose() then
|
|
62815
|
+
* releases session resources.
|
|
62816
|
+
*/
|
|
62817
|
+
abortAndDisposeActiveSessions(reason) {
|
|
62818
|
+
for (const taskId of [...this.activeSubagentSessions.keys()]) {
|
|
62819
|
+
this.disposeSubagentsForTask(taskId, reason);
|
|
62820
|
+
}
|
|
62821
|
+
for (const [taskId, session] of this.activeSessions) {
|
|
62822
|
+
planLog.log(`${reason} \u2014 terminating triage session for ${taskId}`);
|
|
62823
|
+
this.pauseAborted.add(taskId);
|
|
62824
|
+
this.options.stuckTaskDetector?.untrackTask(taskId);
|
|
62825
|
+
const sessionWithAbort = session;
|
|
62826
|
+
if (typeof sessionWithAbort.abort === "function") {
|
|
62827
|
+
void sessionWithAbort.abort().catch((err) => {
|
|
62828
|
+
planLog.warn(`Failed to abort triage session for ${taskId}: ${err}`);
|
|
62829
|
+
});
|
|
62830
|
+
}
|
|
62831
|
+
session.dispose();
|
|
62832
|
+
}
|
|
62833
|
+
}
|
|
62727
62834
|
/**
|
|
62728
62835
|
* Mark a task as stuck-aborted so the catch block knows not to treat
|
|
62729
62836
|
* the disposed session as a genuine failure.
|
|
@@ -63017,6 +63124,17 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
|
|
|
63017
63124
|
resolveAgentPrompt("triage", settings.agentPrompts) || (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : TRIAGE_SYSTEM_PROMPT),
|
|
63018
63125
|
triageInstructions
|
|
63019
63126
|
);
|
|
63127
|
+
const triageContributions = this.options.pluginRunner?.getPromptContributionsForSurface("triage") ?? [];
|
|
63128
|
+
if (triageContributions.length > 0) {
|
|
63129
|
+
planLog.log(`${task.id}: applied ${triageContributions.length} plugin prompt contributions for triage surface`);
|
|
63130
|
+
}
|
|
63131
|
+
const triagePluginContributions = buildPluginPromptSection(
|
|
63132
|
+
"triage",
|
|
63133
|
+
this.options.pluginRunner
|
|
63134
|
+
);
|
|
63135
|
+
const triageSystemPromptFinal = triagePluginContributions ? `${triageSystemPrompt}
|
|
63136
|
+
|
|
63137
|
+
${triagePluginContributions}` : triageSystemPrompt;
|
|
63020
63138
|
const skillContext = await buildSessionSkillContext({
|
|
63021
63139
|
agentStore: this.options.agentStore,
|
|
63022
63140
|
task,
|
|
@@ -63029,7 +63147,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
|
|
|
63029
63147
|
runtimeHint: triageRuntimeHint,
|
|
63030
63148
|
pluginRunner: this.options.pluginRunner,
|
|
63031
63149
|
cwd: this.rootDir,
|
|
63032
|
-
systemPrompt:
|
|
63150
|
+
systemPrompt: triageSystemPromptFinal,
|
|
63033
63151
|
tools: "coding",
|
|
63034
63152
|
customTools,
|
|
63035
63153
|
onText: agentLogger.onText,
|
|
@@ -63193,7 +63311,7 @@ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\
|
|
|
63193
63311
|
runtimeHint: triageRuntimeHint,
|
|
63194
63312
|
pluginRunner: this.options.pluginRunner,
|
|
63195
63313
|
cwd: this.rootDir,
|
|
63196
|
-
systemPrompt:
|
|
63314
|
+
systemPrompt: triageSystemPromptFinal,
|
|
63197
63315
|
tools: "coding",
|
|
63198
63316
|
customTools,
|
|
63199
63317
|
onText: agentLogger.onText,
|
|
@@ -64767,6 +64885,7 @@ Do not refactor, rename broadly, or make opportunistic improvements.
|
|
|
64767
64885
|
taskTitle: taskForSkillContext?.title
|
|
64768
64886
|
})
|
|
64769
64887
|
});
|
|
64888
|
+
options.onSession?.(session);
|
|
64770
64889
|
const runId = mergeRunContext?.runId;
|
|
64771
64890
|
const agentId = mergeRunContext?.agentId ?? "merger";
|
|
64772
64891
|
await store.logEntry(
|
|
@@ -64867,50 +64986,322 @@ function resetMergeWithWarn(rootDir, taskId, label) {
|
|
|
64867
64986
|
mergerLog.warn(`${taskId}: git reset --merge cleanup failed during ${label}: ${msg}`);
|
|
64868
64987
|
}
|
|
64869
64988
|
}
|
|
64870
|
-
async function
|
|
64989
|
+
async function listOrphanedAutostashes(rootDir) {
|
|
64871
64990
|
try {
|
|
64872
|
-
const dirty = await snapshotDirtyFiles(rootDir);
|
|
64873
|
-
if (dirty.size === 0) return null;
|
|
64874
|
-
const label = `fusion-merger-autostash:${taskId}:${Date.now()}`;
|
|
64875
|
-
await execAsync2(
|
|
64876
|
-
`git stash push -u -m "${label}"`,
|
|
64877
|
-
{ cwd: rootDir }
|
|
64878
|
-
);
|
|
64879
64991
|
const { stdout } = await execAsync2(
|
|
64880
|
-
`git stash list --format="%gd %s"`,
|
|
64992
|
+
`git stash list --format="%H %gd %s"`,
|
|
64881
64993
|
{ cwd: rootDir, encoding: "utf-8" }
|
|
64882
64994
|
);
|
|
64883
|
-
const lines = String(stdout).split("\n");
|
|
64884
|
-
const
|
|
64885
|
-
|
|
64995
|
+
const lines = String(stdout).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
64996
|
+
const orphans = [];
|
|
64997
|
+
for (const line of lines) {
|
|
64998
|
+
const idx = line.indexOf(AUTOSTASH_LABEL_PREFIX);
|
|
64999
|
+
if (idx === -1) continue;
|
|
65000
|
+
const parts = line.split(/\s+/);
|
|
65001
|
+
const sha = parts[0] ?? "";
|
|
65002
|
+
const ref = parts[1] ?? "";
|
|
65003
|
+
const label = line.slice(idx);
|
|
65004
|
+
if (sha && ref) orphans.push({ sha, ref, label });
|
|
65005
|
+
}
|
|
65006
|
+
return orphans;
|
|
65007
|
+
} catch {
|
|
65008
|
+
return [];
|
|
65009
|
+
}
|
|
65010
|
+
}
|
|
65011
|
+
async function stashUnrelatedRootDirChanges(rootDir, taskId) {
|
|
65012
|
+
try {
|
|
65013
|
+
const orphans = await listOrphanedAutostashes(rootDir);
|
|
65014
|
+
if (orphans.length > 0) {
|
|
65015
|
+
const refs = orphans.map((o) => `${o.ref}@${o.sha.slice(0, 7)}`).join(", ");
|
|
64886
65016
|
mergerLog.warn(
|
|
64887
|
-
`${taskId}:
|
|
65017
|
+
`${taskId}: ${orphans.length} orphaned fusion-merger-autostash entry(ies) in stash list (${refs}) \u2014 these are uncommitted dev changes from prior merges whose restore failed. Recover with: cd ${rootDir} && git stash list && git stash apply <sha>`
|
|
64888
65018
|
);
|
|
65019
|
+
}
|
|
65020
|
+
} catch {
|
|
65021
|
+
}
|
|
65022
|
+
try {
|
|
65023
|
+
const dirty = await snapshotDirtyFiles(rootDir);
|
|
65024
|
+
if (dirty.size === 0) return null;
|
|
65025
|
+
const label = `${AUTOSTASH_LABEL_PREFIX}${taskId}:${Date.now()}`;
|
|
65026
|
+
await execAsync2("git add -A", { cwd: rootDir });
|
|
65027
|
+
const { stdout: createOut } = await execAsync2("git stash create", {
|
|
65028
|
+
cwd: rootDir,
|
|
65029
|
+
encoding: "utf-8"
|
|
65030
|
+
});
|
|
65031
|
+
const sha = String(createOut).trim();
|
|
65032
|
+
if (!sha) {
|
|
65033
|
+
await execAsync2("git reset", { cwd: rootDir }).catch(() => void 0);
|
|
64889
65034
|
return null;
|
|
64890
65035
|
}
|
|
64891
|
-
|
|
65036
|
+
await execAsync2(
|
|
65037
|
+
`git stash store -m ${quoteArg(label)} ${sha}`,
|
|
65038
|
+
{ cwd: rootDir }
|
|
65039
|
+
);
|
|
65040
|
+
await execAsync2("git reset --hard HEAD", { cwd: rootDir });
|
|
65041
|
+
await execAsync2("git clean -fd", { cwd: rootDir });
|
|
64892
65042
|
mergerLog.log(
|
|
64893
|
-
`${taskId}: stashed ${dirty.size} unrelated dirty path(s) in rootDir as ${
|
|
65043
|
+
`${taskId}: stashed ${dirty.size} unrelated dirty path(s) in rootDir as ${sha.slice(0, 7)} (${label})`
|
|
64894
65044
|
);
|
|
64895
|
-
return
|
|
65045
|
+
return { sha, label };
|
|
64896
65046
|
} catch (err) {
|
|
64897
65047
|
const msg = err instanceof Error ? err.message : String(err);
|
|
64898
65048
|
mergerLog.warn(
|
|
64899
65049
|
`${taskId}: pre-merge autostash failed (${msg}) \u2014 proceeding without stash; concurrent dev edits in rootDir may be wiped`
|
|
64900
65050
|
);
|
|
65051
|
+
try {
|
|
65052
|
+
await execAsync2("git reset", { cwd: rootDir });
|
|
65053
|
+
} catch {
|
|
65054
|
+
}
|
|
65055
|
+
return null;
|
|
65056
|
+
}
|
|
65057
|
+
}
|
|
65058
|
+
async function findStashRefBySha(rootDir, sha) {
|
|
65059
|
+
try {
|
|
65060
|
+
const { stdout } = await execAsync2(
|
|
65061
|
+
`git stash list --format="%H %gd"`,
|
|
65062
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
65063
|
+
);
|
|
65064
|
+
for (const line of String(stdout).split("\n")) {
|
|
65065
|
+
const trimmed = line.trim();
|
|
65066
|
+
if (!trimmed) continue;
|
|
65067
|
+
const [entrySha, ref] = trimmed.split(/\s+/);
|
|
65068
|
+
if (entrySha === sha && ref) return ref;
|
|
65069
|
+
}
|
|
65070
|
+
return null;
|
|
65071
|
+
} catch {
|
|
64901
65072
|
return null;
|
|
64902
65073
|
}
|
|
64903
65074
|
}
|
|
64904
|
-
async function
|
|
65075
|
+
async function dropAutostashBySha(rootDir, taskId, sha) {
|
|
65076
|
+
const ref = await findStashRefBySha(rootDir, sha);
|
|
65077
|
+
if (!ref) {
|
|
65078
|
+
mergerLog.log(`${taskId}: autostash ${sha.slice(0, 7)} no longer in stash list (already dropped)`);
|
|
65079
|
+
return;
|
|
65080
|
+
}
|
|
64905
65081
|
try {
|
|
64906
|
-
await execAsync2(`git stash
|
|
64907
|
-
mergerLog.log(`${taskId}: restored autostash ${stashRef}`);
|
|
65082
|
+
await execAsync2(`git stash drop ${ref}`, { cwd: rootDir });
|
|
64908
65083
|
} catch (err) {
|
|
64909
65084
|
const msg = err instanceof Error ? err.message : String(err);
|
|
65085
|
+
mergerLog.warn(`${taskId}: failed to drop autostash ${ref} (${msg}) \u2014 harmless, will linger in stash list`);
|
|
65086
|
+
}
|
|
65087
|
+
}
|
|
65088
|
+
async function runAiAgentForAutostashConflict(params) {
|
|
65089
|
+
const { store, rootDir, taskId, conflictedFiles, options, settings } = params;
|
|
65090
|
+
const agentLogger = new AgentLogger({
|
|
65091
|
+
store,
|
|
65092
|
+
taskId,
|
|
65093
|
+
agent: "merger",
|
|
65094
|
+
persistAgentToolOutput: settings.persistAgentToolOutput,
|
|
65095
|
+
onAgentText: options.onAgentText ? (_id, delta) => options.onAgentText(delta) : void 0,
|
|
65096
|
+
onAgentTool: options.onAgentTool ? (_id, name) => options.onAgentTool(name) : void 0
|
|
65097
|
+
});
|
|
65098
|
+
let taskForSkillContext = null;
|
|
65099
|
+
let skillContext = void 0;
|
|
65100
|
+
if (options.agentStore) {
|
|
65101
|
+
try {
|
|
65102
|
+
taskForSkillContext = await store.getTask(taskId);
|
|
65103
|
+
skillContext = await buildSessionSkillContext({
|
|
65104
|
+
agentStore: options.agentStore,
|
|
65105
|
+
task: taskForSkillContext,
|
|
65106
|
+
sessionPurpose: "merger",
|
|
65107
|
+
projectRootDir: rootDir,
|
|
65108
|
+
pluginRunner: options.pluginRunner
|
|
65109
|
+
});
|
|
65110
|
+
} catch {
|
|
65111
|
+
}
|
|
65112
|
+
}
|
|
65113
|
+
const assignedAgentId = taskForSkillContext?.assignedAgentId?.trim();
|
|
65114
|
+
const agentStoreWithGetAgent = options.agentStore && typeof options.agentStore.getAgent === "function" ? options.agentStore : null;
|
|
65115
|
+
const assignedAgent = assignedAgentId && agentStoreWithGetAgent ? await agentStoreWithGetAgent.getAgent(assignedAgentId).catch(() => null) : null;
|
|
65116
|
+
const mergerRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
|
65117
|
+
const systemPrompt = `You are an autostash-conflict resolution agent running after a Fusion merge has already committed on the main branch.
|
|
65118
|
+
|
|
65119
|
+
Before the merge ran, the developer had uncommitted local changes in their working tree. The merger snapshotted those changes into a git stash, ran the merge cleanly, and is now reapplying the stash on top of the merged HEAD. The reapply hit conflicts because the merge committed changes that overlap the developer's stashed edits.
|
|
65120
|
+
|
|
65121
|
+
## Your job
|
|
65122
|
+
Edit the conflicted files in place to remove every conflict marker (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`) and produce a coherent merged result that:
|
|
65123
|
+
- Preserves the developer's intended uncommitted changes (the "Updated upstream" / branch-side, depending on which side the stash pop wrote)
|
|
65124
|
+
- Layers them onto the merged HEAD content (the other side)
|
|
65125
|
+
|
|
65126
|
+
## Rules
|
|
65127
|
+
1. Read each conflicted file carefully before editing
|
|
65128
|
+
2. Resolve every conflict marker \u2014 none may remain after you finish
|
|
65129
|
+
3. Do NOT make any git commits. Do NOT run \`git add\` or \`git stash drop\`. Just edit the files.
|
|
65130
|
+
4. Do NOT touch files that are not in the conflicted-files list
|
|
65131
|
+
5. If you genuinely cannot determine the right resolution for a hunk, prefer the developer's stashed edits (their work is the unsaved context) and add a brief \`// TODO(autostash-conflict)\` comment so they can review
|
|
65132
|
+
|
|
65133
|
+
The orchestrator will verify post-run that no conflict markers remain. If any do, this attempt is treated as a failure and the stash is left intact for manual recovery.`;
|
|
65134
|
+
const fileList = conflictedFiles.map((f) => `- ${f}`).join("\n");
|
|
65135
|
+
const prompt = `Resolve autostash apply conflicts for task ${taskId}.
|
|
65136
|
+
|
|
65137
|
+
## Conflicted files
|
|
65138
|
+
${fileList}
|
|
65139
|
+
|
|
65140
|
+
## Steps
|
|
65141
|
+
1. For each file above, read its current contents (it has conflict markers from the failed \`git stash apply\`)
|
|
65142
|
+
2. Edit it to a clean state with no conflict markers \u2014 preserving the developer's intended changes layered on top of the merged HEAD
|
|
65143
|
+
3. After all files are clean, you are done. Do NOT commit or run git stash commands.`;
|
|
65144
|
+
mergerLog.log(`${taskId}: starting autostash-conflict resolution agent (${conflictedFiles.length} file(s))`);
|
|
65145
|
+
const { session } = await createResolvedAgentSession({
|
|
65146
|
+
sessionPurpose: "merger",
|
|
65147
|
+
runtimeHint: mergerRuntimeHint,
|
|
65148
|
+
pluginRunner: options.pluginRunner,
|
|
65149
|
+
cwd: rootDir,
|
|
65150
|
+
systemPrompt,
|
|
65151
|
+
tools: "coding",
|
|
65152
|
+
onText: agentLogger.onText,
|
|
65153
|
+
onThinking: agentLogger.onThinking,
|
|
65154
|
+
onToolStart: agentLogger.onToolStart,
|
|
65155
|
+
onToolEnd: agentLogger.onToolEnd,
|
|
65156
|
+
defaultProvider: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider,
|
|
65157
|
+
defaultModelId: settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId,
|
|
65158
|
+
fallbackProvider: settings.fallbackProvider,
|
|
65159
|
+
fallbackModelId: settings.fallbackModelId,
|
|
65160
|
+
defaultThinkingLevel: settings.defaultThinkingLevel,
|
|
65161
|
+
...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
|
|
65162
|
+
taskId,
|
|
65163
|
+
taskTitle: taskForSkillContext?.title,
|
|
65164
|
+
onFallbackModelUsed: createFallbackModelObserver({
|
|
65165
|
+
agent: "merger",
|
|
65166
|
+
label: "autostash conflict agent",
|
|
65167
|
+
store,
|
|
65168
|
+
taskId,
|
|
65169
|
+
taskTitle: taskForSkillContext?.title
|
|
65170
|
+
})
|
|
65171
|
+
});
|
|
65172
|
+
options.onSession?.(session);
|
|
65173
|
+
try {
|
|
65174
|
+
await store.appendAgentLog(
|
|
65175
|
+
taskId,
|
|
65176
|
+
`Autostash conflict agent started (model: ${describeModel(session)}, files: ${conflictedFiles.length})`,
|
|
65177
|
+
"text",
|
|
65178
|
+
void 0,
|
|
65179
|
+
"merger"
|
|
65180
|
+
);
|
|
65181
|
+
await withRateLimitRetry(async () => {
|
|
65182
|
+
throwIfAborted(options.signal, taskId);
|
|
65183
|
+
await promptWithFallback(session, prompt);
|
|
65184
|
+
checkSessionError(session);
|
|
65185
|
+
}, {
|
|
65186
|
+
onRetry: (attempt, delayMs, error) => {
|
|
65187
|
+
const delaySec = Math.round(delayMs / 1e3);
|
|
65188
|
+
mergerLog.warn(`\u23F3 ${taskId} autostash-conflict agent rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
|
|
65189
|
+
},
|
|
65190
|
+
signal: options.signal
|
|
65191
|
+
});
|
|
65192
|
+
return { success: true };
|
|
65193
|
+
} catch (err) {
|
|
65194
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65195
|
+
mergerLog.warn(`${taskId}: autostash-conflict agent error: ${msg}`);
|
|
65196
|
+
await store.logEntry(taskId, "Autostash conflict agent encountered an error", msg);
|
|
65197
|
+
return { success: false, error: msg };
|
|
65198
|
+
} finally {
|
|
65199
|
+
try {
|
|
65200
|
+
session.dispose();
|
|
65201
|
+
} catch {
|
|
65202
|
+
}
|
|
65203
|
+
}
|
|
65204
|
+
}
|
|
65205
|
+
async function findFilesWithConflictMarkers(rootDir, files) {
|
|
65206
|
+
const stillConflicted = [];
|
|
65207
|
+
for (const file of files) {
|
|
65208
|
+
try {
|
|
65209
|
+
const fullPath = join30(rootDir, file);
|
|
65210
|
+
if (!existsSync24(fullPath)) continue;
|
|
65211
|
+
const { stdout } = await execAsync2(
|
|
65212
|
+
`git grep -l -e "^<<<<<<< " -e "^=======$" -e "^>>>>>>> " --no-index -- ${quoteArg(fullPath)}`,
|
|
65213
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
65214
|
+
).catch(() => ({ stdout: "" }));
|
|
65215
|
+
if (String(stdout).trim()) stillConflicted.push(file);
|
|
65216
|
+
} catch {
|
|
65217
|
+
}
|
|
65218
|
+
}
|
|
65219
|
+
return stillConflicted;
|
|
65220
|
+
}
|
|
65221
|
+
async function restoreUnrelatedRootDirChanges(rootDir, taskId, handle, ctx) {
|
|
65222
|
+
const { sha } = handle;
|
|
65223
|
+
let applyConflicted = false;
|
|
65224
|
+
try {
|
|
65225
|
+
await execAsync2(`git stash apply ${sha}`, { cwd: rootDir });
|
|
65226
|
+
} catch (err) {
|
|
65227
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65228
|
+
const conflicted = await getConflictedFiles(rootDir);
|
|
65229
|
+
if (conflicted.length === 0) {
|
|
65230
|
+
mergerLog.warn(
|
|
65231
|
+
`${taskId}: failed to apply autostash ${sha.slice(0, 7)} (${msg}) \u2014 stash left intact; recover with: cd ${rootDir} && git stash apply ${sha}`
|
|
65232
|
+
);
|
|
65233
|
+
return { status: "failed", stashSha: sha, errorMessage: msg };
|
|
65234
|
+
}
|
|
65235
|
+
applyConflicted = true;
|
|
64910
65236
|
mergerLog.warn(
|
|
64911
|
-
`${taskId}:
|
|
65237
|
+
`${taskId}: autostash apply hit conflict in ${conflicted.length} file(s): ${conflicted.join(", ")}`
|
|
64912
65238
|
);
|
|
64913
65239
|
}
|
|
65240
|
+
if (!applyConflicted) {
|
|
65241
|
+
mergerLog.log(`${taskId}: restored autostash ${sha.slice(0, 7)} cleanly`);
|
|
65242
|
+
await dropAutostashBySha(rootDir, taskId, sha);
|
|
65243
|
+
return { status: "restored", stashSha: sha };
|
|
65244
|
+
}
|
|
65245
|
+
const conflictedFiles = await getConflictedFiles(rootDir);
|
|
65246
|
+
const smartConflictResolution = (ctx.settings.smartConflictResolution ?? ctx.settings.autoResolveConflicts) !== false;
|
|
65247
|
+
if (!smartConflictResolution) {
|
|
65248
|
+
const message = `Autostash apply conflicted in ${conflictedFiles.length} file(s) and smartConflictResolution is disabled. Stash ${sha.slice(0, 7)} left intact; resolve manually with: cd ${rootDir} && # edit files, then git stash drop <ref>`;
|
|
65249
|
+
mergerLog.warn(`${taskId}: ${message}`);
|
|
65250
|
+
return {
|
|
65251
|
+
status: "conflict-needs-manual",
|
|
65252
|
+
stashSha: sha,
|
|
65253
|
+
conflictedFiles,
|
|
65254
|
+
message
|
|
65255
|
+
};
|
|
65256
|
+
}
|
|
65257
|
+
await ctx.store.logEntry(
|
|
65258
|
+
taskId,
|
|
65259
|
+
`Autostash apply conflicted in ${conflictedFiles.length} file(s) \u2014 invoking AI to resolve`,
|
|
65260
|
+
conflictedFiles.join("\n")
|
|
65261
|
+
);
|
|
65262
|
+
const aiResult = await runAiAgentForAutostashConflict({
|
|
65263
|
+
store: ctx.store,
|
|
65264
|
+
rootDir,
|
|
65265
|
+
taskId,
|
|
65266
|
+
conflictedFiles,
|
|
65267
|
+
options: ctx.options,
|
|
65268
|
+
settings: ctx.settings
|
|
65269
|
+
});
|
|
65270
|
+
if (!aiResult.success) {
|
|
65271
|
+
const message = `Autostash apply conflict, AI resolution failed (${aiResult.error ?? "unknown error"}). Stash ${sha.slice(0, 7)} left intact; recover with: cd ${rootDir} && git status (conflicts in working tree) && # resolve, then git stash drop <ref>`;
|
|
65272
|
+
mergerLog.warn(`${taskId}: ${message}`);
|
|
65273
|
+
return {
|
|
65274
|
+
status: "conflict-needs-manual",
|
|
65275
|
+
stashSha: sha,
|
|
65276
|
+
conflictedFiles,
|
|
65277
|
+
message
|
|
65278
|
+
};
|
|
65279
|
+
}
|
|
65280
|
+
const stillConflicted = await findFilesWithConflictMarkers(rootDir, conflictedFiles);
|
|
65281
|
+
if (stillConflicted.length > 0) {
|
|
65282
|
+
const message = `AI agent reported success but conflict markers remain in: ${stillConflicted.join(", ")}. Stash ${sha.slice(0, 7)} left intact; recover manually.`;
|
|
65283
|
+
mergerLog.warn(`${taskId}: ${message}`);
|
|
65284
|
+
return {
|
|
65285
|
+
status: "conflict-needs-manual",
|
|
65286
|
+
stashSha: sha,
|
|
65287
|
+
conflictedFiles: stillConflicted,
|
|
65288
|
+
message
|
|
65289
|
+
};
|
|
65290
|
+
}
|
|
65291
|
+
mergerLog.log(
|
|
65292
|
+
`${taskId}: AI-resolved autostash conflict in ${conflictedFiles.length} file(s); dropping stash ${sha.slice(0, 7)}`
|
|
65293
|
+
);
|
|
65294
|
+
await ctx.store.logEntry(
|
|
65295
|
+
taskId,
|
|
65296
|
+
`Autostash conflict resolved by AI in ${conflictedFiles.length} file(s)`,
|
|
65297
|
+
conflictedFiles.join("\n")
|
|
65298
|
+
);
|
|
65299
|
+
await dropAutostashBySha(rootDir, taskId, sha);
|
|
65300
|
+
return {
|
|
65301
|
+
status: "ai-resolved",
|
|
65302
|
+
stashSha: sha,
|
|
65303
|
+
conflictedFiles
|
|
65304
|
+
};
|
|
64914
65305
|
}
|
|
64915
65306
|
async function generateAiMergeSummary(commitLog, diffStat, settings, rootDir) {
|
|
64916
65307
|
try {
|
|
@@ -65679,6 +66070,7 @@ You are assisting with a paused \`git pull --rebase\`.
|
|
|
65679
66070
|
taskId
|
|
65680
66071
|
})
|
|
65681
66072
|
});
|
|
66073
|
+
options?.onSession?.(session);
|
|
65682
66074
|
const prompt = [
|
|
65683
66075
|
`Resolve rebase conflicts for task ${taskId}.`,
|
|
65684
66076
|
"",
|
|
@@ -65892,7 +66284,8 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
65892
66284
|
if (mergeBlocker) {
|
|
65893
66285
|
throw new Error(`Cannot merge ${taskId}: ${mergeBlocker}`);
|
|
65894
66286
|
}
|
|
65895
|
-
const
|
|
66287
|
+
const autostashHandle = await stashUnrelatedRootDirChanges(rootDir, taskId);
|
|
66288
|
+
let resultForFinally;
|
|
65896
66289
|
try {
|
|
65897
66290
|
const branch = task.branch || `fusion/${taskId.toLowerCase()}`;
|
|
65898
66291
|
const sourceIssueRef = buildSourceIssueRef(task.sourceIssue);
|
|
@@ -65904,6 +66297,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
65904
66297
|
worktreeRemoved: false,
|
|
65905
66298
|
branchDeleted: false
|
|
65906
66299
|
};
|
|
66300
|
+
resultForFinally = result;
|
|
65907
66301
|
const mergeRunId = generateSyntheticRunId("merge", taskId);
|
|
65908
66302
|
const engineRunContext = {
|
|
65909
66303
|
runId: mergeRunId,
|
|
@@ -66858,7 +67252,8 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
66858
67252
|
const pushResult = await pushToRemoteAfterMerge(store, rootDir, taskId, settings, {
|
|
66859
67253
|
onAgentText: options.onAgentText,
|
|
66860
67254
|
signal: options.signal,
|
|
66861
|
-
runtimeHint: pushRuntimeHint
|
|
67255
|
+
runtimeHint: pushRuntimeHint,
|
|
67256
|
+
onSession: options.onSession
|
|
66862
67257
|
});
|
|
66863
67258
|
if (pushResult.pushed) {
|
|
66864
67259
|
mergerLog.log(`${taskId}: pushed merged result to remote`);
|
|
@@ -66910,8 +67305,29 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
|
|
|
66910
67305
|
await completeTask(store, taskId, result);
|
|
66911
67306
|
return result;
|
|
66912
67307
|
} finally {
|
|
66913
|
-
if (
|
|
66914
|
-
|
|
67308
|
+
if (autostashHandle) {
|
|
67309
|
+
try {
|
|
67310
|
+
const settings = await store.getSettings();
|
|
67311
|
+
const outcome = await restoreUnrelatedRootDirChanges(
|
|
67312
|
+
rootDir,
|
|
67313
|
+
taskId,
|
|
67314
|
+
autostashHandle,
|
|
67315
|
+
{ store, options, settings }
|
|
67316
|
+
);
|
|
67317
|
+
if (resultForFinally) {
|
|
67318
|
+
resultForFinally.autostash = outcome;
|
|
67319
|
+
}
|
|
67320
|
+
} catch (err) {
|
|
67321
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67322
|
+
mergerLog.warn(`${taskId}: autostash restore threw unexpectedly (${msg}) \u2014 stash may be left in place; check git stash list`);
|
|
67323
|
+
if (resultForFinally) {
|
|
67324
|
+
resultForFinally.autostash = {
|
|
67325
|
+
status: "failed",
|
|
67326
|
+
stashSha: autostashHandle.sha,
|
|
67327
|
+
errorMessage: msg
|
|
67328
|
+
};
|
|
67329
|
+
}
|
|
67330
|
+
}
|
|
66915
67331
|
}
|
|
66916
67332
|
}
|
|
66917
67333
|
}
|
|
@@ -67962,7 +68378,7 @@ async function completeTask(store, taskId, result) {
|
|
|
67962
68378
|
result.task = task;
|
|
67963
68379
|
store.emit("task:merged", result);
|
|
67964
68380
|
}
|
|
67965
|
-
var execAsync2, execFileAsync, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, VERIFICATION_EXTRA_ENV, FUSION_TASK_ID_TRAILER_KEY;
|
|
68381
|
+
var execAsync2, execFileAsync, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, VerificationError, MergeAbortedError, VERIFICATION_EXTRA_ENV, AUTOSTASH_LABEL_PREFIX, FUSION_TASK_ID_TRAILER_KEY;
|
|
67966
68382
|
var init_merger = __esm({
|
|
67967
68383
|
"../engine/src/merger.ts"() {
|
|
67968
68384
|
"use strict";
|
|
@@ -68044,6 +68460,7 @@ var init_merger = __esm({
|
|
|
68044
68460
|
["FUSION_TEST_WORKSPACE_CONCURRENCY", "4"]
|
|
68045
68461
|
].filter(([key]) => !(key in process.env))
|
|
68046
68462
|
);
|
|
68463
|
+
AUTOSTASH_LABEL_PREFIX = "fusion-merger-autostash:";
|
|
68047
68464
|
FUSION_TASK_ID_TRAILER_KEY = "Fusion-Task-Id";
|
|
68048
68465
|
}
|
|
68049
68466
|
});
|
|
@@ -69968,7 +70385,7 @@ function buildSourceIssueRef2(sourceIssue) {
|
|
|
69968
70385
|
}
|
|
69969
70386
|
return `${sourceIssue.repository}#${issueNumber}`;
|
|
69970
70387
|
}
|
|
69971
|
-
function buildExecutionPrompt(task, rootDir, settings, worktreePath) {
|
|
70388
|
+
function buildExecutionPrompt(task, rootDir, settings, worktreePath, pluginRunner) {
|
|
69972
70389
|
const prompt = scopePromptToWorktree2(task.prompt, rootDir, worktreePath);
|
|
69973
70390
|
const reviewMatch = prompt.match(/##\s*Review Level[:\s]*(\d)/);
|
|
69974
70391
|
const reviewLevel = reviewMatch ? parseInt(reviewMatch[1], 10) : 0;
|
|
@@ -70041,6 +70458,11 @@ git log --oneline
|
|
|
70041
70458
|
}
|
|
70042
70459
|
steeringSection = lines.join("\n");
|
|
70043
70460
|
}
|
|
70461
|
+
const taskPromptContributions = pluginRunner?.getPromptContributionsForSurface("executor-task") ?? [];
|
|
70462
|
+
if (taskPromptContributions.length > 0) {
|
|
70463
|
+
executorLog.log(`${task.id}: applied ${taskPromptContributions.length} plugin prompt contributions for executor-task surface`);
|
|
70464
|
+
}
|
|
70465
|
+
const pluginTaskContributions = buildPluginPromptSection("executor-task", pluginRunner);
|
|
70044
70466
|
return `Execute this task.
|
|
70045
70467
|
|
|
70046
70468
|
## Task: ${task.id}
|
|
@@ -70059,6 +70481,10 @@ ${reviewLevel >= 1 ? `Before implementing each step (except Step 0 and the final
|
|
|
70059
70481
|
${reviewLevel >= 2 ? `After implementing + committing each step, call:
|
|
70060
70482
|
\`fn_review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
|
|
70061
70483
|
${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""}
|
|
70484
|
+
${pluginTaskContributions ? `
|
|
70485
|
+
|
|
70486
|
+
${pluginTaskContributions}
|
|
70487
|
+
` : ""}
|
|
70062
70488
|
|
|
70063
70489
|
## Worktree Boundaries
|
|
70064
70490
|
|
|
@@ -71435,6 +71861,41 @@ The tool prevents your session from being killed by the inactivity watchdog duri
|
|
|
71435
71861
|
return false;
|
|
71436
71862
|
}
|
|
71437
71863
|
}
|
|
71864
|
+
/**
|
|
71865
|
+
* Returns true when execute() should be deferred because the agent bound to
|
|
71866
|
+
* this task has an active heartbeat run and allowParallelExecution=false.
|
|
71867
|
+
*
|
|
71868
|
+
* Only applies to permanent (non-ephemeral) agents. Always returns false
|
|
71869
|
+
* when agentStore is unavailable or the agent cannot be resolved.
|
|
71870
|
+
*/
|
|
71871
|
+
async shouldDeferForHeartbeat(agentId) {
|
|
71872
|
+
if (!this.options.agentStore) return false;
|
|
71873
|
+
const agent = await this.options.agentStore.getAgent(agentId).catch(() => null);
|
|
71874
|
+
if (!agent) return false;
|
|
71875
|
+
if (isEphemeralAgent(agent)) return false;
|
|
71876
|
+
const rc = agent.runtimeConfig ?? {};
|
|
71877
|
+
if (rc.allowParallelExecution !== false) return false;
|
|
71878
|
+
const activeRun = await this.options.agentStore.getActiveHeartbeatRun(agentId).catch(() => null);
|
|
71879
|
+
return activeRun !== null;
|
|
71880
|
+
}
|
|
71881
|
+
/**
|
|
71882
|
+
* Re-dispatch execute() for any unstarted in-progress task belonging to the
|
|
71883
|
+
* given agent. Called after a heartbeat run completes to unblock tasks that
|
|
71884
|
+
* were deferred by the allowParallelExecution=false gate.
|
|
71885
|
+
*/
|
|
71886
|
+
async resumeTaskForAgent(agentId) {
|
|
71887
|
+
const settings = await this.store.getSettings();
|
|
71888
|
+
if (settings.globalPause || settings.enginePaused) return;
|
|
71889
|
+
const tasks = await this.store.listTasks({ slim: true, column: "in-progress" });
|
|
71890
|
+
for (const task of tasks) {
|
|
71891
|
+
if (task.assignedAgentId === agentId && !task.paused && !this.executing.has(task.id) && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id) && !this.activeWorkflowStepSessions.has(task.id)) {
|
|
71892
|
+
executorLog.log(`${task.id}: re-dispatching execute() after heartbeat completion for agent ${agentId}`);
|
|
71893
|
+
this.execute(task).catch(
|
|
71894
|
+
(err) => executorLog.error(`Failed to resume ${task.id} after heartbeat completion:`, err)
|
|
71895
|
+
);
|
|
71896
|
+
}
|
|
71897
|
+
}
|
|
71898
|
+
}
|
|
71438
71899
|
/**
|
|
71439
71900
|
* Resume orphaned in-progress tasks (e.g., after crash/restart).
|
|
71440
71901
|
* Call once after engine startup.
|
|
@@ -71526,28 +71987,6 @@ The tool prevents your session from being killed by the inactivity watchdog duri
|
|
|
71526
71987
|
}
|
|
71527
71988
|
return "";
|
|
71528
71989
|
}
|
|
71529
|
-
resolveDependencyWorktree(task, allTasks) {
|
|
71530
|
-
if (task.dependencies.length === 0) return null;
|
|
71531
|
-
for (const depId of task.dependencies) {
|
|
71532
|
-
const dep = allTasks.find((t) => t.id === depId);
|
|
71533
|
-
if (dep && dep.worktree && (dep.column === "done" || dep.column === "in-review") && existsSync29(dep.worktree)) {
|
|
71534
|
-
return dep.worktree;
|
|
71535
|
-
}
|
|
71536
|
-
}
|
|
71537
|
-
return null;
|
|
71538
|
-
}
|
|
71539
|
-
/**
|
|
71540
|
-
* Reuse an existing worktree directory from a dependency task.
|
|
71541
|
-
* Instead of creating a new worktree with `git worktree add`, this creates
|
|
71542
|
-
* a new branch in the existing worktree via `git checkout -b`. The worktree
|
|
71543
|
-
* directory (and its build caches) are preserved.
|
|
71544
|
-
*/
|
|
71545
|
-
async reuseWorktree(branch, worktreePath) {
|
|
71546
|
-
await execAsync5(`git checkout -b "${branch}"`, {
|
|
71547
|
-
cwd: worktreePath
|
|
71548
|
-
});
|
|
71549
|
-
executorLog.log(`Reused worktree at ${worktreePath}, created branch ${branch}`);
|
|
71550
|
-
}
|
|
71551
71990
|
/**
|
|
71552
71991
|
* Execute a task in an isolated git worktree.
|
|
71553
71992
|
*
|
|
@@ -71561,6 +72000,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
|
|
|
71561
72000
|
async execute(task) {
|
|
71562
72001
|
executorLog.log(`execute() called for ${task.id} (already executing=${this.executing.has(task.id)})`);
|
|
71563
72002
|
if (this.executing.has(task.id)) return;
|
|
72003
|
+
const assignedAgentId = task.assignedAgentId;
|
|
72004
|
+
if (assignedAgentId && await this.shouldDeferForHeartbeat(assignedAgentId)) {
|
|
72005
|
+
executorLog.log(`${task.id}: skipping execute \u2014 agent ${assignedAgentId} has active heartbeat run (allowParallelExecution=false)`);
|
|
72006
|
+
return;
|
|
72007
|
+
}
|
|
71564
72008
|
this.executing.add(task.id);
|
|
71565
72009
|
executorLog.log(`Starting ${task.id}: ${task.title || task.description.slice(0, 60)}`);
|
|
71566
72010
|
const settings = await this.store.getSettings();
|
|
@@ -72150,9 +72594,9 @@ ${summary}`,
|
|
|
72150
72594
|
const sessionRef = { current: null };
|
|
72151
72595
|
const stepCheckpoints = /* @__PURE__ */ new Map();
|
|
72152
72596
|
const stuckDetector = this.options.stuckTaskDetector;
|
|
72153
|
-
const
|
|
72154
|
-
const reflectionTools = this.options.reflectionService && settings.reflectionEnabled &&
|
|
72155
|
-
const assignedAgent =
|
|
72597
|
+
const assignedAgentId2 = detail.assignedAgentId?.trim();
|
|
72598
|
+
const reflectionTools = this.options.reflectionService && settings.reflectionEnabled && assignedAgentId2 ? [createReflectOnPerformanceTool(this.options.reflectionService, assignedAgentId2)] : [];
|
|
72599
|
+
const assignedAgent = assignedAgentId2 && this.options.agentStore ? await this.options.agentStore.getAgent(assignedAgentId2).catch(() => null) : null;
|
|
72156
72600
|
const executorRuntimeHint = extractRuntimeHint(assignedAgent?.runtimeConfig);
|
|
72157
72601
|
if (executionMode === "fast") {
|
|
72158
72602
|
executorLog.log(`${task.id}: fast mode \u2014 fn_review_step tool not injected`);
|
|
@@ -72201,15 +72645,15 @@ ${summary}`,
|
|
|
72201
72645
|
...this.options.agentStore ? [
|
|
72202
72646
|
createListAgentsTool(this.options.agentStore),
|
|
72203
72647
|
createDelegateTaskTool(this.options.agentStore, this.store, { rootDir: this.rootDir }),
|
|
72204
|
-
...
|
|
72205
|
-
createGetAgentConfigTool(this.options.agentStore,
|
|
72206
|
-
createUpdateAgentConfigTool(this.options.agentStore,
|
|
72648
|
+
...assignedAgentId2 ? [
|
|
72649
|
+
createGetAgentConfigTool(this.options.agentStore, assignedAgentId2),
|
|
72650
|
+
createUpdateAgentConfigTool(this.options.agentStore, assignedAgentId2)
|
|
72207
72651
|
] : []
|
|
72208
72652
|
] : [],
|
|
72209
72653
|
// Messaging tools — allows executor agents to send and receive messages.
|
|
72210
|
-
...this.options.messageStore &&
|
|
72211
|
-
createSendMessageTool(this.options.messageStore,
|
|
72212
|
-
createReadMessagesTool(this.options.messageStore,
|
|
72654
|
+
...this.options.messageStore && assignedAgentId2 ? [
|
|
72655
|
+
createSendMessageTool(this.options.messageStore, assignedAgentId2),
|
|
72656
|
+
createReadMessagesTool(this.options.messageStore, assignedAgentId2)
|
|
72213
72657
|
] : [],
|
|
72214
72658
|
// Add plugin tools from PluginRunner
|
|
72215
72659
|
...this.options.pluginRunner?.getPluginTools() ?? []
|
|
@@ -72247,12 +72691,23 @@ ${summary}`,
|
|
|
72247
72691
|
getExecutorSystemPrompt(settings),
|
|
72248
72692
|
executorInstructions
|
|
72249
72693
|
);
|
|
72694
|
+
const executorSystemContributions = this.options.pluginRunner?.getPromptContributionsForSurface("executor-system") ?? [];
|
|
72695
|
+
if (executorSystemContributions.length > 0) {
|
|
72696
|
+
executorLog.log(`${task.id}: applied ${executorSystemContributions.length} plugin prompt contributions for executor-system surface`);
|
|
72697
|
+
}
|
|
72698
|
+
const executorPluginContributions = buildPluginPromptSection(
|
|
72699
|
+
"executor-system",
|
|
72700
|
+
this.options.pluginRunner
|
|
72701
|
+
);
|
|
72702
|
+
const executorSystemPromptFinal = executorPluginContributions ? `${executorSystemPrompt}
|
|
72703
|
+
|
|
72704
|
+
${executorPluginContributions}` : executorSystemPrompt;
|
|
72250
72705
|
let { session, sessionFile } = await createResolvedAgentSession({
|
|
72251
72706
|
sessionPurpose: "executor",
|
|
72252
72707
|
runtimeHint: executorRuntimeHint,
|
|
72253
72708
|
pluginRunner: this.options.pluginRunner,
|
|
72254
72709
|
cwd: worktreePath,
|
|
72255
|
-
systemPrompt:
|
|
72710
|
+
systemPrompt: executorSystemPromptFinal,
|
|
72256
72711
|
tools: "coding",
|
|
72257
72712
|
customTools,
|
|
72258
72713
|
onText: agentLogger.onText,
|
|
@@ -72313,7 +72768,13 @@ ${summary}`,
|
|
|
72313
72768
|
"Review the current state of your worktree and proceed with the next pending step."
|
|
72314
72769
|
].join("\n"));
|
|
72315
72770
|
} else {
|
|
72316
|
-
const agentPrompt = buildExecutionPrompt(
|
|
72771
|
+
const agentPrompt = buildExecutionPrompt(
|
|
72772
|
+
detail,
|
|
72773
|
+
this.rootDir,
|
|
72774
|
+
settings,
|
|
72775
|
+
worktreePath,
|
|
72776
|
+
this.options.pluginRunner
|
|
72777
|
+
);
|
|
72317
72778
|
await promptWithFallback(session, agentPrompt);
|
|
72318
72779
|
}
|
|
72319
72780
|
checkSessionError(session);
|
|
@@ -72489,7 +72950,7 @@ ${summary}`,
|
|
|
72489
72950
|
runtimeHint: executorRuntimeHint,
|
|
72490
72951
|
pluginRunner: this.options.pluginRunner,
|
|
72491
72952
|
cwd: worktreePath,
|
|
72492
|
-
systemPrompt:
|
|
72953
|
+
systemPrompt: executorSystemPromptFinal,
|
|
72493
72954
|
tools: "coding",
|
|
72494
72955
|
customTools,
|
|
72495
72956
|
onText: agentLogger.onText,
|
|
@@ -72539,7 +73000,7 @@ ${summary}`,
|
|
|
72539
73000
|
"Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.",
|
|
72540
73001
|
"",
|
|
72541
73002
|
"Original task:",
|
|
72542
|
-
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
|
|
73003
|
+
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
|
|
72543
73004
|
].join("\n");
|
|
72544
73005
|
} else {
|
|
72545
73006
|
retryPrompt = [
|
|
@@ -72549,7 +73010,7 @@ ${summary}`,
|
|
|
72549
73010
|
"2. If there is remaining work, finish it and then call fn_task_done.",
|
|
72550
73011
|
"",
|
|
72551
73012
|
"Original task:",
|
|
72552
|
-
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
|
|
73013
|
+
buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
|
|
72553
73014
|
].join("\n");
|
|
72554
73015
|
}
|
|
72555
73016
|
stuckDetector?.recordActivity(task.id);
|
|
@@ -73609,7 +74070,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
|
|
|
73609
74070
|
return reRunResult.allPassed;
|
|
73610
74071
|
} finally {
|
|
73611
74072
|
await logger2.flush();
|
|
73612
|
-
|
|
74073
|
+
session.dispose();
|
|
73613
74074
|
}
|
|
73614
74075
|
} catch (err) {
|
|
73615
74076
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -74328,7 +74789,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
|
|
|
74328
74789
|
* like `fusion/fn-2729`) used purely for log messages. `depTip` is the
|
|
74329
74790
|
* resolved SHA of the dep's tip — that's what gets squash-merged.
|
|
74330
74791
|
*/
|
|
74331
|
-
async planSquashImportFromDep(
|
|
74792
|
+
async planSquashImportFromDep(_taskId, depTip, originalStartPoint) {
|
|
74332
74793
|
let settings;
|
|
74333
74794
|
try {
|
|
74334
74795
|
settings = await this.store.getSettings();
|
|
@@ -75217,7 +75678,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
|
|
|
75217
75678
|
this.childSessions.delete(childId);
|
|
75218
75679
|
}
|
|
75219
75680
|
try {
|
|
75220
|
-
await this.options.agentStore?.updateAgentState(childId, "
|
|
75681
|
+
await this.options.agentStore?.updateAgentState(childId, "paused");
|
|
75221
75682
|
} catch (err) {
|
|
75222
75683
|
const msg = err instanceof Error ? err.message : String(err);
|
|
75223
75684
|
executorLog.warn(`Failed to update spawned child ${childId} state to 'terminated' during cleanup: ${msg}`);
|
|
@@ -78260,9 +78721,6 @@ function taskRelevanceScore(agent, task) {
|
|
|
78260
78721
|
}
|
|
78261
78722
|
return score;
|
|
78262
78723
|
}
|
|
78263
|
-
function isBlockedStateDuplicate(current, previous) {
|
|
78264
|
-
return current.blockedBy === previous.blockedBy && current.contextHash === previous.contextHash;
|
|
78265
|
-
}
|
|
78266
78724
|
function truncatePrompt(text, maxChars) {
|
|
78267
78725
|
if (text.length <= maxChars) return text;
|
|
78268
78726
|
return `${text.slice(0, maxChars)}
|
|
@@ -78322,28 +78780,42 @@ var init_agent_heartbeat = __esm({
|
|
|
78322
78780
|
|
|
78323
78781
|
## Your Role
|
|
78324
78782
|
|
|
78325
|
-
|
|
78326
|
-
|
|
78327
|
-
|
|
78783
|
+
This is an ambient heartbeat. Task implementation work (coding, running tests, making commits) runs in a separate
|
|
78784
|
+
execution path handled by the executor. Do NOT do task body work or implementation in this heartbeat.
|
|
78785
|
+
|
|
78786
|
+
Your purpose is to keep momentum through coordination: surface blockers, respond to messages, manage memory,
|
|
78787
|
+
delegate, and route work to the right place. Think in single-pass interventions, not coding sessions.
|
|
78328
78788
|
|
|
78329
78789
|
Your job:
|
|
78330
|
-
1. Check your assigned task \u2014
|
|
78331
|
-
2. Do ONE useful action
|
|
78790
|
+
1. Check your assigned task context \u2014 review its state, blockedBy field, and any new comments.
|
|
78791
|
+
2. Do ONE useful coordination action.
|
|
78332
78792
|
3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations, and fn_task_document_write for durable artifacts.
|
|
78333
78793
|
4. Use fn_list_agents + fn_delegate_task when work should be assigned to a specific capable agent now.
|
|
78334
78794
|
5. Use fn_get_agent_config and fn_update_agent_config to tune direct reports before delegating recurring work.
|
|
78335
|
-
|
|
78795
|
+
6. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
|
|
78336
78796
|
|
|
78337
|
-
|
|
78338
|
-
-
|
|
78797
|
+
**If your bound task is blocked** (blockedBy is set in the task context):
|
|
78798
|
+
- Surface the blocker concretely with fn_task_log.
|
|
78799
|
+
- Chase the dependency: comment on the blocking task, send a message to the responsible agent, or ping an owner.
|
|
78800
|
+
- Look for unblocking work you can spawn or delegate right now.
|
|
78801
|
+
- Pivot to other relevant coordination work if the blocker cannot be immediately resolved.
|
|
78802
|
+
|
|
78803
|
+
**If your bound task is not blocked:**
|
|
78804
|
+
- Surface progress, status, or coordination needs with fn_task_log or fn_task_document_write.
|
|
78805
|
+
- Create follow-up tasks for discovered risks or gaps.
|
|
78806
|
+
- Respond to new steering comments or user messages.
|
|
78807
|
+
|
|
78808
|
+
Examples of ONE useful coordination action:
|
|
78809
|
+
- DO: log a concrete blocker with next steps and message the agent responsible for unblocking.
|
|
78339
78810
|
- DO: create a focused follow-up task when a missing dependency is discovered.
|
|
78340
78811
|
- DO: delegate a well-scoped task to an appropriate idle specialist agent.
|
|
78341
78812
|
- DO: save a short investigation note with fn_task_document_write when the analysis is reusable.
|
|
78342
|
-
- DON'T: attempt full implementation,
|
|
78813
|
+
- DON'T: attempt full implementation, run tests, commit code, or do multi-step coding work.
|
|
78343
78814
|
- DON'T: create vague tasks like "investigate stuff" without actionable scope.
|
|
78344
78815
|
|
|
78345
|
-
Keep work lightweight \u2014 this is a single-pass check, not
|
|
78346
|
-
You have
|
|
78816
|
+
Keep work lightweight \u2014 this is a single-pass coordination check, not an implementation run.
|
|
78817
|
+
You have workspace read tools (for context gathering) plus fn_task_create, fn_task_log, fn_task_document tools,
|
|
78818
|
+
fn_send_message, fn_read_messages, fn_list_agents, fn_delegate_task, and memory tools.
|
|
78347
78819
|
|
|
78348
78820
|
**Task Documents:** Save important findings with fn_task_document_write(key="...", content="...").
|
|
78349
78821
|
Documents persist across sessions and are visible in the dashboard's Documents tab.
|
|
@@ -78362,7 +78834,8 @@ Prefer fn_delegate_task when immediate ownership by a specific agent materially
|
|
|
78362
78834
|
|
|
78363
78835
|
## Common Patterns
|
|
78364
78836
|
|
|
78365
|
-
- **
|
|
78837
|
+
- **Blocked task:** log the concrete blocker, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed.
|
|
78838
|
+
- **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause.
|
|
78366
78839
|
- **Completed task with follow-up risk:** create explicit follow-up task(s) for residual risk instead of burying notes in a long log.
|
|
78367
78840
|
- **New user/agent comments:** summarize what changed, identify required action, and route via task creation/delegation.
|
|
78368
78841
|
- **Dependency drift:** log the mismatch and create reconciliation tasks with clear dependencies.
|
|
@@ -78433,7 +78906,7 @@ You have coding-capable workspace tools (read/write/edit/bash within worktree bo
|
|
|
78433
78906
|
Use this decision rule:
|
|
78434
78907
|
- **fn_task_create:** create executable work when ownership is not predetermined.
|
|
78435
78908
|
- **fn_delegate_task:** assign immediately when a specific agent should own the work now.
|
|
78436
|
-
- **fn_memory_append:**
|
|
78909
|
+
- **fn_memory_append:** use \`scope="agent"\` for your own operating context and \`scope="project"\` for repo-wide durable knowledge; avoid transient run-by-run chatter.
|
|
78437
78910
|
|
|
78438
78911
|
If unsure who should do the work, prefer fn_task_create and let scheduler routing happen naturally.
|
|
78439
78912
|
|
|
@@ -78581,10 +79054,71 @@ not loop on the same plan across heartbeats without recording why.`;
|
|
|
78581
79054
|
if (this.messageStore) {
|
|
78582
79055
|
this.messageStore.setMessageToAgentHook(this.handleMessageToAgent.bind(this));
|
|
78583
79056
|
}
|
|
79057
|
+
void this.reconcileOrphanedRunningAgents();
|
|
78584
79058
|
this.pollInterval = setInterval(() => {
|
|
78585
79059
|
void this.checkMissedHeartbeats();
|
|
78586
79060
|
}, this.pollIntervalMs);
|
|
78587
79061
|
}
|
|
79062
|
+
/**
|
|
79063
|
+
* Find agents in `state="running"` that are not actually running and flip
|
|
79064
|
+
* them to `"active"`. An agent is considered orphaned when either:
|
|
79065
|
+
* (a) it has no active heartbeat run record, or
|
|
79066
|
+
* (b) it is not in this monitor's in-memory tracked set AND its
|
|
79067
|
+
* lastHeartbeatAt is older than 3× the configured timeout.
|
|
79068
|
+
*
|
|
79069
|
+
* Case (a) covers historical bypass paths (governance-skip, supersede-on-
|
|
79070
|
+
* startRun, safety-net run termination) that ended the run record but
|
|
79071
|
+
* never propagated the agent-state transition. Case (b) covers a process
|
|
79072
|
+
* that crashed mid-run, leaving both the run row and the agent row stuck.
|
|
79073
|
+
*
|
|
79074
|
+
* Called on monitor start AND periodically from the polling loop to keep
|
|
79075
|
+
* the system self-healing across versions. Best-effort — failures are
|
|
79076
|
+
* logged but do not block the caller.
|
|
79077
|
+
*/
|
|
79078
|
+
async reconcileOrphanedRunningAgents() {
|
|
79079
|
+
try {
|
|
79080
|
+
const runningAgents = await this.store.listAgents({ state: "running", includeEphemeral: true });
|
|
79081
|
+
const now = Date.now();
|
|
79082
|
+
for (const agent of runningAgents) {
|
|
79083
|
+
let reason = null;
|
|
79084
|
+
const activeRun = await this.store.getActiveHeartbeatRun(agent.id);
|
|
79085
|
+
if (!activeRun) {
|
|
79086
|
+
reason = "no active run";
|
|
79087
|
+
} else if (!this.trackedAgents.has(agent.id)) {
|
|
79088
|
+
const timeoutMs = this.resolveAgentConfig(agent.id).heartbeatTimeoutMs;
|
|
79089
|
+
const lastTs = agent.lastHeartbeatAt ? Date.parse(agent.lastHeartbeatAt) : NaN;
|
|
79090
|
+
const heartbeatAgeMs = Number.isFinite(lastTs) ? Math.max(0, now - lastTs) : Infinity;
|
|
79091
|
+
if (heartbeatAgeMs > timeoutMs * 3) {
|
|
79092
|
+
try {
|
|
79093
|
+
const detail = await this.store.getRunDetail(agent.id, activeRun.id);
|
|
79094
|
+
if (detail && detail.status !== "completed" && detail.status !== "failed" && detail.status !== "terminated") {
|
|
79095
|
+
await this.store.saveRun({
|
|
79096
|
+
...detail,
|
|
79097
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79098
|
+
status: "terminated",
|
|
79099
|
+
stderrExcerpt: `Reconciled stale run (no heartbeat for ${formatDuration(heartbeatAgeMs)}; threshold ${formatDuration(timeoutMs * 3)})`
|
|
79100
|
+
});
|
|
79101
|
+
}
|
|
79102
|
+
await this.store.endHeartbeatRun(activeRun.id, "terminated");
|
|
79103
|
+
} catch (runEndErr) {
|
|
79104
|
+
heartbeatLog.warn(`Failed to terminate stale run ${activeRun.id} for ${agent.id}: ${runEndErr instanceof Error ? runEndErr.message : String(runEndErr)}`);
|
|
79105
|
+
}
|
|
79106
|
+
reason = `stale heartbeat (${formatDuration(heartbeatAgeMs)} since lastHeartbeatAt)`;
|
|
79107
|
+
}
|
|
79108
|
+
}
|
|
79109
|
+
if (!reason) continue;
|
|
79110
|
+
try {
|
|
79111
|
+
await this.store.updateAgentState(agent.id, "active");
|
|
79112
|
+
this.clearRunState(agent.id);
|
|
79113
|
+
heartbeatLog.log(`Reconciled orphaned running agent ${agent.id} \u2192 active (${reason})`);
|
|
79114
|
+
} catch (err) {
|
|
79115
|
+
heartbeatLog.warn(`Failed to reconcile orphaned running agent ${agent.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
79116
|
+
}
|
|
79117
|
+
}
|
|
79118
|
+
} catch (err) {
|
|
79119
|
+
heartbeatLog.warn(`reconcileOrphanedRunningAgents scan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
79120
|
+
}
|
|
79121
|
+
}
|
|
78588
79122
|
/**
|
|
78589
79123
|
* Stop the heartbeat monitoring loop.
|
|
78590
79124
|
* Does not untrack agents - they remain in memory.
|
|
@@ -78772,7 +79306,7 @@ not loop on the same plan across heartbeats without recording why.`;
|
|
|
78772
79306
|
await this.store.updateAgentState(agentId, "error");
|
|
78773
79307
|
await this.store.updateAgent(agentId, { lastError: completionResult.stderrExcerpt ?? "Run failed" });
|
|
78774
79308
|
} else if (completionResult.status === "terminated") {
|
|
78775
|
-
await this.store.updateAgentState(agentId, "
|
|
79309
|
+
await this.store.updateAgentState(agentId, "paused");
|
|
78776
79310
|
} else {
|
|
78777
79311
|
await this.store.updateAgentState(agentId, "active");
|
|
78778
79312
|
}
|
|
@@ -78781,6 +79315,9 @@ not loop on the same plan across heartbeats without recording why.`;
|
|
|
78781
79315
|
}
|
|
78782
79316
|
}
|
|
78783
79317
|
await this.store.endHeartbeatRun(runId, completionResult.status === "completed" ? "completed" : "terminated");
|
|
79318
|
+
if (completionResult.status === "terminated") {
|
|
79319
|
+
this.onTerminated?.(agentId, completionResult.stderrExcerpt ?? "Run terminated");
|
|
79320
|
+
}
|
|
78784
79321
|
this.onRunCompleted?.(agentId, completedRun);
|
|
78785
79322
|
}
|
|
78786
79323
|
/**
|
|
@@ -79306,45 +79843,8 @@ not loop on the same plan across heartbeats without recording why.`;
|
|
|
79306
79843
|
});
|
|
79307
79844
|
return await this.store.getRunDetail(agentId, run.id);
|
|
79308
79845
|
}
|
|
79309
|
-
const blockedBy = typeof liveTaskDetail.blockedBy === "string" ? liveTaskDetail.blockedBy.trim() : "";
|
|
79310
|
-
const isBlockedTask = liveTaskDetail.status === "queued" && blockedBy.length > 0;
|
|
79311
|
-
if (isBlockedTask) {
|
|
79312
|
-
const commentCount = (liveTaskDetail.comments?.length ?? 0) + (liveTaskDetail.steeringComments?.length ?? 0);
|
|
79313
|
-
const lastCommentId = liveTaskDetail.comments?.at(-1)?.id;
|
|
79314
|
-
const lastSteeringCommentId = liveTaskDetail.steeringComments?.at(-1)?.id;
|
|
79315
|
-
const contextHash = Buffer.from(
|
|
79316
|
-
JSON.stringify({ commentCount, lastCommentId, lastSteeringCommentId, blockedBy })
|
|
79317
|
-
).toString("base64").slice(0, 16);
|
|
79318
|
-
const currentBlockedState = {
|
|
79319
|
-
taskId: resolvedTaskId2,
|
|
79320
|
-
blockedBy,
|
|
79321
|
-
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79322
|
-
contextHash
|
|
79323
|
-
};
|
|
79324
|
-
const previousBlockedState = await this.store.getLastBlockedState(agentId);
|
|
79325
|
-
if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
|
|
79326
|
-
await this.completeRun(agentId, run.id, {
|
|
79327
|
-
status: "completed",
|
|
79328
|
-
resultJson: { reason: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
|
|
79329
|
-
});
|
|
79330
|
-
return await this.store.getRunDetail(agentId, run.id);
|
|
79331
|
-
}
|
|
79332
|
-
const blockedMessage = `Task is blocked by ${blockedBy}; waiting for dependency/context changes before retrying.`;
|
|
79333
|
-
await taskStore.addComment(resolvedTaskId2, blockedMessage, "agent", void 0, runContext);
|
|
79334
|
-
await audit.database({ type: "task:comment:add", target: resolvedTaskId2, metadata: { blockedBy } });
|
|
79335
|
-
await this.store.setLastBlockedState(agentId, currentBlockedState);
|
|
79336
|
-
heartbeatLog.log(`Task ${resolvedTaskId2} is blocked by ${blockedBy} \u2014 recorded blocked state`);
|
|
79337
|
-
await this.completeRun(agentId, run.id, {
|
|
79338
|
-
status: "completed",
|
|
79339
|
-
resultJson: { reason: "blocked", taskId: resolvedTaskId2, blockedBy }
|
|
79340
|
-
});
|
|
79341
|
-
return await this.store.getRunDetail(agentId, run.id);
|
|
79342
|
-
}
|
|
79343
79846
|
}
|
|
79344
79847
|
}
|
|
79345
|
-
if (!isNoTaskRun) {
|
|
79346
|
-
await this.store.clearLastBlockedState(agentId);
|
|
79347
|
-
}
|
|
79348
79848
|
const STDOUT_EXCERPT_LIMIT = 4e3;
|
|
79349
79849
|
let outputLength = 0;
|
|
79350
79850
|
let toolCallCount = 0;
|
|
@@ -79450,6 +79950,17 @@ not loop on the same plan across heartbeats without recording why.`;
|
|
|
79450
79950
|
baseHeartbeatSystemPrompt,
|
|
79451
79951
|
[resolvedInstructionsForIdentity, memoryInstructions, selfImprovePrompt].filter((part) => part.trim()).join("\n\n")
|
|
79452
79952
|
);
|
|
79953
|
+
const heartbeatContributions = this.pluginRunner?.getPromptContributionsForSurface("heartbeat") ?? [];
|
|
79954
|
+
if (heartbeatContributions.length > 0) {
|
|
79955
|
+
heartbeatLog.log(`applied ${heartbeatContributions.length} plugin prompt contributions for heartbeat surface`);
|
|
79956
|
+
}
|
|
79957
|
+
const heartbeatPluginContributions = buildPluginPromptSection(
|
|
79958
|
+
"heartbeat",
|
|
79959
|
+
this.pluginRunner
|
|
79960
|
+
);
|
|
79961
|
+
const systemPromptFinal = heartbeatPluginContributions ? `${systemPrompt}
|
|
79962
|
+
|
|
79963
|
+
${heartbeatPluginContributions}` : systemPrompt;
|
|
79453
79964
|
heartbeatTools.push(heartbeatDoneTool);
|
|
79454
79965
|
if (isNoTaskRun) {
|
|
79455
79966
|
agentLogger = new AgentLogger({
|
|
@@ -79471,7 +79982,7 @@ not loop on the same plan across heartbeats without recording why.`;
|
|
|
79471
79982
|
runtimeHint: extractRuntimeHint2(agent.runtimeConfig),
|
|
79472
79983
|
pluginRunner: this.pluginRunner,
|
|
79473
79984
|
cwd: rootDir,
|
|
79474
|
-
systemPrompt,
|
|
79985
|
+
systemPrompt: systemPromptFinal,
|
|
79475
79986
|
tools: "coding",
|
|
79476
79987
|
customTools: heartbeatTools,
|
|
79477
79988
|
...(() => {
|
|
@@ -79674,7 +80185,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
79674
80185
|
try {
|
|
79675
80186
|
const runWithPrompts = {
|
|
79676
80187
|
...run,
|
|
79677
|
-
systemPrompt: truncatePrompt(
|
|
80188
|
+
systemPrompt: truncatePrompt(systemPromptFinal, 1e5),
|
|
79678
80189
|
executionPrompt: truncatePrompt(executionPrompt, 1e5),
|
|
79679
80190
|
heartbeatProcedureSource: customProcedure ? "custom" : "default"
|
|
79680
80191
|
};
|
|
@@ -79821,8 +80332,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
79821
80332
|
let health = "healthy";
|
|
79822
80333
|
if (report.state === "paused") {
|
|
79823
80334
|
health = report.pauseReason ? `paused (${report.pauseReason})` : "paused";
|
|
79824
|
-
} else if (report.state === "terminated") {
|
|
79825
|
-
health = "terminated";
|
|
79826
80335
|
} else if (report.state === "error") {
|
|
79827
80336
|
health = "**stuck**";
|
|
79828
80337
|
} else if (report.state === "running") {
|
|
@@ -79837,7 +80346,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
79837
80346
|
});
|
|
79838
80347
|
const hasStuck = rows.some((row) => row.includes("**stuck**"));
|
|
79839
80348
|
const hasStale = rows.some((row) => row.includes("**stale**"));
|
|
79840
|
-
const hasTerminated = rows.some((row) => row.includes("terminated"));
|
|
79841
80349
|
const actionLines = ["### Actions for Unresponsive Reports"];
|
|
79842
80350
|
if (hasStuck) {
|
|
79843
80351
|
actionLines.push("- For **stuck** reports: consider sending a message via fn_send_message asking for status, or reassigning their task via fn_delegate_task to a healthy agent.");
|
|
@@ -79845,9 +80353,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
79845
80353
|
if (hasStale) {
|
|
79846
80354
|
actionLines.push("- For **stale** reports: the agent may have lost its heartbeat trigger \u2014 create a follow-up task to investigate.");
|
|
79847
80355
|
}
|
|
79848
|
-
if (hasTerminated) {
|
|
79849
|
-
actionLines.push("- For **terminated** reports: if they had active work, reassign their tasks or spawn replacement agents.");
|
|
79850
|
-
}
|
|
79851
80356
|
return [
|
|
79852
80357
|
"## Reports Health Check",
|
|
79853
80358
|
"",
|
|
@@ -80003,6 +80508,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
80003
80508
|
}
|
|
80004
80509
|
}
|
|
80005
80510
|
}
|
|
80511
|
+
await this.reconcileOrphanedRunningAgents();
|
|
80006
80512
|
}
|
|
80007
80513
|
async handleMissedHeartbeat(tracked, reason) {
|
|
80008
80514
|
await this.store.recordHeartbeat(tracked.agentId, "missed", tracked.runId);
|
|
@@ -80013,12 +80519,21 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
80013
80519
|
const elapsed = now - tracked.lastSeen;
|
|
80014
80520
|
const reason = `No heartbeat for ${formatDuration(elapsed)} (2\xD7 timeout threshold: ${formatDuration(heartbeatTimeoutMs * 2)})`;
|
|
80015
80521
|
heartbeatLog.warn(`Recovering unresponsive agent ${tracked.agentId}: ${reason}`);
|
|
80522
|
+
const runIdToTerminate = tracked.runId;
|
|
80016
80523
|
try {
|
|
80017
80524
|
tracked.session.dispose();
|
|
80018
80525
|
} catch (err) {
|
|
80019
80526
|
heartbeatLog.warn(`Error disposing session for ${tracked.agentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
80020
80527
|
}
|
|
80021
80528
|
this.untrackAgent(tracked.agentId);
|
|
80529
|
+
try {
|
|
80530
|
+
await this.completeRun(tracked.agentId, runIdToTerminate, {
|
|
80531
|
+
status: "terminated",
|
|
80532
|
+
stderrExcerpt: reason
|
|
80533
|
+
});
|
|
80534
|
+
} catch (err) {
|
|
80535
|
+
heartbeatLog.warn(`completeRun(terminated) failed for ${tracked.agentId}/${runIdToTerminate}: ${err instanceof Error ? err.message : String(err)}`);
|
|
80536
|
+
}
|
|
80022
80537
|
try {
|
|
80023
80538
|
await this.pauseAgent(tracked.agentId, { pauseReason: "heartbeat-unresponsive", stopActiveRun: false });
|
|
80024
80539
|
} catch (err) {
|
|
@@ -80048,10 +80563,12 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
80048
80563
|
updatedListener = null;
|
|
80049
80564
|
configRevisionListener = null;
|
|
80050
80565
|
deletedListener = null;
|
|
80051
|
-
|
|
80566
|
+
isTaskExecuting;
|
|
80567
|
+
constructor(store, callback, taskStore, options) {
|
|
80052
80568
|
this.store = store;
|
|
80053
80569
|
this.callback = callback;
|
|
80054
80570
|
this.taskStore = taskStore;
|
|
80571
|
+
this.isTaskExecuting = options?.isTaskExecuting;
|
|
80055
80572
|
}
|
|
80056
80573
|
/**
|
|
80057
80574
|
* Start the scheduler. Enables assignment watching.
|
|
@@ -80263,6 +80780,10 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
80263
80780
|
heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (active run)`);
|
|
80264
80781
|
return;
|
|
80265
80782
|
}
|
|
80783
|
+
if (runtimeConfig.allowParallelExecution === false && this.isTaskExecuting?.(taskId)) {
|
|
80784
|
+
heartbeatLog.log(`Assignment tick skipped for ${agent.id} (parallel execution disabled, task ${taskId} executing)`);
|
|
80785
|
+
return;
|
|
80786
|
+
}
|
|
80266
80787
|
let budgetStatus;
|
|
80267
80788
|
try {
|
|
80268
80789
|
budgetStatus = await this.store.getBudgetStatus(agent.id);
|
|
@@ -80433,6 +80954,11 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
|
|
|
80433
80954
|
heartbeatLog.log(`Timer tick skipped for ${agentId} (active run)`);
|
|
80434
80955
|
return;
|
|
80435
80956
|
}
|
|
80957
|
+
const timerRc = agent.runtimeConfig ?? {};
|
|
80958
|
+
if (timerRc.allowParallelExecution === false && agent.taskId && this.isTaskExecuting?.(agent.taskId)) {
|
|
80959
|
+
heartbeatLog.log(`Timer tick skipped for ${agentId} (parallel execution disabled, task ${agent.taskId} executing)`);
|
|
80960
|
+
return;
|
|
80961
|
+
}
|
|
80436
80962
|
if (this.taskStore) {
|
|
80437
80963
|
const settings = await this.taskStore.getSettings();
|
|
80438
80964
|
if (settings.globalPause) {
|
|
@@ -82090,6 +82616,7 @@ var init_stuck_task_detector = __esm({
|
|
|
82090
82616
|
onStuck;
|
|
82091
82617
|
beforeRequeue;
|
|
82092
82618
|
onLoopDetected;
|
|
82619
|
+
paused = false;
|
|
82093
82620
|
/**
|
|
82094
82621
|
* Start the polling loop that checks for stuck tasks.
|
|
82095
82622
|
* Safe to call multiple times (no-ops if already running).
|
|
@@ -82315,6 +82842,29 @@ var init_stuck_task_detector = __esm({
|
|
|
82315
82842
|
}
|
|
82316
82843
|
this.tracked.delete(taskId);
|
|
82317
82844
|
}
|
|
82845
|
+
/**
|
|
82846
|
+
* Pause stuck detection checks while the engine is in a paused lifecycle.
|
|
82847
|
+
* Active tracked sessions are preserved and refreshed on resume.
|
|
82848
|
+
*/
|
|
82849
|
+
pause() {
|
|
82850
|
+
if (this.paused) return;
|
|
82851
|
+
this.paused = true;
|
|
82852
|
+
}
|
|
82853
|
+
/**
|
|
82854
|
+
* Resume stuck detection checks and refresh tracked timestamps so the paused
|
|
82855
|
+
* interval does not count as inactivity/no-progress time.
|
|
82856
|
+
*/
|
|
82857
|
+
resume() {
|
|
82858
|
+
if (!this.paused) return;
|
|
82859
|
+
this.paused = false;
|
|
82860
|
+
if (this.tracked.size === 0) return;
|
|
82861
|
+
const now = Date.now();
|
|
82862
|
+
for (const entry of this.tracked.values()) {
|
|
82863
|
+
entry.lastActivity = now;
|
|
82864
|
+
entry.lastProgressAt = now;
|
|
82865
|
+
entry.activitySinceProgress = 0;
|
|
82866
|
+
}
|
|
82867
|
+
}
|
|
82318
82868
|
/**
|
|
82319
82869
|
* Check for stuck tasks immediately, outside the normal polling cycle.
|
|
82320
82870
|
* Safe to call at any time — will no-op if no tasks are tracked or timeout is disabled.
|
|
@@ -82336,6 +82886,7 @@ var init_stuck_task_detector = __esm({
|
|
|
82336
82886
|
*/
|
|
82337
82887
|
async checkStuckTasks() {
|
|
82338
82888
|
if (this.tracked.size === 0) return;
|
|
82889
|
+
if (this.paused) return;
|
|
82339
82890
|
let settings;
|
|
82340
82891
|
try {
|
|
82341
82892
|
settings = await this.store.getSettings();
|
|
@@ -84751,6 +85302,282 @@ var init_plugin_runner = __esm({
|
|
|
84751
85302
|
}
|
|
84752
85303
|
});
|
|
84753
85304
|
|
|
85305
|
+
// ../engine/src/ephemeral-worker-manager.ts
|
|
85306
|
+
var TERMINAL_TASK_COLUMNS, EphemeralWorkerManager;
|
|
85307
|
+
var init_ephemeral_worker_manager = __esm({
|
|
85308
|
+
"../engine/src/ephemeral-worker-manager.ts"() {
|
|
85309
|
+
"use strict";
|
|
85310
|
+
init_src();
|
|
85311
|
+
TERMINAL_TASK_COLUMNS = /* @__PURE__ */ new Set(["done", "archived"]);
|
|
85312
|
+
EphemeralWorkerManager = class {
|
|
85313
|
+
agentStore;
|
|
85314
|
+
taskStore;
|
|
85315
|
+
log;
|
|
85316
|
+
isDeletionPendingExternal;
|
|
85317
|
+
/** taskId → owner. In-memory only; on-disk fallback covers restart gaps. */
|
|
85318
|
+
taskAgentMap = /* @__PURE__ */ new Map();
|
|
85319
|
+
/** agentIds with in-flight delete; prevents racing parallel cleanup paths. */
|
|
85320
|
+
pendingDeletions = /* @__PURE__ */ new Set();
|
|
85321
|
+
stateChangeListener;
|
|
85322
|
+
constructor(options) {
|
|
85323
|
+
this.agentStore = options.agentStore;
|
|
85324
|
+
this.taskStore = options.taskStore;
|
|
85325
|
+
this.log = options.logger;
|
|
85326
|
+
this.isDeletionPendingExternal = options.isDeletionPendingExternal ?? (() => false);
|
|
85327
|
+
}
|
|
85328
|
+
// ── public surface ───────────────────────────────────────────────────────
|
|
85329
|
+
/**
|
|
85330
|
+
* Establish ownership for a task that just started executing.
|
|
85331
|
+
* - If the task carries an `assignedAgentId` pointing at a durable agent,
|
|
85332
|
+
* bind that agent to the task and flip it through active → running.
|
|
85333
|
+
* - Otherwise spawn (or reclaim) an ephemeral `executor-${task.id}` worker.
|
|
85334
|
+
*
|
|
85335
|
+
* Cross-restart safe: looks up an existing ephemeral by name before
|
|
85336
|
+
* creating a new one.
|
|
85337
|
+
*/
|
|
85338
|
+
async onTaskStart(task) {
|
|
85339
|
+
try {
|
|
85340
|
+
const assignedAgentId = task.assignedAgentId;
|
|
85341
|
+
if (assignedAgentId) {
|
|
85342
|
+
const assignedAgent = await this.agentStore.getAgent(assignedAgentId);
|
|
85343
|
+
if (assignedAgent && !isEphemeralAgent(assignedAgent)) {
|
|
85344
|
+
this.taskAgentMap.set(task.id, { agentId: assignedAgent.id, ephemeral: false });
|
|
85345
|
+
await this.agentStore.syncExecutionTaskLink(assignedAgent.id, task.id);
|
|
85346
|
+
const currentState = assignedAgent.state;
|
|
85347
|
+
if (currentState !== "running") {
|
|
85348
|
+
if (currentState !== "active") {
|
|
85349
|
+
await this.agentStore.updateAgentState(assignedAgent.id, "active");
|
|
85350
|
+
}
|
|
85351
|
+
await this.agentStore.updateAgentState(assignedAgent.id, "running");
|
|
85352
|
+
}
|
|
85353
|
+
return { agentId: assignedAgent.id, ephemeral: false };
|
|
85354
|
+
}
|
|
85355
|
+
}
|
|
85356
|
+
const cached = this.taskAgentMap.get(task.id);
|
|
85357
|
+
if (cached) {
|
|
85358
|
+
this.log.warn(`Skipping task-worker creation for ${task.id}: task already has execution owner`);
|
|
85359
|
+
return cached;
|
|
85360
|
+
}
|
|
85361
|
+
const existing = await this.lookupExistingByName(`executor-${task.id}`);
|
|
85362
|
+
if (existing) {
|
|
85363
|
+
if (existing.taskId === task.id) {
|
|
85364
|
+
this.taskAgentMap.set(task.id, { agentId: existing.id, ephemeral: true });
|
|
85365
|
+
this.log.log(`Reusing existing ephemeral worker ${existing.id} for task ${task.id} after restart`);
|
|
85366
|
+
return { agentId: existing.id, ephemeral: true };
|
|
85367
|
+
}
|
|
85368
|
+
try {
|
|
85369
|
+
await this.agentStore.deleteAgent(existing.id);
|
|
85370
|
+
this.log.log(`Deleted stale ephemeral worker ${existing.id} for task ${task.id} before respawn`);
|
|
85371
|
+
} catch (delErr) {
|
|
85372
|
+
this.log.warn(`Failed to delete stale ephemeral worker ${existing.id} for ${task.id}:`, delErr);
|
|
85373
|
+
}
|
|
85374
|
+
}
|
|
85375
|
+
const agent = await this.agentStore.createAgent({
|
|
85376
|
+
name: `executor-${task.id}`,
|
|
85377
|
+
role: "executor",
|
|
85378
|
+
metadata: {
|
|
85379
|
+
agentKind: "task-worker",
|
|
85380
|
+
taskWorker: true,
|
|
85381
|
+
managedBy: "task-executor"
|
|
85382
|
+
},
|
|
85383
|
+
runtimeConfig: { enabled: false }
|
|
85384
|
+
});
|
|
85385
|
+
this.taskAgentMap.set(task.id, { agentId: agent.id, ephemeral: true });
|
|
85386
|
+
await this.agentStore.assignTask(agent.id, task.id);
|
|
85387
|
+
await this.agentStore.updateAgentState(agent.id, "active");
|
|
85388
|
+
await this.agentStore.updateAgentState(agent.id, "running");
|
|
85389
|
+
return { agentId: agent.id, ephemeral: true };
|
|
85390
|
+
} catch (err) {
|
|
85391
|
+
this.log.warn(`Failed to initialize execution owner for task ${task.id}:`, err);
|
|
85392
|
+
return null;
|
|
85393
|
+
}
|
|
85394
|
+
}
|
|
85395
|
+
/**
|
|
85396
|
+
* Tear down ownership after a task completes or errors.
|
|
85397
|
+
* Final state for durable agents matches the outcome (idle/error).
|
|
85398
|
+
* Ephemeral workers are deleted regardless; if the in-memory owner is
|
|
85399
|
+
* missing (e.g. restart between onStart and this callback), falls back
|
|
85400
|
+
* to a name-based lookup so the worker still gets cleaned up.
|
|
85401
|
+
*/
|
|
85402
|
+
async onTaskComplete(taskId) {
|
|
85403
|
+
return this.finalize(taskId, "active", "completion");
|
|
85404
|
+
}
|
|
85405
|
+
async onTaskError(taskId) {
|
|
85406
|
+
return this.finalize(taskId, "error", "error");
|
|
85407
|
+
}
|
|
85408
|
+
/**
|
|
85409
|
+
* Listener for agent:stateChanged. Cleans up ephemerals that get halted
|
|
85410
|
+
* out-of-band — e.g. by HeartbeatMonitor flipping them to paused/error
|
|
85411
|
+
* outside the onComplete/onError callbacks.
|
|
85412
|
+
*
|
|
85413
|
+
* Returns the listener fn so the caller can detach it on shutdown.
|
|
85414
|
+
*/
|
|
85415
|
+
attachStateChangeListener() {
|
|
85416
|
+
if (this.stateChangeListener) return this.stateChangeListener;
|
|
85417
|
+
const listener = (agentId, from, to) => {
|
|
85418
|
+
if (to !== "paused" && to !== "error") return;
|
|
85419
|
+
if (from === to) return;
|
|
85420
|
+
if (this.pendingDeletions.has(agentId) || this.isDeletionPendingExternal(agentId)) return;
|
|
85421
|
+
void (async () => {
|
|
85422
|
+
try {
|
|
85423
|
+
const agent = await this.agentStore.getAgent(agentId);
|
|
85424
|
+
if (!agent) return;
|
|
85425
|
+
const isWorkerLike = isEphemeralAgent(agent) || agent.metadata?.taskWorker === true || agent.metadata?.agentKind === "task-worker" || agent.metadata?.agentKind === "spawned";
|
|
85426
|
+
if (!isWorkerLike) return;
|
|
85427
|
+
await this.deleteEphemeralAgent(agentId, "halt-listener");
|
|
85428
|
+
} catch (err) {
|
|
85429
|
+
this.log.warn(`Failed to process halt event for agent ${agentId}: ${this.formatError(err)}`);
|
|
85430
|
+
}
|
|
85431
|
+
})();
|
|
85432
|
+
};
|
|
85433
|
+
this.stateChangeListener = listener;
|
|
85434
|
+
this.agentStore.on("agent:stateChanged", listener);
|
|
85435
|
+
return listener;
|
|
85436
|
+
}
|
|
85437
|
+
detachStateChangeListener() {
|
|
85438
|
+
if (!this.stateChangeListener) return;
|
|
85439
|
+
this.agentStore.off("agent:stateChanged", this.stateChangeListener);
|
|
85440
|
+
this.stateChangeListener = void 0;
|
|
85441
|
+
}
|
|
85442
|
+
/**
|
|
85443
|
+
* Startup sweep. Returns the count of zombies cleaned up. Best-effort —
|
|
85444
|
+
* failures are logged and skipped so they never block runtime startup.
|
|
85445
|
+
*
|
|
85446
|
+
* Survivors after this pass: agents bound to a still-in-progress task.
|
|
85447
|
+
* Anything else (no taskId, terminal task column, or halted state) is
|
|
85448
|
+
* by definition a leak.
|
|
85449
|
+
*/
|
|
85450
|
+
async reconcileOrphaned() {
|
|
85451
|
+
let cleanedCount = 0;
|
|
85452
|
+
try {
|
|
85453
|
+
const allAgents = await this.agentStore.listAgents({ includeEphemeral: true });
|
|
85454
|
+
for (const agent of allAgents) {
|
|
85455
|
+
if (!isEphemeralAgent(agent)) continue;
|
|
85456
|
+
if (!await this.shouldDeleteOnSweep(agent)) continue;
|
|
85457
|
+
try {
|
|
85458
|
+
await this.agentStore.deleteAgent(agent.id);
|
|
85459
|
+
cleanedCount += 1;
|
|
85460
|
+
} catch (err) {
|
|
85461
|
+
if (this.isBenignDeleteRace(agent.id, err)) {
|
|
85462
|
+
cleanedCount += 1;
|
|
85463
|
+
continue;
|
|
85464
|
+
}
|
|
85465
|
+
this.log.warn(`Startup sweep failed to delete ephemeral agent ${agent.id}: ${this.formatError(err)}`);
|
|
85466
|
+
}
|
|
85467
|
+
}
|
|
85468
|
+
} catch (err) {
|
|
85469
|
+
this.log.warn(`Startup ephemeral sweep failed: ${this.formatError(err)}`);
|
|
85470
|
+
}
|
|
85471
|
+
if (cleanedCount > 0) {
|
|
85472
|
+
this.log.log(`Startup ephemeral sweep cleaned ${cleanedCount} orphaned agent(s)`);
|
|
85473
|
+
}
|
|
85474
|
+
return cleanedCount;
|
|
85475
|
+
}
|
|
85476
|
+
/** Drop in-memory state. Call on runtime stop. */
|
|
85477
|
+
reset() {
|
|
85478
|
+
this.taskAgentMap.clear();
|
|
85479
|
+
this.pendingDeletions.clear();
|
|
85480
|
+
}
|
|
85481
|
+
/** True if a delete is in flight; lets external callers avoid double-delete races. */
|
|
85482
|
+
isDeletionPending(agentId) {
|
|
85483
|
+
return this.pendingDeletions.has(agentId);
|
|
85484
|
+
}
|
|
85485
|
+
getOwner(taskId) {
|
|
85486
|
+
return this.taskAgentMap.get(taskId);
|
|
85487
|
+
}
|
|
85488
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
85489
|
+
async finalize(taskId, terminalState, reason) {
|
|
85490
|
+
const owner = this.taskAgentMap.get(taskId) ?? await this.recoverOwnerFromDisk(taskId);
|
|
85491
|
+
if (!owner) return;
|
|
85492
|
+
const { agentId, ephemeral } = owner;
|
|
85493
|
+
if (ephemeral) {
|
|
85494
|
+
this.pendingDeletions.add(agentId);
|
|
85495
|
+
}
|
|
85496
|
+
try {
|
|
85497
|
+
await this.agentStore.updateAgentState(agentId, terminalState);
|
|
85498
|
+
} catch (err) {
|
|
85499
|
+
this.log.warn(`Failed to update agent ${agentId} to ${terminalState} (${reason}): ${this.formatError(err)}`);
|
|
85500
|
+
}
|
|
85501
|
+
try {
|
|
85502
|
+
await this.agentStore.syncExecutionTaskLink(agentId, void 0);
|
|
85503
|
+
} catch (err) {
|
|
85504
|
+
this.log.warn(`Failed to clear execution task link for agent ${agentId} on ${reason}: ${this.formatError(err)}`);
|
|
85505
|
+
}
|
|
85506
|
+
this.taskAgentMap.delete(taskId);
|
|
85507
|
+
if (!ephemeral) return;
|
|
85508
|
+
try {
|
|
85509
|
+
await this.agentStore.deleteAgent(agentId);
|
|
85510
|
+
} catch (err) {
|
|
85511
|
+
if (this.isBenignDeleteRace(agentId, err)) return;
|
|
85512
|
+
this.log.warn(`Failed to delete agent ${agentId} after ${reason}: ${this.formatError(err)}`);
|
|
85513
|
+
} finally {
|
|
85514
|
+
this.pendingDeletions.delete(agentId);
|
|
85515
|
+
}
|
|
85516
|
+
}
|
|
85517
|
+
/**
|
|
85518
|
+
* Look up the ephemeral worker on disk when the in-memory map has no
|
|
85519
|
+
* record. Covers the cross-restart case where onComplete fires in a
|
|
85520
|
+
* different process session than the onStart that created the worker.
|
|
85521
|
+
*/
|
|
85522
|
+
async recoverOwnerFromDisk(taskId) {
|
|
85523
|
+
try {
|
|
85524
|
+
const candidate = await this.lookupExistingByName(`executor-${taskId}`);
|
|
85525
|
+
if (candidate) {
|
|
85526
|
+
this.log.log(`Recovered ephemeral owner ${candidate.id} for task ${taskId} from disk (cross-restart)`);
|
|
85527
|
+
return { agentId: candidate.id, ephemeral: true };
|
|
85528
|
+
}
|
|
85529
|
+
} catch (err) {
|
|
85530
|
+
this.log.warn(`Cross-restart owner lookup failed for task ${taskId}: ${this.formatError(err)}`);
|
|
85531
|
+
}
|
|
85532
|
+
return null;
|
|
85533
|
+
}
|
|
85534
|
+
async lookupExistingByName(name) {
|
|
85535
|
+
try {
|
|
85536
|
+
const found = await this.agentStore.findAgentByName(name);
|
|
85537
|
+
if (found && isEphemeralAgent(found)) return found;
|
|
85538
|
+
return null;
|
|
85539
|
+
} catch (err) {
|
|
85540
|
+
this.log.warn(`findAgentByName(${name}) failed: ${this.formatError(err)}`);
|
|
85541
|
+
return null;
|
|
85542
|
+
}
|
|
85543
|
+
}
|
|
85544
|
+
async shouldDeleteOnSweep(agent) {
|
|
85545
|
+
if (agent.state === "paused" || agent.state === "error") return true;
|
|
85546
|
+
if (!agent.taskId) return true;
|
|
85547
|
+
try {
|
|
85548
|
+
const task = await this.taskStore.getTask(agent.taskId);
|
|
85549
|
+
if (!task) return true;
|
|
85550
|
+
if (TERMINAL_TASK_COLUMNS.has(task.column)) return true;
|
|
85551
|
+
return task.column !== "in-progress";
|
|
85552
|
+
} catch {
|
|
85553
|
+
return true;
|
|
85554
|
+
}
|
|
85555
|
+
}
|
|
85556
|
+
async deleteEphemeralAgent(agentId, reason) {
|
|
85557
|
+
if (this.pendingDeletions.has(agentId)) return;
|
|
85558
|
+
this.pendingDeletions.add(agentId);
|
|
85559
|
+
try {
|
|
85560
|
+
await this.agentStore.deleteAgent(agentId);
|
|
85561
|
+
} catch (err) {
|
|
85562
|
+
if (this.isBenignDeleteRace(agentId, err)) return;
|
|
85563
|
+
this.log.warn(`Failed to delete ephemeral agent ${agentId} (${reason}): ${this.formatError(err)}`);
|
|
85564
|
+
} finally {
|
|
85565
|
+
this.pendingDeletions.delete(agentId);
|
|
85566
|
+
}
|
|
85567
|
+
}
|
|
85568
|
+
isBenignDeleteRace(agentId, err) {
|
|
85569
|
+
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
|
|
85570
|
+
if (msg.includes("already deleted") || msg.includes("already removed")) return true;
|
|
85571
|
+
if (msg.includes(`agent ${agentId.toLowerCase()} not found`)) return true;
|
|
85572
|
+
return false;
|
|
85573
|
+
}
|
|
85574
|
+
formatError(err) {
|
|
85575
|
+
return err instanceof Error ? err.message : String(err);
|
|
85576
|
+
}
|
|
85577
|
+
};
|
|
85578
|
+
}
|
|
85579
|
+
});
|
|
85580
|
+
|
|
84754
85581
|
// ../engine/src/runtimes/in-process-runtime.ts
|
|
84755
85582
|
import { EventEmitter as EventEmitter19 } from "node:events";
|
|
84756
85583
|
var InProcessRuntime;
|
|
@@ -84773,6 +85600,7 @@ var init_in_process_runtime = __esm({
|
|
|
84773
85600
|
init_mission_autopilot();
|
|
84774
85601
|
init_mission_execution_loop();
|
|
84775
85602
|
init_triage();
|
|
85603
|
+
init_ephemeral_worker_manager();
|
|
84776
85604
|
InProcessRuntime = class extends EventEmitter19 {
|
|
84777
85605
|
/**
|
|
84778
85606
|
* @param config - Runtime configuration
|
|
@@ -84797,8 +85625,12 @@ var init_in_process_runtime = __esm({
|
|
|
84797
85625
|
agentStore;
|
|
84798
85626
|
heartbeatMonitor;
|
|
84799
85627
|
triggerScheduler;
|
|
84800
|
-
/**
|
|
84801
|
-
|
|
85628
|
+
/**
|
|
85629
|
+
* Coordinates the ephemeral task-worker lifecycle (spawn dedup, finalize,
|
|
85630
|
+
* halt-listener cleanup, startup sweep). See `ephemeral-worker-manager.ts`.
|
|
85631
|
+
* Created once the AgentStore is available; guard call sites with `?`.
|
|
85632
|
+
*/
|
|
85633
|
+
workerManager;
|
|
84802
85634
|
lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
84803
85635
|
pluginRunner;
|
|
84804
85636
|
pluginStore;
|
|
@@ -84811,10 +85643,6 @@ var init_in_process_runtime = __esm({
|
|
|
84811
85643
|
triageProcessor;
|
|
84812
85644
|
messageStore;
|
|
84813
85645
|
concurrencyChangedListener;
|
|
84814
|
-
/** Set of agent IDs with in-flight ephemeral cleanup (prevents duplicate deletion) */
|
|
84815
|
-
pendingEphemeralDeletions = /* @__PURE__ */ new Set();
|
|
84816
|
-
/** Listener for agent:stateChanged events to clean up terminated ephemeral agents */
|
|
84817
|
-
ephemeralTerminationListener;
|
|
84818
85646
|
/**
|
|
84819
85647
|
* Optional callback the runtime forwards to SelfHealingManager so that
|
|
84820
85648
|
* stale-merge recovery can re-enqueue tasks immediately. Set by ProjectEngine
|
|
@@ -84844,7 +85672,7 @@ var init_in_process_runtime = __esm({
|
|
|
84844
85672
|
runtimeLog.log(`Starting InProcessRuntime for project ${this.config.projectId}`);
|
|
84845
85673
|
try {
|
|
84846
85674
|
const {
|
|
84847
|
-
TaskStore:
|
|
85675
|
+
TaskStore: TaskStore3,
|
|
84848
85676
|
PluginStore: PluginStoreClass,
|
|
84849
85677
|
PluginLoader: PluginLoaderClass,
|
|
84850
85678
|
MessageStore: MessageStoreClass
|
|
@@ -84853,7 +85681,7 @@ var init_in_process_runtime = __esm({
|
|
|
84853
85681
|
this.taskStore = this.config.externalTaskStore;
|
|
84854
85682
|
runtimeLog.log(`TaskStore provided externally for project ${this.config.projectId}`);
|
|
84855
85683
|
} else {
|
|
84856
|
-
this.taskStore = new
|
|
85684
|
+
this.taskStore = new TaskStore3(this.config.workingDirectory);
|
|
84857
85685
|
await this.taskStore.init();
|
|
84858
85686
|
runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
|
|
84859
85687
|
}
|
|
@@ -85022,84 +85850,13 @@ var init_in_process_runtime = __esm({
|
|
|
85022
85850
|
onStart: (task, worktreePath) => {
|
|
85023
85851
|
this.recordActivity();
|
|
85024
85852
|
runtimeLog.log(`Started executing task ${task.id} in ${worktreePath}`);
|
|
85025
|
-
|
|
85026
|
-
void (async () => {
|
|
85027
|
-
try {
|
|
85028
|
-
const assignedAgentId = task.assignedAgentId;
|
|
85029
|
-
if (assignedAgentId) {
|
|
85030
|
-
const assignedAgent = await this.agentStore.getAgent(assignedAgentId);
|
|
85031
|
-
if (assignedAgent && !isEphemeralAgent(assignedAgent)) {
|
|
85032
|
-
this.taskAgentMap.set(task.id, { agentId: assignedAgent.id, ephemeral: false });
|
|
85033
|
-
await this.agentStore.syncExecutionTaskLink(assignedAgent.id, task.id);
|
|
85034
|
-
const currentState = assignedAgent.state;
|
|
85035
|
-
if (currentState !== "running") {
|
|
85036
|
-
if (currentState !== "active") {
|
|
85037
|
-
await this.agentStore.updateAgentState(assignedAgent.id, "active");
|
|
85038
|
-
}
|
|
85039
|
-
await this.agentStore.updateAgentState(assignedAgent.id, "running");
|
|
85040
|
-
}
|
|
85041
|
-
return;
|
|
85042
|
-
}
|
|
85043
|
-
}
|
|
85044
|
-
if (this.taskAgentMap.has(task.id)) {
|
|
85045
|
-
runtimeLog.warn(`Skipping task-worker creation for ${task.id}: task already has execution owner`);
|
|
85046
|
-
return;
|
|
85047
|
-
}
|
|
85048
|
-
const agent = await this.agentStore.createAgent({
|
|
85049
|
-
name: `executor-${task.id}`,
|
|
85050
|
-
role: "executor",
|
|
85051
|
-
metadata: {
|
|
85052
|
-
agentKind: "task-worker",
|
|
85053
|
-
taskWorker: true,
|
|
85054
|
-
managedBy: "task-executor"
|
|
85055
|
-
},
|
|
85056
|
-
runtimeConfig: {
|
|
85057
|
-
enabled: false
|
|
85058
|
-
}
|
|
85059
|
-
});
|
|
85060
|
-
this.taskAgentMap.set(task.id, { agentId: agent.id, ephemeral: true });
|
|
85061
|
-
await this.agentStore.assignTask(agent.id, task.id);
|
|
85062
|
-
await this.agentStore.updateAgentState(agent.id, "active");
|
|
85063
|
-
await this.agentStore.updateAgentState(agent.id, "running");
|
|
85064
|
-
} catch (err) {
|
|
85065
|
-
runtimeLog.warn(`Failed to initialize execution owner for task ${task.id}:`, err);
|
|
85066
|
-
}
|
|
85067
|
-
})();
|
|
85853
|
+
void this.workerManager?.onTaskStart(task);
|
|
85068
85854
|
},
|
|
85069
85855
|
onComplete: (task) => {
|
|
85070
85856
|
this.recordActivity();
|
|
85071
85857
|
runtimeLog.log(`Completed task ${task.id}`);
|
|
85072
85858
|
this.recordTaskCompletion(task.id, true);
|
|
85073
|
-
|
|
85074
|
-
if (owner && this.agentStore) {
|
|
85075
|
-
const { agentId, ephemeral } = owner;
|
|
85076
|
-
if (ephemeral) {
|
|
85077
|
-
this.pendingEphemeralDeletions.add(agentId);
|
|
85078
|
-
}
|
|
85079
|
-
void this.agentStore.updateAgentState(agentId, "terminated").catch((err) => {
|
|
85080
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85081
|
-
runtimeLog.warn(`Failed to update agent ${agentId} state to terminated (completion): ${msg}`);
|
|
85082
|
-
});
|
|
85083
|
-
void this.agentStore.syncExecutionTaskLink(agentId, void 0).catch((err) => {
|
|
85084
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85085
|
-
runtimeLog.warn(`Failed to clear execution task link for agent ${agentId} on completion: ${msg}`);
|
|
85086
|
-
});
|
|
85087
|
-
this.taskAgentMap.delete(task.id);
|
|
85088
|
-
if (!ephemeral) return;
|
|
85089
|
-
void (async () => {
|
|
85090
|
-
try {
|
|
85091
|
-
await this.agentStore?.deleteAgent(agentId);
|
|
85092
|
-
} catch (err) {
|
|
85093
|
-
if (this.isBenignEphemeralDeleteRaceError(agentId, err)) {
|
|
85094
|
-
return;
|
|
85095
|
-
}
|
|
85096
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85097
|
-
runtimeLog.warn(`Failed to delete agent ${agentId} after completion: ${msg}`);
|
|
85098
|
-
} finally {
|
|
85099
|
-
this.pendingEphemeralDeletions.delete(agentId);
|
|
85100
|
-
}
|
|
85101
|
-
})();
|
|
85102
|
-
}
|
|
85859
|
+
void this.workerManager?.onTaskComplete(task.id);
|
|
85103
85860
|
},
|
|
85104
85861
|
onError: (task, error) => {
|
|
85105
85862
|
this.recordActivity();
|
|
@@ -85117,36 +85874,7 @@ var init_in_process_runtime = __esm({
|
|
|
85117
85874
|
}
|
|
85118
85875
|
})();
|
|
85119
85876
|
}
|
|
85120
|
-
|
|
85121
|
-
if (owner && this.agentStore) {
|
|
85122
|
-
const { agentId, ephemeral } = owner;
|
|
85123
|
-
if (ephemeral) {
|
|
85124
|
-
this.pendingEphemeralDeletions.add(agentId);
|
|
85125
|
-
}
|
|
85126
|
-
void this.agentStore.updateAgentState(agentId, "terminated").catch((err) => {
|
|
85127
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85128
|
-
runtimeLog.warn(`Failed to update agent ${agentId} state to terminated (error): ${msg}`);
|
|
85129
|
-
});
|
|
85130
|
-
void this.agentStore.syncExecutionTaskLink(agentId, void 0).catch((err) => {
|
|
85131
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85132
|
-
runtimeLog.warn(`Failed to clear execution task link for agent ${agentId} on error: ${msg}`);
|
|
85133
|
-
});
|
|
85134
|
-
this.taskAgentMap.delete(task.id);
|
|
85135
|
-
if (!ephemeral) return;
|
|
85136
|
-
void (async () => {
|
|
85137
|
-
try {
|
|
85138
|
-
await this.agentStore?.deleteAgent(agentId);
|
|
85139
|
-
} catch (err) {
|
|
85140
|
-
if (this.isBenignEphemeralDeleteRaceError(agentId, err)) {
|
|
85141
|
-
return;
|
|
85142
|
-
}
|
|
85143
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85144
|
-
runtimeLog.warn(`Failed to delete agent ${agentId} after error: ${msg}`);
|
|
85145
|
-
} finally {
|
|
85146
|
-
this.pendingEphemeralDeletions.delete(agentId);
|
|
85147
|
-
}
|
|
85148
|
-
})();
|
|
85149
|
-
}
|
|
85877
|
+
void this.workerManager?.onTaskError(task.id);
|
|
85150
85878
|
}
|
|
85151
85879
|
};
|
|
85152
85880
|
this.executor = new TaskExecutor(
|
|
@@ -85173,6 +85901,13 @@ var init_in_process_runtime = __esm({
|
|
|
85173
85901
|
},
|
|
85174
85902
|
onTerminated: (agentId, reason) => {
|
|
85175
85903
|
runtimeLog.warn(`Agent ${agentId} terminated (unresponsive): ${reason}`);
|
|
85904
|
+
},
|
|
85905
|
+
onRunCompleted: (agentId) => {
|
|
85906
|
+
if (this.executor) {
|
|
85907
|
+
void this.executor.resumeTaskForAgent(agentId).catch((err) => {
|
|
85908
|
+
runtimeLog.warn(`resumeTaskForAgent failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
85909
|
+
});
|
|
85910
|
+
}
|
|
85176
85911
|
}
|
|
85177
85912
|
});
|
|
85178
85913
|
this.heartbeatMonitor.start();
|
|
@@ -85192,83 +85927,24 @@ var init_in_process_runtime = __esm({
|
|
|
85192
85927
|
contextSnapshot: { ...context }
|
|
85193
85928
|
});
|
|
85194
85929
|
},
|
|
85195
|
-
this.taskStore
|
|
85930
|
+
this.taskStore,
|
|
85931
|
+
{ isTaskExecuting: (taskId) => this.executor.getExecutingTaskIds().has(taskId) }
|
|
85196
85932
|
);
|
|
85197
85933
|
this.triggerScheduler.start();
|
|
85198
85934
|
const isHeartbeatEnabledAgent = (agent) => !isEphemeralAgent(agent) && agent.runtimeConfig?.enabled !== false;
|
|
85199
85935
|
const isTickableHeartbeatState = (state) => state === "active" || state === "running" || state === "idle";
|
|
85200
85936
|
const isTimerManagedAgent = (agent) => isHeartbeatEnabledAgent(agent) && isTickableHeartbeatState(agent.state);
|
|
85201
|
-
this.
|
|
85202
|
-
|
|
85203
|
-
|
|
85204
|
-
|
|
85205
|
-
|
|
85206
|
-
|
|
85207
|
-
|
|
85208
|
-
|
|
85209
|
-
|
|
85210
|
-
|
|
85211
|
-
|
|
85212
|
-
await this.agentStore?.deleteAgent(agentId);
|
|
85213
|
-
} catch (err) {
|
|
85214
|
-
if (this.isBenignEphemeralDeleteRaceError(agentId, err)) {
|
|
85215
|
-
return;
|
|
85216
|
-
}
|
|
85217
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85218
|
-
runtimeLog.warn(`Failed to delete ephemeral agent ${agentId} after termination: ${msg}`);
|
|
85219
|
-
} finally {
|
|
85220
|
-
this.pendingEphemeralDeletions.delete(agentId);
|
|
85221
|
-
}
|
|
85222
|
-
} catch (err) {
|
|
85223
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85224
|
-
runtimeLog.warn(`Failed to process termination event for agent ${agentId}: ${msg}`);
|
|
85225
|
-
}
|
|
85226
|
-
})();
|
|
85227
|
-
};
|
|
85228
|
-
this.agentStore.on("agent:stateChanged", this.ephemeralTerminationListener);
|
|
85229
|
-
try {
|
|
85230
|
-
const allAgents = await this.agentStore.listAgents({ includeEphemeral: true });
|
|
85231
|
-
let cleanedCount = 0;
|
|
85232
|
-
for (const agent of allAgents) {
|
|
85233
|
-
if (!isEphemeralAgent(agent)) continue;
|
|
85234
|
-
let shouldDelete = agent.state === "terminated" || agent.state === "error";
|
|
85235
|
-
if (!shouldDelete && agent.taskId) {
|
|
85236
|
-
try {
|
|
85237
|
-
const task = await this.taskStore.getTask(agent.taskId);
|
|
85238
|
-
if (!task || task.column !== "in-progress") {
|
|
85239
|
-
shouldDelete = true;
|
|
85240
|
-
}
|
|
85241
|
-
} catch {
|
|
85242
|
-
shouldDelete = true;
|
|
85243
|
-
}
|
|
85244
|
-
}
|
|
85245
|
-
if (!shouldDelete) continue;
|
|
85246
|
-
try {
|
|
85247
|
-
if (agent.state !== "terminated") {
|
|
85248
|
-
await this.agentStore.updateAgentState(agent.id, "terminated");
|
|
85249
|
-
}
|
|
85250
|
-
} catch (err) {
|
|
85251
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85252
|
-
runtimeLog.warn(`Startup sweep failed to set ephemeral agent ${agent.id} terminated: ${msg}`);
|
|
85253
|
-
}
|
|
85254
|
-
try {
|
|
85255
|
-
await this.agentStore.deleteAgent(agent.id);
|
|
85256
|
-
cleanedCount += 1;
|
|
85257
|
-
} catch (err) {
|
|
85258
|
-
if (this.isBenignEphemeralDeleteRaceError(agent.id, err)) {
|
|
85259
|
-
cleanedCount += 1;
|
|
85260
|
-
continue;
|
|
85261
|
-
}
|
|
85262
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85263
|
-
runtimeLog.warn(`Startup sweep failed to delete ephemeral agent ${agent.id}: ${msg}`);
|
|
85264
|
-
}
|
|
85265
|
-
}
|
|
85266
|
-
if (cleanedCount > 0) {
|
|
85267
|
-
runtimeLog.log(`Startup ephemeral sweep cleaned ${cleanedCount} orphaned agent(s)`);
|
|
85268
|
-
}
|
|
85269
|
-
} catch (err) {
|
|
85270
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85271
|
-
runtimeLog.warn(`Startup ephemeral sweep failed (continuing): ${msg}`);
|
|
85937
|
+
if (this.agentStore && !this.workerManager) {
|
|
85938
|
+
this.workerManager = new EphemeralWorkerManager({
|
|
85939
|
+
agentStore: this.agentStore,
|
|
85940
|
+
taskStore: this.taskStore,
|
|
85941
|
+
logger: runtimeLog,
|
|
85942
|
+
isDeletionPendingExternal: (agentId) => this.executor?.isEphemeralDeletionPending(agentId) ?? false
|
|
85943
|
+
});
|
|
85944
|
+
}
|
|
85945
|
+
if (this.workerManager) {
|
|
85946
|
+
this.workerManager.attachStateChangeListener();
|
|
85947
|
+
await this.workerManager.reconcileOrphaned();
|
|
85272
85948
|
}
|
|
85273
85949
|
try {
|
|
85274
85950
|
const agents = await this.agentStore.listAgents();
|
|
@@ -85439,12 +86115,8 @@ var init_in_process_runtime = __esm({
|
|
|
85439
86115
|
this.routineScheduler.stop();
|
|
85440
86116
|
runtimeLog.log("RoutineScheduler stopped");
|
|
85441
86117
|
}
|
|
85442
|
-
|
|
85443
|
-
|
|
85444
|
-
this.ephemeralTerminationListener = void 0;
|
|
85445
|
-
runtimeLog.log("AgentStore agent:stateChanged listener removed");
|
|
85446
|
-
}
|
|
85447
|
-
this.pendingEphemeralDeletions.clear();
|
|
86118
|
+
this.workerManager?.detachStateChangeListener();
|
|
86119
|
+
this.workerManager?.reset();
|
|
85448
86120
|
this.executor?.disposeEphemeralTimers();
|
|
85449
86121
|
if (this.triggerScheduler) {
|
|
85450
86122
|
this.triggerScheduler.stop();
|
|
@@ -85740,21 +86412,6 @@ var init_in_process_runtime = __esm({
|
|
|
85740
86412
|
});
|
|
85741
86413
|
runtimeLog.log("Event forwarding setup complete");
|
|
85742
86414
|
}
|
|
85743
|
-
/**
|
|
85744
|
-
* Returns true when an ephemeral delete failure is expected due to cleanup races
|
|
85745
|
-
* (for example the agent was already removed by a parallel cleanup path).
|
|
85746
|
-
*/
|
|
85747
|
-
isBenignEphemeralDeleteRaceError(agentId, err) {
|
|
85748
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
85749
|
-
const normalized = msg.toLowerCase();
|
|
85750
|
-
if (normalized.includes("already deleted") || normalized.includes("already removed")) {
|
|
85751
|
-
return true;
|
|
85752
|
-
}
|
|
85753
|
-
if (normalized.includes(`agent ${agentId.toLowerCase()} not found`)) {
|
|
85754
|
-
return true;
|
|
85755
|
-
}
|
|
85756
|
-
return /^agent\s+.+\s+not found$/i.test(msg.trim());
|
|
85757
|
-
}
|
|
85758
86415
|
/**
|
|
85759
86416
|
* Update status and emit health-changed event.
|
|
85760
86417
|
*/
|
|
@@ -89487,6 +90144,37 @@ ${detail}`
|
|
|
89487
90144
|
}
|
|
89488
90145
|
}
|
|
89489
90146
|
wireSettingsListeners(store) {
|
|
90147
|
+
const applyDetectorPauseLifecycle = (paused, source) => {
|
|
90148
|
+
try {
|
|
90149
|
+
const detector = this.runtime.stuckTaskDetector;
|
|
90150
|
+
if (paused) {
|
|
90151
|
+
detector?.pause?.();
|
|
90152
|
+
} else {
|
|
90153
|
+
detector?.resume?.();
|
|
90154
|
+
}
|
|
90155
|
+
} catch (err) {
|
|
90156
|
+
runtimeLog.warn(
|
|
90157
|
+
`${source}: stuck detector ${paused ? "pause" : "resume"} hook failed: ${err instanceof Error ? err.message : String(err)}`
|
|
90158
|
+
);
|
|
90159
|
+
}
|
|
90160
|
+
};
|
|
90161
|
+
const onPauseLifecycleTransition = ({
|
|
90162
|
+
settings: s,
|
|
90163
|
+
previous: prev
|
|
90164
|
+
}) => {
|
|
90165
|
+
const wasPaused = prev.globalPause || prev.enginePaused;
|
|
90166
|
+
const isPaused = s.globalPause || s.enginePaused;
|
|
90167
|
+
if (!wasPaused && isPaused) {
|
|
90168
|
+
const source = s.globalPause && !prev.globalPause ? "Global pause" : "Engine pause";
|
|
90169
|
+
applyDetectorPauseLifecycle(true, source);
|
|
90170
|
+
}
|
|
90171
|
+
if (wasPaused && !isPaused) {
|
|
90172
|
+
const source = prev.globalPause && !s.globalPause ? "Global unpause" : "Engine unpause";
|
|
90173
|
+
applyDetectorPauseLifecycle(false, source);
|
|
90174
|
+
}
|
|
90175
|
+
};
|
|
90176
|
+
store.on("settings:updated", onPauseLifecycleTransition);
|
|
90177
|
+
this.settingsHandlers.push(onPauseLifecycleTransition);
|
|
89490
90178
|
const onGlobalPause = ({ settings, previous }) => {
|
|
89491
90179
|
if (settings.globalPause && !previous.globalPause) {
|
|
89492
90180
|
if (this.mergeAbortController) {
|
|
@@ -100018,12 +100706,14 @@ var init_register_settings_sync_inbound_routes = __esm({
|
|
|
100018
100706
|
});
|
|
100019
100707
|
|
|
100020
100708
|
// ../dashboard/src/routes/register-agent-core-routes.ts
|
|
100709
|
+
var MAX_AVATAR_BYTES;
|
|
100021
100710
|
var init_register_agent_core_routes = __esm({
|
|
100022
100711
|
"../dashboard/src/routes/register-agent-core-routes.ts"() {
|
|
100023
100712
|
"use strict";
|
|
100024
100713
|
init_src();
|
|
100025
100714
|
init_api_error();
|
|
100026
100715
|
init_src2();
|
|
100716
|
+
MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
100027
100717
|
}
|
|
100028
100718
|
});
|
|
100029
100719
|
|
|
@@ -101056,6 +101746,7 @@ import { Router as Router3 } from "express";
|
|
|
101056
101746
|
var init_mission_routes = __esm({
|
|
101057
101747
|
"../dashboard/src/mission-routes.ts"() {
|
|
101058
101748
|
"use strict";
|
|
101749
|
+
init_src();
|
|
101059
101750
|
init_project_store_resolver();
|
|
101060
101751
|
init_src();
|
|
101061
101752
|
init_sse_buffer();
|
|
@@ -106938,11 +107629,69 @@ var INSIGHT_CATEGORIES = [
|
|
|
106938
107629
|
var INSIGHT_STATUSES = ["generated", "confirmed", "stale", "dismissed", "archived"];
|
|
106939
107630
|
var INSIGHT_RUN_STATUSES = ["pending", "running", "completed", "failed", "cancelled"];
|
|
106940
107631
|
var INSIGHT_RUN_TRIGGERS = ["schedule", "manual", "task_completion", "merge_event", "api"];
|
|
107632
|
+
function getTaskSourceAgentLabel(task) {
|
|
107633
|
+
const metadataAgentName = task.sourceMetadata?.agentName;
|
|
107634
|
+
if (typeof metadataAgentName === "string" && metadataAgentName.trim().length > 0) {
|
|
107635
|
+
return metadataAgentName.trim();
|
|
107636
|
+
}
|
|
107637
|
+
if (typeof task.sourceAgentId === "string" && task.sourceAgentId.trim().length > 0) {
|
|
107638
|
+
return task.sourceAgentId.trim();
|
|
107639
|
+
}
|
|
107640
|
+
return void 0;
|
|
107641
|
+
}
|
|
107642
|
+
function getTaskSourceLabel(task) {
|
|
107643
|
+
switch (task.sourceType) {
|
|
107644
|
+
case "dashboard_ui":
|
|
107645
|
+
return "Dashboard";
|
|
107646
|
+
case "quick_chat":
|
|
107647
|
+
return "Quick Chat";
|
|
107648
|
+
case "chat_session":
|
|
107649
|
+
return "Chat Session";
|
|
107650
|
+
case "agent_heartbeat": {
|
|
107651
|
+
const sourceAgent = getTaskSourceAgentLabel(task);
|
|
107652
|
+
return sourceAgent ? `Agent (${sourceAgent})` : "Agent";
|
|
107653
|
+
}
|
|
107654
|
+
case "automation": {
|
|
107655
|
+
const sourceAgent = getTaskSourceAgentLabel(task);
|
|
107656
|
+
return sourceAgent ? `Automation (${sourceAgent})` : "Automation";
|
|
107657
|
+
}
|
|
107658
|
+
case "cron":
|
|
107659
|
+
return "Scheduled Task";
|
|
107660
|
+
case "workflow_step":
|
|
107661
|
+
return "Workflow Step";
|
|
107662
|
+
case "github_import": {
|
|
107663
|
+
const issueUrl = task.sourceMetadata?.issueUrl;
|
|
107664
|
+
return typeof issueUrl === "string" && issueUrl.length > 0 ? `GitHub Import (${issueUrl})` : "GitHub Import";
|
|
107665
|
+
}
|
|
107666
|
+
case "research": {
|
|
107667
|
+
const findingLabel = task.sourceMetadata?.findingLabel;
|
|
107668
|
+
if (typeof findingLabel === "string" && findingLabel.length > 0) {
|
|
107669
|
+
return `Research (${findingLabel})`;
|
|
107670
|
+
}
|
|
107671
|
+
const runId = task.sourceMetadata?.runId;
|
|
107672
|
+
return typeof runId === "string" && runId.length > 0 ? `Research (${runId})` : "Research";
|
|
107673
|
+
}
|
|
107674
|
+
case "task_refine":
|
|
107675
|
+
return task.sourceParentTaskId ? `Refinement of ${task.sourceParentTaskId}` : "Refinement";
|
|
107676
|
+
case "task_duplicate":
|
|
107677
|
+
return task.sourceParentTaskId ? `Duplicate of ${task.sourceParentTaskId}` : "Duplicate";
|
|
107678
|
+
case "cli":
|
|
107679
|
+
return "CLI";
|
|
107680
|
+
case "api":
|
|
107681
|
+
return "API";
|
|
107682
|
+
case "recovery":
|
|
107683
|
+
return "Recovery";
|
|
107684
|
+
default:
|
|
107685
|
+
return void 0;
|
|
107686
|
+
}
|
|
107687
|
+
}
|
|
106941
107688
|
function formatTaskLine(t) {
|
|
106942
107689
|
const label = t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "\u2026" : "");
|
|
107690
|
+
const source = getTaskSourceLabel(t);
|
|
107691
|
+
const sourceSuffix = source ? ` [via: ${source}]` : "";
|
|
106943
107692
|
const deps = t.dependencies.length ? ` [deps: ${t.dependencies.join(", ")}]` : "";
|
|
106944
107693
|
const paused = t.paused ? " (paused)" : "";
|
|
106945
|
-
return `${t.id} ${label}${deps}${paused}`;
|
|
107694
|
+
return `${t.id} ${label}${sourceSuffix}${deps}${paused}`;
|
|
106946
107695
|
}
|
|
106947
107696
|
async function getResearchAvailability(store) {
|
|
106948
107697
|
const settings = await store.getSettings();
|
|
@@ -107278,6 +108027,10 @@ Column: triage
|
|
|
107278
108027
|
if (task.dependencies.length) {
|
|
107279
108028
|
lines.push(`Dependencies: ${task.dependencies.join(", ")}`);
|
|
107280
108029
|
}
|
|
108030
|
+
const sourceLabel = getTaskSourceLabel(task);
|
|
108031
|
+
if (sourceLabel) {
|
|
108032
|
+
lines.push(`Created via: ${sourceLabel}`);
|
|
108033
|
+
}
|
|
107281
108034
|
if (task.paused) lines.push("Status: PAUSED");
|
|
107282
108035
|
lines.push("");
|
|
107283
108036
|
if (task.steps.length > 0) {
|