@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/bin.js CHANGED
@@ -139330,6 +139330,12 @@ var init_usage_limit_detector = __esm({
139330
139330
  });
139331
139331
 
139332
139332
  // ../engine/src/transient-error-patterns.ts
139333
+ function isSessionContentionError(errorMessage4) {
139334
+ if (!errorMessage4 || typeof errorMessage4 !== "string") {
139335
+ return false;
139336
+ }
139337
+ return SESSION_CONTENTION_PATTERNS.some((pattern) => pattern.test(errorMessage4));
139338
+ }
139333
139339
  function isTransientError(errorMessage4) {
139334
139340
  if (!errorMessage4 || typeof errorMessage4 !== "string") {
139335
139341
  return false;
@@ -139337,6 +139343,9 @@ function isTransientError(errorMessage4) {
139337
139343
  if (isTransientAuthCredentialError(errorMessage4)) {
139338
139344
  return true;
139339
139345
  }
139346
+ if (isSessionContentionError(errorMessage4)) {
139347
+ return true;
139348
+ }
139340
139349
  return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage4));
139341
139350
  }
139342
139351
  function isTransientAuthCredentialError(errorMessage4) {
@@ -139348,7 +139357,7 @@ function isTransientAuthCredentialError(errorMessage4) {
139348
139357
  }
139349
139358
  return TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN.test(errorMessage4);
139350
139359
  }
139351
- var TRANSIENT_ERROR_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
139360
+ var TRANSIENT_ERROR_PATTERNS, SESSION_CONTENTION_PATTERNS, TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN, OPERATOR_ACTIONABLE_AUTH_EXCLUSION_PATTERN;
139352
139361
  var init_transient_error_patterns = __esm({
139353
139362
  "../engine/src/transient-error-patterns.ts"() {
139354
139363
  "use strict";
@@ -139409,6 +139418,14 @@ var init_transient_error_patterns = __esm({
139409
139418
  /\bACP failed to start\b/i,
139410
139419
  /\bACP session has no live connection\b/i
139411
139420
  ];
139421
+ SESSION_CONTENTION_PATTERNS = [
139422
+ // ActiveSessionPathHeldByForeignTaskError (active-session-registry.ts)
139423
+ /active-session path\s+\S+\s+is held by task\s+\S+/i,
139424
+ // WorkspaceRepoAcquireBusyError (worktree-acquisition.ts)
139425
+ /workspace sub-repo\s+\S+\s+acquisition is in progress for task\s+\S+/i,
139426
+ // WorkspaceRepoLandBusyError (merger-ai.ts)
139427
+ /workspace sub-repo\s+\S+\s+land is in progress for task\s+\S+/i
139428
+ ];
139412
139429
  TRANSIENT_AUTH_CREDENTIAL_ROTATION_PATTERN = /"type":\s*"authentication_error"|invalid authentication credentials|token[_\s]?expired/i;
139413
139430
  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;
139414
139431
  }
@@ -139417,6 +139434,9 @@ var init_transient_error_patterns = __esm({
139417
139434
  // ../engine/src/transient-error-detector.ts
139418
139435
  function isNonPlanDefectPlanReviewFailure(input) {
139419
139436
  if (input.verdict === "REVISE") return false;
139437
+ if (input.failureValue?.trim().toLowerCase() === "session-contention-hold" || (input.errorMessage ? isSessionContentionError(input.errorMessage) : false)) {
139438
+ return false;
139439
+ }
139420
139440
  const failureValue = input.failureValue?.trim().toLowerCase();
139421
139441
  if (failureValue === "exception" || failureValue === "aborted") return true;
139422
139442
  const errorMessage4 = input.errorMessage?.trim();
@@ -163871,6 +163891,23 @@ var init_worktrunk_installer = __esm({
163871
163891
  });
163872
163892
 
163873
163893
  // ../engine/src/active-session-registry.ts
163894
+ function acquireActiveSessionPath(registry3, path14, registration, options = {}) {
163895
+ const existing = registry3.lookupByPath(path14);
163896
+ if (!existing || existing.taskId === registration.taskId) {
163897
+ registry3.registerPath(path14, registration);
163898
+ return { action: "registered" };
163899
+ }
163900
+ const now = options.now?.() ?? Date.now();
163901
+ const ageMs = now - existing.registeredAt;
163902
+ const minIdleMs = options.minIdleMs ?? DEFAULT_SELF_OWNED_MIN_IDLE_MS;
163903
+ const holderIsLive = options.holderLiveProbe?.(existing.taskId, path14) ?? true;
163904
+ if (holderIsLive || ageMs < minIdleMs) {
163905
+ return { action: "contended", holderTaskId: existing.taskId, holderKind: existing.kind, ageMs };
163906
+ }
163907
+ registry3.unregisterPath(path14);
163908
+ registry3.registerPath(path14, registration);
163909
+ return { action: "reclaimed-stale-foreign", holderTaskId: existing.taskId, ageMs };
163910
+ }
163874
163911
  function reconcileSelfOwnedActiveSessionForRemoval(registry3, worktreePath, requestingTaskId, liveBindingProbe, options = {}) {
163875
163912
  const record2 = registry3.lookupByPath(worktreePath);
163876
163913
  if (!record2) {
@@ -174250,7 +174287,7 @@ function hasAdvancedPastPlanning(task) {
174250
174287
  if (task.column === "in-progress" || task.column === "in-review" || task.column === "done" || task.column === "archived") {
174251
174288
  return true;
174252
174289
  }
174253
- if (task.worktree != null) {
174290
+ if (task.firstExecutionAt != null || task.executionStartedAt != null) {
174254
174291
  return true;
174255
174292
  }
174256
174293
  if (task.column === "triage") {
@@ -203014,7 +203051,7 @@ function isNoTaskDoneFailure2(task) {
203014
203051
  function hasStepProgress2(task) {
203015
203052
  return task.steps.some((step) => step.status !== "pending");
203016
203053
  }
203017
- 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;
203054
+ 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;
203018
203055
  var init_self_healing = __esm({
203019
203056
  "../engine/src/self-healing.ts"() {
203020
203057
  "use strict";
@@ -203070,6 +203107,7 @@ var init_self_healing = __esm({
203070
203107
  DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS = 60 * 60 * 1e3;
203071
203108
  PHANTOM_EXECUTOR_BINDING_AGE_MULTIPLIER = 3;
203072
203109
  MAX_NO_PROGRESS_RESUME_ATTEMPTS = 2;
203110
+ PRE_EXECUTION_WORKTREE_MAX_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
203073
203111
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
203074
203112
  STARVED_REFINEMENT_RECOVERY_GRACE_MS = 10 * 6e4;
203075
203113
  STARVED_PEER_PROGRESS_THRESHOLD = 3;
@@ -204710,6 +204748,8 @@ var init_self_healing = __esm({
204710
204748
  { name: "reconcile-workspace-partial-lands", fn: () => this.reconcileWorkspacePartialLands() },
204711
204749
  { name: "reclaim-phantom-workspace-land-leases", fn: () => this.reclaimPhantomWorkspaceLandLeases() },
204712
204750
  { name: "reconcile-orphaned-workspace-worktrees", fn: () => this.reconcileOrphanedWorkspaceWorktrees() },
204751
+ // FNXC:PlanningEvacuation 2026-07-25-23:00: reclaim worktrees acquired at planning time by cards that never executed.
204752
+ { name: "reconcile-pre-execution-worktrees", fn: () => this.reconcilePreExecutionWorktrees() },
204713
204753
  { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
204714
204754
  { name: "recover-already-merged-review", fn: () => this.recoverAlreadyMergedReviewTasks() },
204715
204755
  { name: "recover-post-done-noncontinuable-wedge", fn: () => this.recoverPostDoneNonContinuableWedge() },
@@ -209736,6 +209776,58 @@ var init_self_healing = __esm({
209736
209776
  return 0;
209737
209777
  }
209738
209778
  }
209779
+ /*
209780
+ FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree sweep):
209781
+ Planning acquires the task's own worktree, so cards that never reach execution would accumulate
209782
+ worktrees on disk: withdrawn to an intake column, archived from a planner lane, or simply parked.
209783
+ This sweep reclaims them.
209784
+
209785
+ Candidates are addressed from task ROWS (never a directory walk — AGENTS.md forbids unbounded temp
209786
+ scans): tasks holding `worktree` that sit in a non-executing column and carry no execution
209787
+ timestamp. `todo` is deliberately EXCLUDED: a planned card waiting for a WIP slot legitimately
209788
+ keeps its worktree, and churning it would just force a re-acquire minutes later. Every real safety
209789
+ decision (live session, clean branch, execution evidence) belongs to
209790
+ `TaskExecutor.releasePreExecutionWorktree`, so this sweep cannot diverge from the event-driven path
209791
+ that runs on the move itself.
209792
+ */
209793
+ async reconcilePreExecutionWorktrees() {
209794
+ try {
209795
+ const release2 = this.options.releasePreExecutionWorktree;
209796
+ if (!release2) return 0;
209797
+ const settings = await this.store.getSettings();
209798
+ if (settings.globalPause || settings.enginePaused) return 0;
209799
+ const now = Date.now();
209800
+ const parked = await this.store.listTasks({ slim: true });
209801
+ const candidates = parked.filter((task) => {
209802
+ if (!task.worktree || task.deletedAt) return false;
209803
+ if (task.firstExecutionAt || task.executionStartedAt) return false;
209804
+ if (task.column === "todo" || task.column === "in-progress" || task.column === "in-review" || task.column === "done") return false;
209805
+ if (task.paused || task.userPaused) return false;
209806
+ if (task.status != null) return false;
209807
+ if (task.blockedBy || task.overlapBlockedBy || task.nextRecoveryAt) return false;
209808
+ const lastTouchedMs = Math.max(
209809
+ Date.parse(task.columnMovedAt ?? "") || 0,
209810
+ Date.parse(task.updatedAt ?? "") || 0
209811
+ );
209812
+ if (!lastTouchedMs) return false;
209813
+ return now - lastTouchedMs >= PRE_EXECUTION_WORKTREE_MAX_IDLE_MS;
209814
+ });
209815
+ if (candidates.length === 0) return 0;
209816
+ let released = 0;
209817
+ for (const task of candidates) {
209818
+ try {
209819
+ if (await release2(task.id, `parked pre-execution in '${task.column}'`)) released++;
209820
+ } catch (err) {
209821
+ log28.warn(`reconcilePreExecutionWorktrees: release failed for ${task.id}: ${err instanceof Error ? err.message : String(err)}`);
209822
+ }
209823
+ }
209824
+ if (released > 0) log28.log(`reconcilePreExecutionWorktrees: released ${released} pre-execution worktree(s)`);
209825
+ return released;
209826
+ } catch (err) {
209827
+ log28.error(`reconcilePreExecutionWorktrees sweep failed: ${err instanceof Error ? err.message : String(err)}`);
209828
+ return 0;
209829
+ }
209830
+ }
209739
209831
  /*
209740
209832
  FNXC:Workspace 2026-06-22-09:30 (Phase D U1, KTD4 — per-repo worktree cleanup from STORED paths):
209741
209833
  For done/dead workspace tasks, remove each recorded per-repo worktree. The paths are ADDRESSABLE
@@ -213734,6 +213826,33 @@ var init_triage = __esm({
213734
213826
  if (this.processing.has(task.id) || this.hasLivePlanningWork(task.id)) return;
213735
213827
  this.requestImmediatePoll();
213736
213828
  };
213829
+ this.taskEvacuatedFromPlanningHandler = (task) => {
213830
+ if (!task?.id) return;
213831
+ if (typeof task.column !== "string") return;
213832
+ if (task.column === "todo" || task.column === "triage" || task.column === "in-progress") return;
213833
+ if (this.activeSubagentSessions.has(task.id)) {
213834
+ this.disposeSubagentsForTask(task.id, `task moved to ${task.column}`);
213835
+ }
213836
+ const session = this.activeSessions.get(task.id);
213837
+ if (!session) return;
213838
+ planLog.log(`task moved out of planning to '${task.column}' \u2014 terminating triage session for ${task.id}`);
213839
+ this.pauseAborted.add(task.id);
213840
+ this.options.stuckTaskDetector?.untrackTask(task.id);
213841
+ const sessionWithAbort = session;
213842
+ if (typeof sessionWithAbort.abort === "function") {
213843
+ void sessionWithAbort.abort().catch((err) => {
213844
+ planLog.warn(`Failed to abort triage session for ${task.id}: ${err}`);
213845
+ });
213846
+ }
213847
+ this.recordTriageSessionTokenUsageSoon(task.id, session, { agentId: task.assignedAgentId ?? "triage" });
213848
+ session.dispose();
213849
+ this.activeSessions.delete(task.id);
213850
+ if (task.status === "planning") {
213851
+ void Promise.resolve(this.store.updateTask(task.id, { status: null })).catch((err) => {
213852
+ planLog.warn(`${task.id}: failed to clear planning status after evacuation: ${err instanceof Error ? err.message : String(err)}`);
213853
+ });
213854
+ }
213855
+ };
213737
213856
  }
213738
213857
  running = false;
213739
213858
  polling = false;
@@ -213792,6 +213911,8 @@ var init_triage = __esm({
213792
213911
  taskPausedHandler;
213793
213912
  /** FNXC:CodingIdeasWorkflow 2026-07-25-11:20: store-event wake for planning-eligible columns. */
213794
213913
  taskColumnWakeHandler;
213914
+ /** FNXC:PlanningEvacuation 2026-07-25-23:00: stops planning when a card leaves the planner lanes. */
213915
+ taskEvacuatedFromPlanningHandler;
213795
213916
  _approvalRequestStore;
213796
213917
  /**
213797
213918
  * @param store — Task store instance (also used to listen for `settings:updated` events)
@@ -213900,6 +214021,9 @@ var init_triage = __esm({
213900
214021
  this.store.on("task:updated", this.taskColumnWakeHandler);
213901
214022
  this.store.on("task:created", this.taskColumnWakeHandler);
213902
214023
  }
214024
+ if (this.taskEvacuatedFromPlanningHandler && typeof this.store.on === "function") {
214025
+ this.store.on("task:updated", this.taskEvacuatedFromPlanningHandler);
214026
+ }
213903
214027
  this.clearStaleSpecifyingStatuses().catch((err) => {
213904
214028
  planLog.error("Failed to clear stale planning statuses:", err);
213905
214029
  });
@@ -213947,6 +214071,9 @@ var init_triage = __esm({
213947
214071
  this.store.off("task:updated", this.taskColumnWakeHandler);
213948
214072
  this.store.off("task:created", this.taskColumnWakeHandler);
213949
214073
  }
214074
+ if (this.taskEvacuatedFromPlanningHandler && typeof this.store.off === "function") {
214075
+ this.store.off("task:updated", this.taskEvacuatedFromPlanningHandler);
214076
+ }
213950
214077
  this.abortAndDisposeActiveSessions("engine stop");
213951
214078
  planLog.log("Processor stopped");
213952
214079
  }
@@ -214738,11 +214865,15 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
214738
214865
  planningModel.modelId,
214739
214866
  assignedAgent?.runtimeConfig
214740
214867
  ) : { provider: void 0, modelId: void 0 };
214868
+ const planningCwd = await this.options.acquirePlanningWorktree?.(task.id).catch(() => null) || this.rootDir;
214869
+ if (planningCwd !== this.rootDir) {
214870
+ await this.store.logEntry(task.id, `Planning session running in task worktree ${planningCwd}`).catch(() => void 0);
214871
+ }
214741
214872
  const { session } = await createResolvedAgentSession({
214742
214873
  sessionPurpose: "triage",
214743
214874
  runtimeHint: triageRuntimeHint,
214744
214875
  pluginRunner: this.options.pluginRunner,
214745
- cwd: this.rootDir,
214876
+ cwd: planningCwd,
214746
214877
  systemPrompt: triageSystemPromptFinal,
214747
214878
  systemPromptLayers: triageLayers,
214748
214879
  tools: "coding",
@@ -217203,12 +217334,13 @@ function recoverPassedPlanReviewFromLatestLog(task) {
217203
217334
  completedAt: latest.timestamp
217204
217335
  };
217205
217336
  }
217206
- 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;
217337
+ 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;
217207
217338
  var init_workflow_graph_executor = __esm({
217208
217339
  "../engine/src/workflow-graph-executor.ts"() {
217209
217340
  "use strict";
217210
217341
  init_src2();
217211
217342
  init_transient_error_detector();
217343
+ init_transient_error_patterns();
217212
217344
  init_required_workflow_artifacts();
217213
217345
  init_workflow_node_handlers();
217214
217346
  init_workflow_graph_branches();
@@ -217217,6 +217349,7 @@ var init_workflow_graph_executor = __esm({
217217
217349
  init_workflow_node_execution_needs();
217218
217350
  PLAN_REVIEW_PROVIDER_FAILURE_HOLD_VALUE = "plan-review-provider-failure-hold";
217219
217351
  PLAN_REVIEW_LEASE_HELD_VALUE = "plan-review-lease-held";
217352
+ SESSION_CONTENTION_HOLD_VALUE = "session-contention-hold";
217220
217353
  WORKFLOW_INTERRUPTED_NODE_ID_CONTEXT_KEY = "workflow:interruptedNodeId";
217221
217354
  WORKFLOW_INTERRUPTED_NODE_ABORT_KIND_CONTEXT_KEY = "workflow:interruptedNodeAbortKind";
217222
217355
  WORKFLOW_OPTIONAL_GROUP_CONTEXT_KEY = "workflow:optionalGroupActive";
@@ -217536,6 +217669,15 @@ var init_workflow_graph_executor = __esm({
217536
217669
  };
217537
217670
  context[`node:${node.id}:outcome`] = result3.outcome;
217538
217671
  if (result3.value !== void 0) context[`node:${node.id}:value`] = result3.value;
217672
+ const sessionContentionFailure = stepStatus === "failed" && (verdictRaw === SESSION_CONTENTION_HOLD_VALUE || isSessionContentionError(stepOutput ?? stepNotes ?? ""));
217673
+ if (sessionContentionFailure) {
217674
+ this.deps.logTaskEntry?.(
217675
+ `${logPrefix} ${groupName} is waiting on another task to release its session path \u2014 holding in place`
217676
+ );
217677
+ context[`node:${node.id}:outcome`] = "failure";
217678
+ context[`node:${node.id}:value`] = SESSION_CONTENTION_HOLD_VALUE;
217679
+ return { outcome: "failure", value: SESSION_CONTENTION_HOLD_VALUE };
217680
+ }
217539
217681
  const nonPlanDefectPlanReviewFailure = node.id === PLAN_REVIEW_GROUP_ID && stepStatus === "failed" && (isRequiredArtifactReadFailedValue(verdictRaw) || isNonPlanDefectPlanReviewFailure({
217540
217682
  verdict,
217541
217683
  errorMessage: stepOutput ?? stepNotes,
@@ -217919,6 +218061,7 @@ var init_workflow_graph_executor = __esm({
217919
218061
  } catch (error3) {
217920
218062
  if (signal?.aborted) return this.withEnginePauseAbortContext(node, { outcome: "failure", value: "aborted" });
217921
218063
  lastError = error3;
218064
+ if (isSessionContentionError(error3 instanceof Error ? error3.message : String(error3))) break;
217922
218065
  }
217923
218066
  }
217924
218067
  if (signal?.aborted) {
@@ -217937,11 +218080,13 @@ var init_workflow_graph_executor = __esm({
217937
218080
  }
217938
218081
  return degraded;
217939
218082
  }
218083
+ const lastErrorText = lastError instanceof Error ? lastError.message : String(lastError);
217940
218084
  const failureResult = {
217941
218085
  outcome: "failure",
217942
- value: "exception",
218086
+ // FNXC:SessionContention 2026-07-25-21:30: contention is a retryable hold, not an exception.
218087
+ value: isSessionContentionError(lastErrorText) ? SESSION_CONTENTION_HOLD_VALUE : "exception",
217943
218088
  contextPatch: {
217944
- [`node:${node.id}:error`]: lastError instanceof Error ? lastError.message : String(lastError)
218089
+ [`node:${node.id}:error`]: lastErrorText
217945
218090
  }
217946
218091
  };
217947
218092
  if (recordProgress && this.shouldRecordNodeProgress(node)) {
@@ -223269,7 +223414,7 @@ function preservePreExecutionWorkflowStepResults(task) {
223269
223414
  }
223270
223415
  ];
223271
223416
  }
223272
- 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;
223417
+ 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;
223273
223418
  var init_executor = __esm({
223274
223419
  "../engine/src/executor.ts"() {
223275
223420
  "use strict";
@@ -223382,6 +223527,9 @@ var init_executor = __esm({
223382
223527
  MAX_TASK_DONE_REQUEUE_RETRIES = 3;
223383
223528
  MAX_TRANSIENT_GRAPH_RESUME_RETRIES = 2;
223384
223529
  TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 1e3;
223530
+ MAX_SESSION_CONTENTION_HOLD_RETRIES = 10;
223531
+ SESSION_CONTENTION_HOLD_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 5e3;
223532
+ SESSION_CONTENTION_HOLD_MAX_BACKOFF_MS = 6e4;
223385
223533
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
223386
223534
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
223387
223535
  LOOP_COMPACTION_TIMEOUT_MS = 6e4;
@@ -223742,6 +223890,15 @@ The tool prevents your session from being killed by the inactivity watchdog duri
223742
223890
  }
223743
223891
  })
223744
223892
  );
223893
+ } else if ((from === "todo" || from === "triage") && to2 !== "in-progress" && to2 !== "in-review" && to2 !== "done") {
223894
+ this.trackTaskDisposal(
223895
+ task.id,
223896
+ this.awaitAbortInFlightTaskWork(task.id, `task moved out of planning to ${to2}`, {
223897
+ userCanceled: source === "user"
223898
+ }).then(async () => {
223899
+ await this.releasePreExecutionWorktree(task.id, `moved to ${to2}`);
223900
+ })
223901
+ );
223745
223902
  } else if (from === "in-progress") {
223746
223903
  if (this.workflowLifecycleMovesInFlight.has(task.id) && this.graphRouting.has(task.id)) {
223747
223904
  executorLog.log(
@@ -224331,15 +224488,63 @@ The tool prevents your session from being killed by the inactivity watchdog duri
224331
224488
  getActiveWorktreePaths() consumers that cd into a path are unaffected; only the registry key changes.
224332
224489
  Non-workspace tasks (unique worktree path != rootDir) are returned unchanged.
224333
224490
  */
224491
+ /*
224492
+ FNXC:PlanReviewWorktree 2026-07-25-20:40 (concurrent root-rooted step sessions — single-repo collision):
224493
+ The task-scoped key must apply to the shared repo root in EVERY project mode, not only workspace mode.
224494
+ Read-only graph nodes that need no worktree (Plan Review is the canonical one — it reviews the
224495
+ store-injected PROMPT.md, see FNXC:PlanReviewSpecInjection) run rooted at `this.rootDir`, and a todo
224496
+ task has no worktree of its own. With the bare root as the registry key, two tasks reaching Plan Review
224497
+ at the same time collided: the second failed with "active-session path <root> is held by task <other>;
224498
+ task <self> may not overwrite it", which surfaced as a Plan Review provider failure, burned the
224499
+ in-place retry budget against a hold that retrying can never clear, and left the task parked
224500
+ (reported: FN-1398 holding /home/ubuntu/dev/freemap-svelte while FN-1403 planned).
224501
+ Path-exclusivity on the shared root is not what keeps these sessions correct: write-capable nodes are
224502
+ refused at the root outright (no-worktree-for-write-node above), real per-sub-repo exclusivity is the
224503
+ workspace-repo-acquire lease, and every isPathActive consumer guards removable WORKTREE paths — the
224504
+ root is never one. Liveness still works because the synthetic key stays in the registry under the task.
224505
+ */
224334
224506
  sessionRegistryPath(taskId, worktreePath) {
224335
- if (this.workspaceConfig && worktreePath === this.rootDir) {
224507
+ if (worktreePath === this.rootDir) {
224336
224508
  return `${worktreePath}#session:${taskId}`;
224337
224509
  }
224338
224510
  return worktreePath;
224339
224511
  }
224512
+ /*
224513
+ FNXC:SessionContention 2026-07-25-21:30 (contention prevention at the registration seam):
224514
+ Every executor session registration goes through `acquireActiveSessionPath` instead of the raw
224515
+ `registerPath`, so a LEAKED entry owned by a task with no live session surface in this process is
224516
+ RECLAIMED rather than throwing at the newcomer. That closes the second contention class (a dead
224517
+ holder can never release, so waiting on it is waiting forever). A genuinely live holder still throws
224518
+ the typed error — that case is real serialization, and callers classify it as a retryable contention
224519
+ hold (SESSION_CONTENTION_HOLD_VALUE), never as a provider/model failure.
224520
+ The probe reports LIVE on any uncertainty: an unknown holder with a fresh entry is treated as live by
224521
+ the staleness floor, so the reclaim only ever fires on proven-dead, aged entries.
224522
+ */
224523
+ acquireSessionRegistryPath(taskId, registryPath, kind, ownerKey) {
224524
+ const outcome = acquireActiveSessionPath(activeSessionRegistry, registryPath, { taskId, kind, ownerKey }, {
224525
+ holderLiveProbe: (holderTaskId) => this.hasLiveTaskSessionSurface(holderTaskId) || executingTaskLock.has(holderTaskId)
224526
+ });
224527
+ if (outcome.action === "contended") {
224528
+ throw new ActiveSessionPathHeldByForeignTaskError(registryPath, outcome.holderTaskId, taskId);
224529
+ }
224530
+ if (outcome.action === "reclaimed-stale-foreign") {
224531
+ executorLog.warn(
224532
+ `${taskId}: reclaimed a stale active-session entry on ${registryPath} from dead task ${outcome.holderTaskId} (idle ${outcome.ageMs}ms)`
224533
+ );
224534
+ void this.store.recordRunAuditEvent?.({
224535
+ taskId,
224536
+ agentId: "executor",
224537
+ runId: generateSyntheticRunId("session-path-reclaim", taskId),
224538
+ domain: "database",
224539
+ mutationType: "session:reclaim-stale-foreign-path",
224540
+ target: taskId,
224541
+ metadata: { taskId, holderTaskId: outcome.holderTaskId, kind, ageMs: outcome.ageMs }
224542
+ })?.catch?.(() => void 0);
224543
+ }
224544
+ }
224340
224545
  setActiveSession(taskId, sessionState, worktreePath) {
224341
224546
  this.activeSessions.set(taskId, sessionState);
224342
- activeSessionRegistry.registerPath(this.sessionRegistryPath(taskId, worktreePath), { taskId, kind: "executor", ownerKey: taskId });
224547
+ this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "executor", taskId);
224343
224548
  }
224344
224549
  markGraphExecuteSelfRequeued(taskId) {
224345
224550
  if (this.graphRouting.has(taskId)) {
@@ -224357,7 +224562,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
224357
224562
  setActiveStepExecutor(taskId, stepExecutor, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
224358
224563
  this.activeStepExecutors.set(taskId, stepExecutor);
224359
224564
  this.activeStepExecutorSeenSteeringIds.set(taskId, seenSteeringIds);
224360
- activeSessionRegistry.registerPath(this.sessionRegistryPath(taskId, worktreePath), { taskId, kind: "step-session", ownerKey: `${taskId}#step-session` });
224565
+ this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "step-session", `${taskId}#step-session`);
224361
224566
  }
224362
224567
  deleteActiveStepExecutor(taskId, worktreePath) {
224363
224568
  this.activeStepExecutors.delete(taskId);
@@ -224371,7 +224576,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
224371
224576
  setActiveWorkflowStepSession(taskId, session, worktreePath, seenSteeringIds = /* @__PURE__ */ new Set()) {
224372
224577
  this.activeWorkflowStepSessions.set(taskId, session);
224373
224578
  this.activeWorkflowStepSessionSeenSteeringIds.set(taskId, seenSteeringIds);
224374
- activeSessionRegistry.registerPath(this.sessionRegistryPath(taskId, worktreePath), { taskId, kind: "workflow-step", ownerKey: `${taskId}#workflow-step` });
224579
+ this.acquireSessionRegistryPath(taskId, this.sessionRegistryPath(taskId, worktreePath), "workflow-step", `${taskId}#workflow-step`);
224375
224580
  }
224376
224581
  deleteActiveWorkflowStepSession(taskId, worktreePath) {
224377
224582
  this.activeWorkflowStepSessions.delete(taskId);
@@ -228690,6 +228895,87 @@ ${info.feedback}`, revisionKey),
228690
228895
  this.unregisterConfiguredCommandController(task.id, commandAbortController);
228691
228896
  }
228692
228897
  }
228898
+ /*
228899
+ FNXC:NodeWorktreeIsolation 2026-07-25-22:10 (planning acquires the task worktree):
228900
+ Public seam for the planning/triage lane. Specification runs a CODING-tool session; pointing it at
228901
+ the shared main checkout meant every planning agent had write tools in the operator's tree and every
228902
+ concurrent planner shared one path. Acquire the task's own worktree up front and let the whole
228903
+ lifecycle — planning, Plan Review, implementation, code review — reuse that single worktree.
228904
+ Returns null (caller falls back to the root, unchanged behavior) when the project is a workspace, or
228905
+ when acquisition fails: planning must never be blocked by a worktree problem.
228906
+ */
228907
+ /*
228908
+ FNXC:PlanningEvacuation 2026-07-25-23:00 (pre-execution worktree release):
228909
+ Planning now acquires a worktree, so a card that never reaches execution — withdrawn to Ideas,
228910
+ archived from a planner lane, or parked pre-execution — would otherwise hold one forever. Release
228911
+ it. Safety conditions, all required:
228912
+ - the task never executed (`firstExecutionAt`/`executionStartedAt` unset): execution evidence means
228913
+ the worktree may hold real work, and only the normal merge/archive lifecycle may remove it;
228914
+ - no live session registered on the path (the same isPathActive guard the other sweeps use);
228915
+ - the branch carries no commits beyond its base — planning writes its spec to the task store, not
228916
+ the worktree, so a clean branch means there is genuinely nothing to lose.
228917
+ Metadata (`worktree`/`branch`) is cleared with it, so a later promotion re-acquires cleanly.
228918
+ Fail-soft throughout: a cleanup problem must never block the lifecycle move that triggered it.
228919
+ */
228920
+ async releasePreExecutionWorktree(taskId, reason) {
228921
+ try {
228922
+ const live = await this.store.getTask(taskId);
228923
+ if (!live?.worktree) return false;
228924
+ if (live.firstExecutionAt || live.executionStartedAt) return false;
228925
+ if (activeSessionRegistry.isPathActive(live.worktree) || activeSessionRegistry.isPathActive(resolvePath3(live.worktree))) return false;
228926
+ if (this.hasLiveTaskSessionSurface(taskId) || executingTaskLock.has(taskId)) return false;
228927
+ if (existsSync65(live.worktree)) {
228928
+ if (await this.preExecutionWorktreeHasWork(live.worktree)) {
228929
+ executorLog.log(`${taskId}: keeping pre-execution worktree ${live.worktree} \u2014 it carries commits or uncommitted changes`);
228930
+ return false;
228931
+ }
228932
+ const settings = await this.store.getSettings();
228933
+ await removeWorktree({
228934
+ rootDir: this.rootDir,
228935
+ worktreePath: live.worktree,
228936
+ settings,
228937
+ taskId,
228938
+ reason: RemovalReason.SelfHealingReclaim
228939
+ });
228940
+ }
228941
+ this.activeWorktrees.get(taskId)?.delete(live.worktree);
228942
+ await this.store.updateTask(taskId, { worktree: null, branch: null, baseCommitSha: null, sessionFile: null }, this.getRunContextFor(taskId));
228943
+ 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);
228944
+ executorLog.log(`${taskId}: released pre-execution worktree ${live.worktree} (${reason})`);
228945
+ return true;
228946
+ } catch (error3) {
228947
+ executorLog.warn(`${taskId}: could not release the pre-execution worktree: ${formatError(error3).message}`);
228948
+ return false;
228949
+ }
228950
+ }
228951
+ /** True when a pre-execution worktree holds commits past its base or any uncommitted change. */
228952
+ async preExecutionWorktreeHasWork(worktreePath) {
228953
+ try {
228954
+ const { stdout: dirty } = await execFileAsync19("git", ["status", "--porcelain"], { cwd: worktreePath, timeout: 3e4 });
228955
+ if (dirty.trim()) return true;
228956
+ 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 }));
228957
+ return Boolean(ahead.trim());
228958
+ } catch {
228959
+ return true;
228960
+ }
228961
+ }
228962
+ async ensureTaskWorktreeForPlanning(taskId) {
228963
+ try {
228964
+ if (this.workspaceConfig === void 0) {
228965
+ this.workspaceConfig = await loadWorkspaceConfig(this.rootDir);
228966
+ }
228967
+ if (this.workspaceConfig && (this.workspaceConfig.repos.length ?? 0) > 0) return null;
228968
+ const live = await this.store.getTask(taskId);
228969
+ if (live.worktree && existsSync65(live.worktree)) return live.worktree;
228970
+ const settings = await this.store.getSettings();
228971
+ const acquisitionTask = live.worktree ? { ...live, worktree: void 0, sessionFile: void 0 } : live;
228972
+ const acquired = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, "planning");
228973
+ return acquired.worktree || null;
228974
+ } catch (error3) {
228975
+ executorLog.warn(`${taskId}: could not acquire a planning worktree \u2014 planning falls back to the repo root: ${formatError(error3)}`);
228976
+ return null;
228977
+ }
228978
+ }
228693
228979
  async prepareGraphNodeExecution(node, nodeTask, settings, requirement) {
228694
228980
  if (!requirement.requiresWorktree) return;
228695
228981
  const live = await this.store.getTask(nodeTask.id);
@@ -228816,22 +229102,29 @@ ${info.feedback}`, revisionKey),
228816
229102
  optionalGroupId,
228817
229103
  reviewerInlineFixes: settings.reviewerInlineFixes
228818
229104
  });
228819
- const executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
228820
- if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
228821
- return { outcome: "failure", value: "no-worktree-for-write-node" };
228822
- }
229105
+ let executionTarget = writeCapable ? await this.store.getTask(live.id) : live;
228823
229106
  const nodeDisplayName = typeof cfg.name === "string" && cfg.name.trim() ? cfg.name.trim() : node.id;
228824
229107
  const isPlanReviewNode = node.id === "plan-review-step" || nodeDisplayName === "Plan Review" || optionalGroupId === "plan-review";
228825
- let worktreePath = executionTarget.worktree || this.rootDir;
228826
- if (isPlanReviewNode && !writeCapable && executionTarget.worktree && !existsSync65(executionTarget.worktree)) {
228827
- await this.store.logEntry(
228828
- live.id,
228829
- `Plan Review worktree ${executionTarget.worktree} is missing on disk \u2014 running the reviewer from the repo root (spec is store-injected)`,
228830
- void 0,
228831
- this.getRunContextFor(live.id)
228832
- );
228833
- worktreePath = this.rootDir;
229108
+ if (!this.workspaceConfig) {
229109
+ const recordedWorktreeMissing = Boolean(executionTarget.worktree) && !existsSync65(executionTarget.worktree);
229110
+ const shouldAcquire = !executionTarget.worktree || recordedWorktreeMissing && isPlanReviewNode;
229111
+ if (shouldAcquire) {
229112
+ if (recordedWorktreeMissing) {
229113
+ await this.store.logEntry(
229114
+ live.id,
229115
+ `Plan Review worktree ${executionTarget.worktree} is missing on disk \u2014 re-acquiring a task worktree instead of running in the shared checkout`,
229116
+ void 0,
229117
+ this.getRunContextFor(live.id)
229118
+ );
229119
+ }
229120
+ const acquisitionTask = recordedWorktreeMissing ? { ...executionTarget, worktree: void 0, sessionFile: void 0 } : executionTarget;
229121
+ executionTarget = await this.ensureGraphCustomNodeWorktree(acquisitionTask, settings, node.id);
229122
+ }
228834
229123
  }
229124
+ if (writeCapable && !executionTarget.worktree && !this.workspaceConfig) {
229125
+ return { outcome: "failure", value: "no-worktree-for-write-node" };
229126
+ }
229127
+ const worktreePath = executionTarget.worktree || this.rootDir;
228835
229128
  let prompt = typeof cfg.prompt === "string" ? cfg.prompt : "";
228836
229129
  let modelProvider = typeof cfg.modelProvider === "string" && cfg.modelProvider.trim() ? cfg.modelProvider : void 0;
228837
229130
  let modelId = typeof cfg.modelId === "string" && cfg.modelId.trim() ? cfg.modelId : void 0;
@@ -229128,6 +229421,77 @@ ${prompt}`;
229128
229421
  const latestAction = live.log.at(-1)?.action;
229129
229422
  return latestAction === "Resumed after engine restart" || latestAction === "Resuming execution after unpause";
229130
229423
  }
229424
+ /*
229425
+ FNXC:SessionContention 2026-07-25-21:30:
229426
+ Two ways in, because contention must never slip through to a park:
229427
+ - the typed failure value the graph now publishes (SESSION_CONTENTION_HOLD_VALUE), and
229428
+ - a message-shape fallback over the run's `:error` context patches, so contention arriving from a
229429
+ path that has not been taught the typed value is still recognized.
229430
+ */
229431
+ graphFailureErrorTexts(result2) {
229432
+ if (!result2.context) return [];
229433
+ const texts = [];
229434
+ for (const [key, value] of Object.entries(result2.context)) {
229435
+ if (key.endsWith(":error") && typeof value === "string" && value.trim()) texts.push(value);
229436
+ }
229437
+ return texts;
229438
+ }
229439
+ isSessionContentionGraphFailure(result2) {
229440
+ if (this.graphFailureValue(result2) === SESSION_CONTENTION_HOLD_VALUE) return true;
229441
+ return this.graphFailureErrorTexts(result2).some((text3) => isSessionContentionError(text3));
229442
+ }
229443
+ /*
229444
+ FNXC:SessionContention 2026-07-25-21:30 (self-recovering wait — the task is never parked):
229445
+ Retry the graph in place on an exponential backoff while the holder finishes. The counter is
229446
+ IN-MEMORY on purpose: it needs no schema change, and an engine restart resetting it is the desired
229447
+ behavior (a restart also drops the in-process registry, so the contention is gone anyway).
229448
+ When the ladder is exhausted the task is left cleanly dispatchable — status/error cleared, progress
229449
+ untouched — so ordinary scheduling picks it up later with a fresh budget. There is no terminal branch
229450
+ here by design: lease contention always ends (the holder finishes, or self-healing sweeps it), so
229451
+ parking the task would only require a human to press Retry on a condition that fixed itself.
229452
+ */
229453
+ sessionContentionHoldAttempts = /* @__PURE__ */ new Map();
229454
+ clearSessionContentionHold(taskId) {
229455
+ this.sessionContentionHoldAttempts.delete(taskId);
229456
+ }
229457
+ async holdForSessionContention(task, live, result2) {
229458
+ const detail = this.graphFailureErrorTexts(result2).find((text3) => isSessionContentionError(text3));
229459
+ const priorAttempts = this.sessionContentionHoldAttempts.get(task.id) ?? 0;
229460
+ const attempt = priorAttempts + 1;
229461
+ if (attempt > MAX_SESSION_CONTENTION_HOLD_RETRIES) {
229462
+ this.clearSessionContentionHold(task.id);
229463
+ 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}` : ""}`;
229464
+ executorLog.warn(`${task.id}: ${message2}`);
229465
+ await this.store.logEntry(task.id, message2, void 0, this.getRunContextFor(task.id));
229466
+ if (live.status != null || live.error != null) {
229467
+ await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
229468
+ }
229469
+ return;
229470
+ }
229471
+ this.sessionContentionHoldAttempts.set(task.id, attempt);
229472
+ const message = `Waiting on another task to release a shared session path \u2014 retrying in place (${attempt}/${MAX_SESSION_CONTENTION_HOLD_RETRIES})${detail ? `: ${detail}` : ""}`;
229473
+ executorLog.warn(`${task.id}: ${message}`);
229474
+ await this.store.logEntry(task.id, message, void 0, this.getRunContextFor(task.id));
229475
+ if (live.status != null || live.error != null) {
229476
+ await this.store.updateTask(task.id, { status: null, error: null }, this.getRunContextFor(task.id));
229477
+ }
229478
+ 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));
229479
+ const scheduleRetry = () => {
229480
+ void (async () => {
229481
+ try {
229482
+ const resume = await this.store.getTask(task.id);
229483
+ if (!resume || resume.deletedAt || resume.paused || resume.userPaused) {
229484
+ this.clearSessionContentionHold(task.id);
229485
+ return;
229486
+ }
229487
+ await this.execute(resume);
229488
+ } catch (err) {
229489
+ executorLog.error(`Failed session-contention retry for ${task.id}:`, err);
229490
+ }
229491
+ })();
229492
+ };
229493
+ setTimeout(scheduleRetry, delayMs).unref?.();
229494
+ }
229131
229495
  graphFailureValue(result2) {
229132
229496
  const failedNode = result2.visitedNodeIds[result2.visitedNodeIds.length - 1];
229133
229497
  if (!failedNode || !result2.context) return void 0;
@@ -229719,6 +230083,11 @@ Graph value: ${failureValue}` : ""}`, budget.key),
229719
230083
  await this.persistTokenUsage(task.id);
229720
230084
  return;
229721
230085
  }
230086
+ if (this.isSessionContentionGraphFailure(result2)) {
230087
+ await this.holdForSessionContention(task, live, result2);
230088
+ await this.persistTokenUsage(task.id);
230089
+ return;
230090
+ }
229722
230091
  if (await this.routeUnusableWorktreeGraphFailureToRecovery(task, live, result2)) {
229723
230092
  await this.persistTokenUsage(task.id);
229724
230093
  return;
@@ -254471,6 +254840,9 @@ var init_in_process_runtime = __esm({
254471
254840
  usageLimitPauser: this.usageLimitPauser,
254472
254841
  agentStore: this.agentStore,
254473
254842
  pluginRunner: this.pluginRunner,
254843
+ // FNXC:NodeWorktreeIsolation 2026-07-25-22:10: planning acquires (or reuses) the task's own
254844
+ // worktree through the executor's acquisition path, so no lane runs in the shared checkout.
254845
+ acquirePlanningWorktree: (taskId) => this.executor.ensureTaskWorktreeForPlanning(taskId),
254474
254846
  onSpecifyStart: (t) => {
254475
254847
  this.recordActivity();
254476
254848
  runtimeLog.log(`Specifying ${t.id}...`);
@@ -254559,6 +254931,8 @@ var init_in_process_runtime = __esm({
254559
254931
  getExecutingTaskIds: () => this.executor?.getExecutingTaskIds() ?? /* @__PURE__ */ new Set(),
254560
254932
  clearPhantomExecutorBinding: (taskId, options) => this.executor?.clearPhantomExecutorBinding(taskId, options),
254561
254933
  listWorktreeHolders: () => this.executor?.listWorktreeHolders() ?? [],
254934
+ // FNXC:PlanningEvacuation 2026-07-25-23:00: the executor owns the release safety conditions.
254935
+ releasePreExecutionWorktree: (taskId, reason) => this.executor?.releasePreExecutionWorktree(taskId, reason) ?? Promise.resolve(false),
254562
254936
  recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
254563
254937
  getPlanningTaskIds: () => this.triageProcessor?.getPlanningTaskIds() ?? /* @__PURE__ */ new Set(),
254564
254938
  // FNXC:TaskTiming 2026-08-01-12:00: orphan planning recovery must defer