@runfusion/fusion 0.74.0-beta.1 → 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 +399 -25
- package/dist/child-process-worker.js +396 -22
- package/dist/client/.vite/manifest.json +256 -256
- package/dist/client/assets/{AgentDetailView-Bopzxiqb.js → AgentDetailView-DH4Cn5iG.js} +1 -1
- package/dist/client/assets/{AgentPermissionPolicyEditor-i5N7rUlL.js → AgentPermissionPolicyEditor-BycC7HhP.js} +1 -1
- package/dist/client/assets/{AgentsView-M3GoJDHH.js → AgentsView-Dkij8Sm8.js} +3 -3
- package/dist/client/assets/{ChatView-CqiyA9Kt.js → ChatView-DAsZY1We.js} +1 -1
- package/dist/client/assets/{CommandCenter-CAvMGKhl.js → CommandCenter-jTzBo-pg.js} +1 -1
- package/dist/client/assets/{DevServerView-wqM6h3nV.js → DevServerView-CxlacYJp.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-C8OyXCE-.js → DirectoryPicker-DsCk3Nsw.js} +1 -1
- package/dist/client/assets/{DocumentsView-CjNMZQi8.js → DocumentsView-8EI6KNBY.js} +1 -1
- package/dist/client/assets/{EvalsView-BVlbuB5t.js → EvalsView-kYnaIk11.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-CIwCg8sq.js → ExperimentalAgentOnboardingModal-ce6DROKD.js} +1 -1
- package/dist/client/assets/{GoalsView-Cnw2igil.js → GoalsView-BVlTtw-u.js} +1 -1
- package/dist/client/assets/{InsightsView-D7GmD-Kx.js → InsightsView-BKQeROkb.js} +1 -1
- package/dist/client/assets/{MemoryView-BlqkfIE5.js → MemoryView-DCzTnxsV.js} +1 -1
- package/dist/client/assets/{PiExtensionsManager-EC94zLMq.js → PiExtensionsManager-C94HwDz5.js} +1 -1
- package/dist/client/assets/{PluginManager-pYyCD3uh.js → PluginManager-C3DegklW.js} +1 -1
- package/dist/client/assets/{PullRequestView-BAb0vVhu.js → PullRequestView-CrE89UL7.js} +1 -1
- package/dist/client/assets/{ReportModal-COmReivA.js → ReportModal-BD1ZUDdV.js} +1 -1
- package/dist/client/assets/{ResearchView-DLMo4rFw.js → ResearchView-eMiRw1EM.js} +1 -1
- package/dist/client/assets/{SecretsView-w_O8NLfw.js → SecretsView-6qK8Idb7.js} +1 -1
- package/dist/client/assets/{SessionTerminal-CSPi-qWE.js → SessionTerminal-DtFLyUaK.js} +1 -1
- package/dist/client/assets/SettingsModal-BTfZlghk.css +1 -0
- package/dist/client/assets/{SettingsModal-wilrGXvE.js → SettingsModal-BkOSlfyr.js} +1 -1
- package/dist/client/assets/{SettingsModal-C9bVvYQq.js → SettingsModal-DSq-_qwo.js} +4 -4
- package/dist/client/assets/{SettingsTextareaRow-Bd7_SxVv.js → SettingsTextareaRow-Btyx5kaR.js} +1 -1
- package/dist/client/assets/{SetupWizardModal-sPs6Lw3a.js → SetupWizardModal-Blhx-dRt.js} +2 -2
- package/dist/client/assets/{SkillsView-B9PvYS8t.js → SkillsView-DGE5kYc3.js} +1 -1
- package/dist/client/assets/{TodoView-Df0SD_JT.js → TodoView-YSGI7ATd.js} +1 -1
- package/dist/client/assets/{WorkflowNodeEditor-B3kFD-S0.js → WorkflowNodeEditor-CZiEThu2.js} +1 -1
- package/dist/client/assets/{agent-import-generation-OXfU1-D1.js → agent-import-generation-DQeNZMAm.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B_8zk4ag.js → architectureDiagram-3BPJPVTR-DhaJNKa4.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-CpDKBJkO.js → blockDiagram-GPEHLZMM-pKpg7gJU.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-D3wtfGzI.js → c4Diagram-AAUBKEIU-DVPXKJnr.js} +1 -1
- package/dist/client/assets/channel-xFBfK0sX.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DS9sHIMv.js → chunk-2J33WTMH-CHt90T5z.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-B-xW6cfi.js → chunk-4BX2VUAB-DsWFYy0a.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-Ddxf-sPU.js → chunk-55IACEB6-D8XiXEo8.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-Bv_Z1w57.js → chunk-727SXJPM-DlUYBn_Y.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DYHYN43E.js → chunk-AQP2D5EJ-BSdPOr9q.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-wuuFvvBk.js → chunk-FMBD7UC4-B9IAeBld.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-DfVRkVZo.js → chunk-ND2GUHAM-CmxnYQrR.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-D0t6ryuh.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-DVlKqNFJ.js → cose-bilkent-S5V4N54A-BNl3bhpS.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-DTtiGxz-.js → dagre-BM42HDAG-DFs7WCcz.js} +1 -1
- package/dist/client/assets/{dashboard-view-DH99T6VJ.js → dashboard-view-DnoTcnIo.js} +1 -1
- package/dist/client/assets/{dashboard-view-D-nDgSmP.js → dashboard-view-ZGxLEL9r.js} +1 -1
- package/dist/client/assets/{dashboard-view-CLjYqvE9.js → dashboard-view-vc8kgLMs.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-Tv-YxCWk.js → diagram-2AECGRRQ-C2PVozvu.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-DlAAPkGu.js → diagram-5GNKFQAL-DFJ5b9H8.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DisatTBB.js → diagram-KO2AKTUF-D335CW-c.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-CDrUxjzW.js → diagram-LMA3HP47-DSfhroAW.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BQ34a4Ej.js → diagram-OG6HWLK6-8iGveM-Z.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-s-YHwAEU.js → erDiagram-TEJ5UH35-Xc56MEEi.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-EpXzmG2d.js → flowDiagram-I6XJVG4X-DjCU9WbD.js} +1 -1
- package/dist/client/assets/{folder-open-DN86xUly.js → folder-open-BRrB44UX.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-Dk2WaoX8.js → ganttDiagram-6RSMTGT7-CXQE1bea.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CjchnKW0.js → gitGraphDiagram-PVQCEYII--mrSABLC.js} +1 -1
- package/dist/client/assets/{index-aGZ3DHQw.js → index-C2xsZoNG.js} +8 -8
- package/dist/client/assets/{infoDiagram-5YYISTIA-CiU-twoj.js → infoDiagram-5YYISTIA-CTS_nljP.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BvylyUKK.js → ishikawaDiagram-YF4QCWOH-CpV5x3gk.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Ca6z84KB.js → journeyDiagram-JHISSGLW-BK2QCMnt.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-DXbw2-s-.js → kanban-definition-UN3LZRKU-B4srpspY.js} +1 -1
- package/dist/client/assets/{mermaid.core-DkS33D12.js → mermaid.core-B8DVAt21.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgXuWPDD.js → mindmap-definition-RKZ34NQL-CsmmiPpA.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-CnOzTN6U.js → pieDiagram-4H26LBE5-BahLDqf4.js} +1 -1
- package/dist/client/assets/{puzzle-Dxl1kcLF.js → puzzle-BFPQsuIa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DI3s1u95.js → quadrantDiagram-W4KKPZXB-Bo3u0wTU.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-Ck97mo-W.js → requirementDiagram-4Y6WPE33-DYkYsz7d.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Z3w8NJac.js → sankeyDiagram-5OEKKPKP-BCbh0BSY.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DLGtWgfy.js → sequenceDiagram-3UESZ5HK-C2AP9GlE.js} +1 -1
- package/dist/client/assets/{shield-alert-BCGxgBpM.js → shield-alert-tlZOi02c.js} +1 -1
- package/dist/client/assets/{standing-instructions-template-CUE6ItG6.js → standing-instructions-template-C0Rzv9Mr.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-C6Zl2EJW.js → stateDiagram-AJRCARHV-CARkH0ug.js} +1 -1
- package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-BA77mDAW.js → stateDiagram-v2-BHNVJYJU-C2He043y.js} +1 -1
- package/dist/client/assets/{timeline-definition-PNZ67QCA-D9Cp15Hm.js → timeline-definition-PNZ67QCA-MvbYneEM.js} +1 -1
- package/dist/client/assets/{upload-2jCicylS.js → upload-DWK0F17Z.js} +1 -1
- package/dist/client/assets/{users-C1TTGAfX.js → users-1AkDfM14.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-BwFSh6Tr.js → vennDiagram-CIIHVFJN-DOjzzLqc.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-CQknAo24.js → wardley-L42UT6IY-Di70vhu2.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Dwc8f8bd.js → wardleyDiagram-YWT4CUSO-nK6ARhDv.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPkeHv8g.js → xychartDiagram-2RQKCTM6-C7_y76B7.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/client/version.json +1 -1
- package/dist/extension.js +399 -25
- package/package.json +1 -1
- package/dist/client/assets/SettingsModal-CMLHZBhX.css +0 -1
- package/dist/client/assets/channel-BLzjKIVz.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-DkwPy8u9.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DkwPy8u9.js +0 -1
|
@@ -132897,6 +132897,23 @@ var init_worktrunk_installer = __esm({
|
|
|
132897
132897
|
});
|
|
132898
132898
|
|
|
132899
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
|
+
}
|
|
132900
132917
|
function reconcileSelfOwnedActiveSessionForRemoval(registry3, worktreePath, requestingTaskId, liveBindingProbe, options = {}) {
|
|
132901
132918
|
const record2 = registry3.lookupByPath(worktreePath);
|
|
132902
132919
|
if (!record2) {
|
|
@@ -162576,6 +162593,12 @@ var init_usage_limit_detector = __esm({
|
|
|
162576
162593
|
});
|
|
162577
162594
|
|
|
162578
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
|
+
}
|
|
162579
162602
|
function isTransientError(errorMessage) {
|
|
162580
162603
|
if (!errorMessage || typeof errorMessage !== "string") {
|
|
162581
162604
|
return false;
|
|
@@ -162583,6 +162606,9 @@ function isTransientError(errorMessage) {
|
|
|
162583
162606
|
if (isTransientAuthCredentialError(errorMessage)) {
|
|
162584
162607
|
return true;
|
|
162585
162608
|
}
|
|
162609
|
+
if (isSessionContentionError(errorMessage)) {
|
|
162610
|
+
return true;
|
|
162611
|
+
}
|
|
162586
162612
|
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
162587
162613
|
}
|
|
162588
162614
|
function isTransientAuthCredentialError(errorMessage) {
|
|
@@ -162594,7 +162620,7 @@ function isTransientAuthCredentialError(errorMessage) {
|
|
|
162594
162620
|
}
|
|
162595
162621
|
return TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN.test(errorMessage);
|
|
162596
162622
|
}
|
|
162597
|
-
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;
|
|
162598
162624
|
var init_transient_error_patterns = __esm({
|
|
162599
162625
|
"../engine/src/transient-error-patterns.ts"() {
|
|
162600
162626
|
"use strict";
|
|
@@ -162655,6 +162681,14 @@ var init_transient_error_patterns = __esm({
|
|
|
162655
162681
|
/\bACP failed to start\b/i,
|
|
162656
162682
|
/\bACP session has no live connection\b/i
|
|
162657
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
|
+
];
|
|
162658
162692
|
TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN = /"type":\s*"authentication_error"|invalid authentication credentials|token[_\s]?expired/i;
|
|
162659
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;
|
|
162660
162694
|
}
|
|
@@ -162663,6 +162697,9 @@ var init_transient_error_patterns = __esm({
|
|
|
162663
162697
|
// ../engine/src/transient-error-detector.ts
|
|
162664
162698
|
function isNonPlanDefectPlanReviewFailure(input) {
|
|
162665
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
|
+
}
|
|
162666
162703
|
const failureValue = input.failureValue?.trim().toLowerCase();
|
|
162667
162704
|
if (failureValue === "exception" || failureValue === "aborted") return true;
|
|
162668
162705
|
const errorMessage = input.errorMessage?.trim();
|
|
@@ -191347,7 +191384,7 @@ function hasAdvancedPastPlanning(task) {
|
|
|
191347
191384
|
if (task.column === "in-progress" || task.column === "in-review" || task.column === "done" || task.column === "archived") {
|
|
191348
191385
|
return true;
|
|
191349
191386
|
}
|
|
191350
|
-
if (task.
|
|
191387
|
+
if (task.firstExecutionAt != null || task.executionStartedAt != null) {
|
|
191351
191388
|
return true;
|
|
191352
191389
|
}
|
|
191353
191390
|
if (task.column === "triage") {
|
|
@@ -194544,6 +194581,7 @@ init_src2();
|
|
|
194544
194581
|
// ../engine/src/workflow-graph-executor.ts
|
|
194545
194582
|
init_src2();
|
|
194546
194583
|
init_transient_error_detector();
|
|
194584
|
+
init_transient_error_patterns();
|
|
194547
194585
|
|
|
194548
194586
|
// ../engine/src/required-workflow-artifacts.ts
|
|
194549
194587
|
var REQUIRED_ARTIFACT_MISSING_PREFIX = "required-artifact-missing:";
|
|
@@ -195622,6 +195660,7 @@ function workflowNodeRequiresWorktree(node, { optionalGroupId, reviewerInlineFix
|
|
|
195622
195660
|
// ../engine/src/workflow-graph-executor.ts
|
|
195623
195661
|
var PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE = "plan-review-provider-failure-hold";
|
|
195624
195662
|
var PLAN_REVIEW_LEASE_HELD_VALUE = "plan-review-lease-held";
|
|
195663
|
+
var SESSION_CONTENTION_HOLD_VALUE = "session-contention-hold";
|
|
195625
195664
|
var WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY = "workflow:interruptedNodeId";
|
|
195626
195665
|
var WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY = "workflow:interruptedNodeAbortKind";
|
|
195627
195666
|
var WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY = "workflow:optionalGroupActive";
|
|
@@ -195991,6 +196030,15 @@ var WorkflowGraphExecutor = class {
|
|
|
195991
196030
|
};
|
|
195992
196031
|
context[`node:${node.id}:outcome`] = result2.outcome;
|
|
195993
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
|
+
}
|
|
195994
196042
|
const nonPlanDefectPlanReviewFailure = node.id === PLAN_REVIEW_GROUP_ID && stepStatus === "failed" && (isRequiredArtifactReadFailedValue(verdictRaw) || isNonPlanDefectPlanReviewFailure({
|
|
195995
196043
|
verdict,
|
|
195996
196044
|
errorMessage: stepOutput ?? stepNotes,
|
|
@@ -196374,6 +196422,7 @@ var WorkflowGraphExecutor = class {
|
|
|
196374
196422
|
} catch (error2) {
|
|
196375
196423
|
if (signal?.aborted) return this.withEnginePauseAbortContext(node, { outcome: "failure", value: "aborted" });
|
|
196376
196424
|
lastError = error2;
|
|
196425
|
+
if (isSessionContentionError(error2 instanceof Error ? error2.message : String(error2))) break;
|
|
196377
196426
|
}
|
|
196378
196427
|
}
|
|
196379
196428
|
if (signal?.aborted) {
|
|
@@ -196392,11 +196441,13 @@ var WorkflowGraphExecutor = class {
|
|
|
196392
196441
|
}
|
|
196393
196442
|
return degraded;
|
|
196394
196443
|
}
|
|
196444
|
+
const lastErrorText = lastError instanceof Error ? lastError.message : String(lastError);
|
|
196395
196445
|
const failureResult = {
|
|
196396
196446
|
outcome: "failure",
|
|
196397
|
-
|
|
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",
|
|
196398
196449
|
contextPatch: {
|
|
196399
|
-
[`node:${node.id}:error`]:
|
|
196450
|
+
[`node:${node.id}:error`]: lastErrorText
|
|
196400
196451
|
}
|
|
196401
196452
|
};
|
|
196402
196453
|
if (recordProgress && this.shouldRecordNodeProgress(node)) {
|
|
@@ -207212,6 +207263,7 @@ async function preserveWorktreeChanges(repoDir, worktreePath, taskId) {
|
|
|
207212
207263
|
return null;
|
|
207213
207264
|
}
|
|
207214
207265
|
}
|
|
207266
|
+
var PRE_EXECUTION_WORKTREE_MAX_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
207215
207267
|
var APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
|
|
207216
207268
|
var STARVED_REFINEMENT_RECOVERY_GRACE_MS = 10 * 6e4;
|
|
207217
207269
|
var STARVED_PEER_PROGRESS_THRESHOLD = 3;
|
|
@@ -208970,6 +209022,8 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
208970
209022
|
{ name: "reconcile-workspace-partial-lands", fn: () => this.reconcileWorkspacePartialLands() },
|
|
208971
209023
|
{ name: "reclaim-phantom-workspace-land-leases", fn: () => this.reclaimPhantomWorkspaceLandLeases() },
|
|
208972
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() },
|
|
208973
209027
|
{ name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
|
|
208974
209028
|
{ name: "recover-already-merged-review", fn: () => this.recoverAlreadyMergedReviewTasks() },
|
|
208975
209029
|
{ name: "recover-post-done-noncontinuable-wedge", fn: () => this.recoverPostDoneNonContinuableWedge() },
|
|
@@ -213996,6 +214050,58 @@ var SelfHealingManager = class _SelfHealingManager {
|
|
|
213996
214050
|
return 0;
|
|
213997
214051
|
}
|
|
213998
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
|
+
}
|
|
213999
214105
|
/*
|
|
214000
214106
|
FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD4 — per-repo worktree cleanup from STORED paths):
|
|
214001
214107
|
For done/dead workspace tasks, remove each recorded per-repo worktree. The paths are ADDRESSABLE
|
|
@@ -218601,6 +218707,9 @@ var MAX_TASK_DONE_SESSION_RETRIES = 3;
|
|
|
218601
218707
|
var MAX_TASK_DONE_REQUEUE_RETRIES = 3;
|
|
218602
218708
|
var MAX_TRANSIENT_GRAPH_RESUME_RETRIES = 2;
|
|
218603
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;
|
|
218604
218713
|
var COMPLETED_TASK_WATCHDOG_MS = 6e4;
|
|
218605
218714
|
var WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
|
|
218606
218715
|
var LOOP_COMPACTION_TIMEOUT_MS = 6e4;
|
|
@@ -219340,6 +219449,15 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219340
219449
|
}
|
|
219341
219450
|
})
|
|
219342
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
|
+
);
|
|
219343
219461
|
} else if (from === "in-progress") {
|
|
219344
219462
|
if (this.workflowLifecycleMovesInFlight.has(task.id) && this.graphRouting.has(task.id)) {
|
|
219345
219463
|
executorLog.log(
|
|
@@ -219929,15 +220047,63 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219929
220047
|
getActiveWorktreePaths() consumers that cd into a path are unaffected; only the registry key changes.
|
|
219930
220048
|
Non-workspace tasks (unique worktree path != rootDir) are returned unchanged.
|
|
219931
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
|
+
*/
|
|
219932
220065
|
sessionRegistryPath(taskId, worktreePath) {
|
|
219933
|
-
if (
|
|
220066
|
+
if (worktreePath === this.rootDir) {
|
|
219934
220067
|
return `${worktreePath}#session:${taskId}`;
|
|
219935
220068
|
}
|
|
219936
220069
|
return worktreePath;
|
|
219937
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
|
+
}
|
|
219938
220104
|
setActiveSession(taskId, sessionState, worktreePath) {
|
|
219939
220105
|
this.activeSessions.set(taskId, sessionState);
|
|
219940
|
-
|
|
220106
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "executor", taskId);
|
|
219941
220107
|
}
|
|
219942
220108
|
markGraphExecuteSelfRequeued(taskId) {
|
|
219943
220109
|
if (this.graphRouting.has(taskId)) {
|
|
@@ -219955,7 +220121,7 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219955
220121
|
setActiveStepExecutor(taskId, stepExecutor, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
|
|
219956
220122
|
this.activeStepExecutors.set(taskId, stepExecutor);
|
|
219957
220123
|
this.activeStepExecutorSeenSteeringIds.set(taskId, seenSteeringIds);
|
|
219958
|
-
|
|
220124
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "step-session", `${taskId}#step-session`);
|
|
219959
220125
|
}
|
|
219960
220126
|
deleteActiveStepExecutor(taskId, worktreePath) {
|
|
219961
220127
|
this.activeStepExecutors.delete(taskId);
|
|
@@ -219969,7 +220135,7 @@ var TaskExecutor = class _TaskExecutor {
|
|
|
219969
220135
|
setActiveWorkflowStepSession(taskId, session, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
|
|
219970
220136
|
this.activeWorkflowStepSessions.set(taskId, session);
|
|
219971
220137
|
this.activeWorkflowStepSessionSeenSteeringIds.set(taskId, seenSteeringIds);
|
|
219972
|
-
|
|
220138
|
+
this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "workflow-step", `${taskId}#workflow-step`);
|
|
219973
220139
|
}
|
|
219974
220140
|
deleteActiveWorkflowStepSession(taskId, worktreePath) {
|
|
219975
220141
|
this.activeWorkflowStepSessions.delete(taskId);
|
|
@@ -224288,6 +224454,87 @@ ${info.feedback}`, revisionKey),
|
|
|
224288
224454
|
this.unregisterConfiguredCommandController(task.id, commandAbortController);
|
|
224289
224455
|
}
|
|
224290
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
|
+
}
|
|
224291
224538
|
async prepareGraphNodeExecution(node, nodeTask, settings, requirement) {
|
|
224292
224539
|
if (!requirement.requiresWorktree) return;
|
|
224293
224540
|
const live = await this.store.getTask(nodeTask.id);
|
|
@@ -224414,22 +224661,29 @@ ${info.feedback}`, revisionKey),
|
|
|
224414
224661
|
optionalGroupId,
|
|
224415
224662
|
reviewerInlineFixes: settings.reviewerInlineFixes
|
|
224416
224663
|
});
|
|
224417
|
-
|
|
224418
|
-
if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
|
|
224419
|
-
return { outcome: "failure", value: "no-worktree-for-write-node" };
|
|
224420
|
-
}
|
|
224664
|
+
let executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
|
|
224421
224665
|
const nodeDisplayName = typeof cfg.name === "string" && cfg.name.trim() ? cfg.name.trim() : node.id;
|
|
224422
224666
|
const isPlanReviewNode = node.id === "plan-review-step" || nodeDisplayName === "Plan Review" || optionalGroupId === "plan-review";
|
|
224423
|
-
|
|
224424
|
-
|
|
224425
|
-
|
|
224426
|
-
|
|
224427
|
-
|
|
224428
|
-
|
|
224429
|
-
|
|
224430
|
-
|
|
224431
|
-
|
|
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" };
|
|
224432
224685
|
}
|
|
224686
|
+
const worktreePath = executionTarget.worktree || this.rootDir;
|
|
224433
224687
|
let prompt = typeof cfg.prompt === "string" ? cfg.prompt : "";
|
|
224434
224688
|
let modelProvider = typeof cfg.modelProvider === "string" && cfg.modelProvider.trim() ? cfg.modelProvider : void 0;
|
|
224435
224689
|
let modelId = typeof cfg.modelId === "string" && cfg.modelId.trim() ? cfg.modelId : void 0;
|
|
@@ -224726,6 +224980,77 @@ ${prompt}`;
|
|
|
224726
224980
|
const latestAction = live.log.at(-1)?.action;
|
|
224727
224981
|
return latestAction === "Resumed after engine restart" || latestAction === "Resuming execution after unpause";
|
|
224728
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
|
+
}
|
|
224729
225054
|
graphFailureValue(result) {
|
|
224730
225055
|
const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1];
|
|
224731
225056
|
if (!failedNode || !result.context) return void 0;
|
|
@@ -225317,6 +225642,11 @@ Graph value: ${failureValue}` : ""}`, budget.key),
|
|
|
225317
225642
|
await this.persistTokenUsage(task.id);
|
|
225318
225643
|
return;
|
|
225319
225644
|
}
|
|
225645
|
+
if (this.isSessionContentionGraphFailure(result)) {
|
|
225646
|
+
await this.holdForSessionContention(task, live, result);
|
|
225647
|
+
await this.persistTokenUsage(task.id);
|
|
225648
|
+
return;
|
|
225649
|
+
}
|
|
225320
225650
|
if (await this.routeUnusableWorktreeGraphFailureToRecovery(task, live, result)) {
|
|
225321
225651
|
await this.persistTokenUsage(task.id);
|
|
225322
225652
|
return;
|
|
@@ -241523,6 +241853,33 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241523
241853
|
if (this.processing.has(task.id) || this.hasLivePlanningWork(task.id)) return;
|
|
241524
241854
|
this.requestImmediatePoll();
|
|
241525
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
|
+
};
|
|
241526
241883
|
}
|
|
241527
241884
|
running = false;
|
|
241528
241885
|
polling = false;
|
|
@@ -241581,6 +241938,8 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241581
241938
|
taskPausedHandler;
|
|
241582
241939
|
/** FNXC:CodingIdeasWorkflow 2026-07-25-11:20: store-event wake for planning-eligible columns. */
|
|
241583
241940
|
taskColumnWakeHandler;
|
|
241941
|
+
/** FNXC:PlanningEvacuation 2026-07-25-23:00: stops planning when a card leaves the planner lanes. */
|
|
241942
|
+
taskEvacuatedFromPlanningHandler;
|
|
241584
241943
|
_approvalRequestStore;
|
|
241585
241944
|
/**
|
|
241586
241945
|
* @param store — Task store instance (also used to listen for `settings:updated` events)
|
|
@@ -241689,6 +242048,9 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241689
242048
|
this.store.on("task:updated", this.taskColumnWakeHandler);
|
|
241690
242049
|
this.store.on("task:created", this.taskColumnWakeHandler);
|
|
241691
242050
|
}
|
|
242051
|
+
if (this.taskEvacuatedFromPlanningHandler && typeof this.store.on === "function") {
|
|
242052
|
+
this.store.on("task:updated", this.taskEvacuatedFromPlanningHandler);
|
|
242053
|
+
}
|
|
241692
242054
|
this.clearStaleSpecifyingStatuses().catch((err) => {
|
|
241693
242055
|
planLog.error("Failed to clear stale planning statuses:", err);
|
|
241694
242056
|
});
|
|
@@ -241736,6 +242098,9 @@ var TriageProcessor = class _TriageProcessor {
|
|
|
241736
242098
|
this.store.off("task:updated", this.taskColumnWakeHandler);
|
|
241737
242099
|
this.store.off("task:created", this.taskColumnWakeHandler);
|
|
241738
242100
|
}
|
|
242101
|
+
if (this.taskEvacuatedFromPlanningHandler && typeof this.store.off === "function") {
|
|
242102
|
+
this.store.off("task:updated", this.taskEvacuatedFromPlanningHandler);
|
|
242103
|
+
}
|
|
241739
242104
|
this.abortAndDisposeActiveSessions("engine stop");
|
|
241740
242105
|
planLog.log("Processor stopped");
|
|
241741
242106
|
}
|
|
@@ -242527,11 +242892,15 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
|
|
|
242527
242892
|
planningModel.modelId,
|
|
242528
242893
|
assignedAgent?.runtimeConfig
|
|
242529
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
|
+
}
|
|
242530
242899
|
const { session } = await createResolvedAgentSession({
|
|
242531
242900
|
sessionPurpose: "triage",
|
|
242532
242901
|
runtimeHint: triageRuntimeHint,
|
|
242533
242902
|
pluginRunner: this.options.pluginRunner,
|
|
242534
|
-
cwd:
|
|
242903
|
+
cwd: planningCwd,
|
|
242535
242904
|
systemPrompt: triageSystemPromptFinal,
|
|
242536
242905
|
systemPromptLayers: triageLayers,
|
|
242537
242906
|
tools: "coding",
|
|
@@ -244963,6 +245332,9 @@ var InProcessRuntime = class extends EventEmitter27 {
|
|
|
244963
245332
|
usageLimitPauser: this.usageLimitPauser,
|
|
244964
245333
|
agentStore: this.agentStore,
|
|
244965
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),
|
|
244966
245338
|
onSpecifyStart: (t) => {
|
|
244967
245339
|
this.recordActivity();
|
|
244968
245340
|
runtimeLog.log(`Specifying ${t.id}...`);
|
|
@@ -245051,6 +245423,8 @@ var InProcessRuntime = class extends EventEmitter27 {
|
|
|
245051
245423
|
getExecutingTaskIds: () => this.executor?.getExecutingTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
245052
245424
|
clearPhantomExecutorBinding: (taskId, options) => this.executor?.clearPhantomExecutorBinding(taskId, options),
|
|
245053
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),
|
|
245054
245428
|
recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
|
|
245055
245429
|
getPlanningTaskIds: () => this.triageProcessor?.getPlanningTaskIds() ?? /* @__PURE__ */ new Set(),
|
|
245056
245430
|
// FNXC:TaskTiming 2026-08-01-12:00: orphan planning recovery must defer
|