@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.
Files changed (93) hide show
  1. package/dist/bin.js +399 -25
  2. package/dist/child-process-worker.js +396 -22
  3. package/dist/client/.vite/manifest.json +256 -256
  4. package/dist/client/assets/{AgentDetailView-Bopzxiqb.js → AgentDetailView-DH4Cn5iG.js} +1 -1
  5. package/dist/client/assets/{AgentPermissionPolicyEditor-i5N7rUlL.js → AgentPermissionPolicyEditor-BycC7HhP.js} +1 -1
  6. package/dist/client/assets/{AgentsView-M3GoJDHH.js → AgentsView-Dkij8Sm8.js} +3 -3
  7. package/dist/client/assets/{ChatView-CqiyA9Kt.js → ChatView-DAsZY1We.js} +1 -1
  8. package/dist/client/assets/{CommandCenter-CAvMGKhl.js → CommandCenter-jTzBo-pg.js} +1 -1
  9. package/dist/client/assets/{DevServerView-wqM6h3nV.js → DevServerView-CxlacYJp.js} +1 -1
  10. package/dist/client/assets/{DirectoryPicker-C8OyXCE-.js → DirectoryPicker-DsCk3Nsw.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-CjNMZQi8.js → DocumentsView-8EI6KNBY.js} +1 -1
  12. package/dist/client/assets/{EvalsView-BVlbuB5t.js → EvalsView-kYnaIk11.js} +1 -1
  13. package/dist/client/assets/{ExperimentalAgentOnboardingModal-CIwCg8sq.js → ExperimentalAgentOnboardingModal-ce6DROKD.js} +1 -1
  14. package/dist/client/assets/{GoalsView-Cnw2igil.js → GoalsView-BVlTtw-u.js} +1 -1
  15. package/dist/client/assets/{InsightsView-D7GmD-Kx.js → InsightsView-BKQeROkb.js} +1 -1
  16. package/dist/client/assets/{MemoryView-BlqkfIE5.js → MemoryView-DCzTnxsV.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-EC94zLMq.js → PiExtensionsManager-C94HwDz5.js} +1 -1
  18. package/dist/client/assets/{PluginManager-pYyCD3uh.js → PluginManager-C3DegklW.js} +1 -1
  19. package/dist/client/assets/{PullRequestView-BAb0vVhu.js → PullRequestView-CrE89UL7.js} +1 -1
  20. package/dist/client/assets/{ReportModal-COmReivA.js → ReportModal-BD1ZUDdV.js} +1 -1
  21. package/dist/client/assets/{ResearchView-DLMo4rFw.js → ResearchView-eMiRw1EM.js} +1 -1
  22. package/dist/client/assets/{SecretsView-w_O8NLfw.js → SecretsView-6qK8Idb7.js} +1 -1
  23. package/dist/client/assets/{SessionTerminal-CSPi-qWE.js → SessionTerminal-DtFLyUaK.js} +1 -1
  24. package/dist/client/assets/SettingsModal-BTfZlghk.css +1 -0
  25. package/dist/client/assets/{SettingsModal-wilrGXvE.js → SettingsModal-BkOSlfyr.js} +1 -1
  26. package/dist/client/assets/{SettingsModal-C9bVvYQq.js → SettingsModal-DSq-_qwo.js} +4 -4
  27. package/dist/client/assets/{SettingsTextareaRow-Bd7_SxVv.js → SettingsTextareaRow-Btyx5kaR.js} +1 -1
  28. package/dist/client/assets/{SetupWizardModal-sPs6Lw3a.js → SetupWizardModal-Blhx-dRt.js} +2 -2
  29. package/dist/client/assets/{SkillsView-B9PvYS8t.js → SkillsView-DGE5kYc3.js} +1 -1
  30. package/dist/client/assets/{TodoView-Df0SD_JT.js → TodoView-YSGI7ATd.js} +1 -1
  31. package/dist/client/assets/{WorkflowNodeEditor-B3kFD-S0.js → WorkflowNodeEditor-CZiEThu2.js} +1 -1
  32. package/dist/client/assets/{agent-import-generation-OXfU1-D1.js → agent-import-generation-DQeNZMAm.js} +1 -1
  33. package/dist/client/assets/{architectureDiagram-3BPJPVTR-B_8zk4ag.js → architectureDiagram-3BPJPVTR-DhaJNKa4.js} +1 -1
  34. package/dist/client/assets/{blockDiagram-GPEHLZMM-CpDKBJkO.js → blockDiagram-GPEHLZMM-pKpg7gJU.js} +1 -1
  35. package/dist/client/assets/{c4Diagram-AAUBKEIU-D3wtfGzI.js → c4Diagram-AAUBKEIU-DVPXKJnr.js} +1 -1
  36. package/dist/client/assets/channel-xFBfK0sX.js +1 -0
  37. package/dist/client/assets/{chunk-2J33WTMH-DS9sHIMv.js → chunk-2J33WTMH-CHt90T5z.js} +1 -1
  38. package/dist/client/assets/{chunk-4BX2VUAB-B-xW6cfi.js → chunk-4BX2VUAB-DsWFYy0a.js} +1 -1
  39. package/dist/client/assets/{chunk-55IACEB6-Ddxf-sPU.js → chunk-55IACEB6-D8XiXEo8.js} +1 -1
  40. package/dist/client/assets/{chunk-727SXJPM-Bv_Z1w57.js → chunk-727SXJPM-DlUYBn_Y.js} +1 -1
  41. package/dist/client/assets/{chunk-AQP2D5EJ-DYHYN43E.js → chunk-AQP2D5EJ-BSdPOr9q.js} +1 -1
  42. package/dist/client/assets/{chunk-FMBD7UC4-wuuFvvBk.js → chunk-FMBD7UC4-B9IAeBld.js} +1 -1
  43. package/dist/client/assets/{chunk-ND2GUHAM-DfVRkVZo.js → chunk-ND2GUHAM-CmxnYQrR.js} +1 -1
  44. package/dist/client/assets/{chunk-QZHKN3VN-D0t6ryuh.js → chunk-QZHKN3VN-Dxp6Po_j.js} +1 -1
  45. package/dist/client/assets/classDiagram-4FO5ZUOK-BFBVD968.js +1 -0
  46. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BFBVD968.js +1 -0
  47. package/dist/client/assets/{cose-bilkent-S5V4N54A-DVlKqNFJ.js → cose-bilkent-S5V4N54A-BNl3bhpS.js} +1 -1
  48. package/dist/client/assets/{dagre-BM42HDAG-DTtiGxz-.js → dagre-BM42HDAG-DFs7WCcz.js} +1 -1
  49. package/dist/client/assets/{dashboard-view-DH99T6VJ.js → dashboard-view-DnoTcnIo.js} +1 -1
  50. package/dist/client/assets/{dashboard-view-D-nDgSmP.js → dashboard-view-ZGxLEL9r.js} +1 -1
  51. package/dist/client/assets/{dashboard-view-CLjYqvE9.js → dashboard-view-vc8kgLMs.js} +1 -1
  52. package/dist/client/assets/{diagram-2AECGRRQ-Tv-YxCWk.js → diagram-2AECGRRQ-C2PVozvu.js} +1 -1
  53. package/dist/client/assets/{diagram-5GNKFQAL-DlAAPkGu.js → diagram-5GNKFQAL-DFJ5b9H8.js} +1 -1
  54. package/dist/client/assets/{diagram-KO2AKTUF-DisatTBB.js → diagram-KO2AKTUF-D335CW-c.js} +1 -1
  55. package/dist/client/assets/{diagram-LMA3HP47-CDrUxjzW.js → diagram-LMA3HP47-DSfhroAW.js} +1 -1
  56. package/dist/client/assets/{diagram-OG6HWLK6-BQ34a4Ej.js → diagram-OG6HWLK6-8iGveM-Z.js} +1 -1
  57. package/dist/client/assets/{erDiagram-TEJ5UH35-s-YHwAEU.js → erDiagram-TEJ5UH35-Xc56MEEi.js} +1 -1
  58. package/dist/client/assets/{flowDiagram-I6XJVG4X-EpXzmG2d.js → flowDiagram-I6XJVG4X-DjCU9WbD.js} +1 -1
  59. package/dist/client/assets/{folder-open-DN86xUly.js → folder-open-BRrB44UX.js} +1 -1
  60. package/dist/client/assets/{ganttDiagram-6RSMTGT7-Dk2WaoX8.js → ganttDiagram-6RSMTGT7-CXQE1bea.js} +1 -1
  61. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CjchnKW0.js → gitGraphDiagram-PVQCEYII--mrSABLC.js} +1 -1
  62. package/dist/client/assets/{index-aGZ3DHQw.js → index-C2xsZoNG.js} +8 -8
  63. package/dist/client/assets/{infoDiagram-5YYISTIA-CiU-twoj.js → infoDiagram-5YYISTIA-CTS_nljP.js} +1 -1
  64. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BvylyUKK.js → ishikawaDiagram-YF4QCWOH-CpV5x3gk.js} +1 -1
  65. package/dist/client/assets/{journeyDiagram-JHISSGLW-Ca6z84KB.js → journeyDiagram-JHISSGLW-BK2QCMnt.js} +1 -1
  66. package/dist/client/assets/{kanban-definition-UN3LZRKU-DXbw2-s-.js → kanban-definition-UN3LZRKU-B4srpspY.js} +1 -1
  67. package/dist/client/assets/{mermaid.core-DkS33D12.js → mermaid.core-B8DVAt21.js} +4 -4
  68. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgXuWPDD.js → mindmap-definition-RKZ34NQL-CsmmiPpA.js} +1 -1
  69. package/dist/client/assets/{pieDiagram-4H26LBE5-CnOzTN6U.js → pieDiagram-4H26LBE5-BahLDqf4.js} +1 -1
  70. package/dist/client/assets/{puzzle-Dxl1kcLF.js → puzzle-BFPQsuIa.js} +1 -1
  71. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DI3s1u95.js → quadrantDiagram-W4KKPZXB-Bo3u0wTU.js} +1 -1
  72. package/dist/client/assets/{requirementDiagram-4Y6WPE33-Ck97mo-W.js → requirementDiagram-4Y6WPE33-DYkYsz7d.js} +1 -1
  73. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Z3w8NJac.js → sankeyDiagram-5OEKKPKP-BCbh0BSY.js} +1 -1
  74. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DLGtWgfy.js → sequenceDiagram-3UESZ5HK-C2AP9GlE.js} +1 -1
  75. package/dist/client/assets/{shield-alert-BCGxgBpM.js → shield-alert-tlZOi02c.js} +1 -1
  76. package/dist/client/assets/{standing-instructions-template-CUE6ItG6.js → standing-instructions-template-C0Rzv9Mr.js} +1 -1
  77. package/dist/client/assets/{stateDiagram-AJRCARHV-C6Zl2EJW.js → stateDiagram-AJRCARHV-CARkH0ug.js} +1 -1
  78. package/dist/client/assets/{stateDiagram-v2-BHNVJYJU-BA77mDAW.js → stateDiagram-v2-BHNVJYJU-C2He043y.js} +1 -1
  79. package/dist/client/assets/{timeline-definition-PNZ67QCA-D9Cp15Hm.js → timeline-definition-PNZ67QCA-MvbYneEM.js} +1 -1
  80. package/dist/client/assets/{upload-2jCicylS.js → upload-DWK0F17Z.js} +1 -1
  81. package/dist/client/assets/{users-C1TTGAfX.js → users-1AkDfM14.js} +1 -1
  82. package/dist/client/assets/{vennDiagram-CIIHVFJN-BwFSh6Tr.js → vennDiagram-CIIHVFJN-DOjzzLqc.js} +1 -1
  83. package/dist/client/assets/{wardley-L42UT6IY-CQknAo24.js → wardley-L42UT6IY-Di70vhu2.js} +1 -1
  84. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Dwc8f8bd.js → wardleyDiagram-YWT4CUSO-nK6ARhDv.js} +1 -1
  85. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPkeHv8g.js → xychartDiagram-2RQKCTM6-C7_y76B7.js} +1 -1
  86. package/dist/client/index.html +1 -1
  87. package/dist/client/version.json +1 -1
  88. package/dist/extension.js +399 -25
  89. package/package.json +1 -1
  90. package/dist/client/assets/SettingsModal-CMLHZBhX.css +0 -1
  91. package/dist/client/assets/channel-BLzjKIVz.js +0 -1
  92. package/dist/client/assets/classDiagram-4FO5ZUOK-DkwPy8u9.js +0 -1
  93. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DkwPy8u9.js +0 -1
