opencode-orchestrator 1.2.14 → 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.
@@ -14,9 +14,10 @@ export declare class TaskPoller {
14
14
  private scheduleCleanup;
15
15
  private pruneExpiredTasks;
16
16
  private onTaskComplete?;
17
+ private onTaskError?;
17
18
  private pollingInterval?;
18
19
  private messageCache;
19
- constructor(client: OpencodeClient, store: TaskStore, concurrency: ConcurrencyController, notifyParentIfAllComplete: (parentSessionID: string) => Promise<void>, scheduleCleanup: (taskId: string) => void, pruneExpiredTasks: () => void, onTaskComplete?: ((task: ParallelTask) => void | Promise<void>) | undefined);
20
+ constructor(client: OpencodeClient, store: TaskStore, concurrency: ConcurrencyController, notifyParentIfAllComplete: (parentSessionID: string) => Promise<void>, scheduleCleanup: (taskId: string) => void, pruneExpiredTasks: () => void, onTaskComplete?: ((task: ParallelTask) => void | Promise<void>) | undefined, onTaskError?: ((taskId: string, error: unknown) => void) | undefined);
20
21
  start(): void;
21
22
  stop(): void;
22
23
  isRunning(): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Continuation Lock - Ensures single execution of continuation logic
3
+ *
4
+ * Prevents simultaneous execution of Mission Loop and Todo Continuation systems.
5
+ * This resolves infinite loading issues caused by duplicate prompt injections.
6
+ *
7
+ * @example
8
+ * if (!tryAcquireContinuationLock(sessionID)) {
9
+ * return; // Another system is already processing
10
+ * }
11
+ * try {
12
+ * // continuation logic
13
+ * } finally {
14
+ * releaseContinuationLock(sessionID);
15
+ * }
16
+ */
17
+ interface ContinuationLockState {
18
+ acquired: boolean;
19
+ timestamp: number;
20
+ source?: string;
21
+ }
22
+ /**
23
+ * Try to acquire the continuation lock
24
+ *
25
+ * @param sessionID - Session ID
26
+ * @param source - Source requesting the lock (for debugging)
27
+ * @returns true if acquired, false otherwise
28
+ */
29
+ export declare function tryAcquireContinuationLock(sessionID: string, source?: string): boolean;
30
+ /**
31
+ * Release the continuation lock
32
+ *
33
+ * @param sessionID - Session ID
34
+ */
35
+ export declare function releaseContinuationLock(sessionID: string): void;
36
+ /**
37
+ * Check if the lock is currently active
38
+ *
39
+ * @param sessionID - Session ID
40
+ * @returns true if lock is held
41
+ */
42
+ export declare function hasContinuationLock(sessionID: string): boolean;
43
+ /**
44
+ * Cleanup lock when session is cleaned up
45
+ *
46
+ * @param sessionID - Session ID
47
+ */
48
+ export declare function cleanupContinuationLock(sessionID: string): void;
49
+ /**
50
+ * Clear all locks (for testing)
51
+ */
52
+ export declare function clearAllLocks(): void;
53
+ /**
54
+ * Get lock status (for debugging)
55
+ */
56
+ export declare function getLockStatus(sessionID: string): ContinuationLockState | undefined;
57
+ export {};
@@ -11,11 +11,16 @@ export type { ChecklistItem, ChecklistCategory, ChecklistVerificationResult, Ver
11
11
  export declare const CHECKLIST_FILE: ".opencode/verification-checklist.md";
12
12
  export declare function parseChecklist(content: string): ChecklistItem[];
13
13
  export declare function readChecklist(directory: string): ChecklistItem[];
14
+ export declare function readChecklistAsync(directory: string): Promise<ChecklistItem[]>;
15
+ export declare function clearVerificationCache(): void;
14
16
  export declare function verifyChecklist(directory: string): ChecklistVerificationResult;
17
+ export declare function verifyChecklistAsync(directory: string): Promise<ChecklistVerificationResult>;
15
18
  export declare function hasValidChecklist(directory: string): boolean;
16
19
  export declare function getChecklistSummary(directory: string): string;
17
20
  export declare function buildChecklistFailurePrompt(result: ChecklistVerificationResult): string;
18
21
  export declare function getChecklistCreationInstructions(): string;
22
+ export declare function verifyMissionCompletionSync(directory: string): VerificationResult;
23
+ export declare function verifyMissionCompletionAsync(directory: string): Promise<VerificationResult>;
19
24
  export declare function verifyMissionCompletion(directory: string): VerificationResult;
20
25
  export declare function buildVerificationFailurePrompt(result: VerificationResult): string;
21
26
  export declare function buildTodoIncompletePrompt(result: VerificationResult): string;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Session Health Monitor
3
+ *
4
+ * Periodically checks session health and detects stale sessions.
5
+ * Early detection of infinite loading states improves system stability.
6
+ */
7
+ import type { PluginInput } from "@opencode-ai/plugin";
8
+ type OpencodeClient = PluginInput["client"];
9
+ interface SessionHealthState {
10
+ sessionID: string;
11
+ lastActiveTime: number;
12
+ lastResponseTime: number;
13
+ isStale: boolean;
14
+ activityCount: number;
15
+ }
16
+ /**
17
+ * Record session activity
18
+ *
19
+ * Call this when an activity occurs in the session.
20
+ * (e.g., sending prompt, tool execution)
21
+ *
22
+ * @param sessionID - Session ID
23
+ */
24
+ export declare function recordSessionActivity(sessionID: string): void;
25
+ /**
26
+ * Record session response receipt
27
+ *
28
+ * Call this when a response is received from the session.
29
+ * (e.g., assistant message received)
30
+ *
31
+ * @param sessionID - Session ID
32
+ */
33
+ export declare function recordSessionResponse(sessionID: string): void;
34
+ /**
35
+ * Check if session is stale
36
+ *
37
+ * @param sessionID - Session ID
38
+ * @returns true if stale
39
+ */
40
+ export declare function isSessionStale(sessionID: string): boolean;
41
+ /**
42
+ * Get session response age
43
+ *
44
+ * @param sessionID - Session ID
45
+ * @returns Time elapsed since last response (ms), or -1 if session not found
46
+ */
47
+ export declare function getSessionResponseAge(sessionID: string): number;
48
+ /**
49
+ * Get all stale session IDs
50
+ *
51
+ * @returns Array of stale session IDs
52
+ */
53
+ export declare function getStaleSessions(): string[];
54
+ /**
55
+ * Start health check monitor
56
+ *
57
+ * @param opencodeClient - OpenCode Client
58
+ */
59
+ export declare function startHealthCheck(opencodeClient: OpencodeClient): void;
60
+ /**
61
+ * Stop health check monitor
62
+ */
63
+ export declare function stopHealthCheck(): void;
64
+ /**
65
+ * Perform actual health check (Exported for testing)
66
+ */
67
+ export declare function performHealthCheck(): void;
68
+ /**
69
+ * Cleanup session health state
70
+ *
71
+ * @param sessionID - Session ID
72
+ */
73
+ export declare function cleanupSessionHealth(sessionID: string): void;
74
+ /**
75
+ * Clear all session health info (for testing)
76
+ */
77
+ export declare function clearAllSessionHealth(): void;
78
+ /**
79
+ * Get session health info (for debugging)
80
+ *
81
+ * @param sessionID - Session ID
82
+ */
83
+ export declare function getSessionHealth(sessionID: string): SessionHealthState | undefined;
84
+ /**
85
+ * Get overall health stats (for debugging)
86
+ */
87
+ export declare function getHealthStats(): {
88
+ total: number;
89
+ stale: number;
90
+ healthy: number;
91
+ avgResponseAge: number;
92
+ };
93
+ export {};
package/dist/index.js CHANGED
@@ -865,8 +865,8 @@ var init_types4 = __esm({
865
865
  });
866
866
 
867
867
  // src/core/notification/toast-core.ts
868
- function initToastClient(client) {
869
- tuiClient = client;
868
+ function initToastClient(client2) {
869
+ tuiClient = client2;
870
870
  }
871
871
  function show(options) {
872
872
  const toast = {
@@ -889,9 +889,9 @@ function show(options) {
889
889
  }
890
890
  }
891
891
  if (tuiClient) {
892
- const client = tuiClient;
893
- if (client.tui?.showToast) {
894
- client.tui.showToast({
892
+ const client2 = tuiClient;
893
+ if (client2.tui?.showToast) {
894
+ client2.tui.showToast({
895
895
  body: {
896
896
  title: toast.title,
897
897
  message: toast.message,
@@ -18823,6 +18823,75 @@ Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
18823
18823
  </system-notification>`;
18824
18824
  }
18825
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
+
18826
18895
  // src/core/agents/manager/task-launcher.ts
18827
18896
  init_shared();
18828
18897
  init_shared();
@@ -18837,8 +18906,8 @@ var TaskToastManager = class {
18837
18906
  /**
18838
18907
  * Initialize the manager with OpenCode client
18839
18908
  */
18840
- init(client, concurrency) {
18841
- this.client = client;
18909
+ init(client2, concurrency) {
18910
+ this.client = client2;
18842
18911
  this.concurrency = concurrency ?? null;
18843
18912
  }
18844
18913
  /**
@@ -19098,11 +19167,11 @@ var instance = null;
19098
19167
  function getTaskToastManager() {
19099
19168
  return instance;
19100
19169
  }
19101
- function initTaskToastManager(client, concurrency) {
19170
+ function initTaskToastManager(client2, concurrency) {
19102
19171
  if (!instance) {
19103
19172
  instance = new TaskToastManager();
19104
19173
  }
19105
- instance.init(client, concurrency);
19174
+ instance.init(client2, concurrency);
19106
19175
  return instance;
19107
19176
  }
19108
19177
 
@@ -33314,8 +33383,8 @@ var AgentRegistry = class _AgentRegistry {
33314
33383
 
33315
33384
  // src/core/agents/manager/task-launcher.ts
33316
33385
  var TaskLauncher = class {
33317
- constructor(client, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
33318
- this.client = client;
33386
+ constructor(client2, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
33387
+ this.client = client2;
33319
33388
  this.directory = directory;
33320
33389
  this.store = store;
33321
33390
  this.concurrency = concurrency;
@@ -33475,8 +33544,8 @@ ${action.modifyPrompt}`;
33475
33544
  // src/core/agents/manager/task-resumer.ts
33476
33545
  init_shared();
33477
33546
  var TaskResumer = class {
33478
- constructor(client, store, findBySession, startPolling, notifyParentIfAllComplete) {
33479
- this.client = client;
33547
+ constructor(client2, store, findBySession, startPolling, notifyParentIfAllComplete) {
33548
+ this.client = client2;
33480
33549
  this.store = store;
33481
33550
  this.findBySession = findBySession;
33482
33551
  this.startPolling = startPolling;
@@ -33628,15 +33697,17 @@ var ProgressNotifier = class _ProgressNotifier {
33628
33697
  var progressNotifier = ProgressNotifier.getInstance();
33629
33698
 
33630
33699
  // src/core/agents/manager/task-poller.ts
33700
+ var MAX_TASK_DURATION_MS = 6e5;
33631
33701
  var TaskPoller = class {
33632
- constructor(client, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete) {
33633
- this.client = client;
33702
+ constructor(client2, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete, onTaskError) {
33703
+ this.client = client2;
33634
33704
  this.store = store;
33635
33705
  this.concurrency = concurrency;
33636
33706
  this.notifyParentIfAllComplete = notifyParentIfAllComplete;
33637
33707
  this.scheduleCleanup = scheduleCleanup;
33638
33708
  this.pruneExpiredTasks = pruneExpiredTasks;
33639
33709
  this.onTaskComplete = onTaskComplete;
33710
+ this.onTaskError = onTaskError;
33640
33711
  }
33641
33712
  pollingInterval;
33642
33713
  messageCache = /* @__PURE__ */ new Map();
@@ -33668,6 +33739,17 @@ var TaskPoller = class {
33668
33739
  const allStatuses = statusResult.data ?? {};
33669
33740
  for (const task of running) {
33670
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
+ }
33671
33753
  if (task.status === TASK_STATUS.PENDING) continue;
33672
33754
  const sessionStatus = allStatuses[task.sessionID];
33673
33755
  if (sessionStatus?.type === SESSION_STATUS.IDLE) {
@@ -33780,8 +33862,8 @@ var TaskPoller = class {
33780
33862
  init_shared();
33781
33863
  init_store();
33782
33864
  var TaskCleaner = class {
33783
- constructor(client, store, concurrency, sessionPool2) {
33784
- this.client = client;
33865
+ constructor(client2, store, concurrency, sessionPool2) {
33866
+ this.client = client2;
33785
33867
  this.store = store;
33786
33868
  this.concurrency = concurrency;
33787
33869
  this.sessionPool = sessionPool2;
@@ -33892,9 +33974,69 @@ You will be notified when ALL tasks complete. Continue productive work.`;
33892
33974
 
33893
33975
  // src/core/agents/manager/event-handler.ts
33894
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
33895
34037
  var EventHandler = class {
33896
- constructor(client, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
33897
- this.client = client;
34038
+ constructor(client2, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
34039
+ this.client = client2;
33898
34040
  this.store = store;
33899
34041
  this.concurrency = concurrency;
33900
34042
  this.findBySession = findBySession;
@@ -33912,6 +34054,7 @@ var EventHandler = class {
33912
34054
  if (event.type === SESSION_EVENTS.IDLE) {
33913
34055
  const sessionID = props?.sessionID;
33914
34056
  if (!sessionID) return;
34057
+ recordSessionResponse(sessionID);
33915
34058
  const task = this.findBySession(sessionID);
33916
34059
  if (!task || task.status !== TASK_STATUS.RUNNING) return;
33917
34060
  this.handleSessionIdle(task).catch((err) => {
@@ -33973,6 +34116,8 @@ var EventHandler = class {
33973
34116
  this.store.delete(task.id);
33974
34117
  taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
33975
34118
  });
34119
+ cleanupSessionHealth(task.sessionID);
34120
+ cleanupContinuationLock(task.sessionID);
33976
34121
  progressNotifier.update();
33977
34122
  log(`Cleaned up deleted session task: ${task.id}`);
33978
34123
  }
@@ -34002,18 +34147,18 @@ var SessionPool = class _SessionPool {
34002
34147
  reuseHits: 0,
34003
34148
  creationMisses: 0
34004
34149
  };
34005
- constructor(client, directory, config3 = {}) {
34006
- this.client = client;
34150
+ constructor(client2, directory, config3 = {}) {
34151
+ this.client = client2;
34007
34152
  this.directory = directory;
34008
34153
  this.config = { ...DEFAULT_CONFIG, ...config3 };
34009
34154
  this.startHealthCheck();
34010
34155
  }
34011
- static getInstance(client, directory, config3) {
34156
+ static getInstance(client2, directory, config3) {
34012
34157
  if (!_SessionPool._instance) {
34013
- if (!client || !directory) {
34158
+ if (!client2 || !directory) {
34014
34159
  throw new Error("SessionPool requires client and directory on first call");
34015
34160
  }
34016
- _SessionPool._instance = new _SessionPool(client, directory, config3);
34161
+ _SessionPool._instance = new _SessionPool(client2, directory, config3);
34017
34162
  }
34018
34163
  return _SessionPool._instance;
34019
34164
  }
@@ -34423,27 +34568,29 @@ var ParallelAgentManager = class _ParallelAgentManager {
34423
34568
  poller;
34424
34569
  cleaner;
34425
34570
  eventHandler;
34426
- constructor(client, directory) {
34427
- this.client = client;
34571
+ constructor(client2, directory) {
34572
+ this.client = client2;
34428
34573
  this.directory = directory;
34574
+ startHealthCheck(client2);
34429
34575
  const memory = MemoryManager.getInstance();
34430
34576
  memory.add("system" /* SYSTEM */, CORE_PHILOSOPHY, 1);
34431
34577
  memory.add("project" /* PROJECT */, `Working directory: ${directory}`, 0.9);
34432
34578
  AgentRegistry.getInstance().setDirectory(directory);
34433
34579
  TodoManager.getInstance().setDirectory(directory);
34434
- this.sessionPool = SessionPool.getInstance(client, directory);
34435
- 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);
34436
34582
  this.poller = new TaskPoller(
34437
- client,
34583
+ client2,
34438
34584
  this.store,
34439
34585
  this.concurrency,
34440
34586
  (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
34441
34587
  (taskId) => this.cleaner.scheduleCleanup(taskId),
34442
34588
  () => this.cleaner.pruneExpiredTasks(),
34443
- (task) => this.handleTaskComplete(task)
34589
+ (task) => this.handleTaskComplete(task),
34590
+ (taskId, error92) => this.handleTaskError(taskId, error92)
34444
34591
  );
34445
34592
  this.launcher = new TaskLauncher(
34446
- client,
34593
+ client2,
34447
34594
  directory,
34448
34595
  this.store,
34449
34596
  this.concurrency,
@@ -34452,14 +34599,14 @@ var ParallelAgentManager = class _ParallelAgentManager {
34452
34599
  () => this.poller.start()
34453
34600
  );
34454
34601
  this.resumer = new TaskResumer(
34455
- client,
34602
+ client2,
34456
34603
  this.store,
34457
34604
  (sessionID) => this.findBySession(sessionID),
34458
34605
  () => this.poller.start(),
34459
34606
  (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
34460
34607
  );
34461
34608
  this.eventHandler = new EventHandler(
34462
- client,
34609
+ client2,
34463
34610
  this.store,
34464
34611
  this.concurrency,
34465
34612
  (sessionID) => this.findBySession(sessionID),
@@ -34473,12 +34620,12 @@ var ParallelAgentManager = class _ParallelAgentManager {
34473
34620
  log("Recovery error:", err);
34474
34621
  });
34475
34622
  }
34476
- static getInstance(client, directory) {
34623
+ static getInstance(client2, directory) {
34477
34624
  if (!_ParallelAgentManager._instance) {
34478
- if (!client || !directory) {
34625
+ if (!client2 || !directory) {
34479
34626
  throw new Error("ParallelAgentManager requires client and directory on first call");
34480
34627
  }
34481
- _ParallelAgentManager._instance = new _ParallelAgentManager(client, directory);
34628
+ _ParallelAgentManager._instance = new _ParallelAgentManager(client2, directory);
34482
34629
  }
34483
34630
  return _ParallelAgentManager._instance;
34484
34631
  }
@@ -34557,6 +34704,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
34557
34704
  }
34558
34705
  cleanup() {
34559
34706
  this.poller.stop();
34707
+ stopHealthCheck();
34560
34708
  this.store.clear();
34561
34709
  MemoryManager.getInstance().clearTaskMemory();
34562
34710
  Promise.resolve().then(() => (init_store(), store_exports)).then((store) => store.clearAll()).catch(() => {
@@ -34787,7 +34935,7 @@ async function extractSessionResult(session, sessionID) {
34787
34935
  return "(Error extracting result)";
34788
34936
  }
34789
34937
  }
34790
- var createDelegateTaskTool = (manager, client) => tool({
34938
+ var createDelegateTaskTool = (manager, client2) => tool({
34791
34939
  description: `Delegate a task to an agent.
34792
34940
 
34793
34941
  ${PROMPT_TAGS.MODE.open}
@@ -34840,7 +34988,7 @@ If your task is too complex, please:
34840
34988
  2. Request task decomposition at the ${AGENT_NAMES.PLANNER} level
34841
34989
  3. Complete your assigned file directly without delegation`;
34842
34990
  }
34843
- const sessionClient = client;
34991
+ const sessionClient = client2;
34844
34992
  if (background === void 0) {
34845
34993
  return `${OUTPUT_LABEL.ERROR} 'background' parameter is REQUIRED.`;
34846
34994
  }
@@ -35174,9 +35322,9 @@ var createUpdateTodoTool = () => tool({
35174
35322
 
35175
35323
  // src/tools/parallel/index.ts
35176
35324
  init_shared();
35177
- function createAsyncAgentTools(manager, client) {
35325
+ function createAsyncAgentTools(manager, client2) {
35178
35326
  return {
35179
- [TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager, client),
35327
+ [TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager, client2),
35180
35328
  [TOOL_NAMES.GET_TASK_RESULT]: createGetTaskResultTool(manager),
35181
35329
  [TOOL_NAMES.LIST_TASKS]: createListTasksTool(manager),
35182
35330
  [TOOL_NAMES.CANCEL_TASK]: createCancelTaskTool(manager),
@@ -36470,15 +36618,15 @@ var CONTINUE_INSTRUCTION = `<auto_continue>
36470
36618
  </auto_continue>`;
36471
36619
  var STAGNATION_INTERVENTION = `
36472
36620
  <system_intervention type="stagnation_detected">
36473
- \u26A0\uFE0F **\uACBD\uACE0: \uC9C4\uD589 \uC815\uCCB4 \uAC10\uC9C0 (STAGNATION DETECTED)**
36474
- \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.
36475
36623
 
36476
- **\uC790\uC728\uC801 \uC9C4\uB2E8 \uBC0F \uD574\uACB0 \uC9C0\uCE68:**
36477
- 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.
36478
- 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.
36479
- 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.
36480
36628
 
36481
- **\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.**
36482
36630
  </system_intervention>`;
36483
36631
  var CLEANUP_INSTRUCTION = `
36484
36632
  <system_maintenance type="continuous_hygiene">
@@ -36827,8 +36975,27 @@ function formatElapsedTime(startMs, endMs = Date.now()) {
36827
36975
  // src/core/loop/verification.ts
36828
36976
  init_shared();
36829
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";
36830
36980
  import { join as join8 } from "node:path";
36831
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
+ }
36832
36999
  function parseChecklistLine(line, currentCategory) {
36833
37000
  const trimmedLine = line.trim();
36834
37001
  const idMatch = trimmedLine.match(CHECKLIST_PATTERNS.ITEM_WITH_ID);
@@ -36904,6 +37071,7 @@ function readChecklist(directory) {
36904
37071
  return [];
36905
37072
  }
36906
37073
  }
37074
+ var lastVerificationResult = /* @__PURE__ */ new Map();
36907
37075
  function verifyChecklist(directory) {
36908
37076
  const result = {
36909
37077
  passed: false,
@@ -36943,6 +37111,40 @@ function verifyChecklist(directory) {
36943
37111
  });
36944
37112
  return result;
36945
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
+ }
36946
37148
  var TODO_INCOMPLETE_PATTERN = /^[-*]\s*\[\s*\]/gm;
36947
37149
  var TODO_COMPLETE_PATTERN = /^[-*]\s*\[[xX]\]/gm;
36948
37150
  var SYNC_ISSUE_PATTERNS = [
@@ -36968,7 +37170,7 @@ function hasRealSyncIssues(content) {
36968
37170
  });
36969
37171
  return lines.length > 0;
36970
37172
  }
36971
- function verifyMissionCompletion(directory) {
37173
+ function verifyMissionCompletionSync(directory) {
36972
37174
  const result = {
36973
37175
  passed: false,
36974
37176
  todoComplete: false,
@@ -37049,6 +37251,89 @@ function verifyMissionCompletion(directory) {
37049
37251
  });
37050
37252
  return result;
37051
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
+ }
37052
37337
  function buildVerificationFailurePrompt(result) {
37053
37338
  const errorList = result.errors.map((e) => `\u274C ${e}`).join("\n");
37054
37339
  const hasChecklist = result.checklistProgress !== "0/0";
@@ -37817,11 +38102,18 @@ There was a temporary processing issue. Please continue from where you left off.
37817
38102
  3. Continue execution
37818
38103
  </action>
37819
38104
  </recovery>`;
37820
- async function handleSessionError(client, sessionID, error92, properties) {
38105
+ async function handleSessionError(client2, sessionID, error92, properties) {
37821
38106
  const state2 = getState2(sessionID);
37822
38107
  if (state2.isRecovering) {
37823
- log("[session-recovery] Already recovering, skipping", { sessionID });
37824
- 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
+ }
37825
38117
  }
37826
38118
  const now = Date.now();
37827
38119
  if (now - state2.lastErrorTime < BACKGROUND_TASK.RETRY_COOLDOWN_MS) {
@@ -37842,6 +38134,7 @@ async function handleSessionError(client, sessionID, error92, properties) {
37842
38134
  return false;
37843
38135
  }
37844
38136
  state2.isRecovering = true;
38137
+ state2.recoveryStartTime = Date.now();
37845
38138
  try {
37846
38139
  let recoveryPrompt = null;
37847
38140
  let toastMessage = null;
@@ -37867,23 +38160,19 @@ async function handleSessionError(client, sessionID, error92, properties) {
37867
38160
  log("[session-recovery] Rate limit, waiting", { delay: action.delay });
37868
38161
  await new Promise((r) => setTimeout(r, action.delay));
37869
38162
  }
37870
- state2.isRecovering = false;
37871
38163
  return true;
37872
38164
  case ERROR_TYPE.CONTEXT_OVERFLOW:
37873
38165
  toastMessage = "Context Overflow - Consider compaction";
37874
- state2.isRecovering = false;
37875
38166
  return false;
37876
38167
  case ERROR_TYPE.MESSAGE_ABORTED:
37877
38168
  log("[session-recovery] Message aborted by user, not recovering", { sessionID });
37878
- state2.isRecovering = false;
37879
38169
  return false;
37880
38170
  default:
37881
- state2.isRecovering = false;
37882
38171
  return false;
37883
38172
  }
37884
38173
  if (recoveryPrompt && toastMessage) {
37885
38174
  presets_exports.errorRecovery(toastMessage);
37886
- client.session.prompt({
38175
+ client2.session.prompt({
37887
38176
  path: { id: sessionID },
37888
38177
  body: {
37889
38178
  parts: [{ type: PART_TYPES.TEXT, text: recoveryPrompt }]
@@ -37892,15 +38181,15 @@ async function handleSessionError(client, sessionID, error92, properties) {
37892
38181
  log("[session-recovery] Failed to inject recovery prompt", { sessionID, error: injectionError });
37893
38182
  });
37894
38183
  log("[session-recovery] Recovery prompt injected (async)", { sessionID, errorType });
37895
- state2.isRecovering = false;
37896
38184
  return true;
37897
38185
  }
37898
- state2.isRecovering = false;
37899
38186
  return false;
37900
- } catch (injectionError) {
37901
- log("[session-recovery] Failed to inject recovery prompt", { sessionID, error: injectionError });
37902
- state2.isRecovering = false;
38187
+ } catch (recoveryError) {
38188
+ log("[session-recovery] Recovery failed", { sessionID, error: recoveryError });
37903
38189
  return false;
38190
+ } finally {
38191
+ state2.isRecovering = false;
38192
+ state2.recoveryStartTime = void 0;
37904
38193
  }
37905
38194
  }
37906
38195
  function markRecoveryComplete(sessionID) {
@@ -37961,9 +38250,9 @@ function hasRunningBackgroundTasks(parentSessionID) {
37961
38250
  return false;
37962
38251
  }
37963
38252
  }
37964
- async function showCountdownToast(client, secondsRemaining, incompleteCount) {
38253
+ async function showCountdownToast(client2, secondsRemaining, incompleteCount) {
37965
38254
  try {
37966
- const tuiClient2 = client;
38255
+ const tuiClient2 = client2;
37967
38256
  if (tuiClient2.tui?.showToast) {
37968
38257
  await tuiClient2.tui.showToast({
37969
38258
  body: {
@@ -37977,7 +38266,7 @@ async function showCountdownToast(client, secondsRemaining, incompleteCount) {
37977
38266
  } catch {
37978
38267
  }
37979
38268
  }
37980
- async function injectContinuation(client, directory, sessionID, todos) {
38269
+ async function injectContinuation(client2, directory, sessionID, todos) {
37981
38270
  const state2 = getState3(sessionID);
37982
38271
  if (state2.isAborting) {
37983
38272
  log("[todo-continuation] Skipped: user is aborting", { sessionID });
@@ -38007,24 +38296,22 @@ async function injectContinuation(client, directory, sessionID, todos) {
38007
38296
  return;
38008
38297
  }
38009
38298
  try {
38010
- client.session.prompt({
38299
+ await client2.session.prompt({
38011
38300
  path: { id: sessionID },
38012
38301
  body: {
38013
38302
  parts: [{ type: PART_TYPES.TEXT, text: prompt }]
38014
38303
  }
38015
- }).catch((error92) => {
38016
- log("[todo-continuation] Failed to inject continuation", { sessionID, error: error92 });
38017
38304
  });
38018
- log("[todo-continuation] Injected continuation prompt (async)", {
38305
+ log("[todo-continuation] Injected continuation prompt", {
38019
38306
  sessionID,
38020
38307
  incompleteCount: getIncompleteCount(todos),
38021
38308
  progress: formatProgress(todos)
38022
38309
  });
38023
38310
  } catch (error92) {
38024
- log("[todo-continuation] Failed to trigger async continuation", { sessionID, error: error92 });
38311
+ log("[todo-continuation] Failed to inject continuation", { sessionID, error: error92 });
38025
38312
  }
38026
38313
  }
38027
- async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
38314
+ async function handleSessionIdle(client2, directory, sessionID, mainSessionID) {
38028
38315
  const state2 = getState3(sessionID);
38029
38316
  const now = Date.now();
38030
38317
  if (state2.lastIdleTime && now - state2.lastIdleTime < MIN_TIME_BETWEEN_CONTINUATIONS_MS) {
@@ -38054,18 +38341,23 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
38054
38341
  log("[todo-continuation] Skipped: background tasks running", { sessionID });
38055
38342
  return;
38056
38343
  }
38344
+ if (hasContinuationLock(sessionID)) {
38345
+ log("[todo-continuation] Mission loop has priority, skipping");
38346
+ return;
38347
+ }
38057
38348
  let todos = [];
38058
38349
  try {
38059
- const response = await client.session.todo({ path: { id: sessionID } });
38350
+ const response = await client2.session.todo({ path: { id: sessionID } });
38060
38351
  todos = parseTodos(response.data ?? response);
38061
38352
  } catch (error92) {
38062
38353
  log("[todo-continuation] Failed to fetch todos", { sessionID, error: error92 });
38354
+ cancelCountdown(sessionID);
38063
38355
  return;
38064
38356
  }
38065
38357
  const hasBuiltinWork = hasRemainingWork(todos);
38066
38358
  let hasFileWork = false;
38067
38359
  try {
38068
- const verification = verifyMissionCompletion(directory);
38360
+ const verification = await verifyMissionCompletionAsync(directory);
38069
38361
  hasFileWork = !verification.passed && (verification.todoIncomplete > 0 || verification.checklistProgress !== "0/0" && !verification.checklistComplete);
38070
38362
  } catch (err) {
38071
38363
  log("[todo-continuation] Failed to check file-based todos", err);
@@ -38082,26 +38374,34 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
38082
38374
  incompleteCount,
38083
38375
  nextPending: nextPending?.id
38084
38376
  });
38085
- await showCountdownToast(client, COUNTDOWN_SECONDS, incompleteCount);
38377
+ await showCountdownToast(client2, COUNTDOWN_SECONDS, incompleteCount);
38086
38378
  state2.countdownStartedAt = now;
38087
38379
  state2.countdownTimer = setTimeout(async () => {
38088
38380
  cancelCountdown(sessionID);
38381
+ if (!tryAcquireContinuationLock(sessionID, "todo-continuation")) {
38382
+ log("[todo-continuation] Failed to acquire lock, skipping");
38383
+ return;
38384
+ }
38089
38385
  try {
38090
- const freshResponse = await client.session.todo({ path: { id: sessionID } });
38091
- const freshTodos = parseTodos(freshResponse.data ?? freshResponse);
38092
- let freshFileWork = false;
38093
38386
  try {
38094
- const v = verifyMissionCompletion(directory);
38095
- 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
+ }
38096
38400
  } catch {
38401
+ log("[todo-continuation] Failed to re-fetch todos for continuation", { sessionID });
38097
38402
  }
38098
- if (hasRemainingWork(freshTodos) || freshFileWork) {
38099
- await injectContinuation(client, directory, sessionID, freshTodos);
38100
- } else {
38101
- log("[todo-continuation] All work completed during countdown", { sessionID });
38102
- }
38103
- } catch {
38104
- log("[todo-continuation] Failed to re-fetch todos for continuation", { sessionID });
38403
+ } finally {
38404
+ releaseContinuationLock(sessionID);
38105
38405
  }
38106
38406
  }, COUNTDOWN_SECONDS * TIME.SECOND);
38107
38407
  }
@@ -38134,6 +38434,9 @@ function cleanupSession2(sessionID) {
38134
38434
  cancelCountdown(sessionID);
38135
38435
  sessionStates2.delete(sessionID);
38136
38436
  }
38437
+ function hasPendingContinuation(sessionID) {
38438
+ return !!sessionStates2.get(sessionID)?.countdownTimer;
38439
+ }
38137
38440
 
38138
38441
  // src/hooks/custom/user-activity.ts
38139
38442
  var UserActivityHook = class {
@@ -38447,8 +38750,8 @@ var TodoSyncService = class {
38447
38750
  updateTimeout = null;
38448
38751
  watcher = null;
38449
38752
  activeSessions = /* @__PURE__ */ new Set();
38450
- constructor(client, directory) {
38451
- this.client = client;
38753
+ constructor(client2, directory) {
38754
+ this.client = client2;
38452
38755
  this.directory = directory;
38453
38756
  this.todoPath = path9.join(this.directory, PATHS.TODO);
38454
38757
  }
@@ -38857,9 +39160,9 @@ function hasRunningBackgroundTasks2(parentSessionID) {
38857
39160
  return false;
38858
39161
  }
38859
39162
  }
38860
- async function showCompletedToast(client, state2) {
39163
+ async function showCompletedToast(client2, state2) {
38861
39164
  try {
38862
- const tuiClient2 = client;
39165
+ const tuiClient2 = client2;
38863
39166
  if (tuiClient2.tui?.showToast) {
38864
39167
  await tuiClient2.tui.showToast({
38865
39168
  body: {
@@ -38873,14 +39176,14 @@ async function showCompletedToast(client, state2) {
38873
39176
  } catch {
38874
39177
  }
38875
39178
  }
38876
- async function injectContinuation2(client, directory, sessionID, loopState, customPrompt) {
39179
+ async function injectContinuation2(client2, directory, sessionID, loopState, customPrompt) {
38877
39180
  const handlerState = getState4(sessionID);
38878
39181
  if (handlerState.isAborting) return;
38879
39182
  if (hasRunningBackgroundTasks2(sessionID)) return;
38880
39183
  if (isSessionRecovering(sessionID)) return;
38881
- const verification = verifyMissionCompletion(directory);
39184
+ const verification = await verifyMissionCompletionAsync(directory);
38882
39185
  if (verification.passed) {
38883
- await handleMissionComplete(client, directory, loopState);
39186
+ await handleMissionComplete(client2, directory, loopState);
38884
39187
  return;
38885
39188
  }
38886
39189
  const summary = buildVerificationSummary(verification);
@@ -38891,21 +39194,20 @@ async function injectContinuation2(client, directory, sessionID, loopState, cust
38891
39194
  ${prompt}`;
38892
39195
  }
38893
39196
  try {
38894
- client.session.prompt({
39197
+ await client2.session.prompt({
38895
39198
  path: { id: sessionID },
38896
39199
  body: {
38897
39200
  parts: [{ type: PART_TYPES.TEXT, text: prompt }]
38898
39201
  }
38899
- }).catch((error92) => {
38900
- log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
38901
39202
  });
38902
- } catch {
39203
+ } catch (error92) {
39204
+ log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
38903
39205
  }
38904
39206
  }
38905
- async function handleMissionComplete(client, directory, loopState) {
39207
+ async function handleMissionComplete(client2, directory, loopState) {
38906
39208
  const cleared = clearLoopState(directory);
38907
39209
  if (cleared) {
38908
- await showCompletedToast(client, loopState);
39210
+ await showCompletedToast(client2, loopState);
38909
39211
  await sendMissionCompleteNotification(loopState);
38910
39212
  }
38911
39213
  }
@@ -38924,7 +39226,7 @@ async function sendMissionCompleteNotification(loopState) {
38924
39226
  } catch {
38925
39227
  }
38926
39228
  }
38927
- async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
39229
+ async function handleMissionIdle(client2, directory, sessionID, mainSessionID) {
38928
39230
  const handlerState = getState4(sessionID);
38929
39231
  const now = Date.now();
38930
39232
  if (handlerState.lastCheckTime && now - handlerState.lastCheckTime < LOOP.MIN_TIME_BETWEEN_CHECKS_MS) {
@@ -38944,10 +39246,10 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
38944
39246
  if (loopState.sessionID !== sessionID) {
38945
39247
  return;
38946
39248
  }
38947
- const verification = verifyMissionCompletion(directory);
39249
+ const verification = await verifyMissionCompletionAsync(directory);
38948
39250
  if (verification.passed) {
38949
39251
  log(`[${MISSION_CONTROL.LOG_SOURCE}-handler] Verification passed for ${sessionID}. Completion confirmed.`);
38950
- await handleMissionComplete(client, directory, loopState);
39252
+ await handleMissionComplete(client2, directory, loopState);
38951
39253
  return;
38952
39254
  }
38953
39255
  const currentProgress = verification.todoProgress;
@@ -38970,7 +39272,18 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
38970
39272
  const countdownMsg = isStagnant ? "Stagnation Detected! Intervening..." : `Continuing in ${MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS}s... (iteration ${newState.iteration}/${newState.maxIterations})`;
38971
39273
  handlerState.countdownTimer = setTimeout(async () => {
38972
39274
  cancelCountdown2(sessionID);
38973
- 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
+ }
38974
39287
  }, MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS * 1e3);
38975
39288
  }
38976
39289
  function handleUserMessage2(sessionID) {
@@ -38992,7 +39305,7 @@ function cleanupSession3(sessionID) {
38992
39305
  // src/plugin-handlers/event-handler.ts
38993
39306
  init_shared();
38994
39307
  function createEventHandler(ctx) {
38995
- const { client, directory, sessions, state: state2 } = ctx;
39308
+ const { client: client2, directory, sessions, state: state2 } = ctx;
38996
39309
  return async (input) => {
38997
39310
  const { event } = input;
38998
39311
  try {
@@ -39029,7 +39342,7 @@ function createEventHandler(ctx) {
39029
39342
  }
39030
39343
  if (sessionID && error92) {
39031
39344
  const recovered = await handleSessionError(
39032
- client,
39345
+ client2,
39033
39346
  sessionID,
39034
39347
  error92,
39035
39348
  event.properties
@@ -39067,7 +39380,7 @@ function createEventHandler(ctx) {
39067
39380
  if (session?.active) {
39068
39381
  if (isLoopActive(directory, sessionID)) {
39069
39382
  await handleMissionIdle(
39070
- client,
39383
+ client2,
39071
39384
  directory,
39072
39385
  sessionID,
39073
39386
  sessionID
@@ -39075,7 +39388,7 @@ function createEventHandler(ctx) {
39075
39388
  });
39076
39389
  } else {
39077
39390
  await handleSessionIdle(
39078
- client,
39391
+ client2,
39079
39392
  directory,
39080
39393
  sessionID,
39081
39394
  sessionID
@@ -39133,7 +39446,7 @@ function createToolExecuteAfterHandler(ctx) {
39133
39446
  // src/plugin-handlers/assistant-done-handler.ts
39134
39447
  init_shared();
39135
39448
  function createAssistantDoneHandler(ctx) {
39136
- const { client, directory, sessions } = ctx;
39449
+ const { client: client2, directory, sessions } = ctx;
39137
39450
  const hooks = HookRegistry.getInstance();
39138
39451
  return async (assistantInput, assistantOutput) => {
39139
39452
  const sessionID = assistantInput.sessionID;
@@ -39157,12 +39470,12 @@ function createAssistantDoneHandler(ctx) {
39157
39470
  session.timestamp = now;
39158
39471
  session.lastStepTime = now;
39159
39472
  try {
39160
- if (client?.session?.prompt) {
39473
+ if (client2?.session?.prompt) {
39161
39474
  const parts2 = result.prompts.map((p) => ({
39162
39475
  type: PART_TYPES.TEXT,
39163
39476
  text: p
39164
39477
  }));
39165
- client.session.prompt({
39478
+ client2.session.prompt({
39166
39479
  path: { id: sessionID },
39167
39480
  body: { parts: parts2 }
39168
39481
  }).catch((error92) => {
@@ -39322,24 +39635,24 @@ Use \`delegate_task\` with background=true for parallel work.
39322
39635
  var require2 = createRequire(import.meta.url);
39323
39636
  var { version: PLUGIN_VERSION } = require2("../package.json");
39324
39637
  var OrchestratorPlugin = async (input) => {
39325
- const { directory, client } = input;
39638
+ const { directory, client: client2 } = input;
39326
39639
  initializeHooks();
39327
- initToastClient(client);
39328
- const taskToastManager = initTaskToastManager(client);
39640
+ initToastClient(client2);
39641
+ const taskToastManager = initTaskToastManager(client2);
39329
39642
  const sessions = /* @__PURE__ */ new Map();
39330
- const parallelAgentManager2 = ParallelAgentManager.getInstance(client, directory);
39331
- const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client);
39643
+ const parallelAgentManager2 = ParallelAgentManager.getInstance(client2, directory);
39644
+ const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client2);
39332
39645
  const pluginManager = PluginManager.getInstance();
39333
39646
  await pluginManager.initialize(directory);
39334
39647
  const dynamicTools = pluginManager.getDynamicTools();
39335
39648
  taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
39336
- const todoSync = new TodoSyncService(client, directory);
39649
+ const todoSync = new TodoSyncService(client2, directory);
39337
39650
  await todoSync.start();
39338
39651
  taskToastManager.setTodoSync(todoSync);
39339
39652
  const cleanupScheduler = new CleanupScheduler(directory);
39340
39653
  cleanupScheduler.start();
39341
39654
  const handlerContext = {
39342
- client,
39655
+ client: client2,
39343
39656
  directory,
39344
39657
  sessions,
39345
39658
  state
@@ -21,5 +21,5 @@ export declare const MISSION_MESSAGES: {
21
21
  };
22
22
  export declare const COMPACTION_PROMPT: string;
23
23
  export declare const CONTINUE_INSTRUCTION: string;
24
- export declare const STAGNATION_INTERVENTION = "\n<system_intervention type=\"stagnation_detected\">\n\u26A0\uFE0F **\uACBD\uACE0: \uC9C4\uD589 \uC815\uCCB4 \uAC10\uC9C0 (STAGNATION DETECTED)**\n\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.\n\n**\uC790\uC728\uC801 \uC9C4\uB2E8 \uBC0F \uD574\uACB0 \uC9C0\uCE68:**\n1. **\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.\n2. **\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.\n3. **\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.\n\n**\uC9C0\uAE08 \uBC14\uB85C \uB2A5\uB3D9\uC801\uC73C\uB85C \uAC1C\uC785\uD558\uC2ED\uC2DC\uC624. \uB300\uAE30\uD558\uC9C0 \uB9C8\uC2ED\uC2DC\uC624.**\n</system_intervention>";
24
+ export declare const STAGNATION_INTERVENTION = "\n<system_intervention type=\"stagnation_detected\">\n\u26A0\uFE0F **WARNING: STAGNATION DETECTED**\nNo substantial progress has been detected for several turns. Simply \"monitoring\" or repeating the same actions is prohibited.\n\n**Self-Diagnosis and Resolution Guidelines:**\n1. **Check Live Logs**: Use `check_background_task` or `read_file` to directly check the output logs of running tasks.\n2. **Process Health Diagnosis**: If a task appears to be a zombie or stuck, kill it immediately and restart it with more granular steps.\n3. **Strategy Pivot**: If the same approach keeps failing, use different tools or methods to reach the goal.\n\n**Intervene proactively NOW. Do NOT wait.**\n</system_intervention>";
25
25
  export declare const CLEANUP_INSTRUCTION = "\n<system_maintenance type=\"continuous_hygiene\">\n\uD83E\uDDF9 **DOCUMENTATION & STATE HYGIENE (Iteration %ITER%)**\nYou must maintain a pristine workspace. **As part of your move**, perform these checks:\n\n1. **Relevance Assessment**:\n - Review active documents (`.opencode/*.md`). Are they needed for the *current* objective?\n - If a file represents a solved problem or obsolete context, **Archive it** to `.opencode/archive/` or delete it.\n\n2. **Synchronization**:\n - Verify `TODO.md` matches the actual code state. Mark completed items immediately.\n - Check `sync-issues.md`. If issues are resolved, remove them.\n\n3. **Context Optimization**:\n - If `work-log.md` is getting noisy, summarize key decisions into `summary.md` and truncate the log.\n - Keep context lightweight.\n\n**Rule**: A cluttered workspace leads to hallucinations. Clean as you go.\n</system_maintenance>\n";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "opencode-orchestrator",
3
3
  "displayName": "OpenCode Orchestrator",
4
4
  "description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
5
- "version": "1.2.14",
5
+ "version": "1.2.15",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {