opencode-orchestrator 1.2.13 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -417,6 +417,10 @@ var init_memory_hooks = __esm({
417
417
  COMPLETED: "completed",
418
418
  PROGRESS: "progress",
419
419
  FAILED: "failed"
420
+ },
421
+ PREFIX: {
422
+ TASK: "task-",
423
+ FILE: "file-task-"
420
424
  }
421
425
  };
422
426
  }
@@ -861,8 +865,8 @@ var init_types4 = __esm({
861
865
  });
862
866
 
863
867
  // src/core/notification/toast-core.ts
864
- function initToastClient(client) {
865
- tuiClient = client;
868
+ function initToastClient(client2) {
869
+ tuiClient = client2;
866
870
  }
867
871
  function show(options) {
868
872
  const toast = {
@@ -885,9 +889,9 @@ function show(options) {
885
889
  }
886
890
  }
887
891
  if (tuiClient) {
888
- const client = tuiClient;
889
- if (client.tui?.showToast) {
890
- client.tui.showToast({
892
+ const client2 = tuiClient;
893
+ if (client2.tui?.showToast) {
894
+ client2.tui.showToast({
891
895
  body: {
892
896
  title: toast.title,
893
897
  message: toast.message,
@@ -18819,6 +18823,75 @@ Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
18819
18823
  </system-notification>`;
18820
18824
  }
18821
18825
 
18826
+ // src/core/session/session-health.ts
18827
+ var sessionHealth = /* @__PURE__ */ new Map();
18828
+ var STALE_THRESHOLD_MS = 12e4;
18829
+ var HEALTH_CHECK_INTERVAL_MS = 3e4;
18830
+ var WARNING_THRESHOLD_MS = 6e4;
18831
+ var healthCheckTimer;
18832
+ var client;
18833
+ function recordSessionResponse(sessionID) {
18834
+ const health = sessionHealth.get(sessionID);
18835
+ if (health) {
18836
+ health.lastResponseTime = Date.now();
18837
+ health.isStale = false;
18838
+ }
18839
+ }
18840
+ function isSessionStale(sessionID) {
18841
+ const health = sessionHealth.get(sessionID);
18842
+ if (!health) return false;
18843
+ return health.isStale;
18844
+ }
18845
+ function startHealthCheck(opencodeClient) {
18846
+ if (healthCheckTimer) {
18847
+ log("[session-health] Health check already running");
18848
+ return;
18849
+ }
18850
+ client = opencodeClient;
18851
+ healthCheckTimer = setInterval(() => {
18852
+ performHealthCheck();
18853
+ }, HEALTH_CHECK_INTERVAL_MS);
18854
+ log("[session-health] Health check started", {
18855
+ intervalMs: HEALTH_CHECK_INTERVAL_MS,
18856
+ staleThresholdMs: STALE_THRESHOLD_MS
18857
+ });
18858
+ }
18859
+ function stopHealthCheck() {
18860
+ if (healthCheckTimer) {
18861
+ clearInterval(healthCheckTimer);
18862
+ healthCheckTimer = void 0;
18863
+ log("[session-health] Health check stopped");
18864
+ }
18865
+ }
18866
+ function performHealthCheck() {
18867
+ const now = Date.now();
18868
+ const warnings = [];
18869
+ const stales = [];
18870
+ for (const [sessionID, health] of sessionHealth) {
18871
+ const elapsed = now - health.lastResponseTime;
18872
+ if (elapsed > WARNING_THRESHOLD_MS && elapsed <= STALE_THRESHOLD_MS && !health.isStale) {
18873
+ warnings.push(sessionID.slice(0, 8));
18874
+ }
18875
+ if (elapsed > STALE_THRESHOLD_MS && !health.isStale) {
18876
+ health.isStale = true;
18877
+ stales.push(sessionID.slice(0, 8));
18878
+ }
18879
+ }
18880
+ if (warnings.length > 0) {
18881
+ log("[session-health] Sessions approaching stale threshold", {
18882
+ sessions: warnings
18883
+ });
18884
+ }
18885
+ if (stales.length > 0) {
18886
+ log("[session-health] Sessions marked as stale", {
18887
+ sessions: stales
18888
+ });
18889
+ }
18890
+ }
18891
+ function cleanupSessionHealth(sessionID) {
18892
+ sessionHealth.delete(sessionID);
18893
+ }
18894
+
18822
18895
  // src/core/agents/manager/task-launcher.ts
18823
18896
  init_shared();
18824
18897
  init_shared();
@@ -18833,8 +18906,8 @@ var TaskToastManager = class {
18833
18906
  /**
18834
18907
  * Initialize the manager with OpenCode client
18835
18908
  */
18836
- init(client, concurrency) {
18837
- this.client = client;
18909
+ init(client2, concurrency) {
18910
+ this.client = client2;
18838
18911
  this.concurrency = concurrency ?? null;
18839
18912
  }
18840
18913
  /**
@@ -19094,11 +19167,11 @@ var instance = null;
19094
19167
  function getTaskToastManager() {
19095
19168
  return instance;
19096
19169
  }
19097
- function initTaskToastManager(client, concurrency) {
19170
+ function initTaskToastManager(client2, concurrency) {
19098
19171
  if (!instance) {
19099
19172
  instance = new TaskToastManager();
19100
19173
  }
19101
- instance.init(client, concurrency);
19174
+ instance.init(client2, concurrency);
19102
19175
  return instance;
19103
19176
  }
19104
19177
 
@@ -33310,8 +33383,8 @@ var AgentRegistry = class _AgentRegistry {
33310
33383
 
33311
33384
  // src/core/agents/manager/task-launcher.ts
33312
33385
  var TaskLauncher = class {
33313
- constructor(client, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
33314
- this.client = client;
33386
+ constructor(client2, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
33387
+ this.client = client2;
33315
33388
  this.directory = directory;
33316
33389
  this.store = store;
33317
33390
  this.concurrency = concurrency;
@@ -33471,8 +33544,8 @@ ${action.modifyPrompt}`;
33471
33544
  // src/core/agents/manager/task-resumer.ts
33472
33545
  init_shared();
33473
33546
  var TaskResumer = class {
33474
- constructor(client, store, findBySession, startPolling, notifyParentIfAllComplete) {
33475
- this.client = client;
33547
+ constructor(client2, store, findBySession, startPolling, notifyParentIfAllComplete) {
33548
+ this.client = client2;
33476
33549
  this.store = store;
33477
33550
  this.findBySession = findBySession;
33478
33551
  this.startPolling = startPolling;
@@ -33624,15 +33697,17 @@ var ProgressNotifier = class _ProgressNotifier {
33624
33697
  var progressNotifier = ProgressNotifier.getInstance();
33625
33698
 
33626
33699
  // src/core/agents/manager/task-poller.ts
33700
+ var MAX_TASK_DURATION_MS = 6e5;
33627
33701
  var TaskPoller = class {
33628
- constructor(client, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete) {
33629
- this.client = client;
33702
+ constructor(client2, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete, onTaskError) {
33703
+ this.client = client2;
33630
33704
  this.store = store;
33631
33705
  this.concurrency = concurrency;
33632
33706
  this.notifyParentIfAllComplete = notifyParentIfAllComplete;
33633
33707
  this.scheduleCleanup = scheduleCleanup;
33634
33708
  this.pruneExpiredTasks = pruneExpiredTasks;
33635
33709
  this.onTaskComplete = onTaskComplete;
33710
+ this.onTaskError = onTaskError;
33636
33711
  }
33637
33712
  pollingInterval;
33638
33713
  messageCache = /* @__PURE__ */ new Map();
@@ -33664,6 +33739,17 @@ var TaskPoller = class {
33664
33739
  const allStatuses = statusResult.data ?? {};
33665
33740
  for (const task of running) {
33666
33741
  try {
33742
+ const taskDuration = Date.now() - task.startedAt.getTime();
33743
+ if (isSessionStale(task.sessionID)) {
33744
+ log(`[task-poller] Task ${task.id} session is stale. Marking as error.`);
33745
+ this.onTaskError?.(task.id, new Error("Session became stale (no response from agent)"));
33746
+ continue;
33747
+ }
33748
+ if (taskDuration > MAX_TASK_DURATION_MS) {
33749
+ log(`[task-poller] Task ${task.id} exceeded max duration (${MAX_TASK_DURATION_MS}ms). Marking as error.`);
33750
+ this.onTaskError?.(task.id, new Error("Task exceeded maximum execution time"));
33751
+ continue;
33752
+ }
33667
33753
  if (task.status === TASK_STATUS.PENDING) continue;
33668
33754
  const sessionStatus = allStatuses[task.sessionID];
33669
33755
  if (sessionStatus?.type === SESSION_STATUS.IDLE) {
@@ -33776,8 +33862,8 @@ var TaskPoller = class {
33776
33862
  init_shared();
33777
33863
  init_store();
33778
33864
  var TaskCleaner = class {
33779
- constructor(client, store, concurrency, sessionPool2) {
33780
- this.client = client;
33865
+ constructor(client2, store, concurrency, sessionPool2) {
33866
+ this.client = client2;
33781
33867
  this.store = store;
33782
33868
  this.concurrency = concurrency;
33783
33869
  this.sessionPool = sessionPool2;
@@ -33888,9 +33974,69 @@ You will be notified when ALL tasks complete. Continue productive work.`;
33888
33974
 
33889
33975
  // src/core/agents/manager/event-handler.ts
33890
33976
  init_shared();
33977
+
33978
+ // src/core/loop/continuation-lock.ts
33979
+ var locks = /* @__PURE__ */ new Map();
33980
+ var LOCK_TIMEOUT_MS = 3e4;
33981
+ function tryAcquireContinuationLock(sessionID, source) {
33982
+ const now = Date.now();
33983
+ const existing = locks.get(sessionID);
33984
+ if (existing?.acquired) {
33985
+ const elapsed = now - existing.timestamp;
33986
+ if (elapsed < LOCK_TIMEOUT_MS) {
33987
+ log("[continuation-lock] Lock denied - already held", {
33988
+ sessionID: sessionID.slice(0, 8),
33989
+ heldBy: existing.source,
33990
+ requestedBy: source,
33991
+ elapsedMs: elapsed
33992
+ });
33993
+ return false;
33994
+ }
33995
+ log("[continuation-lock] Forcing stale lock release", {
33996
+ sessionID: sessionID.slice(0, 8),
33997
+ staleSource: existing.source,
33998
+ elapsedMs: elapsed
33999
+ });
34000
+ }
34001
+ locks.set(sessionID, { acquired: true, timestamp: now, source });
34002
+ log("[continuation-lock] Lock acquired", {
34003
+ sessionID: sessionID.slice(0, 8),
34004
+ source
34005
+ });
34006
+ return true;
34007
+ }
34008
+ function releaseContinuationLock(sessionID) {
34009
+ const existing = locks.get(sessionID);
34010
+ if (existing?.acquired) {
34011
+ const duration5 = Date.now() - existing.timestamp;
34012
+ log("[continuation-lock] Lock released", {
34013
+ sessionID: sessionID.slice(0, 8),
34014
+ source: existing.source,
34015
+ heldMs: duration5
34016
+ });
34017
+ }
34018
+ locks.delete(sessionID);
34019
+ }
34020
+ function hasContinuationLock(sessionID) {
34021
+ const lock = locks.get(sessionID);
34022
+ if (!lock?.acquired) return false;
34023
+ if (Date.now() - lock.timestamp >= LOCK_TIMEOUT_MS) {
34024
+ log("[continuation-lock] Stale lock detected during check", {
34025
+ sessionID: sessionID.slice(0, 8)
34026
+ });
34027
+ locks.delete(sessionID);
34028
+ return false;
34029
+ }
34030
+ return true;
34031
+ }
34032
+ function cleanupContinuationLock(sessionID) {
34033
+ locks.delete(sessionID);
34034
+ }
34035
+
34036
+ // src/core/agents/manager/event-handler.ts
33891
34037
  var EventHandler = class {
33892
- constructor(client, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
33893
- this.client = client;
34038
+ constructor(client2, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
34039
+ this.client = client2;
33894
34040
  this.store = store;
33895
34041
  this.concurrency = concurrency;
33896
34042
  this.findBySession = findBySession;
@@ -33908,6 +34054,7 @@ var EventHandler = class {
33908
34054
  if (event.type === SESSION_EVENTS.IDLE) {
33909
34055
  const sessionID = props?.sessionID;
33910
34056
  if (!sessionID) return;
34057
+ recordSessionResponse(sessionID);
33911
34058
  const task = this.findBySession(sessionID);
33912
34059
  if (!task || task.status !== TASK_STATUS.RUNNING) return;
33913
34060
  this.handleSessionIdle(task).catch((err) => {
@@ -33969,6 +34116,8 @@ var EventHandler = class {
33969
34116
  this.store.delete(task.id);
33970
34117
  taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
33971
34118
  });
34119
+ cleanupSessionHealth(task.sessionID);
34120
+ cleanupContinuationLock(task.sessionID);
33972
34121
  progressNotifier.update();
33973
34122
  log(`Cleaned up deleted session task: ${task.id}`);
33974
34123
  }
@@ -33998,18 +34147,18 @@ var SessionPool = class _SessionPool {
33998
34147
  reuseHits: 0,
33999
34148
  creationMisses: 0
34000
34149
  };
34001
- constructor(client, directory, config3 = {}) {
34002
- this.client = client;
34150
+ constructor(client2, directory, config3 = {}) {
34151
+ this.client = client2;
34003
34152
  this.directory = directory;
34004
34153
  this.config = { ...DEFAULT_CONFIG, ...config3 };
34005
34154
  this.startHealthCheck();
34006
34155
  }
34007
- static getInstance(client, directory, config3) {
34156
+ static getInstance(client2, directory, config3) {
34008
34157
  if (!_SessionPool._instance) {
34009
- if (!client || !directory) {
34158
+ if (!client2 || !directory) {
34010
34159
  throw new Error("SessionPool requires client and directory on first call");
34011
34160
  }
34012
- _SessionPool._instance = new _SessionPool(client, directory, config3);
34161
+ _SessionPool._instance = new _SessionPool(client2, directory, config3);
34013
34162
  }
34014
34163
  return _SessionPool._instance;
34015
34164
  }
@@ -34419,27 +34568,29 @@ var ParallelAgentManager = class _ParallelAgentManager {
34419
34568
  poller;
34420
34569
  cleaner;
34421
34570
  eventHandler;
34422
- constructor(client, directory) {
34423
- this.client = client;
34571
+ constructor(client2, directory) {
34572
+ this.client = client2;
34424
34573
  this.directory = directory;
34574
+ startHealthCheck(client2);
34425
34575
  const memory = MemoryManager.getInstance();
34426
34576
  memory.add("system" /* SYSTEM */, CORE_PHILOSOPHY, 1);
34427
34577
  memory.add("project" /* PROJECT */, `Working directory: ${directory}`, 0.9);
34428
34578
  AgentRegistry.getInstance().setDirectory(directory);
34429
34579
  TodoManager.getInstance().setDirectory(directory);
34430
- this.sessionPool = SessionPool.getInstance(client, directory);
34431
- this.cleaner = new TaskCleaner(client, this.store, this.concurrency, this.sessionPool);
34580
+ this.sessionPool = SessionPool.getInstance(client2, directory);
34581
+ this.cleaner = new TaskCleaner(client2, this.store, this.concurrency, this.sessionPool);
34432
34582
  this.poller = new TaskPoller(
34433
- client,
34583
+ client2,
34434
34584
  this.store,
34435
34585
  this.concurrency,
34436
34586
  (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
34437
34587
  (taskId) => this.cleaner.scheduleCleanup(taskId),
34438
34588
  () => this.cleaner.pruneExpiredTasks(),
34439
- (task) => this.handleTaskComplete(task)
34589
+ (task) => this.handleTaskComplete(task),
34590
+ (taskId, error92) => this.handleTaskError(taskId, error92)
34440
34591
  );
34441
34592
  this.launcher = new TaskLauncher(
34442
- client,
34593
+ client2,
34443
34594
  directory,
34444
34595
  this.store,
34445
34596
  this.concurrency,
@@ -34448,14 +34599,14 @@ var ParallelAgentManager = class _ParallelAgentManager {
34448
34599
  () => this.poller.start()
34449
34600
  );
34450
34601
  this.resumer = new TaskResumer(
34451
- client,
34602
+ client2,
34452
34603
  this.store,
34453
34604
  (sessionID) => this.findBySession(sessionID),
34454
34605
  () => this.poller.start(),
34455
34606
  (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
34456
34607
  );
34457
34608
  this.eventHandler = new EventHandler(
34458
- client,
34609
+ client2,
34459
34610
  this.store,
34460
34611
  this.concurrency,
34461
34612
  (sessionID) => this.findBySession(sessionID),
@@ -34469,12 +34620,12 @@ var ParallelAgentManager = class _ParallelAgentManager {
34469
34620
  log("Recovery error:", err);
34470
34621
  });
34471
34622
  }
34472
- static getInstance(client, directory) {
34623
+ static getInstance(client2, directory) {
34473
34624
  if (!_ParallelAgentManager._instance) {
34474
- if (!client || !directory) {
34625
+ if (!client2 || !directory) {
34475
34626
  throw new Error("ParallelAgentManager requires client and directory on first call");
34476
34627
  }
34477
- _ParallelAgentManager._instance = new _ParallelAgentManager(client, directory);
34628
+ _ParallelAgentManager._instance = new _ParallelAgentManager(client2, directory);
34478
34629
  }
34479
34630
  return _ParallelAgentManager._instance;
34480
34631
  }
@@ -34553,6 +34704,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
34553
34704
  }
34554
34705
  cleanup() {
34555
34706
  this.poller.stop();
34707
+ stopHealthCheck();
34556
34708
  this.store.clear();
34557
34709
  MemoryManager.getInstance().clearTaskMemory();
34558
34710
  Promise.resolve().then(() => (init_store(), store_exports)).then((store) => store.clearAll()).catch(() => {
@@ -34783,7 +34935,7 @@ async function extractSessionResult(session, sessionID) {
34783
34935
  return "(Error extracting result)";
34784
34936
  }
34785
34937
  }
34786
- var createDelegateTaskTool = (manager, client) => tool({
34938
+ var createDelegateTaskTool = (manager, client2) => tool({
34787
34939
  description: `Delegate a task to an agent.
34788
34940
 
34789
34941
  ${PROMPT_TAGS.MODE.open}
@@ -34836,7 +34988,7 @@ If your task is too complex, please:
34836
34988
  2. Request task decomposition at the ${AGENT_NAMES.PLANNER} level
34837
34989
  3. Complete your assigned file directly without delegation`;
34838
34990
  }
34839
- const sessionClient = client;
34991
+ const sessionClient = client2;
34840
34992
  if (background === void 0) {
34841
34993
  return `${OUTPUT_LABEL.ERROR} 'background' parameter is REQUIRED.`;
34842
34994
  }
@@ -35170,9 +35322,9 @@ var createUpdateTodoTool = () => tool({
35170
35322
 
35171
35323
  // src/tools/parallel/index.ts
35172
35324
  init_shared();
35173
- function createAsyncAgentTools(manager, client) {
35325
+ function createAsyncAgentTools(manager, client2) {
35174
35326
  return {
35175
- [TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager, client),
35327
+ [TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager, client2),
35176
35328
  [TOOL_NAMES.GET_TASK_RESULT]: createGetTaskResultTool(manager),
35177
35329
  [TOOL_NAMES.LIST_TASKS]: createListTasksTool(manager),
35178
35330
  [TOOL_NAMES.CANCEL_TASK]: createCancelTaskTool(manager),
@@ -36466,15 +36618,15 @@ var CONTINUE_INSTRUCTION = `<auto_continue>
36466
36618
  </auto_continue>`;
36467
36619
  var STAGNATION_INTERVENTION = `
36468
36620
  <system_intervention type="stagnation_detected">
36469
- \u26A0\uFE0F **\uACBD\uACE0: \uC9C4\uD589 \uC815\uCCB4 \uAC10\uC9C0 (STAGNATION DETECTED)**
36470
- \uCD5C\uADFC \uC5EC\uB7EC \uD134 \uB3D9\uC548 \uC2E4\uC9C8\uC801\uC778 \uC9C4\uC804\uC774 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uB2E8\uC21C "\uBAA8\uB2C8\uD130\uB9C1"\uC774\uB098 \uAC19\uC740 \uD589\uB3D9\uC744 \uBC18\uBCF5\uD558\uB294 \uAC83\uC740 \uAE08\uC9C0\uB429\uB2C8\uB2E4.
36621
+ \u26A0\uFE0F **WARNING: STAGNATION DETECTED**
36622
+ No substantial progress has been detected for several turns. Simply "monitoring" or repeating the same actions is prohibited.
36471
36623
 
36472
- **\uC790\uC728\uC801 \uC9C4\uB2E8 \uBC0F \uD574\uACB0 \uC9C0\uCE68:**
36473
- 1. **\uC2E4\uC2DC\uAC04 \uB85C\uADF8 \uD655\uC778**: \`check_background_task\` \uB610\uB294 \`read_file\`\uC744 \uC0AC\uC6A9\uD558\uC5EC \uC9C4\uD589 \uC911\uC778 \uC791\uC5C5\uC758 \uCD9C\uB825 \uB85C\uADF8\uB97C \uC9C1\uC811 \uD655\uC778\uD558\uC2ED\uC2DC\uC624.
36474
- 2. **\uD504\uB85C\uC138\uC2A4 \uC0DD\uC874 \uC9C4\uB2E8**: \uC791\uC5C5\uC774 \uC880\uBE44 \uC0C1\uD0DC\uC774\uAC70\uB098 \uBA48\uCD98 \uAC83 \uAC19\uB2E4\uBA74 \uACFC\uAC10\uD558\uAC8C \`kill\`\uD558\uACE0 \uB2E8\uACC4\uB97C \uC138\uBD84\uD654\uD558\uC5EC \uB2E4\uC2DC \uC2E4\uD589\uD558\uC2ED\uC2DC\uC624.
36475
- 3. **\uC804\uB7B5 \uC804\uD658**: \uB3D9\uC77C\uD55C \uC811\uADFC \uBC29\uC2DD\uC774 \uC2E4\uD328\uD558\uACE0 \uC788\uB2E4\uBA74, \uB2E4\uB978 \uB3C4\uAD6C\uB098 \uBC29\uBC95\uC744 \uC0AC\uC6A9\uD558\uC5EC \uBAA9\uD45C\uC5D0 \uB3C4\uB2EC\uD558\uC2ED\uC2DC\uC624.
36624
+ **Self-Diagnosis and Resolution Guidelines:**
36625
+ 1. **Check Live Logs**: Use \`check_background_task\` or \`read_file\` to directly check the output logs of running tasks.
36626
+ 2. **Process Health Diagnosis**: If a task appears to be a zombie or stuck, kill it immediately and restart it with more granular steps.
36627
+ 3. **Strategy Pivot**: If the same approach keeps failing, use different tools or methods to reach the goal.
36476
36628
 
36477
- **\uC9C0\uAE08 \uBC14\uB85C \uB2A5\uB3D9\uC801\uC73C\uB85C \uAC1C\uC785\uD558\uC2ED\uC2DC\uC624. \uB300\uAE30\uD558\uC9C0 \uB9C8\uC2ED\uC2DC\uC624.**
36629
+ **Intervene proactively NOW. Do NOT wait.**
36478
36630
  </system_intervention>`;
36479
36631
  var CLEANUP_INSTRUCTION = `
36480
36632
  <system_maintenance type="continuous_hygiene">
@@ -36823,8 +36975,27 @@ function formatElapsedTime(startMs, endMs = Date.now()) {
36823
36975
  // src/core/loop/verification.ts
36824
36976
  init_shared();
36825
36977
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
36978
+ import { access, readFile as readFile5 } from "node:fs/promises";
36979
+ import { constants } from "node:fs";
36826
36980
  import { join as join8 } from "node:path";
36827
36981
  var CHECKLIST_FILE = CHECKLIST.FILE;
36982
+ var fileCache = /* @__PURE__ */ new Map();
36983
+ var CACHE_TTL_MS = 5e3;
36984
+ async function readFileWithCache(filePath) {
36985
+ const now = Date.now();
36986
+ const cached3 = fileCache.get(filePath);
36987
+ if (cached3 && now - cached3.timestamp < CACHE_TTL_MS) {
36988
+ return cached3.content;
36989
+ }
36990
+ try {
36991
+ await access(filePath, constants.R_OK);
36992
+ const content = await readFile5(filePath, "utf-8");
36993
+ fileCache.set(filePath, { content, timestamp: now });
36994
+ return content;
36995
+ } catch {
36996
+ return null;
36997
+ }
36998
+ }
36828
36999
  function parseChecklistLine(line, currentCategory) {
36829
37000
  const trimmedLine = line.trim();
36830
37001
  const idMatch = trimmedLine.match(CHECKLIST_PATTERNS.ITEM_WITH_ID);
@@ -36900,6 +37071,7 @@ function readChecklist(directory) {
36900
37071
  return [];
36901
37072
  }
36902
37073
  }
37074
+ var lastVerificationResult = /* @__PURE__ */ new Map();
36903
37075
  function verifyChecklist(directory) {
36904
37076
  const result = {
36905
37077
  passed: false,
@@ -36939,6 +37111,40 @@ function verifyChecklist(directory) {
36939
37111
  });
36940
37112
  return result;
36941
37113
  }
37114
+ async function verifyChecklistAsync(directory) {
37115
+ const result = {
37116
+ passed: false,
37117
+ totalItems: 0,
37118
+ completedItems: 0,
37119
+ incompleteItems: 0,
37120
+ progress: "0/0",
37121
+ incompleteList: [],
37122
+ errors: []
37123
+ };
37124
+ const filePath = join8(directory, CHECKLIST_FILE);
37125
+ const content = await readFileWithCache(filePath);
37126
+ if (!content) {
37127
+ result.errors.push(`Verification checklist not found at ${CHECKLIST_FILE}`);
37128
+ result.errors.push("Create checklist with at least: build, tests, and any environment-specific checks");
37129
+ return result;
37130
+ }
37131
+ const items = parseChecklist(content);
37132
+ if (items.length === 0) {
37133
+ result.errors.push("Verification checklist is empty");
37134
+ result.errors.push("Add verification items (build, tests, environment checks)");
37135
+ return result;
37136
+ }
37137
+ result.totalItems = items.length;
37138
+ result.completedItems = items.filter((i) => i.completed).length;
37139
+ result.incompleteItems = result.totalItems - result.completedItems;
37140
+ result.progress = `${result.completedItems}/${result.totalItems}`;
37141
+ result.incompleteList = items.filter((i) => !i.completed).map((i) => `[${CHECKLIST_CATEGORIES.LABELS[i.category]}] ${i.description}`);
37142
+ if (result.incompleteItems > 0) {
37143
+ result.errors.push(`Checklist incomplete: ${result.progress}`);
37144
+ }
37145
+ result.passed = result.incompleteItems === 0 && result.totalItems > 0;
37146
+ return result;
37147
+ }
36942
37148
  var TODO_INCOMPLETE_PATTERN = /^[-*]\s*\[\s*\]/gm;
36943
37149
  var TODO_COMPLETE_PATTERN = /^[-*]\s*\[[xX]\]/gm;
36944
37150
  var SYNC_ISSUE_PATTERNS = [
@@ -36964,7 +37170,7 @@ function hasRealSyncIssues(content) {
36964
37170
  });
36965
37171
  return lines.length > 0;
36966
37172
  }
36967
- function verifyMissionCompletion(directory) {
37173
+ function verifyMissionCompletionSync(directory) {
36968
37174
  const result = {
36969
37175
  passed: false,
36970
37176
  todoComplete: false,
@@ -37045,6 +37251,89 @@ function verifyMissionCompletion(directory) {
37045
37251
  });
37046
37252
  return result;
37047
37253
  }
37254
+ async function verifyMissionCompletionAsync(directory) {
37255
+ const result = {
37256
+ passed: false,
37257
+ todoComplete: false,
37258
+ todoProgress: "0/0",
37259
+ todoIncomplete: 0,
37260
+ syncIssuesEmpty: true,
37261
+ syncIssuesCount: 0,
37262
+ checklistComplete: false,
37263
+ checklistProgress: "0/0",
37264
+ errors: []
37265
+ };
37266
+ const checklistResult = await verifyChecklistAsync(directory);
37267
+ result.checklistComplete = checklistResult.passed;
37268
+ result.checklistProgress = checklistResult.progress;
37269
+ const hasChecklist = checklistResult.totalItems > 0;
37270
+ if (hasChecklist && !checklistResult.passed) {
37271
+ result.errors.push(`Verification checklist incomplete: ${checklistResult.progress}`);
37272
+ result.errors.push(...checklistResult.incompleteList.slice(0, 5).map((i) => ` - ${i}`));
37273
+ if (checklistResult.incompleteList.length > 5) {
37274
+ result.errors.push(` ... and ${checklistResult.incompleteList.length - 5} more`);
37275
+ }
37276
+ }
37277
+ const todoPath = join8(directory, PATHS.TODO);
37278
+ const todoContent = await readFileWithCache(todoPath);
37279
+ if (todoContent) {
37280
+ try {
37281
+ const incompleteCount = countMatches(todoContent, TODO_INCOMPLETE_PATTERN);
37282
+ const completeCount = countMatches(todoContent, TODO_COMPLETE_PATTERN);
37283
+ const total = incompleteCount + completeCount;
37284
+ result.todoIncomplete = incompleteCount;
37285
+ result.todoComplete = incompleteCount === 0 && total > 0;
37286
+ result.todoProgress = `${completeCount}/${total}`;
37287
+ if (!result.todoComplete && !hasChecklist) {
37288
+ if (total === 0) {
37289
+ result.errors.push("No TODO items found - create tasks first");
37290
+ } else {
37291
+ result.errors.push(
37292
+ `TODO incomplete: ${result.todoProgress} (${incompleteCount} remaining)`
37293
+ );
37294
+ }
37295
+ }
37296
+ } catch (error92) {
37297
+ result.errors.push(`Failed to read TODO: ${error92}`);
37298
+ }
37299
+ } else if (!hasChecklist) {
37300
+ result.errors.push(`TODO file not found at ${PATHS.TODO}`);
37301
+ }
37302
+ const syncPath = join8(directory, PATHS.SYNC_ISSUES);
37303
+ const syncContent = await readFileWithCache(syncPath);
37304
+ if (syncContent) {
37305
+ try {
37306
+ result.syncIssuesEmpty = !hasRealSyncIssues(syncContent);
37307
+ if (!result.syncIssuesEmpty) {
37308
+ const issueLines = syncContent.split("\n").filter(
37309
+ (l) => /^[-*]\s+\S/.test(l.trim()) || /ERROR|FAIL|CONFLICT/i.test(l)
37310
+ );
37311
+ result.syncIssuesCount = issueLines.length;
37312
+ result.errors.push(
37313
+ `Sync issues not resolved: ${result.syncIssuesCount} issue(s) remain`
37314
+ );
37315
+ }
37316
+ } catch (error92) {
37317
+ result.syncIssuesEmpty = true;
37318
+ }
37319
+ }
37320
+ if (hasChecklist) {
37321
+ result.passed = result.checklistComplete && result.syncIssuesEmpty;
37322
+ } else {
37323
+ result.passed = result.todoComplete && result.syncIssuesEmpty;
37324
+ }
37325
+ lastVerificationResult.set(directory, { result, timestamp: Date.now() });
37326
+ return result;
37327
+ }
37328
+ function verifyMissionCompletion(directory) {
37329
+ const cached3 = lastVerificationResult.get(directory);
37330
+ if (cached3 && Date.now() - cached3.timestamp < CACHE_TTL_MS) {
37331
+ return cached3.result;
37332
+ }
37333
+ const result = verifyMissionCompletionSync(directory);
37334
+ lastVerificationResult.set(directory, { result, timestamp: Date.now() });
37335
+ return result;
37336
+ }
37048
37337
  function buildVerificationFailurePrompt(result) {
37049
37338
  const errorList = result.errors.map((e) => `\u274C ${e}`).join("\n");
37050
37339
  const hasChecklist = result.checklistProgress !== "0/0";
@@ -37813,11 +38102,18 @@ There was a temporary processing issue. Please continue from where you left off.
37813
38102
  3. Continue execution
37814
38103
  </action>
37815
38104
  </recovery>`;
37816
- async function handleSessionError(client, sessionID, error92, properties) {
38105
+ async function handleSessionError(client2, sessionID, error92, properties) {
37817
38106
  const state2 = getState2(sessionID);
37818
38107
  if (state2.isRecovering) {
37819
- log("[session-recovery] Already recovering, skipping", { sessionID });
37820
- return false;
38108
+ const RECOVERY_TIMEOUT_MS = 1e4;
38109
+ const elapsed = Date.now() - (state2.recoveryStartTime ?? 0);
38110
+ if (elapsed > RECOVERY_TIMEOUT_MS) {
38111
+ log("[session-recovery] Forcibly clearing stale recovery state", { sessionID, elapsed });
38112
+ state2.isRecovering = false;
38113
+ } else {
38114
+ log("[session-recovery] Already recovering, skipping", { sessionID });
38115
+ return false;
38116
+ }
37821
38117
  }
37822
38118
  const now = Date.now();
37823
38119
  if (now - state2.lastErrorTime < BACKGROUND_TASK.RETRY_COOLDOWN_MS) {
@@ -37838,6 +38134,7 @@ async function handleSessionError(client, sessionID, error92, properties) {
37838
38134
  return false;
37839
38135
  }
37840
38136
  state2.isRecovering = true;
38137
+ state2.recoveryStartTime = Date.now();
37841
38138
  try {
37842
38139
  let recoveryPrompt = null;
37843
38140
  let toastMessage = null;
@@ -37863,23 +38160,19 @@ async function handleSessionError(client, sessionID, error92, properties) {
37863
38160
  log("[session-recovery] Rate limit, waiting", { delay: action.delay });
37864
38161
  await new Promise((r) => setTimeout(r, action.delay));
37865
38162
  }
37866
- state2.isRecovering = false;
37867
38163
  return true;
37868
38164
  case ERROR_TYPE.CONTEXT_OVERFLOW:
37869
38165
  toastMessage = "Context Overflow - Consider compaction";
37870
- state2.isRecovering = false;
37871
38166
  return false;
37872
38167
  case ERROR_TYPE.MESSAGE_ABORTED:
37873
38168
  log("[session-recovery] Message aborted by user, not recovering", { sessionID });
37874
- state2.isRecovering = false;
37875
38169
  return false;
37876
38170
  default:
37877
- state2.isRecovering = false;
37878
38171
  return false;
37879
38172
  }
37880
38173
  if (recoveryPrompt && toastMessage) {
37881
38174
  presets_exports.errorRecovery(toastMessage);
37882
- client.session.prompt({
38175
+ client2.session.prompt({
37883
38176
  path: { id: sessionID },
37884
38177
  body: {
37885
38178
  parts: [{ type: PART_TYPES.TEXT, text: recoveryPrompt }]
@@ -37888,15 +38181,15 @@ async function handleSessionError(client, sessionID, error92, properties) {
37888
38181
  log("[session-recovery] Failed to inject recovery prompt", { sessionID, error: injectionError });
37889
38182
  });
37890
38183
  log("[session-recovery] Recovery prompt injected (async)", { sessionID, errorType });
37891
- state2.isRecovering = false;
37892
38184
  return true;
37893
38185
  }
37894
- state2.isRecovering = false;
37895
38186
  return false;
37896
- } catch (injectionError) {
37897
- log("[session-recovery] Failed to inject recovery prompt", { sessionID, error: injectionError });
37898
- state2.isRecovering = false;
38187
+ } catch (recoveryError) {
38188
+ log("[session-recovery] Recovery failed", { sessionID, error: recoveryError });
37899
38189
  return false;
38190
+ } finally {
38191
+ state2.isRecovering = false;
38192
+ state2.recoveryStartTime = void 0;
37900
38193
  }
37901
38194
  }
37902
38195
  function markRecoveryComplete(sessionID) {
@@ -37957,9 +38250,9 @@ function hasRunningBackgroundTasks(parentSessionID) {
37957
38250
  return false;
37958
38251
  }
37959
38252
  }
37960
- async function showCountdownToast(client, secondsRemaining, incompleteCount) {
38253
+ async function showCountdownToast(client2, secondsRemaining, incompleteCount) {
37961
38254
  try {
37962
- const tuiClient2 = client;
38255
+ const tuiClient2 = client2;
37963
38256
  if (tuiClient2.tui?.showToast) {
37964
38257
  await tuiClient2.tui.showToast({
37965
38258
  body: {
@@ -37973,7 +38266,7 @@ async function showCountdownToast(client, secondsRemaining, incompleteCount) {
37973
38266
  } catch {
37974
38267
  }
37975
38268
  }
37976
- async function injectContinuation(client, directory, sessionID, todos) {
38269
+ async function injectContinuation(client2, directory, sessionID, todos) {
37977
38270
  const state2 = getState3(sessionID);
37978
38271
  if (state2.isAborting) {
37979
38272
  log("[todo-continuation] Skipped: user is aborting", { sessionID });
@@ -38003,24 +38296,22 @@ async function injectContinuation(client, directory, sessionID, todos) {
38003
38296
  return;
38004
38297
  }
38005
38298
  try {
38006
- client.session.prompt({
38299
+ await client2.session.prompt({
38007
38300
  path: { id: sessionID },
38008
38301
  body: {
38009
38302
  parts: [{ type: PART_TYPES.TEXT, text: prompt }]
38010
38303
  }
38011
- }).catch((error92) => {
38012
- log("[todo-continuation] Failed to inject continuation", { sessionID, error: error92 });
38013
38304
  });
38014
- log("[todo-continuation] Injected continuation prompt (async)", {
38305
+ log("[todo-continuation] Injected continuation prompt", {
38015
38306
  sessionID,
38016
38307
  incompleteCount: getIncompleteCount(todos),
38017
38308
  progress: formatProgress(todos)
38018
38309
  });
38019
38310
  } catch (error92) {
38020
- log("[todo-continuation] Failed to trigger async continuation", { sessionID, error: error92 });
38311
+ log("[todo-continuation] Failed to inject continuation", { sessionID, error: error92 });
38021
38312
  }
38022
38313
  }
38023
- async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
38314
+ async function handleSessionIdle(client2, directory, sessionID, mainSessionID) {
38024
38315
  const state2 = getState3(sessionID);
38025
38316
  const now = Date.now();
38026
38317
  if (state2.lastIdleTime && now - state2.lastIdleTime < MIN_TIME_BETWEEN_CONTINUATIONS_MS) {
@@ -38050,18 +38341,23 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
38050
38341
  log("[todo-continuation] Skipped: background tasks running", { sessionID });
38051
38342
  return;
38052
38343
  }
38344
+ if (hasContinuationLock(sessionID)) {
38345
+ log("[todo-continuation] Mission loop has priority, skipping");
38346
+ return;
38347
+ }
38053
38348
  let todos = [];
38054
38349
  try {
38055
- const response = await client.session.todo({ path: { id: sessionID } });
38350
+ const response = await client2.session.todo({ path: { id: sessionID } });
38056
38351
  todos = parseTodos(response.data ?? response);
38057
38352
  } catch (error92) {
38058
38353
  log("[todo-continuation] Failed to fetch todos", { sessionID, error: error92 });
38354
+ cancelCountdown(sessionID);
38059
38355
  return;
38060
38356
  }
38061
38357
  const hasBuiltinWork = hasRemainingWork(todos);
38062
38358
  let hasFileWork = false;
38063
38359
  try {
38064
- const verification = verifyMissionCompletion(directory);
38360
+ const verification = await verifyMissionCompletionAsync(directory);
38065
38361
  hasFileWork = !verification.passed && (verification.todoIncomplete > 0 || verification.checklistProgress !== "0/0" && !verification.checklistComplete);
38066
38362
  } catch (err) {
38067
38363
  log("[todo-continuation] Failed to check file-based todos", err);
@@ -38078,26 +38374,34 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
38078
38374
  incompleteCount,
38079
38375
  nextPending: nextPending?.id
38080
38376
  });
38081
- await showCountdownToast(client, COUNTDOWN_SECONDS, incompleteCount);
38377
+ await showCountdownToast(client2, COUNTDOWN_SECONDS, incompleteCount);
38082
38378
  state2.countdownStartedAt = now;
38083
38379
  state2.countdownTimer = setTimeout(async () => {
38084
38380
  cancelCountdown(sessionID);
38381
+ if (!tryAcquireContinuationLock(sessionID, "todo-continuation")) {
38382
+ log("[todo-continuation] Failed to acquire lock, skipping");
38383
+ return;
38384
+ }
38085
38385
  try {
38086
- const freshResponse = await client.session.todo({ path: { id: sessionID } });
38087
- const freshTodos = parseTodos(freshResponse.data ?? freshResponse);
38088
- let freshFileWork = false;
38089
38386
  try {
38090
- const v = verifyMissionCompletion(directory);
38091
- freshFileWork = !v.passed && (v.todoIncomplete > 0 || v.checklistProgress !== "0/0" && !v.checklistComplete);
38387
+ const freshResponse = await client2.session.todo({ path: { id: sessionID } });
38388
+ const freshTodos = parseTodos(freshResponse.data ?? freshResponse);
38389
+ let freshFileWork = false;
38390
+ try {
38391
+ const v = await verifyMissionCompletionAsync(directory);
38392
+ freshFileWork = !v.passed && (v.todoIncomplete > 0 || v.checklistProgress !== "0/0" && !v.checklistComplete);
38393
+ } catch {
38394
+ }
38395
+ if (hasRemainingWork(freshTodos) || freshFileWork) {
38396
+ await injectContinuation(client2, directory, sessionID, freshTodos);
38397
+ } else {
38398
+ log("[todo-continuation] All work completed during countdown", { sessionID });
38399
+ }
38092
38400
  } catch {
38401
+ log("[todo-continuation] Failed to re-fetch todos for continuation", { sessionID });
38093
38402
  }
38094
- if (hasRemainingWork(freshTodos) || freshFileWork) {
38095
- await injectContinuation(client, directory, sessionID, freshTodos);
38096
- } else {
38097
- log("[todo-continuation] All work completed during countdown", { sessionID });
38098
- }
38099
- } catch {
38100
- log("[todo-continuation] Failed to re-fetch todos for continuation", { sessionID });
38403
+ } finally {
38404
+ releaseContinuationLock(sessionID);
38101
38405
  }
38102
38406
  }, COUNTDOWN_SECONDS * TIME.SECOND);
38103
38407
  }
@@ -38130,6 +38434,9 @@ function cleanupSession2(sessionID) {
38130
38434
  cancelCountdown(sessionID);
38131
38435
  sessionStates2.delete(sessionID);
38132
38436
  }
38437
+ function hasPendingContinuation(sessionID) {
38438
+ return !!sessionStates2.get(sessionID)?.countdownTimer;
38439
+ }
38133
38440
 
38134
38441
  // src/hooks/custom/user-activity.ts
38135
38442
  var UserActivityHook = class {
@@ -38389,11 +38696,12 @@ import * as fs10 from "node:fs";
38389
38696
  import * as path9 from "node:path";
38390
38697
 
38391
38698
  // src/core/sync/todo-parser.ts
38699
+ init_shared();
38392
38700
  function parseTodoMd(content) {
38393
38701
  const lines = content.split("\n");
38394
38702
  const todos = [];
38395
38703
  const generateId = (text, index2) => {
38396
- return `file-task-${index2}-${text.substring(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`;
38704
+ return `${TODO_CONSTANTS.PREFIX.FILE}${index2}-${text.substring(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`;
38397
38705
  };
38398
38706
  let index = 0;
38399
38707
  for (const line of lines) {
@@ -38401,28 +38709,28 @@ function parseTodoMd(content) {
38401
38709
  if (match) {
38402
38710
  const [, statusChar, text] = match;
38403
38711
  const content2 = text.trim();
38404
- let status = "pending";
38712
+ let status = TODO_STATUS2.PENDING;
38405
38713
  switch (statusChar.toLowerCase()) {
38406
38714
  case "x":
38407
- status = "completed";
38715
+ status = TODO_STATUS2.COMPLETED;
38408
38716
  break;
38409
38717
  case "/":
38410
38718
  case ".":
38411
- status = "in_progress";
38719
+ status = TODO_STATUS2.IN_PROGRESS;
38412
38720
  break;
38413
38721
  case "-":
38414
- status = "cancelled";
38722
+ status = TODO_STATUS2.CANCELLED;
38415
38723
  break;
38416
38724
  case " ":
38417
38725
  default:
38418
- status = "pending";
38726
+ status = TODO_STATUS2.PENDING;
38419
38727
  break;
38420
38728
  }
38421
38729
  todos.push({
38422
38730
  id: generateId(content2, index),
38423
38731
  content: content2,
38424
38732
  status,
38425
- priority: "medium",
38733
+ priority: STATUS_LABEL.MEDIUM,
38426
38734
  // Default priority for file items
38427
38735
  createdAt: /* @__PURE__ */ new Date()
38428
38736
  });
@@ -38441,19 +38749,9 @@ var TodoSyncService = class {
38441
38749
  taskTodos = /* @__PURE__ */ new Map();
38442
38750
  updateTimeout = null;
38443
38751
  watcher = null;
38444
- // We only want to sync to the "primary" session or all sessions?
38445
- // The design says `syncTaskStore(sessionID)`.
38446
- // Usually TUI TODO is per session.
38447
- // However, `todo.md` is global (project level).
38448
- // So we should probably broadcast to active sessions or just the one associated with the tasks?
38449
- // Current TUI limitation: we might need to know which session to update.
38450
- // For TUI sidebar, we usually update the session the user is looking at.
38451
- // But we don't know that.
38452
- // We will maintain a set of "active sessions" provided by index.ts or just update relevant ones.
38453
- // For Phase 1, we might just update the sessions we know about (parents of tasks) or register sessions.
38454
38752
  activeSessions = /* @__PURE__ */ new Set();
38455
- constructor(client, directory) {
38456
- this.client = client;
38753
+ constructor(client2, directory) {
38754
+ this.client = client2;
38457
38755
  this.directory = directory;
38458
38756
  this.todoPath = path9.join(this.directory, PATHS.TODO);
38459
38757
  }
@@ -38522,18 +38820,18 @@ var TodoSyncService = class {
38522
38820
  }
38523
38821
  async sendTodosToSession(sessionID) {
38524
38822
  const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
38525
- let status = "pending";
38823
+ let status = TODO_STATUS2.PENDING;
38526
38824
  const s = t.status.toLowerCase();
38527
- if (s.includes("run") || s.includes("wait") || s.includes("que")) status = "in_progress";
38528
- else if (s.includes("complete") || s.includes("done")) status = "completed";
38529
- else if (s.includes("fail") || s.includes("error")) status = "cancelled";
38530
- else if (s.includes("cancel")) status = "cancelled";
38825
+ if (s.includes(STATUS_LABEL.RUNNING) || s.includes("wait") || s.includes("que")) status = TODO_STATUS2.IN_PROGRESS;
38826
+ else if (s.includes(STATUS_LABEL.COMPLETED) || s.includes(STATUS_LABEL.DONE)) status = TODO_STATUS2.COMPLETED;
38827
+ else if (s.includes(STATUS_LABEL.FAILED) || s.includes(STATUS_LABEL.ERROR)) status = TODO_STATUS2.CANCELLED;
38828
+ else if (s.includes(STATUS_LABEL.CANCELLED)) status = TODO_STATUS2.CANCELLED;
38531
38829
  return {
38532
- id: `task-${t.id}`,
38830
+ id: `${TODO_CONSTANTS.PREFIX.TASK}${t.id}`,
38533
38831
  // Prefix to avoid collision
38534
38832
  content: `[${t.agent.toUpperCase()}] ${t.description}`,
38535
38833
  status,
38536
- priority: t.isBackground ? "low" : "high",
38834
+ priority: t.isBackground ? STATUS_LABEL.LOW : STATUS_LABEL.HIGH,
38537
38835
  createdAt: /* @__PURE__ */ new Date()
38538
38836
  };
38539
38837
  });
@@ -38541,11 +38839,17 @@ var TodoSyncService = class {
38541
38839
  ...this.fileTodos,
38542
38840
  ...taskTodosList
38543
38841
  ];
38842
+ const payloadTodos = merged.map((todo) => ({
38843
+ id: todo.id,
38844
+ content: todo.content,
38845
+ status: todo.status,
38846
+ priority: todo.priority
38847
+ }));
38544
38848
  try {
38545
38849
  await this.client.session.todo({
38546
38850
  path: { id: sessionID },
38547
38851
  // Standardize to id
38548
- body: { todos: merged }
38852
+ body: { todos: payloadTodos }
38549
38853
  });
38550
38854
  } catch (error92) {
38551
38855
  }
@@ -38856,9 +39160,9 @@ function hasRunningBackgroundTasks2(parentSessionID) {
38856
39160
  return false;
38857
39161
  }
38858
39162
  }
38859
- async function showCompletedToast(client, state2) {
39163
+ async function showCompletedToast(client2, state2) {
38860
39164
  try {
38861
- const tuiClient2 = client;
39165
+ const tuiClient2 = client2;
38862
39166
  if (tuiClient2.tui?.showToast) {
38863
39167
  await tuiClient2.tui.showToast({
38864
39168
  body: {
@@ -38872,14 +39176,14 @@ async function showCompletedToast(client, state2) {
38872
39176
  } catch {
38873
39177
  }
38874
39178
  }
38875
- async function injectContinuation2(client, directory, sessionID, loopState, customPrompt) {
39179
+ async function injectContinuation2(client2, directory, sessionID, loopState, customPrompt) {
38876
39180
  const handlerState = getState4(sessionID);
38877
39181
  if (handlerState.isAborting) return;
38878
39182
  if (hasRunningBackgroundTasks2(sessionID)) return;
38879
39183
  if (isSessionRecovering(sessionID)) return;
38880
- const verification = verifyMissionCompletion(directory);
39184
+ const verification = await verifyMissionCompletionAsync(directory);
38881
39185
  if (verification.passed) {
38882
- await handleMissionComplete(client, directory, loopState);
39186
+ await handleMissionComplete(client2, directory, loopState);
38883
39187
  return;
38884
39188
  }
38885
39189
  const summary = buildVerificationSummary(verification);
@@ -38890,21 +39194,20 @@ async function injectContinuation2(client, directory, sessionID, loopState, cust
38890
39194
  ${prompt}`;
38891
39195
  }
38892
39196
  try {
38893
- client.session.prompt({
39197
+ await client2.session.prompt({
38894
39198
  path: { id: sessionID },
38895
39199
  body: {
38896
39200
  parts: [{ type: PART_TYPES.TEXT, text: prompt }]
38897
39201
  }
38898
- }).catch((error92) => {
38899
- log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
38900
39202
  });
38901
- } catch {
39203
+ } catch (error92) {
39204
+ log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
38902
39205
  }
38903
39206
  }
38904
- async function handleMissionComplete(client, directory, loopState) {
39207
+ async function handleMissionComplete(client2, directory, loopState) {
38905
39208
  const cleared = clearLoopState(directory);
38906
39209
  if (cleared) {
38907
- await showCompletedToast(client, loopState);
39210
+ await showCompletedToast(client2, loopState);
38908
39211
  await sendMissionCompleteNotification(loopState);
38909
39212
  }
38910
39213
  }
@@ -38923,7 +39226,7 @@ async function sendMissionCompleteNotification(loopState) {
38923
39226
  } catch {
38924
39227
  }
38925
39228
  }
38926
- async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
39229
+ async function handleMissionIdle(client2, directory, sessionID, mainSessionID) {
38927
39230
  const handlerState = getState4(sessionID);
38928
39231
  const now = Date.now();
38929
39232
  if (handlerState.lastCheckTime && now - handlerState.lastCheckTime < LOOP.MIN_TIME_BETWEEN_CHECKS_MS) {
@@ -38943,10 +39246,10 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
38943
39246
  if (loopState.sessionID !== sessionID) {
38944
39247
  return;
38945
39248
  }
38946
- const verification = verifyMissionCompletion(directory);
39249
+ const verification = await verifyMissionCompletionAsync(directory);
38947
39250
  if (verification.passed) {
38948
39251
  log(`[${MISSION_CONTROL.LOG_SOURCE}-handler] Verification passed for ${sessionID}. Completion confirmed.`);
38949
- await handleMissionComplete(client, directory, loopState);
39252
+ await handleMissionComplete(client2, directory, loopState);
38950
39253
  return;
38951
39254
  }
38952
39255
  const currentProgress = verification.todoProgress;
@@ -38969,7 +39272,18 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
38969
39272
  const countdownMsg = isStagnant ? "Stagnation Detected! Intervening..." : `Continuing in ${MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS}s... (iteration ${newState.iteration}/${newState.maxIterations})`;
38970
39273
  handlerState.countdownTimer = setTimeout(async () => {
38971
39274
  cancelCountdown2(sessionID);
38972
- await injectContinuation2(client, directory, sessionID, newState, isStagnant ? STAGNATION_INTERVENTION : void 0);
39275
+ if (hasPendingContinuation(sessionID)) {
39276
+ log("[mission-loop-handler] Todo continuation pending, deferring");
39277
+ return;
39278
+ }
39279
+ if (!tryAcquireContinuationLock(sessionID, "mission-loop")) {
39280
+ return;
39281
+ }
39282
+ try {
39283
+ await injectContinuation2(client2, directory, sessionID, newState, isStagnant ? STAGNATION_INTERVENTION : void 0);
39284
+ } finally {
39285
+ releaseContinuationLock(sessionID);
39286
+ }
38973
39287
  }, MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS * 1e3);
38974
39288
  }
38975
39289
  function handleUserMessage2(sessionID) {
@@ -38991,7 +39305,7 @@ function cleanupSession3(sessionID) {
38991
39305
  // src/plugin-handlers/event-handler.ts
38992
39306
  init_shared();
38993
39307
  function createEventHandler(ctx) {
38994
- const { client, directory, sessions, state: state2 } = ctx;
39308
+ const { client: client2, directory, sessions, state: state2 } = ctx;
38995
39309
  return async (input) => {
38996
39310
  const { event } = input;
38997
39311
  try {
@@ -39028,7 +39342,7 @@ function createEventHandler(ctx) {
39028
39342
  }
39029
39343
  if (sessionID && error92) {
39030
39344
  const recovered = await handleSessionError(
39031
- client,
39345
+ client2,
39032
39346
  sessionID,
39033
39347
  error92,
39034
39348
  event.properties
@@ -39066,7 +39380,7 @@ function createEventHandler(ctx) {
39066
39380
  if (session?.active) {
39067
39381
  if (isLoopActive(directory, sessionID)) {
39068
39382
  await handleMissionIdle(
39069
- client,
39383
+ client2,
39070
39384
  directory,
39071
39385
  sessionID,
39072
39386
  sessionID
@@ -39074,7 +39388,7 @@ function createEventHandler(ctx) {
39074
39388
  });
39075
39389
  } else {
39076
39390
  await handleSessionIdle(
39077
- client,
39391
+ client2,
39078
39392
  directory,
39079
39393
  sessionID,
39080
39394
  sessionID
@@ -39132,7 +39446,7 @@ function createToolExecuteAfterHandler(ctx) {
39132
39446
  // src/plugin-handlers/assistant-done-handler.ts
39133
39447
  init_shared();
39134
39448
  function createAssistantDoneHandler(ctx) {
39135
- const { client, directory, sessions } = ctx;
39449
+ const { client: client2, directory, sessions } = ctx;
39136
39450
  const hooks = HookRegistry.getInstance();
39137
39451
  return async (assistantInput, assistantOutput) => {
39138
39452
  const sessionID = assistantInput.sessionID;
@@ -39156,12 +39470,12 @@ function createAssistantDoneHandler(ctx) {
39156
39470
  session.timestamp = now;
39157
39471
  session.lastStepTime = now;
39158
39472
  try {
39159
- if (client?.session?.prompt) {
39473
+ if (client2?.session?.prompt) {
39160
39474
  const parts2 = result.prompts.map((p) => ({
39161
39475
  type: PART_TYPES.TEXT,
39162
39476
  text: p
39163
39477
  }));
39164
- client.session.prompt({
39478
+ client2.session.prompt({
39165
39479
  path: { id: sessionID },
39166
39480
  body: { parts: parts2 }
39167
39481
  }).catch((error92) => {
@@ -39321,24 +39635,24 @@ Use \`delegate_task\` with background=true for parallel work.
39321
39635
  var require2 = createRequire(import.meta.url);
39322
39636
  var { version: PLUGIN_VERSION } = require2("../package.json");
39323
39637
  var OrchestratorPlugin = async (input) => {
39324
- const { directory, client } = input;
39638
+ const { directory, client: client2 } = input;
39325
39639
  initializeHooks();
39326
- initToastClient(client);
39327
- const taskToastManager = initTaskToastManager(client);
39640
+ initToastClient(client2);
39641
+ const taskToastManager = initTaskToastManager(client2);
39328
39642
  const sessions = /* @__PURE__ */ new Map();
39329
- const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
39330
- const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
39643
+ const parallelAgentManager2 = ParallelAgentManager.getInstance(client2, directory);
39644
+ const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client2);
39331
39645
  const pluginManager = PluginManager.getInstance();
39332
39646
  await pluginManager.initialize(directory);
39333
39647
  const dynamicTools = pluginManager.getDynamicTools();
39334
39648
  taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
39335
- const todoSync = new TodoSyncService(client, directory);
39649
+ const todoSync = new TodoSyncService(client2, directory);
39336
39650
  await todoSync.start();
39337
39651
  taskToastManager.setTodoSync(todoSync);
39338
39652
  const cleanupScheduler = new CleanupScheduler(directory);
39339
39653
  cleanupScheduler.start();
39340
39654
  const handlerContext = {
39341
- client,
39655
+ client: client2,
39342
39656
  directory,
39343
39657
  sessions,
39344
39658
  state