package/dist/extension.js CHANGED
@@ -138568,6 +138568,12 @@ var init_usage_limit_detector = __esm({
138568
138568
  });
138569
138569
 
138570
138570
  // ../engine/src/transient-error-patterns.ts
138571
+ function isSessionContentionError(errorMessage2) {
138572
+ if (!errorMessage2 || typeof errorMessage2 !== "string") {
138573
+ return false;
138574
+ }
138575
+ return SESSION_CONTENTION_PATTERNS.some((pattern) => pattern.test(errorMessage2));
138576
+ }
138571
138577
  function isTransientError(errorMessage2) {
138572
138578
  if (!errorMessage2 || typeof errorMessage2 !== "string") {
138573
138579
  return false;
@@ -138575,6 +138581,9 @@ function isTransientError(errorMessage2) {
138575
138581
  if (isTransientAuthCredentialError(errorMessage2)) {
138576
138582
  return true;
138577
138583
  }
138584
+ if (isSessionContentionError(errorMessage2)) {
138585
+ return true;
138586
+ }
138578
138587
  return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage2));
138579
138588
  }
138580
138589
  function isTransientAuthCredentialError(errorMessage2) {
@@ -138586,7 +138595,7 @@ function isTransientAuthCredentialError(errorMessage2) {
138586
138595
  }
138587
138596
  return TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN.test(errorMessage2);
138588
138597
  }
