@runfusion/fusion 0.74.0-beta.0 → 0.74.0-beta.2
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 +9855 -8554
- package/dist/child-process-worker.js +619 -47
- package/dist/client/.vite/manifest.json +265 -265
- package/dist/client/assets/{AgentDetailView-CUXS0Odh.js → AgentDetailView-DH4Cn5iG.js} +1 -1
- package/dist/client/assets/{AgentPermissionPolicyEditor-CzjHjB5r.js → AgentPermissionPolicyEditor-BycC7HhP.js} +1 -1
- package/dist/client/assets/{AgentsView-B5stgI5m.js → AgentsView-Dkij8Sm8.js} +3 -3
- package/dist/client/assets/ChatView-DAsZY1We.js +13 -0
- package/dist/client/assets/{CommandCenter-BdEQ_ZOX.js → CommandCenter-jTzBo-pg.js} +1 -1
- package/dist/client/assets/{DevServerView-BHgAmF3y.js → DevServerView-CxlacYJp.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-BQXVc5YW.js → DirectoryPicker-DsCk3Nsw.js} +1 -1
- package/dist/client/assets/{DocumentsView-CxN0Ptrj.js → DocumentsView-8EI6KNBY.js} +1 -1
- package/dist/client/assets/{EvalsView-DAgJtpO5.js → EvalsView-kYnaIk11.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-NZcXkGrN.js → ExperimentalAgentOnboardingModal-ce6DROKD.js} +1 -1
- package/dist/client/assets/{GoalsView-Dwq7NW4j.js → GoalsView-BVlTtw-u.js} +1 -1
- package/dist/client/assets/{InsightsView-CUlL5NSC.js → InsightsView-BKQeROkb.js} +1 -1
- package/dist/client/assets/{MemoryView-WfHzJflp.js → MemoryView-DCzTnxsV.js} +1 -1
- package/dist/client/assets/{PiExtensionsManager-BS1dkauB.js → PiExtensionsManager-C94HwDz5.js} +1 -1
- package/dist/client/assets/{PluginManager-B1oLb0pn.js → PluginManager-C3DegklW.js} +1 -1
- package/dist/client/assets/{PullRequestView-Cdp662lx.js → PullRequestView-CrE89UL7.js} +1 -1
- package/dist/client/assets/{ReportModal-C92wQO-k.js → ReportModal-BD1ZUDdV.js} +1 -1
- package/dist/client/assets/{ResearchView-4-SoxmQv.js → ResearchView-eMiRw1EM.js} +1 -1
- package/dist/client/assets/{SecretsView-C5UQ1Mms.js → SecretsView-6qK8Idb7.js} +1 -1
- package/dist/client/assets/{SessionTerminal-BTjX9p3a.js → SessionTerminal-DtFLyUaK.js} +1 -1
- package/dist/client/assets/{SettingsModal-B37HBcKT.css → SettingsModal-BF1KzZ0q.css} +1 -1
- package/dist/client/assets/SettingsModal-BTfZlghk.css +1 -0
- package/dist/client/assets/{SettingsModal-BfHAE14s.js → SettingsModal-BkOSlfyr.js} +1 -1
- package/dist/client/assets/SettingsModal-DSq-_qwo.js +21 -0
- package/dist/client/assets/{SettingsTextareaRow-C2JqzQdD.js → SettingsTextareaRow-Btyx5kaR.js} +1 -1
- package/dist/client/assets/{SetupWizardModal-C1KmMVfU.js → SetupWizardModal-Blhx-dRt.js} +2 -2
- package/dist/client/assets/{SkillsView-DyLNdfDy.js → SkillsView-DGE5kYc3.js} +1 -1
- package/dist/client/assets/{TodoView-DyrniX19.js → TodoView-YSGI7ATd.js} +1 -1
- package/dist/client/assets/{WorkflowNodeEditor-Bh-Hn1Kx.js → WorkflowNodeEditor-CZiEThu2.js} +1 -1
- package/dist/client/assets/{agent-import-generation-C5pyefIj.js → agent-import-generation-DQeNZMAm.js} +1 -1
- package/dist/client/assets/{app-YM47G7Ks.js → app-DJIiVHdA.js} +1 -1
- package/dist/client/assets/{app-DNrauoYS.js → app-DMZ_RusZ.js} +3 -3
- package/dist/client/assets/{app-Bl764LdY.js → app-DaikOPcV.js} +3 -3
- package/dist/client/assets/{app-QmIotwFZ.js → app-Dn1u7zJ9.js} +3 -3
- package/dist/client/assets/{app-DdCnRHSe.js → app-DxTbBEMH.js} +3 -3
- package/dist/client/assets/{app-oUEtzFOj.js → app-hQw_x2KJ.js} +3 -3
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B6axGlaS.js → architectureDiagram-3BPJPVTR-DhaJNKa4.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-BENjJaZD.js → blockDiagram-GPEHLZMM-pKpg7gJU.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-Cl2qKhuO.js → c4Diagram-AAUBKEIU-DVPXKJnr.js} +1 -1
- package/dist/client/assets/channel-xFBfK0sX.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-BWo7R8T7.js → chunk-2J33WTMH-CHt90T5z.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DiRoI7pq.js → chunk-4BX2VUAB-DsWFYy0a.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-DPtv1GVO.js → chunk-55IACEB6-D8XiXEo8.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-BVcm5ITs.js → chunk-727SXJPM-DlUYBn_Y.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-CFT4bmXK.js → chunk-AQP2D5EJ-BSdPOr9q.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-BDcEHQ0o.js → chunk-FMBD7UC4-B9IAeBld.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-CYlDBFdI.js → chunk-ND2GUHAM-CmxnYQrR.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-3Wv3WxYt.js → chunk-QZHKN3VN-Dxp6Po_j.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BFBVD968.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BFBVD968.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-BuMkKBMC.js → cose-bilkent-S5V4N54A-BNl3bhpS.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-D1P4z_OM.js → dagre-BM42HDAG-DFs7WCcz.js} +1 -1
- package/dist/client/assets/{dashboard-view-BgQBLynI.js → dashboard-view-DnoTcnIo.js} +1 -1
- package/dist/client/assets/{dashboard-view-DaRKU5oU.js → dashboard-view-ZGxLEL9r.js} +1 -1
- package/dist/client/assets/{dashboard-view-CcK2sH0v.js → dashboard-view-vc8kgLMs.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-hmY-reWF.js → diagram-2AECGRRQ-C2PVozvu.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CGfRqm_0.js → diagram-5GNKFQAL-DFJ5b9H8.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BfKo4Jd4.js → diagram-KO2AKTUF-D335CW-c.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-BZqpNemd.js → diagram-LMA3HP47-DSfhroAW.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-CxLTV4DH.js → diagram-OG6HWLK6-8iGveM-Z.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-vw3tx2tK.js → erDiagram-TEJ5UH35-Xc56MEEi.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-OEm-Us2U.js → flowDiagram-I6XJVG4X-DjCU9WbD.js} +1 -1
- package/dist/client/assets/{folder-open-D_zgZHWi.js → folder-open-BRrB44UX.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-C5bc1DG1.js → ganttDiagram-6RSMTGT7-CXQE1bea.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DEvcWRBq.js → gitGraphDiagram-PVQCEYII--mrSABLC.js} +1 -1
- package/dist/client/assets/index-C2xsZoNG.js +2671 -0
- package/dist/client/assets/index-Dv-95s50.css +1 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-kFOE9wxd.js → infoDiagram-5YYISTIA-CTS_nljP.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DvLbzHmR.js → ishikawaDiagram-YF4QCWOH-CpV5x3gk.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-ov6Tbcuz.js → journeyDiagram-JHISSGLW-BK2QCMnt.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Dr0foW2M.js → kanban-definition-UN3LZRKU-B4srpspY.js} +1 -1
- package/dist/client/assets/{mermaid.core-CM55309V.js → mermaid.core-B8DVAt21.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-xNyB6Z6o.js → mindmap-definition-RKZ34NQL-CsmmiPpA.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DlS5Kpga.js → pieDiagram-4H26LBE5-BahLDqf4.js} +1 -1
- package/dist/client/assets/{puzzle-BrEvi5DH.js → puzzle-BFPQsuIa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D4z2CHAA.js → quadrantDiagram-W4KKPZXB-Bo3u0wTU.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-Bcwt6-p9.js → requirementDiagram-4Y6WPE33-DYkYsz7d.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-C54N76yk.js → sankeyDiagram-5OEKKPKP-BCbh0BSY.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DtqryztE.js → sequenceDiagram-3UESZ5HK-C2AP9GlE.js} +1 -1
- package/dist/client/assets/{shield-alert-DhxKVKek.js → shield-alert-tlZOi02c.js} +1 -1
- package/dist/client/assets/{standing-instructions-template-CdHJwmLI.js → standing-instructions-template-C0Rzv9Mr.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-DQAvSrHw.js → stateDiagram-AJRCARHV-CARkH0ug.js} +1 -1
- package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-BzIGKq7y.js → stateDiagram-v2-BHNVJYJU-C2He043y.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-GDj_48Um.js → timeline-definition-PNZ67QCA-MvbYneEM.js} +1 -1
- package/dist/client/assets/{upload-B7GKZiF1.js → upload-DWK0F17Z.js} +1 -1
- package/dist/client/assets/{users-CBtvFVLM.js → users-1AkDfM14.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-PQDmLFbj.js → vennDiagram-CIIHVFJN-DOjzzLqc.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-GmGtXctI.js → wardley-L42UT6IY-Di70vhu2.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Bv6slmsW.js → wardleyDiagram-YWT4CUSO-nK6ARhDv.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CK1RJHwB.js → xychartDiagram-2RQKCTM6-C7_y76B7.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/extension.js +6835 -5653
- package/package.json +1 -1
- package/dist/client/assets/ChatView-CpoOgZ4X.js +0 -13
- package/dist/client/assets/SettingsModal-BcnOOo9g.js +0 -21
- package/dist/client/assets/SettingsModal-CMLHZBhX.css +0 -1
- package/dist/client/assets/channel-CktWdAhK.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CrUS4gHO.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CrUS4gHO.js +0 -1
- package/dist/client/assets/index-BinRUCFV.js +0 -2661
- package/dist/client/assets/index-DGmuyTtv.css +0 -1
|
@@ -1663,6 +1663,7 @@ var init_settings_schema = __esm({
|
|
|
1663
1663
|
defaultProvider: void 0,
|
|
1664
1664
|
defaultModelId: void 0,
|
|
1665
1665
|
testMode: void 0,
|
|
1666
|
+
voiceInput: void 0,
|
|
1666
1667
|
modelPricingOverrides: void 0,
|
|
1667
1668
|
modelPricingFetchedAt: void 0,
|
|
1668
1669
|
modelPricingSource: void 0,
|
|
@@ -1954,6 +1955,7 @@ var init_settings_schema = __esm({
|
|
|
1954
1955
|
// with this on but auto-merge off, review threads are resolved but the PR is not merged.
|
|
1955
1956
|
autoResolveReviewComments: true,
|
|
1956
1957
|
testMode: void 0,
|
|
1958
|
+
voiceInput: void 0,
|
|
1957
1959
|
mergeRequestContractShadowEnabled: false,
|
|
1958
1960
|
mergeStrategy: "direct",
|
|
1959
1961
|
directMergeCommitStrategy: "always-squash",
|
|
@@ -18387,9 +18389,13 @@ function buildRefinementSeedPrompt(title, description) {
|
|
|
18387
18389
|
${description}
|
|
18388
18390
|
`;
|
|
18389
18391
|
}
|
|
18392
|
+
function normalizeSeedText(content) {
|
|
18393
|
+
return content.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trimEnd();
|
|
18394
|
+
}
|
|
18390
18395
|
function isUnplannedSeedPrompt(content, taskId, title, description) {
|
|
18391
|
-
|
|
18392
|
-
|
|
18396
|
+
const normalized = normalizeSeedText(content);
|
|
18397
|
+
if (normalized === normalizeSeedText(buildBootstrapPrompt(taskId, title, description))) return true;
|
|
18398
|
+
return title !== void 0 && normalized === normalizeSeedText(buildRefinementSeedPrompt(title, description));
|
|
18393
18399
|
}
|
|
18394
18400
|
var init_mesh_task_replication = __esm({
|
|
18395
18401
|
"../core/src/mesh-task-replication.ts"() {
|
|
@@ -29650,9 +29656,11 @@ var init_agent_store = __esm({
|
|
|
29650
29656
|
`Invalid state transition: ${currentState} -> ${newState}. Valid transitions: ${validTransitions.join(", ")}`
|
|
29651
29657
|
);
|
|
29652
29658
|
}
|
|
29659
|
+
const clearsPauseReasonOnResume = (currentState === "paused" || currentState === "error") && (newState === "active" || newState === "idle" || newState === "running");
|
|
29653
29660
|
const updated = {
|
|
29654
29661
|
...agent,
|
|
29655
29662
|
state: newState,
|
|
29663
|
+
...clearsPauseReasonOnResume && { pauseReason: void 0 },
|
|
29656
29664
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
29657
29665
|
};
|
|
29658
29666
|
await this.writeAgent(updated);
|
|
@@ -64738,11 +64746,18 @@ async function moveTaskInternalImpl(store, id, toColumn, options, internal, curr
|
|
|
64738
64746
|
const isLegacyRecoveryRehome = options?.recoveryRehome === true && sourceIsLegacy && COLUMNS.includes(toColumn);
|
|
64739
64747
|
const isWorkflowDeclaredRecoveryRehome = options?.recoveryRehome === true && !COLUMNS.includes(toColumn) && workflowHasColumn(await resolveTaskWorkflowIrForMove(store, id), toColumn);
|
|
64740
64748
|
if (!isEvacuation && !isLegacyRecoveryRehome && !isWorkflowDeclaredRecoveryRehome) {
|
|
64741
|
-
const
|
|
64742
|
-
if (!
|
|
64743
|
-
|
|
64744
|
-
|
|
64749
|
+
const legacyTargets = sourceIsLegacy ? VALID_TRANSITIONS[task.column] ?? [] : [];
|
|
64750
|
+
if (!legacyTargets.includes(toColumn)) {
|
|
64751
|
+
const workflowTargets = resolveAllowedColumns(
|
|
64752
|
+
await resolveTaskWorkflowIrForMove(store, id),
|
|
64753
|
+
task.column
|
|
64745
64754
|
);
|
|
64755
|
+
const validTargets = [.../* @__PURE__ */ new Set([...legacyTargets, ...workflowTargets])];
|
|
64756
|
+
if (!validTargets.includes(toColumn)) {
|
|
64757
|
+
throw new Error(
|
|
64758
|
+
`Invalid transition: '${task.column}' \u2192 '${toColumn}'. Valid targets: ${validTargets.join(", ") || "none"}`
|
|
64759
|
+
);
|
|
64760
|
+
}
|
|
64746
64761
|
}
|
|
64747
64762
|
}
|
|
64748
64763
|
if (fromColumn === "in-review" && toColumn === "done" && !options?.skipMergeBlocker) {
|
|
@@ -87812,7 +87827,8 @@ async function _createTaskInternalBackendImpl(store, input, title, resolvedWorkf
|
|
|
87812
87827
|
ownsStagingDirectory = true;
|
|
87813
87828
|
if (store.isWatching) store.taskCache.set(id, { ...task });
|
|
87814
87829
|
await store.writeTaskJsonFile(stagingDir, task);
|
|
87815
|
-
const
|
|
87830
|
+
const isUnplannedStartCreate = options?.resolvedEntryColumn !== void 0 && options.resolvedEntryColumn !== "triage" && task.column === "todo";
|
|
87831
|
+
const isIntakeColumn = task.column === "triage" || options?.resolvedEntryColumn !== void 0 && task.column === options.resolvedEntryColumn || isUnplannedStartCreate;
|
|
87816
87832
|
const usedBootstrapPrompt = !options?.promptOverride && isIntakeColumn;
|
|
87817
87833
|
const prompt = options?.promptOverride ?? (isIntakeColumn ? buildBootstrapPrompt(id, task.title, task.description) : store.generateSpecifiedPrompt(task));
|
|
87818
87834
|
if (!usedBootstrapPrompt) {
|
|
@@ -88264,7 +88280,8 @@ async function _createTaskInternalImpl(store, input, title, resolvedWorkflowStep
|
|
|
88264
88280
|
const dir = store.taskDir(id);
|
|
88265
88281
|
await store.atomicCreateTaskJson(dir, task, "createTask");
|
|
88266
88282
|
if (store.isWatching) store.taskCache.set(id, { ...task });
|
|
88267
|
-
const
|
|
88283
|
+
const isUnplannedStartCreate = options?.resolvedEntryColumn !== void 0 && options.resolvedEntryColumn !== "triage" && task.column === "todo";
|
|
88284
|
+
const isIntakeColumn = task.column === "triage" || options?.resolvedEntryColumn !== void 0 && task.column === options.resolvedEntryColumn || isUnplannedStartCreate;
|
|
88268
88285
|
const usedBootstrapPrompt = !options?.promptOverride && isIntakeColumn;
|
|
88269
88286
|
const prompt = options?.promptOverride ?? (isIntakeColumn ? buildBootstrapPrompt(id, task.title, task.description) : store.generateSpecifiedPrompt(task));
|
|
88270
88287
|
if (!usedBootstrapPrompt) {
|
|
@@ -132880,6 +132897,23 @@ var init_worktrunk_installer = __esm({
|
|
|
132880
132897
|
});
|
|
132881
132898
|
|
|
132882
132899
|
// ../engine/src/active-session-registry.ts
|
|
132900
|
+
function acquireActiveSessionPath(registry3, path5, registration, options = {}) {
|
|
132901
|
+
const existing = registry3.lookupByPath(path5);
|
|
132902
|
+
if (!existing || existing.taskId === registration.taskId) {
|
|
132903
|
+
registry3.registerPath(path5, registration);
|
|
132904
|
+
return { action: "registered" };
|
|
132905
|
+
}
|
|
132906
|
+
const now = options.now?.() ?? Date.now();
|
|
132907
|
+
const ageMs = now - existing.registeredAt;
|
|
132908
|
+
const minIdleMs = options.minIdleMs ?? DEFAULT_SELF_OWNED_MIN_IDLE_MS;
|
|
132909
|
+
const holderIsLive = options.holderLiveProbe?.(existing.taskId, path5) ?? true;
|
|
132910
|
+
if (holderIsLive || ageMs < minIdleMs) {
|
|
132911
|
+
return { action: "contended", holderTaskId: existing.taskId, holderKind: existing.kind, ageMs };
|
|
132912
|
+
}
|
|
132913
|
+
registry3.unregisterPath(path5);
|
|
132914
|
+
registry3.registerPath(path5, registration);
|
|
132915
|
+
return { action: "reclaimed-stale-foreign", holderTaskId: existing.taskId, ageMs };
|
|
132916
|
+
}
|
|
132883
132917
|
function reconcileSelfOwnedActiveSessionForRemoval(registry3, worktreePath, requestingTaskId, liveBindingProbe, options = {}) {
|
|
132884
132918
|
const record2 = registry3.lookupByPath(worktreePath);
|
|
132885
132919
|
if (!record2) {
|
|
@@ -136153,15 +136187,26 @@ function countCapacitySlot(allTasks, effectiveWorkflowIdByTask, budgetColumns, w
|
|
|
136153
136187
|
}
|
|
136154
136188
|
return count2;
|
|
136155
136189
|
}
|
|
136190
|
+
function trackHeld(taskId, reason, nowMs) {
|
|
136191
|
+
const existing = heldSince.get(taskId);
|
|
136192
|
+
if (!existing || existing.reason !== reason) {
|
|
136193
|
+
heldSince.set(taskId, { reason, sinceMs: nowMs });
|
|
136194
|
+
return 0;
|
|
136195
|
+
}
|
|
136196
|
+
return nowMs - existing.sinceMs;
|
|
136197
|
+
}
|
|
136156
136198
|
async function runHoldReleaseSweep(store, deps) {
|
|
136157
136199
|
const result = { released: [], held: [] };
|
|
136200
|
+
const sweepStartedMs = deps.now();
|
|
136158
136201
|
const settings = await store.getSettings();
|
|
136159
136202
|
const allTasks = await store.listTasks({ includeArchived: false });
|
|
136160
136203
|
const irCache = /* @__PURE__ */ new Map();
|
|
136161
136204
|
const effectiveWorkflowIdByTask = /* @__PURE__ */ new Map();
|
|
136205
|
+
const prefetchStartedMs = deps.now();
|
|
136162
136206
|
for (const t of allTasks) {
|
|
136163
136207
|
effectiveWorkflowIdByTask.set(t.id, await effectiveWorkflowId(store, t.id));
|
|
136164
136208
|
}
|
|
136209
|
+
const prefetchMs = deps.now() - prefetchStartedMs;
|
|
136165
136210
|
for (const task of allTasks) {
|
|
136166
136211
|
if (task.paused || task.userPaused) {
|
|
136167
136212
|
continue;
|
|
@@ -136176,6 +136221,7 @@ async function runHoldReleaseSweep(store, deps) {
|
|
|
136176
136221
|
if (!column || !holdConfig) continue;
|
|
136177
136222
|
const release2 = typeof holdConfig.release === "string" ? holdConfig.release : "manual";
|
|
136178
136223
|
if (release2 === "manual" || release2 === "external-event") {
|
|
136224
|
+
trackHeld(task.id, `${release2}-only`, deps.now());
|
|
136179
136225
|
result.held.push({ taskId: task.id, reason: `${release2}-only` });
|
|
136180
136226
|
continue;
|
|
136181
136227
|
}
|
|
@@ -136184,18 +136230,21 @@ async function runHoldReleaseSweep(store, deps) {
|
|
|
136184
136230
|
const deadline = resolveTimerDeadline(holdConfig, task);
|
|
136185
136231
|
shouldRelease = deadline !== void 0 && deps.now() >= deadline;
|
|
136186
136232
|
if (!shouldRelease) {
|
|
136233
|
+
trackHeld(task.id, "timer-not-elapsed", deps.now());
|
|
136187
136234
|
result.held.push({ taskId: task.id, reason: "timer-not-elapsed" });
|
|
136188
136235
|
continue;
|
|
136189
136236
|
}
|
|
136190
136237
|
} else if (release2 === "dependency") {
|
|
136191
136238
|
shouldRelease = await allDependenciesSatisfied(store, task, allTasks);
|
|
136192
136239
|
if (!shouldRelease) {
|
|
136240
|
+
trackHeld(task.id, "deps-unsatisfied", deps.now());
|
|
136193
136241
|
result.held.push({ taskId: task.id, reason: "deps-unsatisfied" });
|
|
136194
136242
|
continue;
|
|
136195
136243
|
}
|
|
136196
136244
|
} else if (release2 === "capacity") {
|
|
136197
136245
|
const target2 = resolveReleaseTarget(ir2, task.column, true);
|
|
136198
136246
|
if (!target2) {
|
|
136247
|
+
trackHeld(task.id, "no-downstream-capacity-column", deps.now());
|
|
136199
136248
|
result.held.push({ taskId: task.id, reason: "no-downstream-capacity-column" });
|
|
136200
136249
|
continue;
|
|
136201
136250
|
}
|
|
@@ -136205,6 +136254,7 @@ async function runHoldReleaseSweep(store, deps) {
|
|
|
136205
136254
|
const budgetColumns = new Set(resolveWipBudgetColumns(ir2, target2));
|
|
136206
136255
|
const occupants = countCapacitySlot(allTasks, effectiveWorkflowIdByTask, budgetColumns, workflowId, capacity.countPending);
|
|
136207
136256
|
if (occupants >= capacity.limit) {
|
|
136257
|
+
trackHeld(task.id, "downstream-full", deps.now());
|
|
136208
136258
|
result.held.push({ taskId: task.id, reason: "downstream-full" });
|
|
136209
136259
|
continue;
|
|
136210
136260
|
}
|
|
@@ -136214,16 +136264,40 @@ async function runHoldReleaseSweep(store, deps) {
|
|
|
136214
136264
|
if (!shouldRelease) continue;
|
|
136215
136265
|
const target = resolveReleaseTarget(ir2, task.column, release2 === "capacity");
|
|
136216
136266
|
if (!target) {
|
|
136267
|
+
trackHeld(task.id, "no-release-target", deps.now());
|
|
136217
136268
|
result.held.push({ taskId: task.id, reason: "no-release-target" });
|
|
136218
136269
|
continue;
|
|
136219
136270
|
}
|
|
136220
136271
|
const released = await issueRelease(store, deps, task, target, ir2);
|
|
136221
136272
|
if (released) {
|
|
136273
|
+
const waitedMs = deps.now() - (heldSince.get(task.id)?.sinceMs ?? deps.now());
|
|
136274
|
+
heldSince.delete(task.id);
|
|
136275
|
+
schedulerLog.log(
|
|
136276
|
+
`Hold release for ${task.id} \u2192 ${target} after ${waitedMs}ms held (release=${release2})`
|
|
136277
|
+
);
|
|
136222
136278
|
result.released.push(task.id);
|
|
136223
136279
|
} else {
|
|
136280
|
+
trackHeld(task.id, "move-rejected-or-no-slot", deps.now());
|
|
136224
136281
|
result.held.push({ taskId: task.id, reason: "move-rejected-or-no-slot" });
|
|
136225
136282
|
}
|
|
136226
136283
|
}
|
|
136284
|
+
const sweepMs = deps.now() - sweepStartedMs;
|
|
136285
|
+
const longestHeldMs = result.held.reduce((max2, h) => {
|
|
136286
|
+
const entry = heldSince.get(h.taskId);
|
|
136287
|
+
return entry ? Math.max(max2, deps.now() - entry.sinceMs) : max2;
|
|
136288
|
+
}, 0);
|
|
136289
|
+
const summary = `Hold-release sweep: ${sweepMs}ms (prefetch ${prefetchMs}ms over ${allTasks.length} tasks), released=${result.released.length}, held=${result.held.length}` + (longestHeldMs > 0 ? `, longest held ${longestHeldMs}ms` : "");
|
|
136290
|
+
if (sweepMs >= SLOW_SWEEP_WARN_MS) {
|
|
136291
|
+
schedulerLog.warn(`${summary} \u2014 sweep exceeded ${SLOW_SWEEP_WARN_MS}ms and is itself the delay`);
|
|
136292
|
+
} else if (result.released.length > 0) {
|
|
136293
|
+
schedulerLog.log(summary);
|
|
136294
|
+
} else {
|
|
136295
|
+
schedulerLog.debug(summary);
|
|
136296
|
+
}
|
|
136297
|
+
const stillHeld = new Set(result.held.map((h) => h.taskId));
|
|
136298
|
+
for (const taskId of [...heldSince.keys()]) {
|
|
136299
|
+
if (!stillHeld.has(taskId)) heldSince.delete(taskId);
|
|
136300
|
+
}
|
|
136227
136301
|
return result;
|
|
136228
136302
|
}
|
|
136229
136303
|
async function issueRelease(store, deps, task, target, ir2) {
|
|
@@ -136326,12 +136400,15 @@ async function releaseHeldTaskByEvent(store, taskId, eventTag, deps = {}) {
|
|
|
136326
136400
|
);
|
|
136327
136401
|
return released ? { released: true, toColumn: target } : { released: false, rejection: "capacity-exhausted-or-no-slot" };
|
|
136328
136402
|
}
|
|
136403
|
+
var heldSince, SLOW_SWEEP_WARN_MS;
|
|
136329
136404
|
var init_hold_release = __esm({
|
|
136330
136405
|
"../engine/src/hold-release.ts"() {
|
|
136331
136406
|
"use strict";
|
|
136332
136407
|
init_src2();
|
|
136333
136408
|
init_logger();
|
|
136334
136409
|
init_spec_staleness();
|
|
136410
|
+
heldSince = /* @__PURE__ */ new Map();
|
|
136411
|
+
SLOW_SWEEP_WARN_MS = 2e3;
|
|
136335
136412
|
}
|
|
136336
136413
|
});
|
|
136337
136414
|
|
|
@@ -162516,6 +162593,12 @@ var init_usage_limit_detector = __esm({
|
|
|
162516
162593
|
});
|
|
162517
162594
|
|
|
162518
162595
|
// ../engine/src/transient-error-patterns.ts
|
|
162596
|
+
function isSessionContentionError(errorMessage) {
|
|
162597
|
+
if (!errorMessage || typeof errorMessage !== "string") {
|
|
162598
|
+
return false;
|
|
162599
|
+
}
|
|
162600
|
+
return SESSION_CONTENTION_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
162601
|
+
}
|
|
162519
162602
|
function isTransientError(errorMessage) {
|
|
162520
162603
|
if (!errorMessage || typeof errorMessage !== "string") {
|
|
162521
162604
|
return false;
|
|
@@ -162523,6 +162606,9 @@ function isTransientError(errorMessage) {
|
|
|
162523
162606
|
if (isTransientAuthCredentialError(errorMessage)) {
|
|
162524
162607
|
return true;
|
|
162525
162608
|
}
|
|
162609
|
+
if (isSessionContentionError(errorMessage)) {
|
|
162610
|
+
return true;
|
|
162611
|
+
}
|
|
162526
162612
|
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
162527
162613
|
}
|
|
162528
162614
|
function isTransientAuthCredentialError(errorMessage) {
|
|
@@ -162534,7 +162620,7 @@ function isTransientAuthCredentialError(errorMessage) {
|
|
|
162534
162620
|
}
|
|
162535
162621
|
return TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN.test(errorMessage);
|
|
162536
162622
|
}
|
|
162537
|
-
var TRANSIENT_ERROR_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
|
|
162623
|
+
var TRANSIENT_ERROR_PATTERNS, SESSION_CONTENTION_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
|
|
162538
162624
|
var init_transient_error_patterns = __esm({
|
|
162539
162625
|
"../engine/src/transient-error-patterns.ts"() {
|
|
162540
162626
|
"use strict";
|
|
@@ -162595,6 +162681,14 @@ var init_transient_error_patterns = __esm({
|
|
|
162595
162681
|
/\bACP failed to start\b/i,
|
|
162596
162682
|
/\bACP session has no live connection\b/i
|
|
162597
162683
|
];
|
|
162684
|
+
SESSION_CONTENTION_PATTERNS = [
|
|
162685
|
+
// ActiveSessionPathHeldByForeignTaskError (active-session-registry.ts)
|
|
162686
|
+
/active-session path\s+\S+\s+is held by task\s+\S+/i,
|
|
162687
|
+
// WorkspaceRepoAcquireBusyError (worktree-acquisition.ts)
|
|
162688
|
+
/workspace sub-repo\s+\S+\s+acquisition is in progress for task\s+\S+/i,
|
|
162689
|
+
// WorkspaceRepoLandBusyError (merger-ai.ts)
|
|
162690
|
+
/workspace sub-repo\s+\S+\s+land is in progress for task\s+\S+/i
|
|
162691
|
+
];
|
|
162598
162692
|
TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN = /"type":\s*"authentication_error"|invalid authentication credentials|token[_\s]?expired/i;
|
|
162599
162693
|
OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN = /oauth token does not meet scope|insufficient[_\s-]?scope|invalid[_\s-]?scope|invalid (?:api[_\s-]?key|x-api-key)|missing\s+(?:\S+\s+)?(?:api[_\s-]?)?key|revoked|suspend(?:ed)?|disabled|deactivated|subscription|account (?:is )?(?:locked|closed|inactive)|access denied/i;
|
|
162600
162694
|
}
|
|
@@ -162603,6 +162697,9 @@ var init_transient_error_patterns = __esm({
|
|
|
162603
162697
|
// ../engine/src/transient-error-detector.ts
|
|
162604
162698
|
function isNonPlanDefectPlanReviewFailure(input) {
|
|
162605
162699
|
if (input.verdict === "REVISE") return false;
|
|
162700
|
+
if (input.failureValue?.trim().toLowerCase() === "session-contention-hold" || (input.errorMessage ? isSessionContentionError(input.errorMessage) : false)) {
|
|
162701
|
+
return false;
|
|
162702
|
+
}
|
|
162606
162703
|
const failureValue = input.failureValue?.trim().toLowerCase();
|
|
162607
162704
|
if (failureValue === "exception" || failureValue === "aborted") return true;
|
|
162608
162705
|
const errorMessage = input.errorMessage?.trim();
|
|
@@ -191287,7 +191384,7 @@ function hasAdvancedPastPlanning(task) {
|
|
|
191287
191384
|
if (task.column === "in-progress" || task.column === "in-review" || task.column === "done" || task.column === "archived") {
|
|
191288
191385
|
return true;
|
|
191289
191386
|
}
|
|
191290
|
-
if (task.
|
|
191387
|
+
if (task.firstExecutionAt != null || task.executionStartedAt != null) {
|
|
191291
191388
|
return true;
|
|
191292
191389
|
}
|
|
191293
191390
|
if (task.column === "triage") {
|
|
@@ -191897,6 +191994,15 @@ var Scheduler = class {
|
|
|
191897
191994
|
this.schedule();
|
|
191898
191995
|
}
|
|
191899
191996
|
}
|
|
191997
|
+
if (task.status === "planning") {
|
|
191998
|
+
this.planningTaskIds.add(task.id);
|
|
191999
|
+
} else if (this.planningTaskIds.has(task.id)) {
|
|
192000
|
+
this.planningTaskIds.delete(task.id);
|
|
192001
|
+
if (this.running && !task.status && !task.paused && !task.userPaused && (task.column === "todo" || task.column === "triage")) {
|
|
192002
|
+
schedulerLog.log(`Task ${task.id} finished planning \u2014 triggering scheduling`);
|
|
192003
|
+
this.schedule();
|
|
192004
|
+
}
|
|
192005
|
+
}
|
|
191900
192006
|
if (!this.options.prMonitor) return;
|
|
191901
192007
|
if (task.column !== "in-review") return;
|
|
191902
192008
|
if (!task.prInfo) return;
|
|
@@ -191914,6 +192020,7 @@ var Scheduler = class {
|
|
|
191914
192020
|
this.lastAutoClaimFingerprint.delete(task.id);
|
|
191915
192021
|
this.options.snapshotManager?.invalidate("task:deleted");
|
|
191916
192022
|
this.pausedTaskIds.delete(task.id);
|
|
192023
|
+
this.planningTaskIds.delete(task.id);
|
|
191917
192024
|
this.failedTaskIds.delete(task.id);
|
|
191918
192025
|
this.recentEngineTodoRequeues.delete(task.id);
|
|
191919
192026
|
this.wasNodeDispatchValidationBlocked.delete(task.id);
|
|
@@ -191986,6 +192093,13 @@ var Scheduler = class {
|
|
|
191986
192093
|
activePollMs = null;
|
|
191987
192094
|
/** Tracks which task IDs are currently paused, to detect unpause transitions. */
|
|
191988
192095
|
pausedTaskIds = /* @__PURE__ */ new Set();
|
|
192096
|
+
/**
|
|
192097
|
+
* FNXC:CodingIdeasWorkflow 2026-07-25-13:10:
|
|
192098
|
+
* Tracks task IDs last seen with status "planning", so the planning -> dispatchable transition
|
|
192099
|
+
* can trigger an immediate scheduling pass. Plan-in-place workflows clear status without a
|
|
192100
|
+
* task:moved, so this is the only signal that the card just became executable.
|
|
192101
|
+
*/
|
|
192102
|
+
planningTaskIds = /* @__PURE__ */ new Set();
|
|
191989
192103
|
/** Tracks mission-linked tasks observed with status=failed before moveTask clears status/error. */
|
|
191990
192104
|
failedTaskIds = /* @__PURE__ */ new Set();
|
|
191991
192105
|
/** Tracks tasks blocked by unavailable-node policy to deduplicate block log entries. */
|
|
@@ -192405,7 +192519,7 @@ var Scheduler = class {
|
|
|
192405
192519
|
todo.map(async (t) => {
|
|
192406
192520
|
try {
|
|
192407
192521
|
const content = await readFile30(getPromptPath(this.store.getTasksDir(), t.id), "utf-8");
|
|
192408
|
-
if (content
|
|
192522
|
+
if (isUnplannedSeedPrompt(content, t.id, t.title, t.description)) return null;
|
|
192409
192523
|
} catch {
|
|
192410
192524
|
}
|
|
192411
192525
|
return t;
|
|
@@ -194467,6 +194581,7 @@ init_src2();
|
|
|
194467
194581
|
// ../engine/src/workflow-graph-executor.ts
|
|
194468
194582
|
init_src2();
|
|
194469
194583
|
init_transient_error_detector();
|
|
194584
|
+
init_transient_error_patterns();
|
|
194470
194585
|
|
|
194471
194586
|
// ../engine/src/required-workflow-artifacts.ts
|
|
194472
194587
|
var REQUIRED_ARTIFACT_MISSING_PREFIX = "required-artifact-missing:";
|
|
@@ -195545,6 +195660,7 @@ function workflowNodeRequiresWorktree(node, { optionalGroupId, reviewerInlineFix
|
|
|
195545
195660
|
// ../engine/src/workflow-graph-executor.ts
|
|
195546
195661
|
var PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE = "plan-review-provider-failure-hold";
|
|
195547
195662
|
var PLAN_REVIEW_LEASE_HELD_VALUE = "plan-review-lease-held";
|
|
195663
|
+
var SESSION_CONTENTION_HOLD_VALUE = "session-contention-hold";
|
|
195548
195664
|
var WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY = "workflow:interruptedNodeId";
|
|
195549
195665
|
var WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY = "workflow:interruptedNodeAbortKind";
|
|
195550
195666
|
var WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY = "workflow:optionalGroupActive";
|
|
@@ -195914,6 +196030,15 @@ var WorkflowGraphExecutor = class {
|
|
|
195914
196030
|
};
|
|
195915
196031
|
context[`node:${node.id}:outcome`] = result2.outcome;
|
|
195916
196032
|
if (result2.value !== void 0) context[`node:${node.id}:value`] = result2.value;
|
|
196033
|
+
const sessionContentionFailure = stepStatus === "failed" && (verdictRaw === SESSION_CONTENTION_HOLD_VALUE || isSessionContentionError(stepOutput ?? stepNotes ?? ""));
|
|
196034
|
+
if (sessionContentionFailure) {
|
|
196035
|
+
this.deps.logTaskEntry?.(
|
|
196036
|
+
`${logPrefix} ${groupName} is waiting on another task to release its session path \u2014 holding in place`
|
|
196037
|
+
);
|
|
196038
|
+
context[`node:${node.id}:outcome`] = "failure";
|
|
196039
|
+
context[`node:${node.id}:value`] = SESSION_CONTENTION_HOLD_VALUE;
|
|
196040
|
+
return { outcome: "failure", value: SESSION_CONTENTION_HOLD_VALUE };
|
|
196041
|
+
}
|
|
195917
196042
|
const nonPlanDefectPlanReviewFailure = node.id === PLAN_REVIEW_GROUP_ID && stepStatus === "failed" && (isRequiredArtifactReadFailedValue(verdictRaw) || isNonPlanDefectPlanReviewFailure({
|
|
195918
196043
|
verdict,
|
|
195919
196044
|
errorMessage: stepOutput ?? stepNotes,
|
|
@@ -196297,6 +196422,7 @@ var WorkflowGraphExecutor = class {
|
|
|
196297
196422
|
} catch (error2) {
|
|
196298
196423
|
if (signal?.aborted) return this.withEnginePauseAbortContext(node, { outcome: "failure", value: "aborted" });
|
|
196299
196424
|
lastError = error2;
|
|
196425
|
+
if (isSessionContentionError(error2 instanceof Error ? error2.message : String(error2))) break;
|
|
196300
196426
|
}
|
|
196301
196427
|
}
|
|
196302
196428
|
if (signal?.aborted) {
|
|
@@ -196315,11 +196441,13 @@ var WorkflowGraphExecutor = class {
|
|
|
196315
196441
|
}
|
|
196316
196442
|
return degraded;
|
|
196317
196443
|
}
|
|
196444
|
+
const lastErrorText = lastError instanceof Error ? lastError.message : String(lastError);
|
|
196318
196445
|
const failureResult = {
|
|
196319
196446
|
outcome: "failure",
|
|
196320
|
-
|
|
196447
|
+
// FNXC:SessionContention 2026-07-25-21:30: contention is a retryable hold, not an exception.
|
|
196448
|
+
value: isSessionContentionError(lastErrorText) ? SESSION_CONTENTION_HOLD_VALUE : "exception",
|
|
196321
196449
|
contextPatch: {
|
|
196322
|
-
[`node:${node.id}:error`]:
|
|
196450
|
+
[`node:${node.id}:error`]: lastErrorText
|
|
196323
196451
|
}
|
|
196324
196452
|
};
|
|
196325
196453
|
if (recordProgress && this.shouldRecordNodeProgress(node)) {
|
|
@@ -202218,6 +202346,44 @@ function renderRoomCoordinationPromptBlock(decision, pendingMessage) {
|
|
|
202218
202346
|
];
|
|
202219
202347
|
}
|
|
202220
202348
|
|
|
202349
|
+
// ../engine/src/reports-health.ts
|
|
202350
|
+
var OPERATOR_ACTIONABLE_PAUSE_REASONS = /* @__PURE__ */ new Set([
|
|
202351
|
+
"error-unrecoverable",
|
|
202352
|
+
"error-retry-exhausted",
|
|
202353
|
+
"heartbeat-model-unavailable"
|
|
202354
|
+
]);
|
|
202355
|
+
function classifyReportHealth(input) {
|
|
202356
|
+
const pauseReason = input.pauseReason?.trim();
|
|
202357
|
+
if (pauseReason) {
|
|
202358
|
+
if (OPERATOR_ACTIONABLE_PAUSE_REASONS.has(pauseReason)) {
|
|
202359
|
+
return {
|
|
202360
|
+
bucket: "operator-actionable",
|
|
202361
|
+
cellText: `**needs operator repair** (${pauseReason})`
|
|
202362
|
+
};
|
|
202363
|
+
}
|
|
202364
|
+
return {
|
|
202365
|
+
bucket: "paused",
|
|
202366
|
+
cellText: `paused (${pauseReason})`
|
|
202367
|
+
};
|
|
202368
|
+
}
|
|
202369
|
+
if (input.state === "error") {
|
|
202370
|
+
return { bucket: "operator-actionable", cellText: "**needs operator repair**" };
|
|
202371
|
+
}
|
|
202372
|
+
if (input.staleParkedAssignment) {
|
|
202373
|
+
return { bucket: "stale-assignment", cellText: "**stale** assignment" };
|
|
202374
|
+
}
|
|
202375
|
+
if (input.state === "running" && input.heartbeatAgeMs > input.heartbeatTimeoutMs * 2) {
|
|
202376
|
+
return { bucket: "stuck", cellText: "**stuck**" };
|
|
202377
|
+
}
|
|
202378
|
+
if ((input.state === "active" || input.state === "idle") && input.heartbeatAgeMs > input.staleThresholdMs) {
|
|
202379
|
+
return { bucket: "stale", cellText: "**stale**" };
|
|
202380
|
+
}
|
|
202381
|
+
if (input.state === "paused") {
|
|
202382
|
+
return { bucket: "paused", cellText: "paused" };
|
|
202383
|
+
}
|
|
202384
|
+
return { bucket: "healthy", cellText: "healthy" };
|
|
202385
|
+
}
|
|
202386
|
+
|
|
202221
202387
|
// ../engine/src/agent-heartbeat.ts
|
|
202222
202388
|
init_session_token_usage();
|
|
202223
202389
|
|
|
@@ -205323,26 +205489,28 @@ ${trimPromptMd(taskDetail.prompt, promptTemplate)}` : "No PROMPT.md available.",
|
|
|
205323
205489
|
heartbeatLog.warn(`[reports-health] failed to validate task link for ${report.id}/${report.taskId}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
205324
205490
|
}
|
|
205325
205491
|
}
|
|
205326
|
-
|
|
205327
|
-
|
|
205328
|
-
|
|
205329
|
-
|
|
205330
|
-
|
|
205331
|
-
|
|
205332
|
-
|
|
205333
|
-
}
|
|
205334
|
-
|
|
205335
|
-
} else if ((report.state === "active" || report.state === "idle") && heartbeatAgeMs > staleThresholdMs) {
|
|
205336
|
-
health = "**stale**";
|
|
205492
|
+
const classification = classifyReportHealth({
|
|
205493
|
+
state: report.state,
|
|
205494
|
+
pauseReason: report.pauseReason,
|
|
205495
|
+
heartbeatAgeMs,
|
|
205496
|
+
heartbeatTimeoutMs,
|
|
205497
|
+
staleThresholdMs,
|
|
205498
|
+
staleParkedAssignment
|
|
205499
|
+
});
|
|
205500
|
+
if (classification.bucket === "stale") {
|
|
205337
205501
|
heartbeatLog.log(`[reports-health] stale report ${report.id} intervalSource=${intervalSource} staleThresholdMs=${staleThresholdMs} heartbeatAgeMs=${heartbeatAgeMs}`);
|
|
205338
205502
|
}
|
|
205339
205503
|
const task = renderedTask;
|
|
205340
205504
|
const state = renderedState;
|
|
205341
205505
|
const heartbeat = formatRelativeTime(report.lastHeartbeatAt);
|
|
205342
|
-
return
|
|
205506
|
+
return {
|
|
205507
|
+
classification,
|
|
205508
|
+
row: `| ${report.name} | ${state} | ${task} | ${heartbeat} | ${classification.cellText} |`
|
|
205509
|
+
};
|
|
205343
205510
|
}));
|
|
205344
|
-
const hasStuck = rows.some((
|
|
205345
|
-
const hasStale = rows.some((
|
|
205511
|
+
const hasStuck = rows.some(({ classification }) => classification.bucket === "stuck");
|
|
205512
|
+
const hasStale = rows.some(({ classification }) => classification.bucket === "stale" || classification.bucket === "stale-assignment");
|
|
205513
|
+
const hasOperatorActionable = rows.some(({ classification }) => classification.bucket === "operator-actionable");
|
|
205346
205514
|
const actionLines = ["### Actions for Unresponsive Reports"];
|
|
205347
205515
|
if (hasStuck) {
|
|
205348
205516
|
actionLines.push("- For **stuck** reports: first check task-log/step progress, the active heartbeat run, and worktree/session liveness. If work is live, do not stop or reassign it; request status if needed. Reassign only after no live session and no forward progress are confirmed.");
|
|
@@ -205350,6 +205518,9 @@ ${trimPromptMd(taskDetail.prompt, promptTemplate)}` : "No PROMPT.md available.",
|
|
|
205350
205518
|
if (hasStale) {
|
|
205351
205519
|
actionLines.push("- For **stale** reports: the agent may have lost its heartbeat trigger \u2014 create a follow-up task to investigate.");
|
|
205352
205520
|
}
|
|
205521
|
+
if (hasOperatorActionable) {
|
|
205522
|
+
actionLines.push("- For reports that **need operator repair**: notify the operator and create a follow-up task; do not reassign work until the parked agent's configuration or access issue is repaired.");
|
|
205523
|
+
}
|
|
205353
205524
|
return [
|
|
205354
205525
|
"## Reports Health Check",
|
|
205355
205526
|
"",
|
|
@@ -205357,7 +205528,7 @@ ${trimPromptMd(taskDetail.prompt, promptTemplate)}` : "No PROMPT.md available.",
|
|
|
205357
205528
|
"",
|
|
205358
205529
|
"| Name | State | Task | Last Heartbeat | Health |",
|
|
205359
205530
|
"|------|-------|------|----------------|--------|",
|
|
205360
|
-
...rows,
|
|
205531
|
+
...rows.map(({ row }) => row),
|
|
205361
205532
|
"",
|
|
205362
205533
|
...actionLines
|
|
205363
205534
|
].join("\n");
|
|
@@ -207092,6 +207263,7 @@ async function preserveWorktreeChanges(repoDir, worktreePath, taskId) {
|
|
|
207092
207263
|
return null;
|
|
207093
207264
|
}
|
|
207094
207265
|
}
|
|
207266
|
+
var PRE_EXECUTION_WORKTREE_MAX_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
207095
207267
|
var APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
|
|
207096
207268
|
var STARVED_REFINEMENT_RECOVERY_GRACE_MS = 10 * 6e4;
|
|
207097
207269
|
var STARVED_PEER_PROGRESS_THRESHOLD = 3;
|
|
@@ -208850,6 +209022,8 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
208850
209022
|
{ name: "reconcile-workspace-partial-lands", fn: () => this.reconcileWorkspacePartialLands() },
|
|
208851
209023
|
{ name: "reclaim-phantom-workspace-land-leases", fn: () => this.reclaimPhantomWorkspaceLandLeases() },
|
|
208852
209024
|
{ name: "reconcile-orphaned-workspace-worktrees", fn: () => this.reconcileOrphanedWorkspaceWorktrees() },
|
|
209025
|
+
// FNXC:PlanningEvacuation 2026-07-25-23:00: reclaim worktrees acquired at planning time by cards that never executed.
|
|
209026
|
+
{ name: "reconcile-pre-execution-worktrees", fn: () => this.reconcilePreExecutionWorktrees() },
|
|
208853
209027
|
{ name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
|
|
208854
209028
|
{ name: "recover-already-merged-review", fn: () => this.recoverAlreadyMergedReviewTasks() },
|
|
208855
209029
|
{ name: "recover-post-done-noncontinuable-wedge", fn: () => this.recoverPostDoneNonContinuableWedge() },
|
|
@@ -213876,6 +214050,58 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
213876
214050
|
return 0;
|
|
213877
214051
|
}
|
|
213878
214052
|
}
|
|
214053
|
+
/*
|
|
214054
|
+
FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree sweep):
|
|
214055
|
+
Planning acquires the task's own worktree, so cards that never reach execution would accumulate
|
|
214056
|
+
worktrees on disk: withdrawn to an intake column, archived from a planner lane, or simply parked.
|
|
214057
|
+
This sweep reclaims them.
|
|
214058
|
+
|
|
214059
|
+
Candidates are addressed from task ROWS (never a directory walk — AGENTS.md forbids unbounded temp
|
|
214060
|
+
scans): tasks holding `worktree` that sit in a non-executing column and carry no execution
|
|
214061
|
+
timestamp. `todo` is deliberately EXCLUDED: a planned card waiting for a WIP slot legitimately
|
|
214062
|
+
keeps its worktree, and churning it would just force a re-acquire minutes later. Every real safety
|
|
214063
|
+
decision (live session, clean branch, execution evidence) belongs to
|
|
214064
|
+
`TaskExecutor.releasePreExecutionWorktree`, so this sweep cannot diverge from the event-driven path
|
|
214065
|
+
that runs on the move itself.
|
|
214066
|
+
*/
|
|
214067
|
+
async reconcilePreExecutionWorktrees() {
|
|
214068
|
+
try {
|
|
214069
|
+
const release2 = this.options.releasePreExecutionWorktree;
|
|
214070
|
+
if (!release2) return 0;
|
|
214071
|
+
const settings = await this.store.getSettings();
|
|
214072
|
+
if (settings.globalPause || settings.enginePaused) return 0;
|
|
214073
|
+
const now = Date.now();
|
|
214074
|
+
const parked = await this.store.listTasks({ slim: true });
|
|
214075
|
+
const candidates = parked.filter((task) => {
|
|
214076
|
+
if (!task.worktree || task.deletedAt) return false;
|
|
214077
|
+
if (task.firstExecutionAt || task.executionStartedAt) return false;
|
|
214078
|
+
if (task.column === "todo" || task.column === "in-progress" || task.column === "in-review" || task.column === "done") return false;
|
|
214079
|
+
if (task.paused || task.userPaused) return false;
|
|
214080
|
+
if (task.status != null) return false;
|
|
214081
|
+
if (task.blockedBy || task.overlapBlockedBy || task.nextRecoveryAt) return false;
|
|
214082
|
+
const lastTouchedMs = Math.max(
|
|
214083
|
+
Date.parse(task.columnMovedAt ?? "") || 0,
|
|
214084
|
+
Date.parse(task.updatedAt ?? "") || 0
|
|
214085
|
+
);
|
|
214086
|
+
if (!lastTouchedMs) return false;
|
|
214087
|
+
return now - lastTouchedMs >= PRE_EXECUTION_WORKTREE_MAX_IDLE_MS;
|
|
214088
|
+
});
|
|
214089
|
+
if (candidates.length === 0) return 0;
|
|
214090
|
+
let released = 0;
|
|
214091
|
+
for (const task of candidates) {
|
|
214092
|
+
try {
|
|
214093
|
+
if (await release2(task.id, `parked pre-execution in '${task.column}'`)) released++;
|
|
214094
|
+
} catch (err) {
|
|
214095
|
+
log28.warn(`reconcilePreExecutionWorktrees: release failed for ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
214096
|
+
}
|
|
214097
|
+
}
|
|
214098
|
+
if (released > 0) log28.log(`reconcilePreExecutionWorktrees: released ${released} pre-execution worktree(s)`);
|
|
214099
|
+
return released;
|
|
214100
|
+
} catch (err) {
|
|
214101
|
+
log28.error(`reconcilePreExecutionWorktrees sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
214102
|
+
return 0;
|
|
214103
|
+
}
|
|
214104
|
+
}
|
|
213879
214105
|
/*
|
|
213880
214106
|
FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD4 — per-repo worktree cleanup from STORED paths):
|
|
213881
214107
|
For done/dead workspace tasks, remove each recorded per-repo worktree. The paths are ADDRESSABLE
|
|
@@ -218481,6 +218707,9 @@ var MAX_TASK_DONE_SESSION_RETRIES = 3;
|
|
|
218481
218707
|
var MAX_TASK_DONE_REQUEUE_RETRIES = 3;
|
|
218482
218708
|
var MAX_TRANSIENT_GRAPH_RESUME_RETRIES = 2;
|
|
218483
218709
|
var TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 1e3;
|
|
218710
|
+
var MAX_SESSION_CONTENTION_HOLD_RETRIES = 10;
|
|
218711
|
+
var SESSION_CONTENTION_HOLD_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 5e3;
|
|
218712
|
+
var SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS = 6e4;
|
|
218484
218713
|
var COMPLETED_TASK_WATCHDOG_MS = 6e4;
|
|
218485
218714
|
var WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
|
|
218486
218715
|
var LOOP_COMPACTION_TIMEOUT_MS = 6e4;
|
|
@@ -219220,6 +219449,15 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219220
219449
|
}
|
|
219221
219450
|
})
|
|
219222
219451
|
);
|
|
219452
|
+
} else if ((from === "todo" || from === "triage") && to2 !== "in-progress" && to2 !== "in-review" && to2 !== "done") {
|
|
219453
|
+
this.trackTaskDisposal(
|
|
219454
|
+
task.id,
|
|
219455
|
+
this.awaitAbortInFlightTaskWork(task.id, `task moved out of planning to ${to2}`, {
|
|
219456
|
+
userCanceled: source === "user"
|
|
219457
|
+
}).then(async () => {
|
|
219458
|
+
await this.releasePreExecutionWorktree(task.id, `moved to ${to2}`);
|
|
219459
|
+
})
|
|
219460
|
+
);
|
|
219223
219461
|
} else if (from === "in-progress") {
|
|
219224
219462
|
if (this.workflowLifecycleMovesInFlight.has(task.id) && this.graphRouting.has(task.id)) {
|
|
219225
219463
|
executorLog.log(
|
|
@@ -219809,15 +220047,63 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219809
220047
|
getActiveWorktreePaths() consumers that cd into a path are unaffected; only the registry key changes.
|
|
219810
220048
|
Non-workspace tasks (unique worktree path != rootDir) are returned unchanged.
|
|
219811
220049
|
*/
|
|
220050
|
+
/*
|
|
220051
|
+
FNXC:PlanReviewWorktree 2026-07-25-20:40 (concurrent root-rooted step sessions — single-repo collision):
|
|
220052
|
+
The task-scoped key must apply to the shared repo root in EVERY project mode, not only workspace mode.
|
|
220053
|
+
Read-only graph nodes that need no worktree (Plan Review is the canonical one — it reviews the
|
|
220054
|
+
store-injected PROMPT.md, see FNXC:PlanReviewSpecInjection) run rooted at `this.rootDir`, and a todo
|
|
220055
|
+
task has no worktree of its own. With the bare root as the registry key, two tasks reaching Plan Review
|
|
220056
|
+
at the same time collided: the second failed with "active-session path <root> is held by task <other>;
|
|
220057
|
+
task <self> may not overwrite it", which surfaced as a Plan Review provider failure, burned the
|
|
220058
|
+
in-place retry budget against a hold that retrying can never clear, and left the task parked
|
|
220059
|
+
(reported: FN-1398 holding /home/ubuntu/dev/freemap-svelte while FN-1403 planned).
|
|
220060
|
+
Path-exclusivity on the shared root is not what keeps these sessions correct: write-capable nodes are
|
|
220061
|
+
refused at the root outright (no-worktree-for-write-node above), real per-sub-repo exclusivity is the
|
|
220062
|
+
workspace-repo-acquire lease, and every isPathActive consumer guards removable WORKTREE paths — the
|
|
220063
|
+
root is never one. Liveness still works because the synthetic key stays in the registry under the task.
|
|
220064
|
+
*/
|
|
219812
220065
|
sessionRegistryPath(taskId, worktreePath) {
|
|
219813
|
-
if (
|
|
220066
|
+
if (worktreePath === this.rootDir) {
|
|
219814
220067
|
return `${worktreePath}#session:${taskId}`;
|
|
219815
220068
|
}
|
|
219816
220069
|
return worktreePath;
|
|
219817
220070
|
}
|
|
220071
|
+
/*
|
|
220072
|
+
FNXC:SessionContention 2026-07-25-21:30 (contention prevention at the registration seam):
|
|
220073
|
+
Every executor session registration goes through `acquireActiveSessionPath` instead of the raw
|
|
220074
|
+
`registerPath`, so a LEAKED entry owned by a task with no live session surface in this process is
|
|
220075
|
+
RECLAIMED rather than throwing at the newcomer. That closes the second contention class (a dead
|
|
220076
|
+
holder can never release, so waiting on it is waiting forever). A genuinely live holder still throws
|
|
220077
|
+
the typed error — that case is real serialization, and callers classify it as a retryable contention
|
|
220078
|
+
hold (SESSION_CONTENTION_HOLD_VALUE), never as a provider/model failure.
|
|
220079
|
+
The probe reports LIVE on any uncertainty: an unknown holder with a fresh entry is treated as live by
|
|
220080
|
+
the staleness floor, so the reclaim only ever fires on proven-dead, aged entries.
|
|
220081
|
+
*/
|
|
220082
|
+
acquireSessionRegistryPath(taskId, registryPath, kind, ownerKey) {
|
|
220083
|
+
const outcome = acquireActiveSessionPath(activeSessionRegistry, registryPath, { taskId, kind, ownerKey }, {
|
|
220084
|
+
holderLiveProbe: (holderTaskId) => this.hasLiveTaskSessionSurface(holderTaskId) || executingTaskLock.has(holderTaskId)
|
|
220085
|
+
});
|
|
220086
|
+
if (outcome.action === "contended") {
|
|
220087
|
+
throw new ActiveSessionPathHeldByForeignTaskError(registryPath, outcome.holderTaskId, taskId);
|
|
220088
|
+
}
|
|
220089
|
+
if (outcome.action === "reclaimed-stale-foreign") {
|
|
220090
|
+
executorLog.warn(
|
|
220091
|
+
`${taskId}: reclaimed a stale active-session entry on ${registryPath} from dead task ${outcome.holderTaskId} (idle ${outcome.ageMs}ms)`
|
|
220092
|
+
);
|
|
220093
|
+
void this.store.recordRunAuditEvent?.({
|
|
220094
|
+
taskId,
|
|
220095
|
+
agentId: "executor",
|
|
220096
|
+
runId: generateSyntheticRunId("session-path-reclaim", taskId),
|
|
220097
|
+
domain: "database",
|
|
220098
|
+
mutationType: "session:reclaim-stale-foreign-path",
|
|
220099
|
+
target: taskId,
|
|
220100
|
+
metadata: { taskId, holderTaskId: outcome.holderTaskId, kind, ageMs: outcome.ageMs }
|
|
220101
|
+
})?.catch?.(() => void 0);
|
|
220102
|
+
}
|
|
220103
|
+
}
|
|
219818
220104
|
setActiveSession(taskId, sessionState, worktreePath) {
|
|
219819
220105
|
this.activeSessions.set(taskId, sessionState);
|
|
219820
|
-
|
|
220106
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "executor", taskId);
|
|
219821
220107
|
}
|
|
219822
220108
|
markGraphExecuteSelfRequeued(taskId) {
|
|
219823
220109
|
if (this.graphRouting.has(taskId)) {
|
|
@@ -219835,7 +220121,7 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219835
220121
|
setActiveStepExecutor(taskId, stepExecutor, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
|
|
219836
220122
|
this.activeStepExecutors.set(taskId, stepExecutor);
|
|
219837
220123
|
this.activeStepExecutorSeenSteeringIds.set(taskId, seenSteeringIds);
|
|
219838
|
-
|
|
220124
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "step-session", `${taskId}#step-session`);
|
|
219839
220125
|
}
|
|
219840
220126
|
deleteActiveStepExecutor(taskId, worktreePath) {
|
|
219841
220127
|
this.activeStepExecutors.delete(taskId);
|
|
@@ -219849,7 +220135,7 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219849
220135
|
setActiveWorkflowStepSession(taskId, session, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
|
|
219850
220136
|
this.activeWorkflowStepSessions.set(taskId, session);
|
|
219851
220137
|
this.activeWorkflowStepSessionSeenSteeringIds.set(taskId, seenSteeringIds);
|
|
219852
|
-
|
|
220138
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "workflow-step", `${taskId}#workflow-step`);
|
|
219853
220139
|
}
|
|
219854
220140
|
deleteActiveWorkflowStepSession(taskId, worktreePath) {
|
|
219855
220141
|
this.activeWorkflowStepSessions.delete(taskId);
|
|
@@ -224168,6 +224454,87 @@ ${info.feedback}`, revisionKey),
|
|
|
224168
224454
|
this.unregisterConfiguredCommandController(task.id, commandAbortController);
|
|
224169
224455
|
}
|
|
224170
224456
|
}
|
|
224457
|
+
/*
|
|
224458
|
+
FNXC:NodeWorktreeIsolation 2026-07-25-22:10 (planning acquires the task worktree):
|
|
224459
|
+
Public seam for the planning/triage lane. Specification runs a CODING-tool session; pointing it at
|
|
224460
|
+
the shared main checkout meant every planning agent had write tools in the operator's tree and every
|
|
224461
|
+
concurrent planner shared one path. Acquire the task's own worktree up front and let the whole
|
|
224462
|
+
lifecycle — planning, Plan Review, implementation, code review — reuse that single worktree.
|
|
224463
|
+
Returns null (caller falls back to the root, unchanged behavior) when the project is a workspace, or
|
|
224464
|
+
when acquisition fails: planning must never be blocked by a worktree problem.
|
|
224465
|
+
*/
|
|
224466
|
+
/*
|
|
224467
|
+
FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree release):
|
|
224468
|
+
Planning now acquires a worktree, so a card that never reaches execution — withdrawn to Ideas,
|
|
224469
|
+
archived from a planner lane, or parked pre-execution — would otherwise hold one forever. Release
|
|
224470
|
+
it. Safety conditions, all required:
|
|
224471
|
+
- the task never executed (`firstExecutionAt`/`executionStartedAt` unset): execution evidence means
|
|
224472
|
+
the worktree may hold real work, and only the normal merge/archive lifecycle may remove it;
|
|
224473
|
+
- no live session registered on the path (the same isPathActive guard the other sweeps use);
|
|
224474
|
+
- the branch carries no commits beyond its base — planning writes its spec to the task store, not
|
|
224475
|
+
the worktree, so a clean branch means there is genuinely nothing to lose.
|
|
224476
|
+
Metadata (`worktree`/`branch`) is cleared with it, so a later promotion re-acquires cleanly.
|
|
224477
|
+
Fail-soft throughout: a cleanup problem must never block the lifecycle move that triggered it.
|
|
224478
|
+
*/
|
|
224479
|
+
async releasePreExecutionWorktree(taskId, reason) {
|
|
224480
|
+
try {
|
|
224481
|
+
const live = await this.store.getTask(taskId);
|
|
224482
|
+
if (!live?.worktree) return false;
|
|
224483
|
+
if (live.firstExecutionAt || live.executionStartedAt) return false;
|
|
224484
|
+
if (activeSessionRegistry.isPathActive(live.worktree) || activeSessionRegistry.isPathActive(resolvePath3(live.worktree))) return false;
|
|
224485
|
+
if (this.hasLiveTaskSessionSurface(taskId) || executingTaskLock.has(taskId)) return false;
|
|
224486
|
+
if (existsSync65(live.worktree)) {
|
|
224487
|
+
if (await this.preExecutionWorktreeHasWork(live.worktree)) {
|
|
224488
|
+
executorLog.log(`${taskId}: keeping pre-execution worktree ${live.worktree} \u2014 it carries commits or uncommitted changes`);
|
|
224489
|
+
return false;
|
|
224490
|
+
}
|
|
224491
|
+
const settings = await this.store.getSettings();
|
|
224492
|
+
await removeWorktree({
|
|
224493
|
+
rootDir: this.rootDir,
|
|
224494
|
+
worktreePath: live.worktree,
|
|
224495
|
+
settings,
|
|
224496
|
+
taskId,
|
|
224497
|
+
reason: RemovalReason.SelfHealingReclaim
|
|
224498
|
+
});
|
|
224499
|
+
}
|
|
224500
|
+
this.activeWorktrees.get(taskId)?.delete(live.worktree);
|
|
224501
|
+
await this.store.updateTask(taskId, { worktree: null, branch: null, baseCommitSha: null, sessionFile: null }, this.getRunContextFor(taskId));
|
|
224502
|
+
await this.store.logEntry(taskId, `Released the pre-execution worktree (${reason}) \u2014 it will be re-acquired when planning or execution resumes`, void 0, this.getRunContextFor(taskId)).catch(() => void 0);
|
|
224503
|
+
executorLog.log(`${taskId}: released pre-execution worktree ${live.worktree} (${reason})`);
|
|
224504
|
+
return true;
|
|
224505
|
+
} catch (error2) {
|
|
224506
|
+
executorLog.warn(`${taskId}: could not release the pre-execution worktree: ${formatError(error2).message}`);
|
|
224507
|
+
return false;
|
|
224508
|
+
}
|
|
224509
|
+
}
|
|
224510
|
+
/** True when a pre-execution worktree holds commits past its base or any uncommitted change. */
|
|
224511
|
+
async preExecutionWorktreeHasWork(worktreePath) {
|
|
224512
|
+
try {
|
|
224513
|
+
const { stdout: dirty } = await execFileAsync19("git", ["status", "--porcelain"], { cwd: worktreePath, timeout: 3e4 });
|
|
224514
|
+
if (dirty.trim()) return true;
|
|
224515
|
+
const { stdout: ahead } = await execFileAsync19("git", ["log", "--oneline", "@{upstream}..HEAD"], { cwd: worktreePath, timeout: 3e4 }).catch(async () => await execFileAsync19("git", ["log", "--oneline", "-1", "HEAD", "--not", "--remotes", "--branches=main", "--branches=master"], { cwd: worktreePath, timeout: 3e4 }));
|
|
224516
|
+
return Boolean(ahead.trim());
|
|
224517
|
+
} catch {
|
|
224518
|
+
return true;
|
|
224519
|
+
}
|
|
224520
|
+
}
|
|
224521
|
+
async ensureTaskWorktreeForPlanning(taskId) {
|
|
224522
|
+
try {
|
|
224523
|
+
if (this.workspaceConfig === void 0) {
|
|
224524
|
+
this.workspaceConfig = await loadWorkspaceConfig(this.rootDir);
|
|
224525
|
+
}
|
|
224526
|
+
if (this.workspaceConfig && (this.workspaceConfig.repos.length ?? 0) > 0) return null;
|
|
224527
|
+
const live = await this.store.getTask(taskId);
|
|
224528
|
+
if (live.worktree && existsSync65(live.worktree)) return live.worktree;
|
|
224529
|
+
const settings = await this.store.getSettings();
|
|
224530
|
+
const acquisitionTask = live.worktree ? { ...live, worktree: void 0, sessionFile: void 0 } : live;
|
|
224531
|
+
const acquired = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, "planning");
|
|
224532
|
+
return acquired.worktree || null;
|
|
224533
|
+
} catch (error2) {
|
|
224534
|
+
executorLog.warn(`${taskId}: could not acquire a planning worktree \u2014 planning falls back to the repo root: ${formatError(error2)}`);
|
|
224535
|
+
return null;
|
|
224536
|
+
}
|
|
224537
|
+
}
|
|
224171
224538
|
async prepareGraphNodeExecution(node, nodeTask, settings, requirement) {
|
|
224172
224539
|
if (!requirement.requiresWorktree) return;
|
|
224173
224540
|
const live = await this.store.getTask(nodeTask.id);
|
|
@@ -224294,22 +224661,29 @@ ${info.feedback}`, revisionKey),
|
|
|
224294
224661
|
optionalGroupId,
|
|
224295
224662
|
reviewerInlineFixes: settings.reviewerInlineFixes
|
|
224296
224663
|
});
|
|
224297
|
-
|
|
224298
|
-
if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
|
|
224299
|
-
return { outcome: "failure", value: "no-worktree-for-write-node" };
|
|
224300
|
-
}
|
|
224664
|
+
let executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
|
|
224301
224665
|
const nodeDisplayName = typeof cfg.name === "string" && cfg.name.trim() ? cfg.name.trim() : node.id;
|
|
224302
224666
|
const isPlanReviewNode = node.id === "plan-review-step" || nodeDisplayName === "Plan Review" || optionalGroupId === "plan-review";
|
|
224303
|
-
|
|
224304
|
-
|
|
224305
|
-
|
|
224306
|
-
|
|
224307
|
-
|
|
224308
|
-
|
|
224309
|
-
|
|
224310
|
-
|
|
224311
|
-
|
|
224667
|
+
if (!this.workspaceConfig) {
|
|
224668
|
+
const recordedWorktreeMissing = Boolean(executionTarget.worktree) && !existsSync65(executionTarget.worktree);
|
|
224669
|
+
const shouldAcquire = !executionTarget.worktree || recordedWorktreeMissing && isPlanReviewNode;
|
|
224670
|
+
if (shouldAcquire) {
|
|
224671
|
+
if (recordedWorktreeMissing) {
|
|
224672
|
+
await this.store.logEntry(
|
|
224673
|
+
live.id,
|
|
224674
|
+
`Plan Review worktree ${executionTarget.worktree} is missing on disk \u2014 re-acquiring a task worktree instead of running in the shared checkout`,
|
|
224675
|
+
void 0,
|
|
224676
|
+
this.getRunContextFor(live.id)
|
|
224677
|
+
);
|
|
224678
|
+
}
|
|
224679
|
+
const acquisitionTask = recordedWorktreeMissing ? { ...executionTarget, worktree: void 0, sessionFile: void 0 } : executionTarget;
|
|
224680
|
+
executionTarget = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, node.id);
|
|
224681
|
+
}
|
|
224682
|
+
}
|
|
224683
|
+
if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
|
|
224684
|
+
return { outcome: "failure", value: "no-worktree-for-write-node" };
|
|
224312
224685
|
}
|
|
224686
|
+
const worktreePath = executionTarget.worktree || this.rootDir;
|
|
224313
224687
|
let prompt = typeof cfg.prompt === "string" ? cfg.prompt : "";
|
|
224314
224688
|
let modelProvider = typeof cfg.modelProvider === "string" && cfg.modelProvider.trim() ? cfg.modelProvider : void 0;
|
|
224315
224689
|
let modelId = typeof cfg.modelId === "string" && cfg.modelId.trim() ? cfg.modelId : void 0;
|
|
@@ -224606,6 +224980,77 @@ ${prompt}`;
|
|
|
224606
224980
|
const latestAction = live.log.at(-1)?.action;
|
|
224607
224981
|
return latestAction === "Resumed after engine restart" || latestAction === "Resuming execution after unpause";
|
|
224608
224982
|
}
|
|
224983
|
+
/*
|
|
224984
|
+
FNXC:SessionContention 2026-07-25-21:30:
|
|
224985
|
+
Two ways in, because contention must never slip through to a park:
|
|
224986
|
+
- the typed failure value the graph now publishes (SESSION_CONTENTION_HOLD_VALUE), and
|
|
224987
|
+
- a message-shape fallback over the run's `:error` context patches, so contention arriving from a
|
|
224988
|
+
path that has not been taught the typed value is still recognized.
|
|
224989
|
+
*/
|
|
224990
|
+
graphFailureErrorTexts(result) {
|
|
224991
|
+
if (!result.context) return [];
|
|
224992
|
+
const texts = [];
|
|
224993
|
+
for (const [key, value] of Object.entries(result.context)) {
|
|
224994
|
+
if (key.endsWith(":error") && typeof value === "string" && value.trim()) texts.push(value);
|
|
224995
|
+
}
|
|
224996
|
+
return texts;
|
|
224997
|
+
}
|
|
224998
|
+
isSessionContentionGraphFailure(result) {
|
|
224999
|
+
if (this.graphFailureValue(result) === SESSION_CONTENTION_HOLD_VALUE) return true;
|
|
225000
|
+
return this.graphFailureErrorTexts(result).some((text2) => isSessionContentionError(text2));
|
|
225001
|
+
}
|
|
225002
|
+
/*
|
|
225003
|
+
FNXC:SessionContention 2026-07-25-21:30 (self-recovering wait — the task is never parked):
|
|
225004
|
+
Retry the graph in place on an exponential backoff while the holder finishes. The counter is
|
|
225005
|
+
IN-MEMORY on purpose: it needs no schema change, and an engine restart resetting it is the desired
|
|
225006
|
+
behavior (a restart also drops the in-process registry, so the contention is gone anyway).
|
|
225007
|
+
When the ladder is exhausted the task is left cleanly dispatchable — status/error cleared, progress
|
|
225008
|
+
untouched — so ordinary scheduling picks it up later with a fresh budget. There is no terminal branch
|
|
225009
|
+
here by design: lease contention always ends (the holder finishes, or self-healing sweeps it), so
|
|
225010
|
+
parking the task would only require a human to press Retry on a condition that fixed itself.
|
|
225011
|
+
*/
|
|
225012
|
+
sessionContentionHoldAttempts = /* @__PURE__ */ new Map();
|
|
225013
|
+
clearSessionContentionHold(taskId) {
|
|
225014
|
+
this.sessionContentionHoldAttempts.delete(taskId);
|
|
225015
|
+
}
|
|
225016
|
+
async holdForSessionContention(task, live, result) {
|
|
225017
|
+
const detail = this.graphFailureErrorTexts(result).find((text2) => isSessionContentionError(text2));
|
|
225018
|
+
const priorAttempts = this.sessionContentionHoldAttempts.get(task.id) ?? 0;
|
|
225019
|
+
const attempt = priorAttempts + 1;
|
|
225020
|
+
if (attempt > MAX_SESSION_CONTENTION_HOLD_RETRIES) {
|
|
225021
|
+
this.clearSessionContentionHold(task.id);
|
|
225022
|
+
const message2 = `Still waiting on another task to release a shared session path after ${MAX_SESSION_CONTENTION_HOLD_RETRIES} attempts \u2014 leaving the task queued for normal re-dispatch (not a failure)${detail ? `: ${detail}` : ""}`;
|
|
225023
|
+
executorLog.warn(`${task.id}: ${message2}`);
|
|
225024
|
+
await this.store.logEntry(task.id, message2, void 0, this.getRunContextFor(task.id));
|
|
225025
|
+
if (live.status != null || live.error != null) {
|
|
225026
|
+
await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
|
|
225027
|
+
}
|
|
225028
|
+
return;
|
|
225029
|
+
}
|
|
225030
|
+
this.sessionContentionHoldAttempts.set(task.id, attempt);
|
|
225031
|
+
const message = `Waiting on another task to release a shared session path \u2014 retrying in place (${attempt}/${MAX_SESSION_CONTENTION_HOLD_RETRIES})${detail ? `: ${detail}` : ""}`;
|
|
225032
|
+
executorLog.warn(`${task.id}: ${message}`);
|
|
225033
|
+
await this.store.logEntry(task.id, message, void 0, this.getRunContextFor(task.id));
|
|
225034
|
+
if (live.status != null || live.error != null) {
|
|
225035
|
+
await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
|
|
225036
|
+
}
|
|
225037
|
+
const delayMs = SESSION_CONTENTION_HOLD_BACKOFF_MS === 0 ? 0 : Math.min(SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS, SESSION_CONTENTION_HOLD_BACKOFF_MS * 2 ** (attempt - 1));
|
|
225038
|
+
const scheduleRetry = () => {
|
|
225039
|
+
void (async () => {
|
|
225040
|
+
try {
|
|
225041
|
+
const resume = await this.store.getTask(task.id);
|
|
225042
|
+
if (!resume || resume.deletedAt || resume.paused || resume.userPaused) {
|
|
225043
|
+
this.clearSessionContentionHold(task.id);
|
|
225044
|
+
return;
|
|
225045
|
+
}
|
|
225046
|
+
await this.execute(resume);
|
|
225047
|
+
} catch (err) {
|
|
225048
|
+
executorLog.error(`Failed session-contention retry for ${task.id}:`, err);
|
|
225049
|
+
}
|
|
225050
|
+
})();
|
|
225051
|
+
};
|
|
225052
|
+
setTimeout(scheduleRetry, delayMs).unref?.();
|
|
225053
|
+
}
|
|
224609
225054
|
graphFailureValue(result) {
|
|
224610
225055
|
const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1];
|
|
224611
225056
|
if (!failedNode || !result.context) return void 0;
|
|
@@ -225197,6 +225642,11 @@ Graph value: ${failureValue}` : ""}`, budget.key),
|
|
|
225197
225642
|
await this.persistTokenUsage(task.id);
|
|
225198
225643
|
return;
|
|
225199
225644
|
}
|
|
225645
|
+
if (this.isSessionContentionGraphFailure(result)) {
|
|
225646
|
+
await this.holdForSessionContention(task, live, result);
|
|
225647
|
+
await this.persistTokenUsage(task.id);
|
|
225648
|
+
return;
|
|
225649
|
+
}
|
|
225200
225650
|
if (await this.routeUnusableWorktreeGraphFailureToRecovery(task, live, result)) {
|
|
225201
225651
|
await this.persistTokenUsage(task.id);
|
|
225202
225652
|
return;
|
|
@@ -241396,12 +241846,57 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241396
241846
|
this.activeSessions.delete(task.id);
|
|
241397
241847
|
}
|
|
241398
241848
|
};
|
|
241849
|
+
this.taskColumnWakeHandler = (task) => {
|
|
241850
|
+
if (!task?.id) return;
|
|
241851
|
+
if (task.column !== "todo" && task.column !== "triage") return;
|
|
241852
|
+
if (task.paused === true || task.userPaused === true) return;
|
|
241853
|
+
if (this.processing.has(task.id) || this.hasLivePlanningWork(task.id)) return;
|
|
241854
|
+
this.requestImmediatePoll();
|
|
241855
|
+
};
|
|
241856
|
+
this.taskEvacuatedFromPlanningHandler = (task) => {
|
|
241857
|
+
if (!task?.id) return;
|
|
241858
|
+
if (typeof task.column !== "string") return;
|
|
241859
|
+
if (task.column === "todo" || task.column === "triage" || task.column === "in-progress") return;
|
|
241860
|
+
if (this.activeSubagentSessions.has(task.id)) {
|
|
241861
|
+
this.disposeSubagentsForTask(task.id, `task moved to ${task.column}`);
|
|
241862
|
+
}
|
|
241863
|
+
const session = this.activeSessions.get(task.id);
|
|
241864
|
+
if (!session) return;
|
|
241865
|
+
planLog.log(`task moved out of planning to '${task.column}' \u2014 terminating triage session for ${task.id}`);
|
|
241866
|
+
this.pauseAborted.add(task.id);
|
|
241867
|
+
this.options.stuckTaskDetector?.untrackTask(task.id);
|
|
241868
|
+
const sessionWithAbort = session;
|
|
241869
|
+
if (typeof sessionWithAbort.abort === "function") {
|
|
241870
|
+
void sessionWithAbort.abort().catch((err) => {
|
|
241871
|
+
planLog.warn(`Failed to abort triage session for ${task.id}: ${err}`);
|
|
241872
|
+
});
|
|
241873
|
+
}
|
|
241874
|
+
this.recordTriageSessionTokenUsageSoon(task.id, session, { agentId: task.assignedAgentId ?? "triage" });
|
|
241875
|
+
session.dispose();
|
|
241876
|
+
this.activeSessions.delete(task.id);
|
|
241877
|
+
if (task.status === "planning") {
|
|
241878
|
+
void Promise.resolve(this.store.updateTask(task.id, { status: null })).catch((err) => {
|
|
241879
|
+
planLog.warn(`${task.id}: failed to clear planning status after evacuation: ${err instanceof Error ? err.message : String(err)}`);
|
|
241880
|
+
});
|
|
241881
|
+
}
|
|
241882
|
+
};
|
|
241399
241883
|
}
|
|
241400
241884
|
running = false;
|
|
241401
241885
|
polling = false;
|
|
241402
241886
|
pollInterval = null;
|
|
241403
241887
|
/** The interval (ms) of the currently active `setInterval` timer. */
|
|
241404
241888
|
activePollMs = null;
|
|
241889
|
+
/*
|
|
241890
|
+
FNXC:CodingIdeasWorkflow 2026-07-25-11:20:
|
|
241891
|
+
Event-wake state for requestImmediatePoll(). Planning discovery is timer-driven, so pressing
|
|
241892
|
+
Start on an Ideas card (which only writes a column change) used to wait out the remainder of the
|
|
241893
|
+
poll interval — up to pollIntervalMs, 15s by default — before anything even looked at the card.
|
|
241894
|
+
`nudgeTimer` debounces a burst of moves into one poll; `nudgeDuringPoll` remembers a nudge that
|
|
241895
|
+
arrived while a poll was already in flight, since that poll may have snapshotted the task list
|
|
241896
|
+
before the move landed and would otherwise drop the wake entirely.
|
|
241897
|
+
*/
|
|
241898
|
+
nudgeTimer = null;
|
|
241899
|
+
nudgeDuringPoll = false;
|
|
241405
241900
|
processing = /* @__PURE__ */ new Set();
|
|
241406
241901
|
/** Synchronous ownership fence shared with advanced-triage self-healing. */
|
|
241407
241902
|
advancedRecoveryReservations = /* @__PURE__ */ new Set();
|
|
@@ -241441,6 +241936,10 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241441
241936
|
stuckAborted = /* @__PURE__ */ new Set();
|
|
241442
241937
|
taskDeletedHandler;
|
|
241443
241938
|
taskPausedHandler;
|
|
241939
|
+
/** FNXC:CodingIdeasWorkflow 2026-07-25-11:20: store-event wake for planning-eligible columns. */
|
|
241940
|
+
taskColumnWakeHandler;
|
|
241941
|
+
/** FNXC:PlanningEvacuation 2026-07-25-23:00: stops planning when a card leaves the planner lanes. */
|
|
241942
|
+
taskEvacuatedFromPlanningHandler;
|
|
241444
241943
|
_approvalRequestStore;
|
|
241445
241944
|
/**
|
|
241446
241945
|
* @param store — Task store instance (also used to listen for `settings:updated` events)
|
|
@@ -241545,6 +242044,13 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241545
242044
|
if (this.taskPausedHandler && typeof this.store.on === "function") {
|
|
241546
242045
|
this.store.on("task:updated", this.taskPausedHandler);
|
|
241547
242046
|
}
|
|
242047
|
+
if (this.taskColumnWakeHandler && typeof this.store.on === "function") {
|
|
242048
|
+
this.store.on("task:updated", this.taskColumnWakeHandler);
|
|
242049
|
+
this.store.on("task:created", this.taskColumnWakeHandler);
|
|
242050
|
+
}
|
|
242051
|
+
if (this.taskEvacuatedFromPlanningHandler && typeof this.store.on === "function") {
|
|
242052
|
+
this.store.on("task:updated", this.taskEvacuatedFromPlanningHandler);
|
|
242053
|
+
}
|
|
241548
242054
|
this.clearStaleSpecifyingStatuses().catch((err) => {
|
|
241549
242055
|
planLog.error("Failed to clear stale planning statuses:", err);
|
|
241550
242056
|
});
|
|
@@ -241577,12 +242083,24 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241577
242083
|
this.pollInterval = null;
|
|
241578
242084
|
this.activePollMs = null;
|
|
241579
242085
|
}
|
|
242086
|
+
if (this.nudgeTimer) {
|
|
242087
|
+
clearTimeout(this.nudgeTimer);
|
|
242088
|
+
this.nudgeTimer = null;
|
|
242089
|
+
}
|
|
242090
|
+
this.nudgeDuringPoll = false;
|
|
241580
242091
|
if (this.taskDeletedHandler && typeof this.store.off === "function") {
|
|
241581
242092
|
this.store.off("task:deleted", this.taskDeletedHandler);
|
|
241582
242093
|
}
|
|
241583
242094
|
if (this.taskPausedHandler && typeof this.store.off === "function") {
|
|
241584
242095
|
this.store.off("task:updated", this.taskPausedHandler);
|
|
241585
242096
|
}
|
|
242097
|
+
if (this.taskColumnWakeHandler && typeof this.store.off === "function") {
|
|
242098
|
+
this.store.off("task:updated", this.taskColumnWakeHandler);
|
|
242099
|
+
this.store.off("task:created", this.taskColumnWakeHandler);
|
|
242100
|
+
}
|
|
242101
|
+
if (this.taskEvacuatedFromPlanningHandler && typeof this.store.off === "function") {
|
|
242102
|
+
this.store.off("task:updated", this.taskEvacuatedFromPlanningHandler);
|
|
242103
|
+
}
|
|
241586
242104
|
this.abortAndDisposeActiveSessions("engine stop");
|
|
241587
242105
|
planLog.log("Processor stopped");
|
|
241588
242106
|
}
|
|
@@ -241990,7 +242508,17 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241990
242508
|
if (isUnplannedSeedPrompt(content, todoTask.id, todoTask.title, todoTask.description)) {
|
|
241991
242509
|
eligibleTodoTasks.push(todoTask);
|
|
241992
242510
|
}
|
|
241993
|
-
} catch {
|
|
242511
|
+
} catch (err) {
|
|
242512
|
+
if (err?.code === "ENOENT") {
|
|
242513
|
+
planLog.warn(
|
|
242514
|
+
`${todoTask.id}: PROMPT.md is missing \u2014 treating as unplanned and admitting it for planning`
|
|
242515
|
+
);
|
|
242516
|
+
eligibleTodoTasks.push(todoTask);
|
|
242517
|
+
} else {
|
|
242518
|
+
planLog.warn(
|
|
242519
|
+
`${todoTask.id}: PROMPT.md unreadable (${err?.code ?? "unknown"}) \u2014 skipping planning discovery for this poll`
|
|
242520
|
+
);
|
|
242521
|
+
}
|
|
241994
242522
|
}
|
|
241995
242523
|
}
|
|
241996
242524
|
return [...eligibleTriageTasks, ...eligibleTodoTasks].sort((a, b3) => {
|
|
@@ -242004,10 +242532,41 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
242004
242532
|
return numeric2 !== 0 ? numeric2 : a.id.localeCompare(b3.id);
|
|
242005
242533
|
});
|
|
242006
242534
|
}
|
|
242535
|
+
/**
|
|
242536
|
+
* Run a planning-discovery poll now instead of waiting for the next timer tick.
|
|
242537
|
+
*
|
|
242538
|
+
* FNXC:CodingIdeasWorkflow 2026-07-25-11:20:
|
|
242539
|
+
* Requirement: starting a task must begin planning immediately, not "within 15 seconds".
|
|
242540
|
+
* The Start affordance on an intake (Ideas) card performs a bare column move — there is no
|
|
242541
|
+
* dispatch call in that path — so planning only began when the next `setInterval` tick happened
|
|
242542
|
+
* to fire. The store-event wake (taskColumnWakeHandler) is the primary caller.
|
|
242543
|
+
*
|
|
242544
|
+
* Contract: advisory and idempotent. It never plans a task by itself, it only advances WHEN the
|
|
242545
|
+
* existing poll runs — every pause, seed-prompt, dependency, and concurrency gate still applies,
|
|
242546
|
+
* so a nudge on a capacity-blocked card is a no-op rather than an admission bypass. Returns false
|
|
242547
|
+
* when the processor is not running.
|
|
242548
|
+
*/
|
|
242549
|
+
requestImmediatePoll() {
|
|
242550
|
+
if (!this.running) return false;
|
|
242551
|
+
if (this.polling) {
|
|
242552
|
+
this.nudgeDuringPoll = true;
|
|
242553
|
+
return true;
|
|
242554
|
+
}
|
|
242555
|
+
if (this.nudgeTimer) return true;
|
|
242556
|
+
this.nudgeTimer = setTimeout(() => {
|
|
242557
|
+
this.nudgeTimer = null;
|
|
242558
|
+
void this.poll();
|
|
242559
|
+
}, _TriageProcessor.NUDGE_DEBOUNCE_MS);
|
|
242560
|
+
this.nudgeTimer.unref?.();
|
|
242561
|
+
return true;
|
|
242562
|
+
}
|
|
242563
|
+
/** Coalescing window for requestImmediatePoll, so a multi-card drag causes one poll, not N. */
|
|
242564
|
+
static NUDGE_DEBOUNCE_MS = 150;
|
|
242007
242565
|
async poll() {
|
|
242008
242566
|
if (!this.running) return;
|
|
242009
242567
|
if (this.polling) return;
|
|
242010
242568
|
this.polling = true;
|
|
242569
|
+
this.nudgeDuringPoll = false;
|
|
242011
242570
|
try {
|
|
242012
242571
|
const settings = await this.store.getSettings();
|
|
242013
242572
|
this.refreshPollInterval(settings.pollIntervalMs);
|
|
@@ -242113,6 +242672,10 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
242113
242672
|
planLog.error("Poll error:", err);
|
|
242114
242673
|
} finally {
|
|
242115
242674
|
this.polling = false;
|
|
242675
|
+
if (this.nudgeDuringPoll && this.running) {
|
|
242676
|
+
this.nudgeDuringPoll = false;
|
|
242677
|
+
this.requestImmediatePoll();
|
|
242678
|
+
}
|
|
242116
242679
|
}
|
|
242117
242680
|
}
|
|
242118
242681
|
async backfillBlankTitleAfterTerminalTriageFailure(task) {
|
|
@@ -242329,11 +242892,15 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
|
|
|
242329
242892
|
planningModel.modelId,
|
|
242330
242893
|
assignedAgent?.runtimeConfig
|
|
242331
242894
|
) : { provider: void 0, modelId: void 0 };
|
|
242895
|
+
const planningCwd = await this.options.acquirePlanningWorktree?.(task.id).catch(() => null) || this.rootDir;
|
|
242896
|
+
if (planningCwd !== this.rootDir) {
|
|
242897
|
+
await this.store.logEntry(task.id, `Planning session running in task worktree ${planningCwd}`).catch(() => void 0);
|
|
242898
|
+
}
|
|
242332
242899
|
const { session } = await createResolvedAgentSession({
|
|
242333
242900
|
sessionPurpose: "triage",
|
|
242334
242901
|
runtimeHint: triageRuntimeHint,
|
|
242335
242902
|
pluginRunner: this.options.pluginRunner,
|
|
242336
|
-
cwd:
|
|
242903
|
+
cwd: planningCwd,
|
|
242337
242904
|
systemPrompt: triageSystemPromptFinal,
|
|
242338
242905
|
systemPromptLayers: triageLayers,
|
|
242339
242906
|
tools: "coding",
|
|
@@ -244765,6 +245332,9 @@ var InProcessRuntime = class extends EventEmitter27 {
|
|
|
244765
245332
|
usageLimitPauser: this.usageLimitPauser,
|
|
244766
245333
|
agentStore: this.agentStore,
|
|
244767
245334
|
pluginRunner: this.pluginRunner,
|
|
245335
|
+
// FNXC:NodeWorktreeIsolation 2026-07-25-22:10: planning acquires (or reuses) the task's own
|
|
245336
|
+
// worktree through the executor's acquisition path, so no lane runs in the shared checkout.
|
|
245337
|
+
acquirePlanningWorktree: (taskId) => this.executor.ensureTaskWorktreeForPlanning(taskId),
|
|
244768
245338
|
onSpecifyStart: (t) => {
|
|
244769
245339
|
this.recordActivity();
|
|
244770
245340
|
runtimeLog.log(`Specifying ${t.id}...`);
|
|
@@ -244853,6 +245423,8 @@ var InProcessRuntime = class extends EventEmitter27 {
|
|
|
244853
245423
|
getExecutingTaskIds: () => this.executor?.getExecutingTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
244854
245424
|
clearPhantomExecutorBinding: (taskId, options) => this.executor?.clearPhantomExecutorBinding(taskId, options),
|
|
244855
245425
|
listWorktreeHolders: () => this.executor?.listWorktreeHolders() ?? [],
|
|
245426
|
+
// FNXC:PlanningEvacuation 2026-07-25-23:00: the executor owns the release safety conditions.
|
|
245427
|
+
releasePreExecutionWorktree: (taskId, reason) => this.executor?.releasePreExecutionWorktree(taskId, reason) ?? Promise.resolve(false),
|
|
244856
245428
|
recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
|
|
244857
245429
|
getPlanningTaskIds: () => this.triageProcessor?.getPlanningTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
244858
245430
|
// FNXC:TaskTiming 2026-08-01-12:00: orphan planning recovery must defer
|