@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.
Files changed (38) hide show
  1. package/dist/bin.js +434 -103
  2. package/dist/client/assets/{AgentDetailView-Ca1euvPo.js → AgentDetailView-khrspqE3.js} +3 -3
  3. package/dist/client/assets/{AgentsView-pile-9ZM.js → AgentsView-DvN5EsoE.js} +11 -11
  4. package/dist/client/assets/AgentsView-MotzGhZJ.css +1 -0
  5. package/dist/client/assets/{ChatView-C4SCF_Hj.js → ChatView-DsETFRQp.js} +1 -1
  6. package/dist/client/assets/{DevServerView-1HvWligZ.js → DevServerView-D1_7SL1h.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-B1eztZna.js → DirectoryPicker-BBcm5G9F.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-D4m9s6Ye.js → DocumentsView-OuVpmTPp.js} +1 -1
  9. package/dist/client/assets/InsightsView-4KiUKzbz.css +1 -0
  10. package/dist/client/assets/InsightsView-DMC_bLUc.js +11 -0
  11. package/dist/client/assets/{MemoryView-DEmn8fA2.js → MemoryView-cI4IK-lz.js} +1 -1
  12. package/dist/client/assets/{NodesView-Blg8NHiu.js → NodesView-C_LWLFNr.js} +2 -2
  13. package/dist/client/assets/{PiExtensionsManager-DkXenPK0.js → PiExtensionsManager-DCTHvy2u.js} +1 -1
  14. package/dist/client/assets/{PluginManager-Czso7ZUF.js → PluginManager-BXMpkZx-.js} +1 -1
  15. package/dist/client/assets/{RoadmapsView-DATopkaE.js → RoadmapsView-BHTePn2u.js} +1 -1
  16. package/dist/client/assets/SettingsModal-2s-L1oWD.js +31 -0
  17. package/dist/client/assets/{SettingsModal-D_mcRJO2.js → SettingsModal-BSZIno8y.js} +1 -1
  18. package/dist/client/assets/{SetupWizardModal-JBNr-XIW.js → SetupWizardModal-DVoRhy_V.js} +1 -1
  19. package/dist/client/assets/{SkillsView-CkT6-elZ.js → SkillsView-CK52SRz5.js} +1 -1
  20. package/dist/client/assets/{TodoView-CstzLvjw.js → TodoView-BKqIV8P6.js} +1 -1
  21. package/dist/client/assets/{folder-open-BEDPztlF.js → folder-open-C0SfzRFt.js} +1 -1
  22. package/dist/client/assets/index-DawWARY5.css +1 -0
  23. package/dist/client/assets/index-DiC9GfBH.js +656 -0
  24. package/dist/client/assets/{list-checks-DgZgg3rh.js → list-checks-D5c428sr.js} +1 -1
  25. package/dist/client/assets/{star-DXieIAlP.js → star-GGmbVi2b.js} +1 -1
  26. package/dist/client/assets/{upload-Cbvs_TSB.js → upload-CrJJybRJ.js} +1 -1
  27. package/dist/client/assets/{users-DAMIrlue.js → users-C56SMdh4.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/extension.js +427 -103
  31. package/dist/pi-claude-cli/package.json +1 -1
  32. package/package.json +1 -1
  33. package/dist/client/assets/AgentsView-Cq-SEhLc.css +0 -1
  34. package/dist/client/assets/InsightsView-6LHF7OdE.css +0 -1
  35. package/dist/client/assets/InsightsView-u535o96R.js +0 -11
  36. package/dist/client/assets/SettingsModal-Cs5qO84M.js +0 -31
  37. package/dist/client/assets/index-CPStj9Az.css +0 -1
  38. 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 subject = `${prefix}: merge ${branch}`;
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
- aiSummary = await summarizeCommitBody(trimmedDiffStat, rootDir, provider, modelId, {
56516
- branch,
56517
- taskId,
56518
- commitLog: trimmedCommitLog,
56519
- signal
56520
- }).catch(() => null);
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(nodeStatus, policy, isLocal) {
65487
- if (isLocal) {
65488
- return { allowed: true, fallbackToLocal: false, reason: "local-execution" };
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 (nodeStatus === void 0) {
65491
- return { allowed: true, fallbackToLocal: false, reason: "unknown-health" };
65779
+ if (nodeHealth === "online" || nodeHealth === void 0) {
65780
+ return { allowed: true, fallbackToLocal: false };
65492
65781
  }
65493
- if (nodeStatus === "online") {
65494
- return { allowed: true, fallbackToLocal: false, reason: "healthy" };
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: `fallback-local:${nodeStatus}`
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
- fallbackToLocal: false,
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
- blockedNodeTaskIds = /* @__PURE__ */ new Set();
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.blockedNodeTaskIds.clear();
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 !== void 0 && this.options.nodeHealthMonitor) {
66009
- const nodeStatus = this.options.nodeHealthMonitor.getNodeHealth(effectiveNode.nodeId);
66010
- const policyResult = applyUnavailableNodePolicy(
66011
- nodeStatus,
66012
- settings.unavailableNodePolicy,
66013
- false
66014
- );
66015
- if (!policyResult.allowed) {
66016
- if (!this.blockedNodeTaskIds.has(task.id)) {
66017
- this.blockedNodeTaskIds.add(task.id);
66018
- schedulerLog.log(
66019
- `Task ${task.id} dispatch blocked \u2014 node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"} (policy: block)`
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
- this.blockedNodeTaskIds.delete(task.id);
66029
- if (policyResult.fallbackToLocal) {
66030
- schedulerLog.log(
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 (!agentHasIdentity || isAgentEphemeral) {
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
- const isNoTaskRun = !taskId;
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.checkedOutBy && taskDetail.checkedOutBy !== agentId) {
71206
- heartbeatLog.warn(
71207
- `Agent ${agentId} does not hold checkout for ${resolvedTaskId2} (held by ${taskDetail.checkedOutBy}) \u2014 graceful exit`
71208
- );
71209
- await this.completeRun(agentId, run.id, {
71210
- status: "completed",
71211
- resultJson: {
71212
- reason: "checkout_conflict",
71213
- taskId: resolvedTaskId2,
71214
- checkedOutBy: taskDetail.checkedOutBy
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
- return await this.store.getRunDetail(agentId, run.id);
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
- const blockedBy = typeof taskDetail.blockedBy === "string" ? taskDetail.blockedBy.trim() : "";
71220
- const isBlockedTask = taskDetail.status === "queued" && blockedBy.length > 0;
71221
- if (isBlockedTask) {
71222
- const commentCount = (taskDetail.comments?.length ?? 0) + (taskDetail.steeringComments?.length ?? 0);
71223
- const lastCommentId = taskDetail.comments?.at(-1)?.id;
71224
- const lastSteeringCommentId = taskDetail.steeringComments?.at(-1)?.id;
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: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
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");