138589
- var TRANSIENT_ERROR_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
138598
+ var TRANSIENT_ERROR_PATTERNS, SESSION_CONTENTION_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
138590
138599
  var init_transient_error_patterns = __esm({
138591
138600
  "../engine/src/transient-error-patterns.ts"() {
138592
138601
  "use strict";
@@ -138647,6 +138656,14 @@ var init_transient_error_patterns = __esm({
138647
138656
  /\bACP failed to start\b/i,
138648
138657
  /\bACP session has no live connection\b/i
138649
138658
  ];
138659
+ SESSION_CONTENTION_PATTERNS = [
138660
+ // ActiveSessionPathHeldByForeignTaskError (active-session-registry.ts)
138661
+ /active-session path\s+\S+\s+is held by task\s+\S+/i,
138662
+ // WorkspaceRepoAcquireBusyError (worktree-acquisition.ts)
138663
+ /workspace sub-repo\s+\S+\s+acquisition is in progress for task\s+\S+/i,
138664
+ // WorkspaceRepoLandBusyError (merger-ai.ts)
138665
+ /workspace sub-repo\s+\S+\s+land is in progress for task\s+\S+/i
138666
+ ];
138650
138667
  TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN = /"type":\s*"authentication_error"|invalid authentication credentials|token[_\s]?expired/i;
138651
138668
  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;
138652
138669
  }
@@ -138655,6 +138672,9 @@ var init_transient_error_patterns = __esm({
138655
138672
  // ../engine/src/transient-error-detector.ts
138656
138673
  function isNonPlanDefectPlanReviewFailure(input) {
138657
138674
  if (input.verdict === "REVISE") return false;
138675
+ if (input.failureValue?.trim().toLowerCase() === "session-contention-hold" || (input.errorMessage ? isSessionContentionError(input.errorMessage) : false)) {
138676
+ return false;
138677
+ }
138658
138678
  const failureValue = input.failureValue?.trim().toLowerCase();
138659
138679
  if (failureValue === "exception" || failureValue === "aborted") return true;
138660
138680
  const errorMessage2 = input.errorMessage?.trim();
@@ -163109,6 +163129,23 @@ var init_worktrunk_installer = __esm({
163109
163129
  });
163110
163130
 
163111
163131
  // ../engine/src/active-session-registry.ts
163132
+ function acquireActiveSessionPath(registry3, path10, registration, options = {}) {
163133
+ const existing = registry3.lookupByPath(path10);
163134
+ if (!existing || existing.taskId === registration.taskId) {
163135
+ registry3.registerPath(path10, registration);
163136
+ return { action: "registered" };
163137
+ }
163138
+ const now = options.now?.() ?? Date.now();
163139
+ const ageMs = now - existing.registeredAt;
163140
+ const minIdleMs = options.minIdleMs ?? DEFAULT_SELF_OWNED_MIN_IDLE_MS;
163141
+ const holderIsLive = options.holderLiveProbe?.(existing.taskId, path10) ?? true;
163142
+ if (holderIsLive || ageMs < minIdleMs) {
163143
+ return { action: "contended", holderTaskId: existing.taskId, holderKind: existing.kind, ageMs };
163144
+ }
163145
+ registry3.unregisterPath(path10);
163146
+ registry3.registerPath(path10, registration);
163147
+ return { action: "reclaimed-stale-foreign", holderTaskId: existing.taskId, ageMs };
163148
+ }
163112
163149
  function reconcileSelfOwnedActiveSessionForRemoval(registry3, worktreePath, requestingTaskId, liveBindingProbe, options = {}) {
163113
163150
  const record2 = registry3.lookupByPath(worktreePath);
163114
163151
  if (!record2) {
@@ -173488,7 +173525,7 @@ function hasAdvancedPastPlanning(task) {
173488
173525
  if (task.column === "in-progress" || task.column === "in-review" || task.column === "done" || task.column === "archived") {
173489
173526
  return true;
173490
173527
  }
173491
- if (task.worktree != null) {
173528
+ if (task.firstExecutionAt != null || task.executionStartedAt != null) {
173492
173529
  return true;
173493
173530
  }
173494
173531
  if (task.column === "triage") {
@@ -202252,7 +202289,7 @@ function isNoTaskDoneFailure2(task) {
202252
202289
  function hasStepProgress2(task) {
202253
202290
  return task.steps.some((step) => step.status !== "pending");
202254
202291
  }
202255
- var log28, worktreeMetadataReconcileLog, execAsync30, yieldEventLoop2, DONE_TASK_INTEGRITY_SWEEP_LIMIT, BOARD_STALL_NOTIFICATION_COOLDOWN_MS, DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS, PHANTOM_EXECUTOR_BINDING_AGE_MULTIPLIER, MAX_NO_PROGRESS_RESUME_ATTEMPTS, APPROVED_TRIAGE_RECOVERY_GRACE_MS, STARVED_REFINEMENT_RECOVERY_GRACE_MS, STARVED_PEER_PROGRESS_THRESHOLD, STARVED_REFINEMENT_ESCALATION_COOLDOWN_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, LEAKED_WORKTREE_SLOT_GRACE_MS, NON_TERMINAL_STEP_STATUSES2, STRANDED_COMPLETED_TODO_ACTIVE_STATUSES, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, RECONCILE_SCOPE_OVERRIDE_MERGE_ACTIVE_STATUS_SET, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_FANOUT_MIN_AGE_MS, DEFAULT_UNBACKED_MERGING_FANOUT_GRACE_MS, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, RUNNING_ON_INACTIVE_TASK_STALE_RUN_MS, SelfHealingManager;
202292
+ var log28, worktreeMetadataReconcileLog, execAsync30, yieldEventLoop2, DONE_TASK_INTEGRITY_SWEEP_LIMIT, BOARD_STALL_NOTIFICATION_COOLDOWN_MS, DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS, PHANTOM_EXECUTOR_BINDING_AGE_MULTIPLIER, MAX_NO_PROGRESS_RESUME_ATTEMPTS, PRE_EXECUTION_WORKTREE_MAX_IDLE_MS, APPROVED_TRIAGE_RECOVERY_GRACE_MS, STARVED_REFINEMENT_RECOVERY_GRACE_MS, STARVED_PEER_PROGRESS_THRESHOLD, STARVED_REFINEMENT_ESCALATION_COOLDOWN_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, LEAKED_WORKTREE_SLOT_GRACE_MS, NON_TERMINAL_STEP_STATUSES2, STRANDED_COMPLETED_TODO_ACTIVE_STATUSES, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, RECONCILE_SCOPE_OVERRIDE_MERGE_ACTIVE_STATUS_SET, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_FANOUT_MIN_AGE_MS, DEFAULT_UNBACKED_MERGING_FANOUT_GRACE_MS, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, RUNNING_ON_INACTIVE_TASK_STALE_RUN_MS, SelfHealingManager;
202256
202293
  var init_self_healing = __esm({
202257
202294
  "../engine/src/self-healing.ts"() {
202258
202295
  "use strict";
@@ -202308,6 +202345,7 @@ var init_self_healing = __esm({
202308
202345
  DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS = 60 * 60 * 1e3;
202309
202346
  PHANTOM_EXECUTOR_BINDING_AGE_MULTIPLIER = 3;
202310
202347
  MAX_NO_PROGRESS_RESUME_ATTEMPTS = 2;
202348
+ PRE_EXECUTION_WORKTREE_MAX_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
202311
202349
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
202312
202350
  STARVED_REFINEMENT_RECOVERY_GRACE_MS = 10 * 6e4;
202313
202351
  STARVED_PEER_PROGRESS_THRESHOLD = 3;
@@ -203948,6 +203986,8 @@ var init_self_healing = __esm({
203948
203986
  { name: "reconcile-workspace-partial-lands", fn: () => this.reconcileWorkspacePartialLands() },
203949
203987
  { name: "reclaim-phantom-workspace-land-leases", fn: () => this.reclaimPhantomWorkspaceLandLeases() },
203950
203988
  { name: "reconcile-orphaned-workspace-worktrees", fn: () => this.reconcileOrphanedWorkspaceWorktrees() },
203989
+ // FNXC:PlanningEvacuation 2026-07-25-23:00: reclaim worktrees acquired at planning time by cards that never executed.
203990
+ { name: "reconcile-pre-execution-worktrees", fn: () => this.reconcilePreExecutionWorktrees() },
203951
203991
  { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
203952
203992
  { name: "recover-already-merged-review", fn: () => this.recoverAlreadyMergedReviewTasks() },
203953
203993
  { name: "recover-post-done-noncontinuable-wedge", fn: () => this.recoverPostDoneNonContinuableWedge() },
@@ -208974,6 +209014,58 @@ var init_self_healing = __esm({
208974
209014
  return 0;
208975
209015
  }
208976
209016
  }
209017
+ /*
209018
+ FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree sweep):
209019
+ Planning acquires the task's own worktree, so cards that never reach execution would accumulate
209020
+ worktrees on disk: withdrawn to an intake column, archived from a planner lane, or simply parked.
209021
+ This sweep reclaims them.
209022
+
209023
+ Candidates are addressed from task ROWS (never a directory walk — AGENTS.md forbids unbounded temp
209024
+ scans): tasks holding `worktree` that sit in a non-executing column and carry no execution
209025
+ timestamp. `todo` is deliberately EXCLUDED: a planned card waiting for a WIP slot legitimately
209026
+ keeps its worktree, and churning it would just force a re-acquire minutes later. Every real safety
209027
+ decision (live session, clean branch, execution evidence) belongs to
209028
+ `TaskExecutor.releasePreExecutionWorktree`, so this sweep cannot diverge from the event-driven path
209029
+ that runs on the move itself.
209030
+ */
209031
+ async reconcilePreExecutionWorktrees() {
209032
+ try {
209033
+ const release2 = this.options.releasePreExecutionWorktree;
209034
+ if (!release2) return 0;
209035
+ const settings = await this.store.getSettings();
209036
+ if (settings.globalPause || settings.enginePaused) return 0;
209037
+ const now = Date.now();
209038
+ const parked = await this.store.listTasks({ slim: true });
209039
+ const candidates = parked.filter((task) => {
209040
+ if (!task.worktree || task.deletedAt) return false;
209041
+ if (task.firstExecutionAt || task.executionStartedAt) return false;
209042
+ if (task.column === "todo" || task.column === "in-progress" || task.column === "in-review" || task.column === "done") return false;
209043
+ if (task.paused || task.userPaused) return false;
209044
+ if (task.status != null) return false;
209045
+ if (task.blockedBy || task.overlapBlockedBy || task.nextRecoveryAt) return false;
209046
+ const lastTouchedMs = Math.max(
209047
+ Date.parse(task.columnMovedAt ?? "") || 0,
209048
+ Date.parse(task.updatedAt ?? "") || 0
209049
+ );
209050
+ if (!lastTouchedMs) return false;
209051
+ return now - lastTouchedMs >= PRE_EXECUTION_WORKTREE_MAX_IDLE_MS;
209052
+ });
209053
+ if (candidates.length === 0) return 0;
209054
+ let released = 0;
209055
+ for (const task of candidates) {
209056
+ try {
209057
+ if (await release2(task.id, `parked pre-execution in '${task.column}'`)) released++;
209058
+ } catch (err) {
209059
+ log28.warn(`reconcilePreExecutionWorktrees: release failed for ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
209060
+ }
209061
+ }
209062
+ if (released > 0) log28.log(`reconcilePreExecutionWorktrees: released ${released} pre-execution worktree(s)`);
209063
+ return released;
209064
+ } catch (err) {
209065
+ log28.error(`reconcilePreExecutionWorktrees sweep failed: ${err instanceof Error ? err.message : String(err)}`);
209066
+ return 0;
209067
+ }
209068
+ }
208977
209069
  /*
208978
209070
  FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD4 — per-repo worktree cleanup from STORED paths):
208979
209071
  For done/dead workspace tasks, remove each recorded per-repo worktree. The paths are ADDRESSABLE
@@ -212972,6 +213064,33 @@ var init_triage = __esm({
212972
213064
  if (this.processing.has(task.id) || this.hasLivePlanningWork(task.id)) return;
212973
213065
  this.requestImmediatePoll();
212974
213066
  };
213067
+ this.taskEvacuatedFromPlanningHandler = (task) => {
213068
+ if (!task?.id) return;
213069
+ if (typeof task.column !== "string") return;
213070
+ if (task.column === "todo" || task.column === "triage" || task.column === "in-progress") return;
213071
+ if (this.activeSubagentSessions.has(task.id)) {
213072
+ this.disposeSubagentsForTask(task.id, `task moved to ${task.column}`);
213073
+ }
213074
+ const session = this.activeSessions.get(task.id);
213075
+ if (!session) return;
213076
+ planLog.log(`task moved out of planning to '${task.column}' \u2014 terminating triage session for ${task.id}`);
213077
+ this.pauseAborted.add(task.id);
213078
+ this.options.stuckTaskDetector?.untrackTask(task.id);
213079
+ const sessionWithAbort = session;
213080
+ if (typeof sessionWithAbort.abort === "function") {
213081
+ void sessionWithAbort.abort().catch((err) => {
213082
+ planLog.warn(`Failed to abort triage session for ${task.id}: ${err}`);
213083
+ });
213084
+ }
213085
+ this.recordTriageSessionTokenUsageSoon(task.id, session, { agentId: task.assignedAgentId ?? "triage" });
213086
+ session.dispose();
213087
+ this.activeSessions.delete(task.id);
213088
+ if (task.status === "planning") {
213089
+ void Promise.resolve(this.store.updateTask(task.id, { status: null })).catch((err) => {
213090
+ planLog.warn(`${task.id}: failed to clear planning status after evacuation: ${err instanceof Error ? err.message : String(err)}`);
213091
+ });
213092
+ }
213093
+ };
212975
213094
  }
212976
213095
  running = false;
212977
213096
  polling = false;
@@ -213030,6 +213149,8 @@ var init_triage = __esm({
213030
213149
  taskPausedHandler;
213031
213150
  /** FNXC:CodingIdeasWorkflow 2026-07-25-11:20: store-event wake for planning-eligible columns. */
213032
213151
  taskColumnWakeHandler;
213152
+ /** FNXC:PlanningEvacuation 2026-07-25-23:00: stops planning when a card leaves the planner lanes. */
213153
+ taskEvacuatedFromPlanningHandler;
213033
213154
  _approvalRequestStore;
213034
213155
  /**
213035
213156
  * @param store — Task store instance (also used to listen for `settings:updated` events)
@@ -213138,6 +213259,9 @@ var init_triage = __esm({
213138
213259
  this.store.on("task:updated", this.taskColumnWakeHandler);
213139
213260
  this.store.on("task:created", this.taskColumnWakeHandler);
213140
213261
  }
213262
+ if (this.taskEvacuatedFromPlanningHandler && typeof this.store.on === "function") {
213263
+ this.store.on("task:updated", this.taskEvacuatedFromPlanningHandler);
213264
+ }
213141
213265
  this.clearStaleSpecifyingStatuses().catch((err) => {
213142
213266
  planLog.error("Failed to clear stale planning statuses:", err);
213143
213267
  });
@@ -213185,6 +213309,9 @@ var init_triage = __esm({
213185
213309
  this.store.off("task:updated", this.taskColumnWakeHandler);
213186
213310
  this.store.off("task:created", this.taskColumnWakeHandler);
213187
213311
  }
213312
+ if (this.taskEvacuatedFromPlanningHandler && typeof this.store.off === "function") {
213313
+ this.store.off("task:updated", this.taskEvacuatedFromPlanningHandler);
213314
+ }
213188
213315
  this.abortAndDisposeActiveSessions("engine stop");
213189
213316
  planLog.log("Processor stopped");
213190
213317
  }
@@ -213976,11 +214103,15 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
213976
214103
  planningModel.modelId,
213977
214104
  assignedAgent?.runtimeConfig
213978
214105
  ) : { provider: void 0, modelId: void 0 };
214106
+ const planningCwd = await this.options.acquirePlanningWorktree?.(task.id).catch(() => null) || this.rootDir;
214107
+ if (planningCwd !== this.rootDir) {
214108
+ await this.store.logEntry(task.id, `Planning session running in task worktree ${planningCwd}`).catch(() => void 0);
214109
+ }
213979
214110
  const { session } = await createResolvedAgentSession({
213980
214111
  sessionPurpose: "triage",
213981
214112
  runtimeHint: triageRuntimeHint,
213982
214113
  pluginRunner: this.options.pluginRunner,
213983
- cwd: this.rootDir,
214114
+ cwd: planningCwd,
213984
214115
  systemPrompt: triageSystemPromptFinal,
213985
214116
  systemPromptLayers: triageLayers,
213986
214117
  tools: "coding",
@@ -216441,12 +216572,13 @@ function recoverPassedPlanReviewFromLatestLog(task) {
216441
216572
  completedAt: latest.timestamp
216442
216573
  };
216443
216574
  }
216444
- var PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE, PLAN_REVIEW_LEASE_HELD_VALUE, WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY, WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY, WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY, WORKFLOW_NODE_ENGINE_PAUSE_ABORT_KIND, WORKFLOW_DRIFT_PARK_CONTEXT_KEY, WorkflowGraphSuspended, MERGE_REGION_KINDS, WorkflowGraphExecutor;
216575
+ var PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE, PLAN_REVIEW_LEASE_HELD_VALUE, SESSION_CONTENTION_HOLD_VALUE, WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY, WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY, WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY, WORKFLOW_NODE_ENGINE_PAUSE_ABORT_KIND, WORKFLOW_DRIFT_PARK_CONTEXT_KEY, WorkflowGraphSuspended, MERGE_REGION_KINDS, WorkflowGraphExecutor;
216445
216576
  var init_workflow_graph_executor = __esm({
216446
216577
  "../engine/src/workflow-graph-executor.ts"() {
216447
216578
  "use strict";
216448
216579
  init_src2();
216449
216580
  init_transient_error_detector();
216581
+ init_transient_error_patterns();
216450
216582
  init_required_workflow_artifacts();
216451
216583
  init_workflow_node_handlers();
216452
216584
  init_workflow_graph_branches();
@@ -216455,6 +216587,7 @@ var init_workflow_graph_executor = __esm({
216455
216587
  init_workflow_node_execution_needs();
216456
216588
  PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE = "plan-review-provider-failure-hold";
216457
216589
  PLAN_REVIEW_LEASE_HELD_VALUE = "plan-review-lease-held";
216590
+ SESSION_CONTENTION_HOLD_VALUE = "session-contention-hold";
216458
216591
  WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY = "workflow:interruptedNodeId";
216459
216592
  WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY = "workflow:interruptedNodeAbortKind";
216460
216593
  WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY = "workflow:optionalGroupActive";
@@ -216774,6 +216907,15 @@ var init_workflow_graph_executor = __esm({
216774
216907
  };
216775
216908
  context[`node:${node.id}:outcome`] = result3.outcome;
216776
216909
  if (result3.value !== void 0) context[`node:${node.id}:value`] = result3.value;
216910
+ const sessionContentionFailure = stepStatus === "failed" && (verdictRaw === SESSION_CONTENTION_HOLD_VALUE || isSessionContentionError(stepOutput ?? stepNotes ?? ""));
216911
+ if (sessionContentionFailure) {
216912
+ this.deps.logTaskEntry?.(
216913
+ `${logPrefix} ${groupName} is waiting on another task to release its session path \u2014 holding in place`
216914
+ );
216915
+ context[`node:${node.id}:outcome`] = "failure";
216916
+ context[`node:${node.id}:value`] = SESSION_CONTENTION_HOLD_VALUE;
216917
+ return { outcome: "failure", value: SESSION_CONTENTION_HOLD_VALUE };
216918
+ }
216777
216919
  const nonPlanDefectPlanReviewFailure = node.id === PLAN_REVIEW_GROUP_ID && stepStatus === "failed" && (isRequiredArtifactReadFailedValue(verdictRaw) || isNonPlanDefectPlanReviewFailure({
216778
216920
  verdict,
216779
216921
  errorMessage: stepOutput ?? stepNotes,
@@ -217157,6 +217299,7 @@ var init_workflow_graph_executor = __esm({
217157
217299
  } catch (error3) {
217158
217300
  if (signal?.aborted) return this.withEnginePauseAbortContext(node, { outcome: "failure", value: "aborted" });
217159
217301
  lastError = error3;
217302
+ if (isSessionContentionError(error3 instanceof Error ? error3.message : String(error3))) break;
217160
217303
  }
217161
217304
  }
217162
217305
  if (signal?.aborted) {
@@ -217175,11 +217318,13 @@ var init_workflow_graph_executor = __esm({
217175
217318
  }
217176
217319
  return degraded;
217177
217320
  }
217321
+ const lastErrorText = lastError instanceof Error ? lastError.message : String(lastError);
217178
217322
  const failureResult = {
217179
217323
  outcome: "failure",
217180
- value: "exception",
217324
+ // FNXC:SessionContention 2026-07-25-21:30: contention is a retryable hold, not an exception.
217325
+ value: isSessionContentionError(lastErrorText) ? SESSION_CONTENTION_HOLD_VALUE : "exception",
217181
217326
  contextPatch: {
217182
- [`node:${node.id}:error`]: lastError instanceof Error ? lastError.message : String(lastError)
217327
+ [`node:${node.id}:error`]: lastErrorText
217183
217328
  }
217184
217329
  };
217185
217330
  if (recordProgress && this.shouldRecordNodeProgress(node)) {
@@ -222507,7 +222652,7 @@ function preservePreExecutionWorkflowStepResults(task) {
222507
222652
  }
222508
222653
  ];
222509
222654
  }
222510
- var execAsync35, execFileAsync19, WORKFLOW_THINKING_LEVEL_SET, yieldEventLoop3, tokenCacheMetricsLog, OPTIONAL_STEP_REVISION_KEY_MARKER2, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, MAX_TRANSIENT_GRAPH_RESUME_RETRIES, TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, LOOP_COMPACTION_TIMEOUT_MS, TASK_DONE_REFUSAL_SUFFIX, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, USER_QUESTION_TOOL_NAMES, FUSION_WORKFLOW_STEP_CONVENTIONS_PREAMBLE, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
222655
+ var execAsync35, execFileAsync19, WORKFLOW_THINKING_LEVEL_SET, yieldEventLoop3, tokenCacheMetricsLog, OPTIONAL_STEP_REVISION_KEY_MARKER2, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, MAX_TRANSIENT_GRAPH_RESUME_RETRIES, TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS, MAX_SESSION_CONTENTION_HOLD_RETRIES, SESSION_CONTENTION_HOLD_BACKOFF_MS, SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, LOOP_COMPACTION_TIMEOUT_MS, TASK_DONE_REFUSAL_SUFFIX, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, USER_QUESTION_TOOL_NAMES, FUSION_WORKFLOW_STEP_CONVENTIONS_PREAMBLE, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
222511
222656
  var init_executor = __esm({
222512
222657
  "../engine/src/executor.ts"() {
222513
222658
  "use strict";
@@ -222620,6 +222765,9 @@ var init_executor = __esm({
222620
222765
  MAX_TASK_DONE_REQUEUE_RETRIES = 3;
222621
222766
  MAX_TRANSIENT_GRAPH_RESUME_RETRIES = 2;
222622
222767
  TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 1e3;
222768
+ MAX_SESSION_CONTENTION_HOLD_RETRIES = 10;
222769
+ SESSION_CONTENTION_HOLD_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 5e3;
222770
+ SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS = 6e4;
222623
222771
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
222624
222772
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
222625
222773
  LOOP_COMPACTION_TIMEOUT_MS = 6e4;
@@ -222980,6 +223128,15 @@ The tool prevents your session from being killed by the inactivity watchdog duri
222980
223128
  }
222981
223129
  })
222982
223130
  );
223131
+ } else if ((from === "todo" || from === "triage") && to2 !== "in-progress" && to2 !== "in-review" && to2 !== "done") {
223132
+ this.trackTaskDisposal(
223133
+ task.id,
223134
+ this.awaitAbortInFlightTaskWork(task.id, `task moved out of planning to ${to2}`, {
223135
+ userCanceled: source === "user"
223136
+ }).then(async () => {
223137
+ await this.releasePreExecutionWorktree(task.id, `moved to ${to2}`);
223138
+ })
223139
+ );
222983
223140
  } else if (from === "in-progress") {
222984
223141
  if (this.workflowLifecycleMovesInFlight.has(task.id) && this.graphRouting.has(task.id)) {
222985
223142
  executorLog.log(
@@ -223569,15 +223726,63 @@ The tool prevents your session from being killed by the inactivity watchdog duri
223569
223726
  getActiveWorktreePaths() consumers that cd into a path are unaffected; only the registry key changes.
223570
223727
  Non-workspace tasks (unique worktree path != rootDir) are returned unchanged.
223571
223728
  */
223729
+ /*
223730
+ FNXC:PlanReviewWorktree 2026-07-25-20:40 (concurrent root-rooted step sessions — single-repo collision):
223731
+ The task-scoped key must apply to the shared repo root in EVERY project mode, not only workspace mode.
223732
+ Read-only graph nodes that need no worktree (Plan Review is the canonical one — it reviews the
223733
+ store-injected PROMPT.md, see FNXC:PlanReviewSpecInjection) run rooted at `this.rootDir`, and a todo
223734
+ task has no worktree of its own. With the bare root as the registry key, two tasks reaching Plan Review
223735
+ at the same time collided: the second failed with "active-session path <root> is held by task <other>;
223736
+ task <self> may not overwrite it", which surfaced as a Plan Review provider failure, burned the
223737
+ in-place retry budget against a hold that retrying can never clear, and left the task parked
223738
+ (reported: FN-1398 holding /home/ubuntu/dev/freemap-svelte while FN-1403 planned).
223739
+ Path-exclusivity on the shared root is not what keeps these sessions correct: write-capable nodes are
223740
+ refused at the root outright (no-worktree-for-write-node above), real per-sub-repo exclusivity is the
223741
+ workspace-repo-acquire lease, and every isPathActive consumer guards removable WORKTREE paths — the
223742
+ root is never one. Liveness still works because the synthetic key stays in the registry under the task.
223743
+ */
223572
223744
  sessionRegistryPath(taskId, worktreePath) {
223573
- if (this.workspaceConfig && worktreePath === this.rootDir) {
223745
+ if (worktreePath === this.rootDir) {
223574
223746
  return `${worktreePath}#session:${taskId}`;
223575
223747
  }
223576
223748
  return worktreePath;
223577
223749
  }
223750
+ /*
223751
+ FNXC:SessionContention 2026-07-25-21:30 (contention prevention at the registration seam):
223752
+ Every executor session registration goes through `acquireActiveSessionPath` instead of the raw
223753
+ `registerPath`, so a LEAKED entry owned by a task with no live session surface in this process is
223754
+ RECLAIMED rather than throwing at the newcomer. That closes the second contention class (a dead
223755
+ holder can never release, so waiting on it is waiting forever). A genuinely live holder still throws
223756
+ the typed error — that case is real serialization, and callers classify it as a retryable contention
223757
+ hold (SESSION_CONTENTION_HOLD_VALUE), never as a provider/model failure.
223758
+ The probe reports LIVE on any uncertainty: an unknown holder with a fresh entry is treated as live by
223759
+ the staleness floor, so the reclaim only ever fires on proven-dead, aged entries.
223760
+ */
223761
+ acquireSessionRegistryPath(taskId, registryPath, kind, ownerKey) {
223762
+ const outcome = acquireActiveSessionPath(activeSessionRegistry, registryPath, { taskId, kind, ownerKey }, {
223763
+ holderLiveProbe: (holderTaskId) => this.hasLiveTaskSessionSurface(holderTaskId) || executingTaskLock.has(holderTaskId)
223764
+ });
223765
+ if (outcome.action === "contended") {
223766
+ throw new ActiveSessionPathHeldByForeignTaskError(registryPath, outcome.holderTaskId, taskId);
223767
+ }
223768
+ if (outcome.action === "reclaimed-stale-foreign") {
223769
+ executorLog.warn(
223770
+ `${taskId}: reclaimed a stale active-session entry on ${registryPath} from dead task ${outcome.holderTaskId} (idle ${outcome.ageMs}ms)`
223771
+ );
223772
+ void this.store.recordRunAuditEvent?.({
223773
+ taskId,
223774
+ agentId: "executor",
223775
+ runId: generateSyntheticRunId("session-path-reclaim", taskId),
223776
+ domain: "database",
223777
+ mutationType: "session:reclaim-stale-foreign-path",
223778
+ target: taskId,
223779
+ metadata: { taskId, holderTaskId: outcome.holderTaskId, kind, ageMs: outcome.ageMs }
223780
+ })?.catch?.(() => void 0);
223781
+ }
223782
+ }
223578
223783
  setActiveSession(taskId, sessionState, worktreePath) {
223579
223784
  this.activeSessions.set(taskId, sessionState);
223580
- activeSessionRegistry.registerPath(this.sessionRegistryPath(taskId, worktreePath), { taskId, kind: "executor", ownerKey: taskId });
223785
+ this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "executor", taskId);
223581
223786
  }
223582
223787
  markGraphExecuteSelfRequeued(taskId) {
223583
223788
  if (this.graphRouting.has(taskId)) {
@@ -223595,7 +223800,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
223595
223800
  setActiveStepExecutor(taskId, stepExecutor, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
223596
223801
  this.activeStepExecutors.set(taskId, stepExecutor);
223597
223802
  this.activeStepExecutorSeenSteeringIds.set(taskId, seenSteeringIds);
223598
- activeSessionRegistry.registerPath(this.sessionRegistryPath(taskId, worktreePath), { taskId, kind: "step-session", ownerKey: `${taskId}#step-session` });
223803
+ this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "step-session", `${taskId}#step-session`);
223599
223804
  }
223600
223805
  deleteActiveStepExecutor(taskId, worktreePath) {
223601
223806
  this.activeStepExecutors.delete(taskId);
@@ -223609,7 +223814,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
223609
223814
  setActiveWorkflowStepSession(taskId, session, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
223610
223815
  this.activeWorkflowStepSessions.set(taskId, session);
223611
223816
  this.activeWorkflowStepSessionSeenSteeringIds.set(taskId, seenSteeringIds);
223612
- activeSessionRegistry.registerPath(this.sessionRegistryPath(taskId, worktreePath), { taskId, kind: "workflow-step", ownerKey: `${taskId}#workflow-step` });
223817
+ this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "workflow-step", `${taskId}#workflow-step`);
223613
223818
  }
223614
223819
  deleteActiveWorkflowStepSession(taskId, worktreePath) {
223615
223820
  this.activeWorkflowStepSessions.delete(taskId);
@@ -227928,6 +228133,87 @@ ${info.feedback}`, revisionKey),
227928
228133
  this.unregisterConfiguredCommandController(task.id, commandAbortController);
227929
228134
  }
227930
228135
  }
228136
+ /*
228137
+ FNXC:NodeWorktreeIsolation 2026-07-25-22:10 (planning acquires the task worktree):
228138
+ Public seam for the planning/triage lane. Specification runs a CODING-tool session; pointing it at
228139
+ the shared main checkout meant every planning agent had write tools in the operator's tree and every
228140
+ concurrent planner shared one path. Acquire the task's own worktree up front and let the whole
228141
+ lifecycle — planning, Plan Review, implementation, code review — reuse that single worktree.
228142
+ Returns null (caller falls back to the root, unchanged behavior) when the project is a workspace, or
228143
+ when acquisition fails: planning must never be blocked by a worktree problem.
228144
+ */
228145
+ /*
228146
+ FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree release):
228147
+ Planning now acquires a worktree, so a card that never reaches execution — withdrawn to Ideas,
228148
+ archived from a planner lane, or parked pre-execution — would otherwise hold one forever. Release
228149
+ it. Safety conditions, all required:
228150
+ - the task never executed (`firstExecutionAt`/`executionStartedAt` unset): execution evidence means
228151
+ the worktree may hold real work, and only the normal merge/archive lifecycle may remove it;
228152
+ - no live session registered on the path (the same isPathActive guard the other sweeps use);
228153
+ - the branch carries no commits beyond its base — planning writes its spec to the task store, not
228154
+ the worktree, so a clean branch means there is genuinely nothing to lose.
228155
+ Metadata (`worktree`/`branch`) is cleared with it, so a later promotion re-acquires cleanly.
228156
+ Fail-soft throughout: a cleanup problem must never block the lifecycle move that triggered it.
228157
+ */
228158
+ async releasePreExecutionWorktree(taskId, reason) {
228159
+ try {
228160
+ const live = await this.store.getTask(taskId);
228161
+ if (!live?.worktree) return false;
228162
+ if (live.firstExecutionAt || live.executionStartedAt) return false;
228163
+ if (activeSessionRegistry.isPathActive(live.worktree) || activeSessionRegistry.isPathActive(resolvePath3(live.worktree))) return false;
228164
+ if (this.hasLiveTaskSessionSurface(taskId) || executingTaskLock.has(taskId)) return false;
228165
+ if (existsSync65(live.worktree)) {
228166
+ if (await this.preExecutionWorktreeHasWork(live.worktree)) {
228167
+ executorLog.log(`${taskId}: keeping pre-execution worktree ${live.worktree} \u2014 it carries commits or uncommitted changes`);
228168
+ return false;
228169
+ }
228170
+ const settings = await this.store.getSettings();
228171
+ await removeWorktree({
228172
+ rootDir: this.rootDir,
228173
+ worktreePath: live.worktree,
228174
+ settings,
228175
+ taskId,
228176
+ reason: RemovalReason.SelfHealingReclaim
228177
+ });
228178
+ }
228179
+ this.activeWorktrees.get(taskId)?.delete(live.worktree);
228180
+ await this.store.updateTask(taskId, { worktree: null, branch: null, baseCommitSha: null, sessionFile: null }, this.getRunContextFor(taskId));
228181
+ 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);
228182
+ executorLog.log(`${taskId}: released pre-execution worktree ${live.worktree} (${reason})`);
228183
+ return true;
228184
+ } catch (error3) {
228185
+ executorLog.warn(`${taskId}: could not release the pre-execution worktree: ${formatError(error3).message}`);
228186
+ return false;
228187
+ }
228188
+ }
228189
+ /** True when a pre-execution worktree holds commits past its base or any uncommitted change. */
228190
+ async preExecutionWorktreeHasWork(worktreePath) {
228191
+ try {
228192
+ const { stdout: dirty } = await execFileAsync19("git", ["status", "--porcelain"], { cwd: worktreePath, timeout: 3e4 });
228193
+ if (dirty.trim()) return true;
228194
+ 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 }));
228195
+ return Boolean(ahead.trim());
228196
+ } catch {
228197
+ return true;
228198
+ }
228199
+ }
228200
+ async ensureTaskWorktreeForPlanning(taskId) {
228201
+ try {
228202
+ if (this.workspaceConfig === void 0) {
228203
+ this.workspaceConfig = await loadWorkspaceConfig(this.rootDir);
228204
+ }
228205
+ if (this.workspaceConfig && (this.workspaceConfig.repos.length ?? 0) > 0) return null;
228206
+ const live = await this.store.getTask(taskId);
228207
+ if (live.worktree && existsSync65(live.worktree)) return live.worktree;
228208
+ const settings = await this.store.getSettings();
228209
+ const acquisitionTask = live.worktree ? { ...live, worktree: void 0, sessionFile: void 0 } : live;
228210
+ const acquired = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, "planning");
228211
+ return acquired.worktree || null;
228212
+ } catch (error3) {
228213
+ executorLog.warn(`${taskId}: could not acquire a planning worktree \u2014 planning falls back to the repo root: ${formatError(error3)}`);
228214
+ return null;
228215
+ }
228216
+ }
227931
228217
  async prepareGraphNodeExecution(node, nodeTask, settings, requirement) {
227932
228218
  if (!requirement.requiresWorktree) return;
227933
228219
  const live = await this.store.getTask(nodeTask.id);
@@ -228054,22 +228340,29 @@ ${info.feedback}`, revisionKey),
228054
228340
  optionalGroupId,
228055
228341
  reviewerInlineFixes: settings.reviewerInlineFixes
228056
228342
  });
228057
- const executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
228058
- if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
228059
- return { outcome: "failure", value: "no-worktree-for-write-node" };
228060
- }
228343
+ let executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
228061
228344
  const nodeDisplayName = typeof cfg.name === "string" && cfg.name.trim() ? cfg.name.trim() : node.id;
228062
228345
  const isPlanReviewNode = node.id === "plan-review-step" || nodeDisplayName === "Plan Review" || optionalGroupId === "plan-review";
228063
- let worktreePath = executionTarget.worktree || this.rootDir;
228064
- if (isPlanReviewNode && !writeCapable && executionTarget.worktree && !existsSync65(executionTarget.worktree)) {
228065
- await this.store.logEntry(
228066
- live.id,
228067
- `Plan Review worktree ${executionTarget.worktree} is missing on disk \u2014 running the reviewer from the repo root (spec is store-injected)`,
228068
- void 0,
228069
- this.getRunContextFor(live.id)
228070
- );
228071
- worktreePath = this.rootDir;
228346
+ if (!this.workspaceConfig) {
228347
+ const recordedWorktreeMissing = Boolean(executionTarget.worktree) && !existsSync65(executionTarget.worktree);
228348
+ const shouldAcquire = !executionTarget.worktree || recordedWorktreeMissing && isPlanReviewNode;
228349
+ if (shouldAcquire) {
228350
+ if (recordedWorktreeMissing) {
228351
+ await this.store.logEntry(
228352
+ live.id,
228353
+ `Plan Review worktree ${executionTarget.worktree} is missing on disk \u2014 re-acquiring a task worktree instead of running in the shared checkout`,
228354
+ void 0,
228355
+ this.getRunContextFor(live.id)
228356
+ );
228357
+ }
228358
+ const acquisitionTask = recordedWorktreeMissing ? { ...executionTarget, worktree: void 0, sessionFile: void 0 } : executionTarget;
228359
+ executionTarget = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, node.id);
228360
+ }
228072
228361
  }
228362
+ if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
228363
+ return { outcome: "failure", value: "no-worktree-for-write-node" };
228364
+ }
228365
+ const worktreePath = executionTarget.worktree || this.rootDir;
228073
228366
  let prompt = typeof cfg.prompt === "string" ? cfg.prompt : "";
228074
228367
  let modelProvider = typeof cfg.modelProvider === "string" && cfg.modelProvider.trim() ? cfg.modelProvider : void 0;
228075
228368
  let modelId = typeof cfg.modelId === "string" && cfg.modelId.trim() ? cfg.modelId : void 0;
@@ -228366,6 +228659,77 @@ ${prompt}`;
228366
228659
  const latestAction = live.log.at(-1)?.action;
228367
228660
  return latestAction === "Resumed after engine restart" || latestAction === "Resuming execution after unpause";
228368
228661
  }
228662
+ /*
228663
+ FNXC:SessionContention 2026-07-25-21:30:
228664
+ Two ways in, because contention must never slip through to a park:
228665
+ - the typed failure value the graph now publishes (SESSION_CONTENTION_HOLD_VALUE), and
228666
+ - a message-shape fallback over the run's `:error` context patches, so contention arriving from a
228667
+ path that has not been taught the typed value is still recognized.
228668
+ */
228669
+ graphFailureErrorTexts(result2) {
228670
+ if (!result2.context) return [];
228671
+ const texts = [];
228672
+ for (const [key, value] of Object.entries(result2.context)) {
228673
+ if (key.endsWith(":error") && typeof value === "string" && value.trim()) texts.push(value);
228674
+ }
228675
+ return texts;
228676
+ }
228677
+ isSessionContentionGraphFailure(result2) {
228678
+ if (this.graphFailureValue(result2) === SESSION_CONTENTION_HOLD_VALUE) return true;
228679
+ return this.graphFailureErrorTexts(result2).some((text2) => isSessionContentionError(text2));
228680
+ }
228681
+ /*
228682
+ FNXC:SessionContention 2026-07-25-21:30 (self-recovering wait — the task is never parked):
228683
+ Retry the graph in place on an exponential backoff while the holder finishes. The counter is
228684
+ IN-MEMORY on purpose: it needs no schema change, and an engine restart resetting it is the desired
228685
+ behavior (a restart also drops the in-process registry, so the contention is gone anyway).
228686
+ When the ladder is exhausted the task is left cleanly dispatchable — status/error cleared, progress
228687
+ untouched — so ordinary scheduling picks it up later with a fresh budget. There is no terminal branch
228688
+ here by design: lease contention always ends (the holder finishes, or self-healing sweeps it), so
228689
+ parking the task would only require a human to press Retry on a condition that fixed itself.
228690
+ */
228691
+ sessionContentionHoldAttempts = /* @__PURE__ */ new Map();
228692
+ clearSessionContentionHold(taskId) {
228693
+ this.sessionContentionHoldAttempts.delete(taskId);
228694
+ }
228695
+ async holdForSessionContention(task, live, result2) {
228696
+ const detail = this.graphFailureErrorTexts(result2).find((text2) => isSessionContentionError(text2));
228697
+ const priorAttempts = this.sessionContentionHoldAttempts.get(task.id) ?? 0;
228698
+ const attempt = priorAttempts + 1;
228699
+ if (attempt > MAX_SESSION_CONTENTION_HOLD_RETRIES) {
228700
+ this.clearSessionContentionHold(task.id);
228701
+ 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}` : ""}`;
228702
+ executorLog.warn(`${task.id}: ${message2}`);
228703
+ await this.store.logEntry(task.id, message2, void 0, this.getRunContextFor(task.id));
228704
+ if (live.status != null || live.error != null) {
228705
+ await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
228706
+ }
228707
+ return;
228708
+ }
228709
+ this.sessionContentionHoldAttempts.set(task.id, attempt);
228710
+ const message = `Waiting on another task to release a shared session path \u2014 retrying in place (${attempt}/${MAX_SESSION_CONTENTION_HOLD_RETRIES})${detail ? `: ${detail}` : ""}`;
228711
+ executorLog.warn(`${task.id}: ${message}`);
228712
+ await this.store.logEntry(task.id, message, void 0, this.getRunContextFor(task.id));
228713
+ if (live.status != null || live.error != null) {
228714
+ await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
228715
+ }
228716
+ 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));
228717
+ const scheduleRetry = () => {
228718
+ void (async () => {
228719
+ try {
228720
+ const resume = await this.store.getTask(task.id);
228721
+ if (!resume || resume.deletedAt || resume.paused || resume.userPaused) {
228722
+ this.clearSessionContentionHold(task.id);
228723
+ return;
228724
+ }
228725
+ await this.execute(resume);
228726
+ } catch (err) {
228727
+ executorLog.error(`Failed session-contention retry for ${task.id}:`, err);
228728
+ }
228729
+ })();
228730
+ };
228731
+ setTimeout(scheduleRetry, delayMs).unref?.();
228732
+ }
228369
228733
  graphFailureValue(result2) {
228370
228734
  const failedNode = result2.visitedNodeIds[result2.visitedNodeIds.length - 1];
228371
228735
  if (!failedNode || !result2.context) return void 0;
@@ -228957,6 +229321,11 @@ Graph value: ${failureValue}` : ""}`, budget.key),
228957
229321
  await this.persistTokenUsage(task.id);
228958
229322
  return;
228959
229323
  }
229324
+ if (this.isSessionContentionGraphFailure(result2)) {
229325
+ await this.holdForSessionContention(task, live, result2);
229326
+ await this.persistTokenUsage(task.id);
229327
+ return;
229328
+ }
228960
229329
  if (await this.routeUnusableWorktreeGraphFailureToRecovery(task, live, result2)) {
228961
229330
  await this.persistTokenUsage(task.id);
228962
229331
  return;
@@ -253709,6 +254078,9 @@ var init_in_process_runtime = __esm({
253709
254078
  usageLimitPauser: this.usageLimitPauser,
253710
254079
  agentStore: this.agentStore,
253711
254080
  pluginRunner: this.pluginRunner,
254081
+ // FNXC:NodeWorktreeIsolation 2026-07-25-22:10: planning acquires (or reuses) the task's own
254082
+ // worktree through the executor's acquisition path, so no lane runs in the shared checkout.
254083
+ acquirePlanningWorktree: (taskId) => this.executor.ensureTaskWorktreeForPlanning(taskId),
253712
254084
  onSpecifyStart: (t) => {
253713
254085
  this.recordActivity();
253714
254086
  runtimeLog.log(`Specifying ${t.id}...`);
@@ -253797,6 +254169,8 @@ var init_in_process_runtime = __esm({
253797
254169
  getExecutingTaskIds: () => this.executor?.getExecutingTaskIds() ?? /* @__PURE__ */ new Set(),
253798
254170
  clearPhantomExecutorBinding: (taskId, options) => this.executor?.clearPhantomExecutorBinding(taskId, options),
253799
254171
  listWorktreeHolders: () => this.executor?.listWorktreeHolders() ?? [],
254172
+ // FNXC:PlanningEvacuation 2026-07-25-23:00: the executor owns the release safety conditions.
254173
+ releasePreExecutionWorktree: (taskId, reason) => this.executor?.releasePreExecutionWorktree(taskId, reason) ?? Promise.resolve(false),
253800
254174
  recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
253801
254175
  getPlanningTaskIds: () => this.triageProcessor?.getPlanningTaskIds() ?? /* @__PURE__ */ new Set(),
253802
254176
  // FNXC:TaskTiming 2026-08-01-12:00: orphan planning recovery must defer