@runfusion/fusion 0.9.0 → 0.9.1
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 +434 -103
- package/dist/client/assets/{AgentDetailView-Ca1euvPo.js → AgentDetailView-khrspqE3.js} +3 -3
- package/dist/client/assets/{AgentsView-pile-9ZM.js → AgentsView-DvN5EsoE.js} +11 -11
- package/dist/client/assets/AgentsView-MotzGhZJ.css +1 -0
- package/dist/client/assets/{ChatView-C4SCF_Hj.js → ChatView-DsETFRQp.js} +1 -1
- package/dist/client/assets/{DevServerView-1HvWligZ.js → DevServerView-D1_7SL1h.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-B1eztZna.js → DirectoryPicker-BBcm5G9F.js} +1 -1
- package/dist/client/assets/{DocumentsView-D4m9s6Ye.js → DocumentsView-OuVpmTPp.js} +1 -1
- package/dist/client/assets/InsightsView-4KiUKzbz.css +1 -0
- package/dist/client/assets/InsightsView-DMC_bLUc.js +11 -0
- package/dist/client/assets/{MemoryView-DEmn8fA2.js → MemoryView-cI4IK-lz.js} +1 -1
- package/dist/client/assets/{NodesView-Blg8NHiu.js → NodesView-C_LWLFNr.js} +2 -2
- package/dist/client/assets/{PiExtensionsManager-DkXenPK0.js → PiExtensionsManager-DCTHvy2u.js} +1 -1
- package/dist/client/assets/{PluginManager-Czso7ZUF.js → PluginManager-BXMpkZx-.js} +1 -1
- package/dist/client/assets/{RoadmapsView-DATopkaE.js → RoadmapsView-BHTePn2u.js} +1 -1
- package/dist/client/assets/SettingsModal-2s-L1oWD.js +31 -0
- package/dist/client/assets/{SettingsModal-D_mcRJO2.js → SettingsModal-BSZIno8y.js} +1 -1
- package/dist/client/assets/{SetupWizardModal-JBNr-XIW.js → SetupWizardModal-DVoRhy_V.js} +1 -1
- package/dist/client/assets/{SkillsView-CkT6-elZ.js → SkillsView-CK52SRz5.js} +1 -1
- package/dist/client/assets/{TodoView-CstzLvjw.js → TodoView-BKqIV8P6.js} +1 -1
- package/dist/client/assets/{folder-open-BEDPztlF.js → folder-open-C0SfzRFt.js} +1 -1
- package/dist/client/assets/index-DawWARY5.css +1 -0
- package/dist/client/assets/index-DiC9GfBH.js +656 -0
- package/dist/client/assets/{list-checks-DgZgg3rh.js → list-checks-D5c428sr.js} +1 -1
- package/dist/client/assets/{star-DXieIAlP.js → star-GGmbVi2b.js} +1 -1
- package/dist/client/assets/{upload-Cbvs_TSB.js → upload-CrJJybRJ.js} +1 -1
- package/dist/client/assets/{users-DAMIrlue.js → users-C56SMdh4.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/extension.js +427 -103
- package/dist/pi-claude-cli/package.json +1 -1
- package/package.json +1 -1
- package/dist/client/assets/AgentsView-Cq-SEhLc.css +0 -1
- package/dist/client/assets/InsightsView-6LHF7OdE.css +0 -1
- package/dist/client/assets/InsightsView-u535o96R.js +0 -11
- package/dist/client/assets/SettingsModal-Cs5qO84M.js +0 -31
- package/dist/client/assets/index-CPStj9Az.css +0 -1
- package/dist/client/assets/index-DZB5f-Bl.js +0 -656
package/dist/bin.js
CHANGED
|
@@ -1344,7 +1344,7 @@ function getAvailableTemplates(config) {
|
|
|
1344
1344
|
function getTemplatesForRole(role, config) {
|
|
1345
1345
|
return getAvailableTemplates(config).filter((t) => t.role === role);
|
|
1346
1346
|
}
|
|
1347
|
-
var EXECUTOR_PROMPT_TEXT, TRIAGE_PROMPT_TEXT, REVIEWER_PROMPT_TEXT, MERGER_BASE_PROMPT_TEXT, SENIOR_ENGINEER_PROMPT_TEXT, STRICT_REVIEWER_PROMPT_TEXT, CONCISE_TRIAGE_PROMPT_TEXT, BUILTIN_AGENT_PROMPTS;
|
|
1347
|
+
var EXECUTOR_PROMPT_TEXT, TRIAGE_PROMPT_TEXT, REVIEWER_PROMPT_TEXT, MERGER_BASE_PROMPT_TEXT, SENIOR_ENGINEER_PROMPT_TEXT, STRICT_REVIEWER_PROMPT_TEXT, CONCISE_TRIAGE_PROMPT_TEXT, EXECUTOR_HEARTBEAT_GUIDANCE, TRIAGE_HEARTBEAT_GUIDANCE, REVIEWER_HEARTBEAT_GUIDANCE, MERGER_HEARTBEAT_GUIDANCE, SENIOR_ENGINEER_HEARTBEAT_GUIDANCE, STRICT_REVIEWER_HEARTBEAT_GUIDANCE, CONCISE_TRIAGE_HEARTBEAT_GUIDANCE, BUILTIN_AGENT_PROMPTS;
|
|
1348
1348
|
var init_agent_prompts = __esm({
|
|
1349
1349
|
"../core/src/agent-prompts.ts"() {
|
|
1350
1350
|
"use strict";
|
|
@@ -2166,13 +2166,64 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2166
2166
|
5. **No placeholders:** Real content only.
|
|
2167
2167
|
6. **Read first:** Examine codebase before writing spec.
|
|
2168
2168
|
7. **Be concise:** Short descriptions, minimal prose. Focus on what matters.`;
|
|
2169
|
+
EXECUTOR_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2170
|
+
|
|
2171
|
+
Treat each heartbeat as a short autonomous execution cycle.
|
|
2172
|
+
|
|
2173
|
+
- If a task is assigned: inspect the latest task state, continue the next concrete implementation step, run the smallest useful verification, and either advance the task or log the blocker precisely.
|
|
2174
|
+
- If no task is assigned: execute your standing instructions. Review unread messages, scan for blocked or failing engineering work, create narrowly scoped follow-up tasks, and capture durable implementation notes other agents will need later.
|
|
2175
|
+
- Do not idle simply because no task is linked. Use heartbeat time to reduce engineering risk, unblock work, and keep execution moving in small, concrete increments.`;
|
|
2176
|
+
TRIAGE_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2177
|
+
|
|
2178
|
+
Use heartbeat runs to keep the planning pipeline healthy.
|
|
2179
|
+
|
|
2180
|
+
- If a task is assigned: turn the rough request into a complete, execution-ready PROMPT.md with clear scope, steps, dependencies, and verification criteria.
|
|
2181
|
+
- If no task is assigned: execute your planning instructions. Patrol for vague requests, blocked tasks that need better specification, review follow-ups that should become new tasks, and dependency gaps that are slowing executors down.
|
|
2182
|
+
- Favor ambiguity reduction over busywork. Every heartbeat should leave the queue more actionable than you found it.`;
|
|
2183
|
+
REVIEWER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2184
|
+
|
|
2185
|
+
Use heartbeat runs to keep review quality high and queues moving.
|
|
2186
|
+
|
|
2187
|
+
- If a task is assigned: perform the review with findings first, focusing on correctness, regressions, missing tests, and operational risk.
|
|
2188
|
+
- If no task is assigned: execute your review instructions. Look for work waiting on review, failed validations, suspicious recent changes, and places where a second pass would prevent a bad merge.
|
|
2189
|
+
- Prefer surfacing concrete findings, follow-up tasks, or merge blockers over rewriting implementation yourself.`;
|
|
2190
|
+
MERGER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2191
|
+
|
|
2192
|
+
Use heartbeat runs to keep merge-ready work from stalling.
|
|
2193
|
+
|
|
2194
|
+
- If a task is assigned: verify merge preconditions, resolve the next safe merge step, and surface conflicts or missing gates immediately.
|
|
2195
|
+
- If no task is assigned: execute your merge instructions. Inspect the in-review and merge-ready queue, look for unresolved conflicts, missing approvals, broken post-review state, and tasks that are ready for the final merge push.
|
|
2196
|
+
- Optimize for safe flow, not raw throughput. Clear blockers, communicate risks, and only move merge work forward when the repository stays trustworthy.`;
|
|
2197
|
+
SENIOR_ENGINEER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2198
|
+
|
|
2199
|
+
Treat each heartbeat as an autonomous senior-engineering pass.
|
|
2200
|
+
|
|
2201
|
+
- If a task is assigned: push the implementation forward decisively, making sound architectural choices, validating risky changes early, and documenting trade-offs that downstream agents should inherit.
|
|
2202
|
+
- If no task is assigned: execute your standing instructions. Hunt for architectural drift, flaky quality gates, latent integration risk, and follow-up work that needs a strong technical owner.
|
|
2203
|
+
- Spend heartbeat time where leverage is highest: unblock teams, reduce complexity, and turn vague engineering risk into concrete next actions.`;
|
|
2204
|
+
STRICT_REVIEWER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2205
|
+
|
|
2206
|
+
Use heartbeat runs to enforce a high review bar.
|
|
2207
|
+
|
|
2208
|
+
- If a task is assigned: review for worst-case failure modes first, especially security, backward compatibility, edge cases, and missing regression coverage.
|
|
2209
|
+
- If no task is assigned: execute your review instructions. Look for merges that feel under-reviewed, risky diffs that deserve another pass, and follow-up work needed before code should land.
|
|
2210
|
+
- Bias toward precise findings and explicit risk articulation. A quiet heartbeat should mean the code is genuinely clean, not that you stopped looking.`;
|
|
2211
|
+
CONCISE_TRIAGE_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
|
|
2212
|
+
|
|
2213
|
+
Keep heartbeat output lean and useful.
|
|
2214
|
+
|
|
2215
|
+
- If a task is assigned: produce the minimum complete PROMPT.md needed for an executor to act safely.
|
|
2216
|
+
- If no task is assigned: execute your planning instructions, scan for underspecified or blocked work, and turn it into short, actionable task specs or follow-up tickets.
|
|
2217
|
+
- Prefer crisp decisions, clear file scope, and concrete verification steps over narrative detail.`;
|
|
2169
2218
|
BUILTIN_AGENT_PROMPTS = [
|
|
2170
2219
|
{
|
|
2171
2220
|
id: "default-executor",
|
|
2172
2221
|
name: "Default Executor",
|
|
2173
2222
|
description: "Standard task execution agent with full tooling and review support.",
|
|
2174
2223
|
role: "executor",
|
|
2175
|
-
prompt: EXECUTOR_PROMPT_TEXT
|
|
2224
|
+
prompt: `${EXECUTOR_PROMPT_TEXT}
|
|
2225
|
+
|
|
2226
|
+
${EXECUTOR_HEARTBEAT_GUIDANCE}`,
|
|
2176
2227
|
builtIn: true
|
|
2177
2228
|
},
|
|
2178
2229
|
{
|
|
@@ -2180,7 +2231,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2180
2231
|
name: "Default Triage",
|
|
2181
2232
|
description: "Standard task specification agent producing detailed PROMPT.md files.",
|
|
2182
2233
|
role: "triage",
|
|
2183
|
-
prompt: TRIAGE_PROMPT_TEXT
|
|
2234
|
+
prompt: `${TRIAGE_PROMPT_TEXT}
|
|
2235
|
+
|
|
2236
|
+
${TRIAGE_HEARTBEAT_GUIDANCE}`,
|
|
2184
2237
|
builtIn: true
|
|
2185
2238
|
},
|
|
2186
2239
|
{
|
|
@@ -2188,7 +2241,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2188
2241
|
name: "Default Reviewer",
|
|
2189
2242
|
description: "Standard independent code and plan reviewer with balanced criteria.",
|
|
2190
2243
|
role: "reviewer",
|
|
2191
|
-
prompt: REVIEWER_PROMPT_TEXT
|
|
2244
|
+
prompt: `${REVIEWER_PROMPT_TEXT}
|
|
2245
|
+
|
|
2246
|
+
${REVIEWER_HEARTBEAT_GUIDANCE}`,
|
|
2192
2247
|
builtIn: true
|
|
2193
2248
|
},
|
|
2194
2249
|
{
|
|
@@ -2196,7 +2251,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2196
2251
|
name: "Default Merger",
|
|
2197
2252
|
description: "Standard merge agent for squash merges with conflict resolution.",
|
|
2198
2253
|
role: "merger",
|
|
2199
|
-
prompt: MERGER_BASE_PROMPT_TEXT
|
|
2254
|
+
prompt: `${MERGER_BASE_PROMPT_TEXT}
|
|
2255
|
+
|
|
2256
|
+
${MERGER_HEARTBEAT_GUIDANCE}`,
|
|
2200
2257
|
builtIn: true
|
|
2201
2258
|
},
|
|
2202
2259
|
{
|
|
@@ -2204,7 +2261,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2204
2261
|
name: "Senior Engineer",
|
|
2205
2262
|
description: "Autonomous executor with architectural awareness, performance focus, and minimal hand-holding. Makes independent decisions on routine matters.",
|
|
2206
2263
|
role: "executor",
|
|
2207
|
-
prompt: SENIOR_ENGINEER_PROMPT_TEXT
|
|
2264
|
+
prompt: `${SENIOR_ENGINEER_PROMPT_TEXT}
|
|
2265
|
+
|
|
2266
|
+
${SENIOR_ENGINEER_HEARTBEAT_GUIDANCE}`,
|
|
2208
2267
|
builtIn: true
|
|
2209
2268
|
},
|
|
2210
2269
|
{
|
|
@@ -2212,7 +2271,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2212
2271
|
name: "Strict Reviewer",
|
|
2213
2272
|
description: "Rigorous reviewer with stricter criteria for security, edge cases, backward compatibility, and type safety. Issues REVISE more readily.",
|
|
2214
2273
|
role: "reviewer",
|
|
2215
|
-
prompt: STRICT_REVIEWER_PROMPT_TEXT
|
|
2274
|
+
prompt: `${STRICT_REVIEWER_PROMPT_TEXT}
|
|
2275
|
+
|
|
2276
|
+
${STRICT_REVIEWER_HEARTBEAT_GUIDANCE}`,
|
|
2216
2277
|
builtIn: true
|
|
2217
2278
|
},
|
|
2218
2279
|
{
|
|
@@ -2220,7 +2281,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
|
|
|
2220
2281
|
name: "Concise Triage",
|
|
2221
2282
|
description: "Shorter, more focused specification format with minimal prose. Produces compact PROMPT.md files with essential information only.",
|
|
2222
2283
|
role: "triage",
|
|
2223
|
-
prompt: CONCISE_TRIAGE_PROMPT_TEXT
|
|
2284
|
+
prompt: `${CONCISE_TRIAGE_PROMPT_TEXT}
|
|
2285
|
+
|
|
2286
|
+
${CONCISE_TRIAGE_HEARTBEAT_GUIDANCE}`,
|
|
2224
2287
|
builtIn: true
|
|
2225
2288
|
}
|
|
2226
2289
|
];
|
|
@@ -31994,6 +32057,9 @@ ${newTask.description}
|
|
|
31994
32057
|
task.nextRecoveryAt = void 0;
|
|
31995
32058
|
}
|
|
31996
32059
|
await this.atomicWriteTaskJson(dir2, task);
|
|
32060
|
+
if (toColumn === "done") {
|
|
32061
|
+
this.clearLinkedAgentTaskIds(id, task.updatedAt);
|
|
32062
|
+
}
|
|
31997
32063
|
if (this.isWatching) this.taskCache.set(id, { ...task });
|
|
31998
32064
|
this.emit("task:moved", { task, from: fromColumn, to: toColumn });
|
|
31999
32065
|
return task;
|
|
@@ -32701,11 +32767,34 @@ ${task.description}
|
|
|
32701
32767
|
}
|
|
32702
32768
|
rewrittenDependents.push(updatedDependent);
|
|
32703
32769
|
}
|
|
32770
|
+
this.clearLinkedAgentTaskIds(taskId);
|
|
32704
32771
|
this.db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
|
|
32705
32772
|
this.db.bumpLastModified();
|
|
32706
32773
|
});
|
|
32707
32774
|
return rewrittenDependents;
|
|
32708
32775
|
}
|
|
32776
|
+
/**
|
|
32777
|
+
* Clear `agent.taskId` links that point at a task which has transitioned out
|
|
32778
|
+
* of active work. This keeps heartbeat scheduling aligned with live task
|
|
32779
|
+
* storage and prevents stale task-scoped heartbeat runs.
|
|
32780
|
+
*/
|
|
32781
|
+
clearLinkedAgentTaskIds(taskId, updatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
32782
|
+
const linkedAgents = this.db.prepare("SELECT id FROM agents WHERE taskId = ?").all(taskId);
|
|
32783
|
+
if (linkedAgents.length === 0) {
|
|
32784
|
+
return;
|
|
32785
|
+
}
|
|
32786
|
+
this.db.prepare(`
|
|
32787
|
+
UPDATE agents
|
|
32788
|
+
SET
|
|
32789
|
+
taskId = NULL,
|
|
32790
|
+
updatedAt = ?,
|
|
32791
|
+
data = CASE
|
|
32792
|
+
WHEN json_valid(data) THEN json_set(json_remove(data, '$.taskId'), '$.updatedAt', ?)
|
|
32793
|
+
ELSE data
|
|
32794
|
+
END
|
|
32795
|
+
WHERE taskId = ?
|
|
32796
|
+
`).run(updatedAt, updatedAt, taskId);
|
|
32797
|
+
}
|
|
32709
32798
|
/**
|
|
32710
32799
|
* Clean up the git branch associated with a task.
|
|
32711
32800
|
*
|
|
@@ -32987,6 +33076,7 @@ ${task.description}
|
|
|
32987
33076
|
});
|
|
32988
33077
|
if (!cleanup) {
|
|
32989
33078
|
await this.atomicWriteTaskJson(dir2, task);
|
|
33079
|
+
this.clearLinkedAgentTaskIds(id, task.updatedAt);
|
|
32990
33080
|
if (this.isWatching) this.taskCache.set(id, { ...task });
|
|
32991
33081
|
this.emit("task:moved", { task, from: "done", to: "archived" });
|
|
32992
33082
|
return task;
|
|
@@ -33000,6 +33090,7 @@ ${task.description}
|
|
|
33000
33090
|
}
|
|
33001
33091
|
const entry = await this.taskToArchiveEntry(task, task.columnMovedAt);
|
|
33002
33092
|
this.archiveDb.upsert(entry);
|
|
33093
|
+
this.clearLinkedAgentTaskIds(id, task.updatedAt);
|
|
33003
33094
|
this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
33004
33095
|
this.db.bumpLastModified();
|
|
33005
33096
|
const { rm: rm6 } = await import("node:fs/promises");
|
|
@@ -36913,10 +37004,105 @@ async function summarizeCommitBody(diffStat, rootDir, provider, modelId, opts) {
|
|
|
36913
37004
|
}
|
|
36914
37005
|
}
|
|
36915
37006
|
}
|
|
37007
|
+
async function summarizeCommitSubject(diffStat, rootDir, provider, modelId, opts) {
|
|
37008
|
+
const trimmedStat = (diffStat ?? "").trim();
|
|
37009
|
+
const trimmedCommitLog = (opts?.commitLog ?? "").trim();
|
|
37010
|
+
if (trimmedStat.length === 0 && trimmedCommitLog.length === 0) {
|
|
37011
|
+
return null;
|
|
37012
|
+
}
|
|
37013
|
+
const truncatedStat = trimmedStat.length > MAX_COMMIT_BODY_INPUT_LENGTH ? trimmedStat.slice(0, MAX_COMMIT_BODY_INPUT_LENGTH) + "\n\u2026(truncated)" : trimmedStat;
|
|
37014
|
+
const truncatedCommitLog = trimmedCommitLog.length > MAX_COMMIT_BODY_INPUT_LENGTH ? trimmedCommitLog.slice(0, MAX_COMMIT_BODY_INPUT_LENGTH) + "\n\u2026(truncated)" : trimmedCommitLog;
|
|
37015
|
+
const userPromptParts = [];
|
|
37016
|
+
if (opts?.branch) userPromptParts.push(`Branch: ${opts.branch}`);
|
|
37017
|
+
if (opts?.taskId) userPromptParts.push(`Task: ${opts.taskId}`);
|
|
37018
|
+
if (userPromptParts.length > 0) userPromptParts.push("");
|
|
37019
|
+
if (truncatedCommitLog.length > 0) {
|
|
37020
|
+
userPromptParts.push("Step commits being merged in (most recent first):");
|
|
37021
|
+
userPromptParts.push(truncatedCommitLog);
|
|
37022
|
+
userPromptParts.push("");
|
|
37023
|
+
}
|
|
37024
|
+
if (truncatedStat.length > 0) {
|
|
37025
|
+
userPromptParts.push("Files changed (`git diff --stat`):");
|
|
37026
|
+
userPromptParts.push(truncatedStat);
|
|
37027
|
+
userPromptParts.push("");
|
|
37028
|
+
}
|
|
37029
|
+
userPromptParts.push("Write the commit subject now.");
|
|
37030
|
+
const userPrompt = userPromptParts.join("\n");
|
|
37031
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS;
|
|
37032
|
+
const aborter = new AbortController();
|
|
37033
|
+
const timer = setTimeout(() => aborter.abort(), timeoutMs);
|
|
37034
|
+
if (opts?.signal) {
|
|
37035
|
+
if (opts.signal.aborted) aborter.abort();
|
|
37036
|
+
else opts.signal.addEventListener("abort", () => aborter.abort(), { once: true });
|
|
37037
|
+
}
|
|
37038
|
+
let session;
|
|
37039
|
+
try {
|
|
37040
|
+
const createFnAgent12 = await getFnAgent();
|
|
37041
|
+
if (!createFnAgent12) {
|
|
37042
|
+
if (DEBUG) console.log("[ai-summarize] AI engine not available for commit subject");
|
|
37043
|
+
return null;
|
|
37044
|
+
}
|
|
37045
|
+
const agentOptions = {
|
|
37046
|
+
cwd: rootDir,
|
|
37047
|
+
systemPrompt: COMMIT_SUBJECT_SYSTEM_PROMPT,
|
|
37048
|
+
tools: "readonly"
|
|
37049
|
+
};
|
|
37050
|
+
if (provider && modelId) {
|
|
37051
|
+
agentOptions.defaultProvider = provider;
|
|
37052
|
+
agentOptions.defaultModelId = modelId;
|
|
37053
|
+
}
|
|
37054
|
+
const agentResult = await createFnAgent12(agentOptions);
|
|
37055
|
+
if (!agentResult?.session) return null;
|
|
37056
|
+
session = agentResult.session;
|
|
37057
|
+
await session.prompt(userPrompt);
|
|
37058
|
+
if (aborter.signal.aborted) return null;
|
|
37059
|
+
if (session.state?.error) {
|
|
37060
|
+
if (DEBUG) console.log(`[ai-summarize] Commit-subject session error: ${session.state.error}`);
|
|
37061
|
+
return null;
|
|
37062
|
+
}
|
|
37063
|
+
const messages = session.state?.messages ?? [];
|
|
37064
|
+
const assistant = messages.filter((m) => m.role === "assistant").pop();
|
|
37065
|
+
if (!assistant?.content) return null;
|
|
37066
|
+
let raw = "";
|
|
37067
|
+
if (typeof assistant.content === "string") {
|
|
37068
|
+
raw = assistant.content;
|
|
37069
|
+
} else if (Array.isArray(assistant.content)) {
|
|
37070
|
+
raw = assistant.content.filter(
|
|
37071
|
+
(c) => c.type === "text" && typeof c.text === "string"
|
|
37072
|
+
).map((c) => c.text).join("");
|
|
37073
|
+
}
|
|
37074
|
+
return sanitizeCommitSubject(raw);
|
|
37075
|
+
} catch (err) {
|
|
37076
|
+
if (DEBUG) {
|
|
37077
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37078
|
+
console.log(`[ai-summarize] Commit-subject generation failed: ${message}`);
|
|
37079
|
+
}
|
|
37080
|
+
return null;
|
|
37081
|
+
} finally {
|
|
37082
|
+
clearTimeout(timer);
|
|
37083
|
+
try {
|
|
37084
|
+
session?.dispose?.();
|
|
37085
|
+
} catch {
|
|
37086
|
+
}
|
|
37087
|
+
}
|
|
37088
|
+
}
|
|
37089
|
+
function sanitizeCommitSubject(raw) {
|
|
37090
|
+
if (!raw) return null;
|
|
37091
|
+
const firstLine = raw.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
37092
|
+
if (!firstLine) return null;
|
|
37093
|
+
let subject = firstLine.replace(/^[-*]\s+/, "").replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
37094
|
+
subject = subject.replace(/^[a-z]+(?:\([^)]+\))?:\s*/i, "").trim();
|
|
37095
|
+
subject = subject.replace(/\.+$/, "").trim();
|
|
37096
|
+
if (!subject) return null;
|
|
37097
|
+
if (subject.length > MAX_COMMIT_SUBJECT_LENGTH) {
|
|
37098
|
+
subject = subject.slice(0, MAX_COMMIT_SUBJECT_LENGTH).trim();
|
|
37099
|
+
}
|
|
37100
|
+
return subject || null;
|
|
37101
|
+
}
|
|
36916
37102
|
function __resetSummarizeState() {
|
|
36917
37103
|
rateLimits.clear();
|
|
36918
37104
|
}
|
|
36919
|
-
var SUMMARIZE_SYSTEM_PROMPT, MAX_DESCRIPTION_LENGTH, MIN_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, MAX_REQUESTS_PER_HOUR, RATE_LIMIT_WINDOW_MS, CLEANUP_INTERVAL_MS, rateLimits, cleanupInterval, ValidationError, RateLimitError, AiServiceError, DEBUG, COMMIT_BODY_SYSTEM_PROMPT, MAX_COMMIT_BODY_INPUT_LENGTH, MAX_COMMIT_BODY_LENGTH, DEFAULT_COMMIT_BODY_TIMEOUT_MS;
|
|
37105
|
+
var SUMMARIZE_SYSTEM_PROMPT, MAX_DESCRIPTION_LENGTH, MIN_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, MAX_REQUESTS_PER_HOUR, RATE_LIMIT_WINDOW_MS, CLEANUP_INTERVAL_MS, rateLimits, cleanupInterval, ValidationError, RateLimitError, AiServiceError, DEBUG, COMMIT_BODY_SYSTEM_PROMPT, MAX_COMMIT_BODY_INPUT_LENGTH, MAX_COMMIT_BODY_LENGTH, DEFAULT_COMMIT_BODY_TIMEOUT_MS, COMMIT_SUBJECT_SYSTEM_PROMPT, MAX_COMMIT_SUBJECT_LENGTH, DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS;
|
|
36920
37106
|
var init_ai_summarize = __esm({
|
|
36921
37107
|
"../core/src/ai-summarize.ts"() {
|
|
36922
37108
|
"use strict";
|
|
@@ -36976,6 +37162,20 @@ Your job is to summarize what landed \u2014 using the branch's step commit subje
|
|
|
36976
37162
|
MAX_COMMIT_BODY_INPUT_LENGTH = 4e3;
|
|
36977
37163
|
MAX_COMMIT_BODY_LENGTH = 2e3;
|
|
36978
37164
|
DEFAULT_COMMIT_BODY_TIMEOUT_MS = 3e4;
|
|
37165
|
+
COMMIT_SUBJECT_SYSTEM_PROMPT = `You write commit message subjects for merge commits.
|
|
37166
|
+
|
|
37167
|
+
Your job is to summarize what landed \u2014 using the branch's step commit subjects (when provided) and the \`git diff --stat\` \u2014 into a single subject line that conveys the change's essence at a glance.
|
|
37168
|
+
|
|
37169
|
+
## Guidelines
|
|
37170
|
+
- Output ONLY the subject text \u2014 no quotes, no markdown, no body, no trailing period
|
|
37171
|
+
- Do NOT include any \`feat:\`, \`fix:\`, scope, or task-id prefix \u2014 the caller adds that
|
|
37172
|
+
- Imperative mood ("add X", "fix Y", "refactor Z") and lower-case first word
|
|
37173
|
+
- Hard cap: 60 characters; aim for 40\u201355
|
|
37174
|
+
- Be specific: name the most consequential module/feature/behavior that changed
|
|
37175
|
+
- If the branch has one clear theme, describe it; if it's mixed, lead with the largest change
|
|
37176
|
+
- Do not invent details that aren't in the input`;
|
|
37177
|
+
MAX_COMMIT_SUBJECT_LENGTH = 60;
|
|
37178
|
+
DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS = 15e3;
|
|
36979
37179
|
}
|
|
36980
37180
|
});
|
|
36981
37181
|
|
|
@@ -49156,6 +49356,7 @@ __export(src_exports, {
|
|
|
49156
49356
|
COLUMN_DESCRIPTIONS: () => COLUMN_DESCRIPTIONS,
|
|
49157
49357
|
COLUMN_LABELS: () => COLUMN_LABELS,
|
|
49158
49358
|
COMMIT_BODY_SYSTEM_PROMPT: () => COMMIT_BODY_SYSTEM_PROMPT,
|
|
49359
|
+
COMMIT_SUBJECT_SYSTEM_PROMPT: () => COMMIT_SUBJECT_SYSTEM_PROMPT,
|
|
49159
49360
|
COMPACT_MEMORY_SYSTEM_PROMPT: () => COMPACT_MEMORY_SYSTEM_PROMPT,
|
|
49160
49361
|
CentralCore: () => CentralCore,
|
|
49161
49362
|
CentralDatabase: () => CentralDatabase,
|
|
@@ -49166,6 +49367,7 @@ __export(src_exports, {
|
|
|
49166
49367
|
DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS: () => DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS,
|
|
49167
49368
|
DEFAULT_AUTO_SUMMARIZE_SCHEDULE: () => DEFAULT_AUTO_SUMMARIZE_SCHEDULE,
|
|
49168
49369
|
DEFAULT_COMMIT_BODY_TIMEOUT_MS: () => DEFAULT_COMMIT_BODY_TIMEOUT_MS,
|
|
49370
|
+
DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS: () => DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS,
|
|
49169
49371
|
DEFAULT_EXECUTION_MODE: () => DEFAULT_EXECUTION_MODE,
|
|
49170
49372
|
DEFAULT_GLOBAL_SETTINGS: () => DEFAULT_GLOBAL_SETTINGS,
|
|
49171
49373
|
DEFAULT_INSIGHT_SCHEDULE: () => DEFAULT_INSIGHT_SCHEDULE,
|
|
@@ -49189,6 +49391,7 @@ __export(src_exports, {
|
|
|
49189
49391
|
InsightStore: () => InsightStore,
|
|
49190
49392
|
MAX_COMMIT_BODY_INPUT_LENGTH: () => MAX_COMMIT_BODY_INPUT_LENGTH,
|
|
49191
49393
|
MAX_COMMIT_BODY_LENGTH: () => MAX_COMMIT_BODY_LENGTH,
|
|
49394
|
+
MAX_COMMIT_SUBJECT_LENGTH: () => MAX_COMMIT_SUBJECT_LENGTH,
|
|
49192
49395
|
MAX_DESCRIPTION_LENGTH: () => MAX_DESCRIPTION_LENGTH,
|
|
49193
49396
|
MAX_REQUESTS_PER_HOUR: () => MAX_REQUESTS_PER_HOUR,
|
|
49194
49397
|
MAX_ROUTINE_RUN_HISTORY: () => MAX_ROUTINE_RUN_HISTORY,
|
|
@@ -49425,6 +49628,7 @@ __export(src_exports, {
|
|
|
49425
49628
|
runGhAsync: () => runGhAsync,
|
|
49426
49629
|
runGhJson: () => runGhJson,
|
|
49427
49630
|
runGhJsonAsync: () => runGhJsonAsync,
|
|
49631
|
+
sanitizeCommitSubject: () => sanitizeCommitSubject,
|
|
49428
49632
|
scheduleQmdInstallAndRefresh: () => scheduleQmdInstallAndRefresh,
|
|
49429
49633
|
scheduleQmdProjectMemoryRefresh: () => scheduleQmdProjectMemoryRefresh,
|
|
49430
49634
|
searchProjectMemory: () => searchProjectMemory,
|
|
@@ -49434,6 +49638,7 @@ __export(src_exports, {
|
|
|
49434
49638
|
slugify: () => slugify,
|
|
49435
49639
|
sortTasksByPriorityThenAgeAndId: () => sortTasksByPriorityThenAgeAndId,
|
|
49436
49640
|
summarizeCommitBody: () => summarizeCommitBody,
|
|
49641
|
+
summarizeCommitSubject: () => summarizeCommitSubject,
|
|
49437
49642
|
summarizeTitle: () => summarizeTitle,
|
|
49438
49643
|
syncAutoSummarizeAutomation: () => syncAutoSummarizeAutomation,
|
|
49439
49644
|
syncBackupAutomation: () => syncBackupAutomation,
|
|
@@ -56503,21 +56708,37 @@ function resetMergeWithWarn(rootDir, taskId, label) {
|
|
|
56503
56708
|
async function buildDeterministicMergeMessage(params) {
|
|
56504
56709
|
const { taskId, branch, commitLog, diffStat, includeTaskId, rootDir, settings, signal } = params;
|
|
56505
56710
|
const prefix = includeTaskId ? `feat(${taskId})` : "feat";
|
|
56506
|
-
const
|
|
56711
|
+
const fallbackSubject = `${prefix}: merge ${branch}`;
|
|
56507
56712
|
const trimmedCommitLog = commitLog?.trim() ?? "";
|
|
56508
56713
|
const trimmedDiffStat = diffStat?.trim() ?? "";
|
|
56509
56714
|
const commitsSection = trimmedCommitLog.length > 0 ? trimmedCommitLog : `- merge ${branch}`;
|
|
56510
56715
|
let aiSummary = null;
|
|
56716
|
+
let aiSubject = null;
|
|
56511
56717
|
if (rootDir && settings && (trimmedCommitLog.length > 0 || trimmedDiffStat.length > 0)) {
|
|
56512
56718
|
const useTitleSummarizer = !!settings.titleSummarizerProvider && !!settings.titleSummarizerModelId;
|
|
56513
56719
|
const provider = useTitleSummarizer ? settings.titleSummarizerProvider : settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider;
|
|
56514
56720
|
const modelId = useTitleSummarizer ? settings.titleSummarizerModelId : settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId;
|
|
56515
|
-
|
|
56516
|
-
|
|
56517
|
-
|
|
56518
|
-
|
|
56519
|
-
|
|
56520
|
-
|
|
56721
|
+
const [bodyResult, subjectResult] = await Promise.all([
|
|
56722
|
+
summarizeCommitBody(trimmedDiffStat, rootDir, provider, modelId, {
|
|
56723
|
+
branch,
|
|
56724
|
+
taskId,
|
|
56725
|
+
commitLog: trimmedCommitLog,
|
|
56726
|
+
signal
|
|
56727
|
+
}).catch(() => null),
|
|
56728
|
+
summarizeCommitSubject(trimmedDiffStat, rootDir, provider, modelId, {
|
|
56729
|
+
branch,
|
|
56730
|
+
taskId,
|
|
56731
|
+
commitLog: trimmedCommitLog,
|
|
56732
|
+
signal
|
|
56733
|
+
}).catch(() => null)
|
|
56734
|
+
]);
|
|
56735
|
+
aiSummary = bodyResult;
|
|
56736
|
+
aiSubject = subjectResult;
|
|
56737
|
+
}
|
|
56738
|
+
let subject = fallbackSubject;
|
|
56739
|
+
if (aiSubject && aiSubject.length > 0) {
|
|
56740
|
+
const candidate = `${prefix}: ${aiSubject}`;
|
|
56741
|
+
subject = candidate.length > 72 ? candidate.slice(0, 72).trimEnd() : candidate;
|
|
56521
56742
|
}
|
|
56522
56743
|
const sections = [];
|
|
56523
56744
|
if (aiSummary && aiSummary.trim().length > 0) {
|
|
@@ -56578,11 +56799,18 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
|
|
|
56578
56799
|
);
|
|
56579
56800
|
return false;
|
|
56580
56801
|
}
|
|
56802
|
+
const actualContext = await computeActualMergeCommitContext({
|
|
56803
|
+
rootDir,
|
|
56804
|
+
integrationTargetSha: preAttemptHeadSha,
|
|
56805
|
+
branch
|
|
56806
|
+
});
|
|
56807
|
+
const messageCommitLog = actualContext.commitLog || commitLog;
|
|
56808
|
+
const messageDiffStat = actualContext.diffStat || diffStat;
|
|
56581
56809
|
const { subjectArg, bodyArg } = await buildDeterministicMergeMessage({
|
|
56582
56810
|
taskId,
|
|
56583
56811
|
branch,
|
|
56584
|
-
commitLog,
|
|
56585
|
-
diffStat,
|
|
56812
|
+
commitLog: messageCommitLog,
|
|
56813
|
+
diffStat: messageDiffStat,
|
|
56586
56814
|
includeTaskId,
|
|
56587
56815
|
rootDir,
|
|
56588
56816
|
settings,
|
|
@@ -57034,6 +57262,52 @@ async function collectPatchIds(rootDir, target, windowSize) {
|
|
|
57034
57262
|
}
|
|
57035
57263
|
return ids;
|
|
57036
57264
|
}
|
|
57265
|
+
async function computeActualMergeCommitContext(params) {
|
|
57266
|
+
const { rootDir, integrationTargetSha, branch } = params;
|
|
57267
|
+
const targetArg = quoteArg(integrationTargetSha);
|
|
57268
|
+
let diffStat = "";
|
|
57269
|
+
try {
|
|
57270
|
+
const { stdout: stagedStat } = await execAsync2(
|
|
57271
|
+
`git diff --cached ${targetArg} --stat`,
|
|
57272
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
57273
|
+
);
|
|
57274
|
+
diffStat = stagedStat.trim();
|
|
57275
|
+
if (diffStat.length === 0) {
|
|
57276
|
+
const { stdout: headStat } = await execAsync2(
|
|
57277
|
+
`git diff ${targetArg} HEAD --stat`,
|
|
57278
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
57279
|
+
);
|
|
57280
|
+
diffStat = headStat.trim();
|
|
57281
|
+
}
|
|
57282
|
+
} catch {
|
|
57283
|
+
}
|
|
57284
|
+
let commitLog = "";
|
|
57285
|
+
try {
|
|
57286
|
+
const targetPatchIds = await collectPatchIds(rootDir, integrationTargetSha, 200);
|
|
57287
|
+
const { stdout: branchShas } = await execAsync2(
|
|
57288
|
+
`git log ${targetArg}..${quoteArg(branch)} --format=%H`,
|
|
57289
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
57290
|
+
);
|
|
57291
|
+
const shas = branchShas.trim().split("\n").filter(Boolean);
|
|
57292
|
+
const lines = [];
|
|
57293
|
+
for (const sha of shas) {
|
|
57294
|
+
const pid = await commitPatchId(rootDir, sha);
|
|
57295
|
+
if (pid && targetPatchIds.has(pid)) continue;
|
|
57296
|
+
try {
|
|
57297
|
+
const { stdout: subj } = await execAsync2(
|
|
57298
|
+
`git log -1 ${quoteArg(sha)} --format=%s`,
|
|
57299
|
+
{ cwd: rootDir, encoding: "utf-8" }
|
|
57300
|
+
);
|
|
57301
|
+
const s = subj.trim();
|
|
57302
|
+
if (s) lines.push(`- ${s}`);
|
|
57303
|
+
} catch {
|
|
57304
|
+
}
|
|
57305
|
+
}
|
|
57306
|
+
commitLog = lines.join("\n");
|
|
57307
|
+
} catch {
|
|
57308
|
+
}
|
|
57309
|
+
return { commitLog, diffStat };
|
|
57310
|
+
}
|
|
57037
57311
|
async function listBranchCommits(rootDir, target, branch) {
|
|
57038
57312
|
try {
|
|
57039
57313
|
const { stdout } = await execAsync2(
|
|
@@ -58647,11 +58921,25 @@ async function executeMergeAttempt(params, aiTracker) {
|
|
|
58647
58921
|
}
|
|
58648
58922
|
try {
|
|
58649
58923
|
const authorArg = getCommitAuthorArg(params.settings);
|
|
58924
|
+
let integrationTargetSha;
|
|
58925
|
+
try {
|
|
58926
|
+
const { stdout } = await execAsync2("git rev-parse HEAD~1", {
|
|
58927
|
+
cwd: rootDir,
|
|
58928
|
+
encoding: "utf-8"
|
|
58929
|
+
});
|
|
58930
|
+
integrationTargetSha = stdout.trim() || void 0;
|
|
58931
|
+
} catch {
|
|
58932
|
+
}
|
|
58933
|
+
const actualContext = integrationTargetSha ? await computeActualMergeCommitContext({
|
|
58934
|
+
rootDir,
|
|
58935
|
+
integrationTargetSha,
|
|
58936
|
+
branch
|
|
58937
|
+
}) : { commitLog: "", diffStat: "" };
|
|
58650
58938
|
const { subjectArg, bodyArg } = await buildDeterministicMergeMessage({
|
|
58651
58939
|
taskId,
|
|
58652
58940
|
branch,
|
|
58653
|
-
commitLog,
|
|
58654
|
-
diffStat,
|
|
58941
|
+
commitLog: actualContext.commitLog || commitLog,
|
|
58942
|
+
diffStat: actualContext.diffStat || diffStat,
|
|
58655
58943
|
includeTaskId,
|
|
58656
58944
|
rootDir,
|
|
58657
58945
|
settings: params.settings,
|
|
@@ -65483,30 +65771,27 @@ var init_effective_node = __esm({
|
|
|
65483
65771
|
});
|
|
65484
65772
|
|
|
65485
65773
|
// ../engine/src/node-routing-policy.ts
|
|
65486
|
-
function applyUnavailableNodePolicy(
|
|
65487
|
-
|
|
65488
|
-
|
|
65774
|
+
function applyUnavailableNodePolicy(params) {
|
|
65775
|
+
const { effectiveNode, nodeHealth, policy } = params;
|
|
65776
|
+
if (effectiveNode.source === "local") {
|
|
65777
|
+
return { allowed: true, fallbackToLocal: false };
|
|
65489
65778
|
}
|
|
65490
|
-
if (
|
|
65491
|
-
return { allowed: true, fallbackToLocal: false
|
|
65779
|
+
if (nodeHealth === "online" || nodeHealth === void 0) {
|
|
65780
|
+
return { allowed: true, fallbackToLocal: false };
|
|
65492
65781
|
}
|
|
65493
|
-
if (
|
|
65494
|
-
return { allowed: true, fallbackToLocal: false
|
|
65495
|
-
}
|
|
65496
|
-
if (!UNHEALTHY_STATUSES.has(nodeStatus)) {
|
|
65497
|
-
return { allowed: true, fallbackToLocal: false, reason: "healthy" };
|
|
65782
|
+
if (!effectiveNode.nodeId || !UNHEALTHY_STATUSES.has(nodeHealth)) {
|
|
65783
|
+
return { allowed: true, fallbackToLocal: false };
|
|
65498
65784
|
}
|
|
65499
65785
|
if (policy === "fallback-local") {
|
|
65500
65786
|
return {
|
|
65501
65787
|
allowed: true,
|
|
65502
65788
|
fallbackToLocal: true,
|
|
65503
|
-
reason: `
|
|
65789
|
+
reason: `Node ${effectiveNode.nodeId} is ${nodeHealth}; falling back to local per policy`
|
|
65504
65790
|
};
|
|
65505
65791
|
}
|
|
65506
65792
|
return {
|
|
65507
65793
|
allowed: false,
|
|
65508
|
-
|
|
65509
|
-
reason: `blocked:${nodeStatus}`
|
|
65794
|
+
reason: `Node ${effectiveNode.nodeId} is ${nodeHealth}; policy is block`
|
|
65510
65795
|
};
|
|
65511
65796
|
}
|
|
65512
65797
|
var UNHEALTHY_STATUSES;
|
|
@@ -65681,7 +65966,7 @@ var init_scheduler = __esm({
|
|
|
65681
65966
|
/** Tracks mission-linked tasks observed with status=failed before moveTask clears status/error. */
|
|
65682
65967
|
failedTaskIds = /* @__PURE__ */ new Set();
|
|
65683
65968
|
/** Tracks tasks blocked by unavailable-node policy to deduplicate block log entries. */
|
|
65684
|
-
|
|
65969
|
+
wasNodeBlocked = /* @__PURE__ */ new Set();
|
|
65685
65970
|
/**
|
|
65686
65971
|
* Validate that a task's filesystem state is intact.
|
|
65687
65972
|
* Checks that the task directory exists and PROMPT.md is present and non-empty.
|
|
@@ -65743,7 +66028,7 @@ var init_scheduler = __esm({
|
|
|
65743
66028
|
this.options.missionAutopilot.stop();
|
|
65744
66029
|
}
|
|
65745
66030
|
this.failedTaskIds.clear();
|
|
65746
|
-
this.
|
|
66031
|
+
this.wasNodeBlocked.clear();
|
|
65747
66032
|
schedulerLog.log("Stopped");
|
|
65748
66033
|
}
|
|
65749
66034
|
/**
|
|
@@ -66005,35 +66290,24 @@ var init_scheduler = __esm({
|
|
|
66005
66290
|
}
|
|
66006
66291
|
let effectiveNode = resolveEffectiveNode(freshTask, settings);
|
|
66007
66292
|
schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
|
|
66008
|
-
if (effectiveNode.nodeId
|
|
66009
|
-
const
|
|
66010
|
-
const
|
|
66011
|
-
|
|
66012
|
-
|
|
66013
|
-
|
|
66014
|
-
);
|
|
66015
|
-
if (!
|
|
66016
|
-
if (!this.
|
|
66017
|
-
this.
|
|
66018
|
-
schedulerLog.
|
|
66019
|
-
|
|
66020
|
-
);
|
|
66021
|
-
await this.store.logEntry(
|
|
66022
|
-
task.id,
|
|
66023
|
-
`Routing blocked: node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"}, policy=block`
|
|
66024
|
-
);
|
|
66293
|
+
if (effectiveNode.nodeId && this.options.nodeHealthMonitor) {
|
|
66294
|
+
const nodeHealth = this.options.nodeHealthMonitor.getNodeHealth(effectiveNode.nodeId);
|
|
66295
|
+
const decision = applyUnavailableNodePolicy({
|
|
66296
|
+
effectiveNode,
|
|
66297
|
+
nodeHealth,
|
|
66298
|
+
policy: settings.unavailableNodePolicy
|
|
66299
|
+
});
|
|
66300
|
+
if (!decision.allowed) {
|
|
66301
|
+
if (!this.wasNodeBlocked.has(task.id)) {
|
|
66302
|
+
this.wasNodeBlocked.add(task.id);
|
|
66303
|
+
schedulerLog.warn(`Task ${task.id} blocked: ${decision.reason}`);
|
|
66304
|
+
await this.store.logEntry(task.id, decision.reason);
|
|
66025
66305
|
}
|
|
66026
66306
|
continue;
|
|
66027
66307
|
}
|
|
66028
|
-
|
|
66029
|
-
|
|
66030
|
-
|
|
66031
|
-
`Task ${task.id} falling back to local \u2014 node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"} (policy: fallback-local)`
|
|
66032
|
-
);
|
|
66033
|
-
await this.store.logEntry(
|
|
66034
|
-
task.id,
|
|
66035
|
-
`Routing fallback to local: node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"}, policy=fallback-local`
|
|
66036
|
-
);
|
|
66308
|
+
if (decision.fallbackToLocal) {
|
|
66309
|
+
schedulerLog.log(`Task ${task.id} falling back to local: ${decision.reason}`);
|
|
66310
|
+
await this.store.logEntry(task.id, decision.reason);
|
|
66037
66311
|
effectiveNode = { nodeId: void 0, source: "local" };
|
|
66038
66312
|
}
|
|
66039
66313
|
}
|
|
@@ -66047,6 +66321,7 @@ var init_scheduler = __esm({
|
|
|
66047
66321
|
effectiveNodeSource: effectiveNode.source
|
|
66048
66322
|
});
|
|
66049
66323
|
await this.store.moveTask(task.id, "in-progress");
|
|
66324
|
+
this.wasNodeBlocked.delete(task.id);
|
|
66050
66325
|
await this.store.logEntry(task.id, `Node routing resolved: ${effectiveNode.nodeId ?? "local"} (source: ${effectiveNode.source})`);
|
|
66051
66326
|
this.options.onSchedule?.(task);
|
|
66052
66327
|
started++;
|
|
@@ -71130,6 +71405,7 @@ When sending messages:
|
|
|
71130
71405
|
}
|
|
71131
71406
|
const agentHasIdentity = hasAgentIdentity(agent);
|
|
71132
71407
|
const isAgentEphemeral = isEphemeralAgent(agent);
|
|
71408
|
+
const canRunNoTaskHeartbeat = agentHasIdentity && !isAgentEphemeral;
|
|
71133
71409
|
let taskId = explicitTaskId ?? agent.taskId;
|
|
71134
71410
|
let inboxSelection = null;
|
|
71135
71411
|
if (!taskId) {
|
|
@@ -71166,7 +71442,7 @@ When sending messages:
|
|
|
71166
71442
|
engineRunContext.taskId = taskId;
|
|
71167
71443
|
}
|
|
71168
71444
|
if (!taskId) {
|
|
71169
|
-
if (!
|
|
71445
|
+
if (!canRunNoTaskHeartbeat) {
|
|
71170
71446
|
heartbeatLog.log(`Agent ${agentId} has no task assignment \u2014 graceful exit`);
|
|
71171
71447
|
await this.completeRun(agentId, run.id, {
|
|
71172
71448
|
status: "completed",
|
|
@@ -71176,7 +71452,7 @@ When sending messages:
|
|
|
71176
71452
|
}
|
|
71177
71453
|
heartbeatLog.log(`Agent ${agentId} has no task but has identity \u2014 running no-task heartbeat`);
|
|
71178
71454
|
}
|
|
71179
|
-
|
|
71455
|
+
let isNoTaskRun = !taskId;
|
|
71180
71456
|
if (!isNoTaskRun) {
|
|
71181
71457
|
const validStates = ["active", "running", "idle"];
|
|
71182
71458
|
if (!validStates.includes(agent.state)) {
|
|
@@ -71202,53 +71478,99 @@ When sending messages:
|
|
|
71202
71478
|
});
|
|
71203
71479
|
return await this.store.getRunDetail(agentId, run.id);
|
|
71204
71480
|
}
|
|
71205
|
-
if (taskDetail.
|
|
71206
|
-
|
|
71207
|
-
|
|
71208
|
-
|
|
71209
|
-
|
|
71210
|
-
|
|
71211
|
-
|
|
71212
|
-
|
|
71213
|
-
|
|
71214
|
-
|
|
71481
|
+
if (taskDetail.column === "done" || taskDetail.column === "archived") {
|
|
71482
|
+
if (agent.taskId === resolvedTaskId2) {
|
|
71483
|
+
heartbeatLog.log(
|
|
71484
|
+
`Agent ${agentId} linked task ${resolvedTaskId2} is ${taskDetail.column} \u2014 clearing assignment and running heartbeat without task context`
|
|
71485
|
+
);
|
|
71486
|
+
try {
|
|
71487
|
+
await this.store.assignTask(agentId, void 0, runContext);
|
|
71488
|
+
} catch (clearErr) {
|
|
71489
|
+
heartbeatLog.warn(
|
|
71490
|
+
`Failed to clear terminal task assignment ${resolvedTaskId2} for ${agentId}: ${clearErr instanceof Error ? clearErr.message : String(clearErr)}`
|
|
71491
|
+
);
|
|
71215
71492
|
}
|
|
71216
|
-
|
|
71217
|
-
|
|
71493
|
+
taskId = void 0;
|
|
71494
|
+
taskDetail = void 0;
|
|
71495
|
+
isNoTaskRun = true;
|
|
71496
|
+
if (!canRunNoTaskHeartbeat) {
|
|
71497
|
+
await this.completeRun(agentId, run.id, {
|
|
71498
|
+
status: "completed",
|
|
71499
|
+
resultJson: { reason: "no_assignment" }
|
|
71500
|
+
});
|
|
71501
|
+
return await this.store.getRunDetail(agentId, run.id);
|
|
71502
|
+
}
|
|
71503
|
+
} else {
|
|
71504
|
+
heartbeatLog.log(
|
|
71505
|
+
`Heartbeat for ${agentId} targeted terminal task ${resolvedTaskId2} (${taskDetail.column}) \u2014 graceful exit`
|
|
71506
|
+
);
|
|
71507
|
+
await this.completeRun(agentId, run.id, {
|
|
71508
|
+
status: "completed",
|
|
71509
|
+
resultJson: { reason: "terminal_task", taskId: resolvedTaskId2, column: taskDetail.column }
|
|
71510
|
+
});
|
|
71511
|
+
return await this.store.getRunDetail(agentId, run.id);
|
|
71512
|
+
}
|
|
71218
71513
|
}
|
|
71219
|
-
|
|
71220
|
-
|
|
71221
|
-
|
|
71222
|
-
const
|
|
71223
|
-
|
|
71224
|
-
|
|
71225
|
-
const contextHash = Buffer.from(
|
|
71226
|
-
JSON.stringify({ commentCount, lastCommentId, lastSteeringCommentId, blockedBy })
|
|
71227
|
-
).toString("base64").slice(0, 16);
|
|
71228
|
-
const currentBlockedState = {
|
|
71229
|
-
taskId: resolvedTaskId2,
|
|
71230
|
-
blockedBy,
|
|
71231
|
-
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71232
|
-
contextHash
|
|
71233
|
-
};
|
|
71234
|
-
const previousBlockedState = await this.store.getLastBlockedState(agentId);
|
|
71235
|
-
if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
|
|
71514
|
+
if (isNoTaskRun) {
|
|
71515
|
+
heartbeatLog.log(`Agent ${agentId} terminal task assignment resolved into no-task heartbeat`);
|
|
71516
|
+
} else {
|
|
71517
|
+
const liveTaskDetail = taskDetail;
|
|
71518
|
+
if (!liveTaskDetail) {
|
|
71519
|
+
heartbeatLog.warn(`Task ${resolvedTaskId2} lost detail after terminal-assignment handling \u2014 graceful exit`);
|
|
71236
71520
|
await this.completeRun(agentId, run.id, {
|
|
71237
71521
|
status: "completed",
|
|
71238
|
-
resultJson: { reason: "
|
|
71522
|
+
resultJson: { reason: "task_not_found", taskId: resolvedTaskId2 }
|
|
71523
|
+
});
|
|
71524
|
+
return await this.store.getRunDetail(agentId, run.id);
|
|
71525
|
+
}
|
|
71526
|
+
if (liveTaskDetail.checkedOutBy && liveTaskDetail.checkedOutBy !== agentId) {
|
|
71527
|
+
heartbeatLog.warn(
|
|
71528
|
+
`Agent ${agentId} does not hold checkout for ${resolvedTaskId2} (held by ${liveTaskDetail.checkedOutBy}) \u2014 graceful exit`
|
|
71529
|
+
);
|
|
71530
|
+
await this.completeRun(agentId, run.id, {
|
|
71531
|
+
status: "completed",
|
|
71532
|
+
resultJson: {
|
|
71533
|
+
reason: "checkout_conflict",
|
|
71534
|
+
taskId: resolvedTaskId2,
|
|
71535
|
+
checkedOutBy: liveTaskDetail.checkedOutBy
|
|
71536
|
+
}
|
|
71537
|
+
});
|
|
71538
|
+
return await this.store.getRunDetail(agentId, run.id);
|
|
71539
|
+
}
|
|
71540
|
+
const blockedBy = typeof liveTaskDetail.blockedBy === "string" ? liveTaskDetail.blockedBy.trim() : "";
|
|
71541
|
+
const isBlockedTask = liveTaskDetail.status === "queued" && blockedBy.length > 0;
|
|
71542
|
+
if (isBlockedTask) {
|
|
71543
|
+
const commentCount = (liveTaskDetail.comments?.length ?? 0) + (liveTaskDetail.steeringComments?.length ?? 0);
|
|
71544
|
+
const lastCommentId = liveTaskDetail.comments?.at(-1)?.id;
|
|
71545
|
+
const lastSteeringCommentId = liveTaskDetail.steeringComments?.at(-1)?.id;
|
|
71546
|
+
const contextHash = Buffer.from(
|
|
71547
|
+
JSON.stringify({ commentCount, lastCommentId, lastSteeringCommentId, blockedBy })
|
|
71548
|
+
).toString("base64").slice(0, 16);
|
|
71549
|
+
const currentBlockedState = {
|
|
71550
|
+
taskId: resolvedTaskId2,
|
|
71551
|
+
blockedBy,
|
|
71552
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
71553
|
+
contextHash
|
|
71554
|
+
};
|
|
71555
|
+
const previousBlockedState = await this.store.getLastBlockedState(agentId);
|
|
71556
|
+
if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
|
|
71557
|
+
await this.completeRun(agentId, run.id, {
|
|
71558
|
+
status: "completed",
|
|
71559
|
+
resultJson: { reason: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
|
|
71560
|
+
});
|
|
71561
|
+
return await this.store.getRunDetail(agentId, run.id);
|
|
71562
|
+
}
|
|
71563
|
+
const blockedMessage = `Task is blocked by ${blockedBy}; waiting for dependency/context changes before retrying.`;
|
|
71564
|
+
await taskStore.addComment(resolvedTaskId2, blockedMessage, "agent", void 0, runContext);
|
|
71565
|
+
await audit.database({ type: "task:comment:add", target: resolvedTaskId2, metadata: { blockedBy } });
|
|
71566
|
+
await this.store.setLastBlockedState(agentId, currentBlockedState);
|
|
71567
|
+
heartbeatLog.log(`Task ${resolvedTaskId2} is blocked by ${blockedBy} \u2014 recorded blocked state`);
|
|
71568
|
+
await this.completeRun(agentId, run.id, {
|
|
71569
|
+
status: "completed",
|
|
71570
|
+
resultJson: { reason: "blocked", taskId: resolvedTaskId2, blockedBy }
|
|
71239
71571
|
});
|
|
71240
71572
|
return await this.store.getRunDetail(agentId, run.id);
|
|
71241
71573
|
}
|
|
71242
|
-
const blockedMessage = `Task is blocked by ${blockedBy}; waiting for dependency/context changes before retrying.`;
|
|
71243
|
-
await taskStore.addComment(resolvedTaskId2, blockedMessage, "agent", void 0, runContext);
|
|
71244
|
-
await audit.database({ type: "task:comment:add", target: resolvedTaskId2, metadata: { blockedBy } });
|
|
71245
|
-
await this.store.setLastBlockedState(agentId, currentBlockedState);
|
|
71246
|
-
heartbeatLog.log(`Task ${resolvedTaskId2} is blocked by ${blockedBy} \u2014 recorded blocked state`);
|
|
71247
|
-
await this.completeRun(agentId, run.id, {
|
|
71248
|
-
status: "completed",
|
|
71249
|
-
resultJson: { reason: "blocked", taskId: resolvedTaskId2, blockedBy }
|
|
71250
|
-
});
|
|
71251
|
-
return await this.store.getRunDetail(agentId, run.id);
|
|
71252
71574
|
}
|
|
71253
71575
|
}
|
|
71254
71576
|
if (!isNoTaskRun) {
|
|
@@ -79360,6 +79682,7 @@ __export(src_exports2, {
|
|
|
79360
79682
|
WebhookNotificationProvider: () => WebhookNotificationProvider,
|
|
79361
79683
|
WorktreePool: () => WorktreePool,
|
|
79362
79684
|
aiMergeTask: () => aiMergeTask,
|
|
79685
|
+
applyUnavailableNodePolicy: () => applyUnavailableNodePolicy,
|
|
79363
79686
|
buildAgentChatPrompt: () => buildAgentChatPrompt,
|
|
79364
79687
|
buildNtfyClickUrl: () => buildNtfyClickUrl,
|
|
79365
79688
|
buildRuntimeResolutionContext: () => buildRuntimeResolutionContext,
|
|
@@ -79443,6 +79766,7 @@ var init_src2 = __esm({
|
|
|
79443
79766
|
init_project_engine();
|
|
79444
79767
|
init_project_engine_manager();
|
|
79445
79768
|
init_node_health_monitor();
|
|
79769
|
+
init_node_routing_policy();
|
|
79446
79770
|
init_peer_exchange_service();
|
|
79447
79771
|
init_remote_access();
|
|
79448
79772
|
init_remote_node_client();
|
|
@@ -92902,6 +93226,13 @@ function registerSettingsMemoryRoutes(ctx, deps) {
|
|
|
92902
93226
|
if (clientSettings.archiveAgentLogMode !== void 0 && !["none", "compact", "full"].includes(clientSettings.archiveAgentLogMode)) {
|
|
92903
93227
|
throw badRequest("archiveAgentLogMode must be one of: none, compact, full");
|
|
92904
93228
|
}
|
|
93229
|
+
if (clientSettings.unavailableNodePolicy !== void 0) {
|
|
93230
|
+
const validatedUnavailableNodePolicy = validateUnavailableNodePolicy(clientSettings.unavailableNodePolicy);
|
|
93231
|
+
if (validatedUnavailableNodePolicy === void 0) {
|
|
93232
|
+
throw badRequest("unavailableNodePolicy must be one of: block, fallback-local");
|
|
93233
|
+
}
|
|
93234
|
+
clientSettings.unavailableNodePolicy = validatedUnavailableNodePolicy;
|
|
93235
|
+
}
|
|
92905
93236
|
if (clientSettings.memoryBackendType !== void 0) {
|
|
92906
93237
|
if (clientSettings.memoryBackendType !== null && typeof clientSettings.memoryBackendType !== "string") {
|
|
92907
93238
|
throw badRequest("memoryBackendType must be a string or null");
|