opencode-orchestrator 1.2.17 → 1.2.21

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
@@ -500,24 +500,26 @@ var init_parallel_task = __esm({
500
500
  * Worker and Reviewer are terminal nodes by design.
501
501
  */
502
502
  TERMINAL_DEPTH: 2,
503
- // Concurrency limits (Aggressive for intense processing)
504
- DEFAULT_CONCURRENCY: 3,
505
- // Reduced from 10
503
+ // Concurrency limits (FIXED at 5 for stability - no auto-scaling)
504
+ CONCURRENCY: 5,
505
+ // Fixed concurrency (removed DEFAULT/MAX split)
506
+ // Legacy support (deprecated - use CONCURRENCY instead)
507
+ DEFAULT_CONCURRENCY: 5,
506
508
  MAX_CONCURRENCY: 5,
507
- // Reduced from 50
508
509
  // Sync polling (for delegate_task sync mode)
509
- // Optimized: Reduced polling frequency while relying more on events
510
- SYNC_TIMEOUT_MS: 5 * TIME.MINUTE,
511
- POLL_INTERVAL_MS: 5e3,
512
- // 2000ms → 5000ms (To reduce load)
513
- MIN_IDLE_TIME_MS: 5 * TIME.SECOND,
514
- // 3s 5s (Less jittery)
515
- MIN_STABILITY_MS: 5 * TIME.SECOND,
516
- // 2s 5s (More stable)
510
+ // Optimized: 3s polling for faster response while maintaining stability
511
+ SYNC_TIMEOUT_MS: 30 * TIME.MINUTE,
512
+ // 5min → 30min (longer tasks)
513
+ POLL_INTERVAL_MS: 3e3,
514
+ // 5000ms 3000ms (faster polling)
515
+ MIN_IDLE_TIME_MS: 3 * TIME.SECOND,
516
+ // 5s 3s (quicker detection)
517
+ MIN_STABILITY_MS: 3 * TIME.SECOND,
518
+ // 5s → 3s (faster completion)
517
519
  STABLE_POLLS_REQUIRED: 2,
518
- // 3 2 (Faster completion)
519
- MAX_POLL_COUNT: 150,
520
- // 600150 (Adjusted for 2s interval)
520
+ // Keep at 2 for reliability
521
+ MAX_POLL_COUNT: 600,
522
+ // 150600 (30min = 600 * 3s)
521
523
  // Session naming
522
524
  SESSION_TITLE_PREFIX: "Parallel",
523
525
  // Labels for output
@@ -1377,6 +1379,20 @@ var init_special_events = __esm({
1377
1379
  }
1378
1380
  });
1379
1381
 
1382
+ // src/shared/session/constants/events/hook-events.ts
1383
+ var HOOK_EVENTS;
1384
+ var init_hook_events = __esm({
1385
+ "src/shared/session/constants/events/hook-events.ts"() {
1386
+ "use strict";
1387
+ HOOK_EVENTS = {
1388
+ PRE_TOOL: "hook.pre_tool",
1389
+ POST_TOOL: "hook.post_tool",
1390
+ CHAT: "hook.chat",
1391
+ DONE: "hook.done"
1392
+ };
1393
+ }
1394
+ });
1395
+
1380
1396
  // src/shared/session/constants/events/index.ts
1381
1397
  var EVENT_TYPES;
1382
1398
  var init_events = __esm({
@@ -1389,6 +1405,7 @@ var init_events = __esm({
1389
1405
  init_mission_events();
1390
1406
  init_message_events();
1391
1407
  init_special_events();
1408
+ init_hook_events();
1392
1409
  init_task_events();
1393
1410
  init_todo_events();
1394
1411
  init_session_events();
@@ -1396,6 +1413,7 @@ var init_events = __esm({
1396
1413
  init_mission_events();
1397
1414
  init_message_events();
1398
1415
  init_special_events();
1416
+ init_hook_events();
1399
1417
  EVENT_TYPES = {
1400
1418
  ...TASK_EVENTS,
1401
1419
  ...TODO_EVENTS,
@@ -1403,7 +1421,8 @@ var init_events = __esm({
1403
1421
  ...DOCUMENT_EVENTS,
1404
1422
  ...MISSION_EVENTS,
1405
1423
  ...MESSAGE_EVENTS,
1406
- ...SPECIAL_EVENTS
1424
+ ...SPECIAL_EVENTS,
1425
+ ...HOOK_EVENTS
1407
1426
  };
1408
1427
  }
1409
1428
  });
@@ -5128,13 +5147,13 @@ __export(store_exports, {
5128
5147
  addDecision: () => addDecision,
5129
5148
  addDocument: () => addDocument,
5130
5149
  addFinding: () => addFinding,
5131
- clear: () => clear2,
5150
+ clear: () => clear,
5132
5151
  clearAll: () => clearAll,
5133
5152
  create: () => create,
5134
5153
  get: () => get,
5135
5154
  getChildren: () => getChildren,
5136
5155
  getMerged: () => getMerged,
5137
- getStats: () => getStats2
5156
+ getStats: () => getStats
5138
5157
  });
5139
5158
  function create(sessionId, parentId) {
5140
5159
  const context = {
@@ -5202,7 +5221,7 @@ function addDecision(sessionId, decision) {
5202
5221
  function getChildren(parentId) {
5203
5222
  return Array.from(parentChildMap.get(parentId) || []);
5204
5223
  }
5205
- function clear2(sessionId) {
5224
+ function clear(sessionId) {
5206
5225
  const context = contexts.get(sessionId);
5207
5226
  if (context?.parentId) {
5208
5227
  parentChildMap.get(context.parentId)?.delete(sessionId);
@@ -5213,7 +5232,7 @@ function clearAll() {
5213
5232
  contexts.clear();
5214
5233
  parentChildMap.clear();
5215
5234
  }
5216
- function getStats2() {
5235
+ function getStats() {
5217
5236
  let totalDocuments = 0;
5218
5237
  let totalFindings = 0;
5219
5238
  let totalDecisions = 0;
@@ -18581,34 +18600,20 @@ var ConcurrencyController = class {
18581
18600
  }
18582
18601
  }
18583
18602
  /**
18584
- * Report success/failure to adjust concurrency dynamically
18603
+ * Report success/failure (for metrics only - auto-scaling DISABLED)
18604
+ *
18605
+ * Auto-scaling has been removed for stability. Concurrency is now fixed at 5.
18606
+ * This method is kept for backwards compatibility and metrics tracking.
18585
18607
  */
18586
18608
  reportResult(key, success3) {
18587
18609
  if (success3) {
18588
18610
  const streak = (this.successStreak.get(key) ?? 0) + 1;
18589
18611
  this.successStreak.set(key, streak);
18590
18612
  this.failureCount.set(key, 0);
18591
- if (streak >= 5) {
18592
- const currentLimit = this.getConcurrencyLimit(key);
18593
- if (currentLimit < PARALLEL_TASK.MAX_CONCURRENCY) {
18594
- this.setLimit(key, currentLimit + 1);
18595
- this.successStreak.set(key, 0);
18596
- log(`[concurrency] Auto-scaling UP for ${key}: ${currentLimit + 1}`);
18597
- }
18598
- }
18599
18613
  } else {
18600
18614
  const failures = (this.failureCount.get(key) ?? 0) + 1;
18601
18615
  this.failureCount.set(key, failures);
18602
18616
  this.successStreak.set(key, 0);
18603
- if (failures >= 2) {
18604
- const currentLimit = this.getConcurrencyLimit(key);
18605
- const minLimit = 1;
18606
- if (currentLimit > minLimit) {
18607
- this.setLimit(key, currentLimit - 1);
18608
- this.failureCount.set(key, 0);
18609
- log(`[concurrency] Auto-scaling DOWN for ${key}: ${currentLimit - 1} (due to ${failures} failures)`);
18610
- }
18611
- }
18612
18617
  }
18613
18618
  }
18614
18619
  getQueueLength(key) {
@@ -18811,19 +18816,6 @@ function formatDuration(start, end) {
18811
18816
  if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
18812
18817
  return `${seconds}s`;
18813
18818
  }
18814
- function buildNotificationMessage(tasks) {
18815
- const summary = tasks.map((t) => {
18816
- const status = t.status === TASK_STATUS.COMPLETED ? "\u2705" : "\u274C";
18817
- return `${status} \`${t.id}\`: ${t.description}`;
18818
- }).join("\n");
18819
- return `<system-notification>
18820
- **All Parallel Tasks Complete**
18821
-
18822
- ${summary}
18823
-
18824
- Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
18825
- </system-notification>`;
18826
- }
18827
18819
 
18828
18820
  // src/core/session/session-health.ts
18829
18821
  var sessionHealth = /* @__PURE__ */ new Map();
@@ -18832,13 +18824,6 @@ var HEALTH_CHECK_INTERVAL_MS = 6e4;
18832
18824
  var WARNING_THRESHOLD_MS = 3e5;
18833
18825
  var healthCheckTimer;
18834
18826
  var client;
18835
- function recordSessionResponse(sessionID) {
18836
- const health = sessionHealth.get(sessionID);
18837
- if (health) {
18838
- health.lastResponseTime = Date.now();
18839
- health.isStale = false;
18840
- }
18841
- }
18842
18827
  function startHealthCheck(opencodeClient) {
18843
18828
  if (healthCheckTimer) {
18844
18829
  log("[session-health] Health check already running");
@@ -18885,292 +18870,6 @@ function performHealthCheck() {
18885
18870
  });
18886
18871
  }
18887
18872
  }
18888
- function cleanupSessionHealth(sessionID) {
18889
- sessionHealth.delete(sessionID);
18890
- }
18891
-
18892
- // src/core/agents/manager/task-launcher.ts
18893
- init_shared();
18894
- init_shared();
18895
-
18896
- // src/core/notification/task-toast-manager.ts
18897
- init_shared();
18898
- var TaskToastManager = class {
18899
- tasks = /* @__PURE__ */ new Map();
18900
- client = null;
18901
- concurrency = null;
18902
- todoSync = null;
18903
- /**
18904
- * Initialize the manager with OpenCode client
18905
- */
18906
- init(client2, concurrency) {
18907
- this.client = client2;
18908
- this.concurrency = concurrency ?? null;
18909
- }
18910
- /**
18911
- * Set concurrency controller (can be set after init)
18912
- */
18913
- setConcurrencyController(concurrency) {
18914
- this.concurrency = concurrency;
18915
- }
18916
- /**
18917
- * Set TodoSyncService for TUI status synchronization
18918
- */
18919
- setTodoSync(service) {
18920
- this.todoSync = service;
18921
- }
18922
- /**
18923
- * Add a new task and show consolidated toast
18924
- */
18925
- addTask(task) {
18926
- const trackedTask = {
18927
- id: task.id,
18928
- description: task.description,
18929
- agent: task.agent,
18930
- status: task.status ?? STATUS_LABEL.RUNNING,
18931
- startedAt: /* @__PURE__ */ new Date(),
18932
- isBackground: task.isBackground,
18933
- parentSessionID: task.parentSessionID,
18934
- sessionID: task.sessionID
18935
- };
18936
- this.tasks.set(task.id, trackedTask);
18937
- this.todoSync?.updateTaskStatus(trackedTask);
18938
- this.showTaskListToast(trackedTask);
18939
- }
18940
- /**
18941
- * Update task status
18942
- */
18943
- updateTask(id, status) {
18944
- const task = this.tasks.get(id);
18945
- if (task) {
18946
- task.status = status;
18947
- this.todoSync?.updateTaskStatus(task);
18948
- }
18949
- }
18950
- /**
18951
- * Remove a task
18952
- */
18953
- removeTask(id) {
18954
- this.tasks.delete(id);
18955
- this.todoSync?.removeTask(id);
18956
- }
18957
- /**
18958
- * Get all running tasks (newest first)
18959
- */
18960
- getRunningTasks() {
18961
- return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.RUNNING).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
18962
- }
18963
- /**
18964
- * Get all queued tasks (oldest first - FIFO)
18965
- */
18966
- getQueuedTasks() {
18967
- return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.QUEUED).sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
18968
- }
18969
- /**
18970
- * Get tasks by parent session
18971
- */
18972
- getTasksByParent(parentSessionID) {
18973
- return Array.from(this.tasks.values()).filter((t) => t.parentSessionID === parentSessionID);
18974
- }
18975
- /**
18976
- * Format duration since task started
18977
- */
18978
- formatDuration(startedAt) {
18979
- const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
18980
- if (seconds < 60) return `${seconds}s`;
18981
- const minutes = Math.floor(seconds / 60);
18982
- if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
18983
- const hours = Math.floor(minutes / 60);
18984
- return `${hours}h ${minutes % 60}m`;
18985
- }
18986
- /**
18987
- * Get concurrency info string (e.g., " [2/5]")
18988
- */
18989
- /**
18990
- * Get concurrency info string (e.g., " [2/5]")
18991
- */
18992
- getConcurrencyInfo() {
18993
- if (!this.concurrency) return "";
18994
- const running = this.getRunningTasks();
18995
- const queued = this.getQueuedTasks();
18996
- const total = running.length;
18997
- const limit = this.concurrency.getConcurrencyLimit("default");
18998
- if (limit === Infinity) return "";
18999
- const filled = TUI_BLOCKS.FILLED.repeat(total);
19000
- const empty = TUI_BLOCKS.EMPTY.repeat(Math.max(0, limit - total));
19001
- return ` [${filled}${empty} ${total}/${limit}]`;
19002
- }
19003
- /**
19004
- * Build consolidated task list message
19005
- */
19006
- buildTaskListMessage(newTask) {
19007
- const running = this.getRunningTasks();
19008
- const queued = this.getQueuedTasks();
19009
- const concurrencyInfo = this.getConcurrencyInfo();
19010
- const lines = [];
19011
- if (running.length > 0) {
19012
- lines.push(`${TUI_ICONS.RUNNING} Running (${running.length}) ${concurrencyInfo}`);
19013
- for (const task of running) {
19014
- const duration5 = this.formatDuration(task.startedAt);
19015
- const bgTag = task.isBackground ? TUI_TAGS.BACKGROUND : TUI_TAGS.FOREGROUND;
19016
- const isNew = newTask && task.id === newTask.id ? TUI_ICONS.NEW : "";
19017
- lines.push(`${bgTag} ${task.description} (${task.agent}) - ${duration5}${isNew}`);
19018
- }
19019
- }
19020
- if (queued.length > 0) {
19021
- if (lines.length > 0) lines.push("");
19022
- lines.push(`${TUI_ICONS.QUEUED} Queued (${queued.length}):`);
19023
- for (const task of queued) {
19024
- const bgTag = task.isBackground ? TUI_TAGS.WAITING : TUI_TAGS.PENDING;
19025
- lines.push(`${bgTag} ${task.description} (${task.agent})`);
19026
- }
19027
- }
19028
- return lines.join("\n");
19029
- }
19030
- /**
19031
- * Show consolidated toast with all running/queued tasks
19032
- */
19033
- showTaskListToast(newTask) {
19034
- if (!this.client || !this.client.tui) return;
19035
- const message = this.buildTaskListMessage(newTask);
19036
- const running = this.getRunningTasks();
19037
- const queued = this.getQueuedTasks();
19038
- const title = newTask.isBackground ? `Background Task Started` : `Task Started`;
19039
- this.client.tui.showToast({
19040
- body: {
19041
- title,
19042
- message: message || `${newTask.description} (${newTask.agent})`,
19043
- variant: STATUS_LABEL.INFO,
19044
- duration: running.length + queued.length > 2 ? 5e3 : 3e3
19045
- }
19046
- }).catch(() => {
19047
- });
19048
- }
19049
- /**
19050
- * Show task completion toast
19051
- */
19052
- showCompletionToast(info) {
19053
- if (!this.client || !this.client.tui) return;
19054
- this.removeTask(info.id);
19055
- const remaining = this.getRunningTasks();
19056
- const queued = this.getQueuedTasks();
19057
- let message;
19058
- let title;
19059
- let variant;
19060
- if (info.status === STATUS_LABEL.ERROR || info.status === STATUS_LABEL.CANCELLED || info.status === STATUS_LABEL.FAILED) {
19061
- title = info.status === STATUS_LABEL.ERROR ? "Task Failed" : "Task Cancelled";
19062
- message = `[FAIL] "${info.description}" ${info.status}
19063
- ${info.error || ""}`;
19064
- variant = STATUS_LABEL.ERROR;
19065
- } else {
19066
- title = "Task Completed";
19067
- message = `[DONE] "${info.description}" finished in ${info.duration}`;
19068
- variant = STATUS_LABEL.SUCCESS;
19069
- }
19070
- if (remaining.length > 0 || queued.length > 0) {
19071
- message += `
19072
-
19073
- Still running: ${remaining.length} | Queued: ${queued.length}`;
19074
- }
19075
- this.client.tui.showToast({
19076
- body: {
19077
- title,
19078
- message,
19079
- variant,
19080
- duration: 5e3
19081
- }
19082
- }).catch(() => {
19083
- });
19084
- }
19085
- /**
19086
- * Show all-tasks-complete summary toast
19087
- */
19088
- showAllCompleteToast(parentSessionID, completedTasks) {
19089
- if (!this.client || !this.client.tui) return;
19090
- const successCount = completedTasks.filter((t) => t.status === STATUS_LABEL.COMPLETED).length;
19091
- const failCount = completedTasks.filter((t) => t.status === STATUS_LABEL.ERROR || t.status === STATUS_LABEL.CANCELLED || t.status === STATUS_LABEL.FAILED).length;
19092
- const taskList = completedTasks.map((t) => `- [${t.status === STATUS_LABEL.COMPLETED ? "OK" : "FAIL"}] ${t.description} (${t.duration})`).join("\n");
19093
- this.client.tui.showToast({
19094
- body: {
19095
- title: "All Tasks Completed",
19096
- message: `${successCount} succeeded, ${failCount} failed
19097
-
19098
- ${taskList}`,
19099
- variant: failCount > 0 ? STATUS_LABEL.WARNING : STATUS_LABEL.SUCCESS,
19100
- duration: 7e3
19101
- }
19102
- }).catch(() => {
19103
- });
19104
- }
19105
- /**
19106
- * Show Mission Complete toast (Grand Finale)
19107
- */
19108
- showMissionCompleteToast(title = "Mission Complete", message = "All tasks completed successfully.") {
19109
- if (!this.client || !this.client.tui) return;
19110
- const decoratedMessage = `
19111
- ${TUI_ICONS.MISSION_COMPLETE} ${TUI_MESSAGES.MISSION_COMPLETE_TITLE} ${TUI_ICONS.MISSION_COMPLETE}
19112
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
19113
- ${message}
19114
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
19115
- ${TUI_MESSAGES.MISSION_COMPLETE_SUBTITLE}
19116
- `.trim();
19117
- this.client.tui.showToast({
19118
- body: {
19119
- title: `${TUI_ICONS.SHIELD} ${title}`,
19120
- message: decoratedMessage,
19121
- variant: STATUS_LABEL.SUCCESS,
19122
- duration: 1e4
19123
- // Longer duration for the finale
19124
- }
19125
- }).catch(() => {
19126
- });
19127
- }
19128
- /**
19129
- * Show progress toast (for long-running tasks)
19130
- */
19131
- showProgressToast(taskId, progress) {
19132
- if (!this.client || !this.client.tui) return;
19133
- const task = this.tasks.get(taskId);
19134
- if (!task) return;
19135
- const percentage = Math.round(progress.current / progress.total * 100);
19136
- const progressBar = `[${"#".repeat(Math.floor(percentage / 10))}${"-".repeat(10 - Math.floor(percentage / 10))}]`;
19137
- this.client.tui.showToast({
19138
- body: {
19139
- title: `Task Progress: ${task.description}`,
19140
- message: `${progressBar} ${percentage}%
19141
- ${progress.message || ""}`,
19142
- variant: STATUS_LABEL.INFO,
19143
- duration: 2e3
19144
- }
19145
- }).catch(() => {
19146
- });
19147
- }
19148
- /**
19149
- * Clear all tracked tasks
19150
- */
19151
- clear() {
19152
- this.tasks.clear();
19153
- }
19154
- /**
19155
- * Get task count stats
19156
- */
19157
- getStats() {
19158
- const running = this.getRunningTasks().length;
19159
- const queued = this.getQueuedTasks().length;
19160
- return { running, queued, total: this.tasks.size };
19161
- }
19162
- };
19163
- var instance = null;
19164
- function getTaskToastManager() {
19165
- return instance;
19166
- }
19167
- function initTaskToastManager(client2, concurrency) {
19168
- if (!instance) {
19169
- instance = new TaskToastManager();
19170
- }
19171
- instance.init(client2, concurrency);
19172
- return instance;
19173
- }
19174
18873
 
19175
18874
  // src/core/agents/persistence/task-wal.ts
19176
18875
  init_shared();
@@ -19266,130 +18965,1048 @@ var TaskWAL = class {
19266
18965
  };
19267
18966
  var taskWAL = new TaskWAL();
19268
18967
 
19269
- // src/core/recovery/constants.ts
18968
+ // src/core/notification/task-toast-manager.ts
19270
18969
  init_shared();
19271
- var MAX_RETRIES = RECOVERY.MAX_ATTEMPTS;
19272
- var BASE_DELAY = RECOVERY.BASE_DELAY_MS;
19273
- var MAX_HISTORY = HISTORY.MAX_RECOVERY;
18970
+ var TaskToastManager = class {
18971
+ tasks = /* @__PURE__ */ new Map();
18972
+ client = null;
18973
+ concurrency = null;
18974
+ todoSync = null;
18975
+ /**
18976
+ * Initialize the manager with OpenCode client
18977
+ */
18978
+ init(client2, concurrency) {
18979
+ this.client = client2;
18980
+ this.concurrency = concurrency ?? null;
18981
+ }
18982
+ /**
18983
+ * Set concurrency controller (can be set after init)
18984
+ */
18985
+ setConcurrencyController(concurrency) {
18986
+ this.concurrency = concurrency;
18987
+ }
18988
+ /**
18989
+ * Set TodoSyncService for TUI status synchronization
18990
+ */
18991
+ setTodoSync(service) {
18992
+ this.todoSync = service;
18993
+ }
18994
+ /**
18995
+ * Add a new task and show consolidated toast
18996
+ */
18997
+ addTask(task) {
18998
+ const trackedTask = {
18999
+ id: task.id,
19000
+ description: task.description,
19001
+ agent: task.agent,
19002
+ status: task.status ?? STATUS_LABEL.RUNNING,
19003
+ startedAt: /* @__PURE__ */ new Date(),
19004
+ isBackground: task.isBackground,
19005
+ parentSessionID: task.parentSessionID,
19006
+ sessionID: task.sessionID
19007
+ };
19008
+ this.tasks.set(task.id, trackedTask);
19009
+ this.todoSync?.updateTaskStatus(trackedTask);
19010
+ this.showTaskListToast(trackedTask);
19011
+ }
19012
+ /**
19013
+ * Update task status
19014
+ */
19015
+ updateTask(id, status) {
19016
+ const task = this.tasks.get(id);
19017
+ if (task) {
19018
+ task.status = status;
19019
+ this.todoSync?.updateTaskStatus(task);
19020
+ }
19021
+ }
19022
+ /**
19023
+ * Remove a task
19024
+ */
19025
+ removeTask(id) {
19026
+ this.tasks.delete(id);
19027
+ this.todoSync?.removeTask(id);
19028
+ }
19029
+ /**
19030
+ * Get all running tasks (newest first)
19031
+ */
19032
+ getRunningTasks() {
19033
+ return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.RUNNING).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
19034
+ }
19035
+ /**
19036
+ * Get all queued tasks (oldest first - FIFO)
19037
+ */
19038
+ getQueuedTasks() {
19039
+ return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.QUEUED).sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
19040
+ }
19041
+ /**
19042
+ * Get tasks by parent session
19043
+ */
19044
+ getTasksByParent(parentSessionID) {
19045
+ return Array.from(this.tasks.values()).filter((t) => t.parentSessionID === parentSessionID);
19046
+ }
19047
+ /**
19048
+ * Format duration since task started
19049
+ */
19050
+ formatDuration(startedAt) {
19051
+ const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
19052
+ if (seconds < 60) return `${seconds}s`;
19053
+ const minutes = Math.floor(seconds / 60);
19054
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
19055
+ const hours = Math.floor(minutes / 60);
19056
+ return `${hours}h ${minutes % 60}m`;
19057
+ }
19058
+ /**
19059
+ * Get concurrency info string (e.g., " [2/5]")
19060
+ */
19061
+ /**
19062
+ * Get concurrency info string (e.g., " [2/5]")
19063
+ */
19064
+ getConcurrencyInfo() {
19065
+ if (!this.concurrency) return "";
19066
+ const running = this.getRunningTasks();
19067
+ const queued = this.getQueuedTasks();
19068
+ const total = running.length;
19069
+ const limit = this.concurrency.getConcurrencyLimit("default");
19070
+ if (limit === Infinity) return "";
19071
+ const filled = TUI_BLOCKS.FILLED.repeat(total);
19072
+ const empty = TUI_BLOCKS.EMPTY.repeat(Math.max(0, limit - total));
19073
+ return ` [${filled}${empty} ${total}/${limit}]`;
19074
+ }
19075
+ /**
19076
+ * Build consolidated task list message
19077
+ */
19078
+ buildTaskListMessage(newTask) {
19079
+ const running = this.getRunningTasks();
19080
+ const queued = this.getQueuedTasks();
19081
+ const concurrencyInfo = this.getConcurrencyInfo();
19082
+ const lines = [];
19083
+ if (running.length > 0) {
19084
+ lines.push(`${TUI_ICONS.RUNNING} Running (${running.length}) ${concurrencyInfo}`);
19085
+ for (const task of running) {
19086
+ const duration5 = this.formatDuration(task.startedAt);
19087
+ const bgTag = task.isBackground ? TUI_TAGS.BACKGROUND : TUI_TAGS.FOREGROUND;
19088
+ const isNew = newTask && task.id === newTask.id ? TUI_ICONS.NEW : "";
19089
+ lines.push(`${bgTag} ${task.description} (${task.agent}) - ${duration5}${isNew}`);
19090
+ }
19091
+ }
19092
+ if (queued.length > 0) {
19093
+ if (lines.length > 0) lines.push("");
19094
+ lines.push(`${TUI_ICONS.QUEUED} Queued (${queued.length}):`);
19095
+ for (const task of queued) {
19096
+ const bgTag = task.isBackground ? TUI_TAGS.WAITING : TUI_TAGS.PENDING;
19097
+ lines.push(`${bgTag} ${task.description} (${task.agent})`);
19098
+ }
19099
+ }
19100
+ return lines.join("\n");
19101
+ }
19102
+ /**
19103
+ * Show consolidated toast with all running/queued tasks
19104
+ */
19105
+ showTaskListToast(newTask) {
19106
+ if (!this.client || !this.client.tui) return;
19107
+ const message = this.buildTaskListMessage(newTask);
19108
+ const running = this.getRunningTasks();
19109
+ const queued = this.getQueuedTasks();
19110
+ const title = newTask.isBackground ? `Background Task Started` : `Task Started`;
19111
+ this.client.tui.showToast({
19112
+ body: {
19113
+ title,
19114
+ message: message || `${newTask.description} (${newTask.agent})`,
19115
+ variant: STATUS_LABEL.INFO,
19116
+ duration: running.length + queued.length > 2 ? 5e3 : 3e3
19117
+ }
19118
+ }).catch(() => {
19119
+ });
19120
+ }
19121
+ /**
19122
+ * Show task completion toast
19123
+ */
19124
+ showCompletionToast(info) {
19125
+ if (!this.client || !this.client.tui) return;
19126
+ this.removeTask(info.id);
19127
+ const remaining = this.getRunningTasks();
19128
+ const queued = this.getQueuedTasks();
19129
+ let message;
19130
+ let title;
19131
+ let variant;
19132
+ if (info.status === STATUS_LABEL.ERROR || info.status === STATUS_LABEL.CANCELLED || info.status === STATUS_LABEL.FAILED) {
19133
+ title = info.status === STATUS_LABEL.ERROR ? "Task Failed" : "Task Cancelled";
19134
+ message = `[FAIL] "${info.description}" ${info.status}
19135
+ ${info.error || ""}`;
19136
+ variant = STATUS_LABEL.ERROR;
19137
+ } else {
19138
+ title = "Task Completed";
19139
+ message = `[DONE] "${info.description}" finished in ${info.duration}`;
19140
+ variant = STATUS_LABEL.SUCCESS;
19141
+ }
19142
+ if (remaining.length > 0 || queued.length > 0) {
19143
+ message += `
19274
19144
 
19275
- // src/core/notification/toast.ts
19276
- init_toast_core();
19277
- init_shared();
19145
+ Still running: ${remaining.length} | Queued: ${queued.length}`;
19146
+ }
19147
+ this.client.tui.showToast({
19148
+ body: {
19149
+ title,
19150
+ message,
19151
+ variant,
19152
+ duration: 5e3
19153
+ }
19154
+ }).catch(() => {
19155
+ });
19156
+ }
19157
+ /**
19158
+ * Show all-tasks-complete summary toast
19159
+ */
19160
+ showAllCompleteToast(parentSessionID, completedTasks) {
19161
+ if (!this.client || !this.client.tui) return;
19162
+ const successCount = completedTasks.filter((t) => t.status === STATUS_LABEL.COMPLETED).length;
19163
+ const failCount = completedTasks.filter((t) => t.status === STATUS_LABEL.ERROR || t.status === STATUS_LABEL.CANCELLED || t.status === STATUS_LABEL.FAILED).length;
19164
+ const taskList = completedTasks.map((t) => `- [${t.status === STATUS_LABEL.COMPLETED ? "OK" : "FAIL"}] ${t.description} (${t.duration})`).join("\n");
19165
+ this.client.tui.showToast({
19166
+ body: {
19167
+ title: "All Tasks Completed",
19168
+ message: `${successCount} succeeded, ${failCount} failed
19278
19169
 
19279
- // src/core/recovery/patterns.ts
19280
- var errorPatterns = [
19281
- // Rate limiting
19282
- {
19283
- pattern: /rate.?limit|too.?many.?requests|429/i,
19284
- category: "rate_limit",
19285
- handler: (ctx) => {
19286
- const delay = BASE_DELAY * Math.pow(2, ctx.attempt);
19287
- presets_exports.warningRateLimited();
19288
- return { type: "retry", delay, attempt: ctx.attempt + 1 };
19170
+ ${taskList}`,
19171
+ variant: failCount > 0 ? STATUS_LABEL.WARNING : STATUS_LABEL.SUCCESS,
19172
+ duration: 7e3
19173
+ }
19174
+ }).catch(() => {
19175
+ });
19176
+ }
19177
+ /**
19178
+ * Show Mission Complete toast (Grand Finale)
19179
+ */
19180
+ showMissionCompleteToast(title = "Mission Complete", message = "All tasks completed successfully.") {
19181
+ if (!this.client || !this.client.tui) return;
19182
+ const decoratedMessage = `
19183
+ ${TUI_ICONS.MISSION_COMPLETE} ${TUI_MESSAGES.MISSION_COMPLETE_TITLE} ${TUI_ICONS.MISSION_COMPLETE}
19184
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
19185
+ ${message}
19186
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
19187
+ ${TUI_MESSAGES.MISSION_COMPLETE_SUBTITLE}
19188
+ `.trim();
19189
+ this.client.tui.showToast({
19190
+ body: {
19191
+ title: `${TUI_ICONS.SHIELD} ${title}`,
19192
+ message: decoratedMessage,
19193
+ variant: STATUS_LABEL.SUCCESS,
19194
+ duration: 1e4
19195
+ // Longer duration for the finale
19196
+ }
19197
+ }).catch(() => {
19198
+ });
19199
+ }
19200
+ /**
19201
+ * Show progress toast (for long-running tasks)
19202
+ */
19203
+ showProgressToast(taskId, progress) {
19204
+ if (!this.client || !this.client.tui) return;
19205
+ const task = this.tasks.get(taskId);
19206
+ if (!task) return;
19207
+ const percentage = Math.round(progress.current / progress.total * 100);
19208
+ const progressBar = `[${"#".repeat(Math.floor(percentage / 10))}${"-".repeat(10 - Math.floor(percentage / 10))}]`;
19209
+ this.client.tui.showToast({
19210
+ body: {
19211
+ title: `Task Progress: ${task.description}`,
19212
+ message: `${progressBar} ${percentage}%
19213
+ ${progress.message || ""}`,
19214
+ variant: STATUS_LABEL.INFO,
19215
+ duration: 2e3
19216
+ }
19217
+ }).catch(() => {
19218
+ });
19219
+ }
19220
+ /**
19221
+ * Clear all tracked tasks
19222
+ */
19223
+ clear() {
19224
+ this.tasks.clear();
19225
+ }
19226
+ /**
19227
+ * Get task count stats
19228
+ */
19229
+ getStats() {
19230
+ const running = this.getRunningTasks().length;
19231
+ const queued = this.getQueuedTasks().length;
19232
+ return { running, queued, total: this.tasks.size };
19233
+ }
19234
+ };
19235
+ var instance = null;
19236
+ function getTaskToastManager() {
19237
+ return instance;
19238
+ }
19239
+ function initTaskToastManager(client2, concurrency) {
19240
+ if (!instance) {
19241
+ instance = new TaskToastManager();
19242
+ }
19243
+ instance.init(client2, concurrency);
19244
+ return instance;
19245
+ }
19246
+
19247
+ // src/core/agents/unified-task-executor.ts
19248
+ init_shared();
19249
+ var UnifiedTaskExecutor = class {
19250
+ client;
19251
+ directory;
19252
+ store;
19253
+ concurrency;
19254
+ sessionPool;
19255
+ // Polling state
19256
+ pollInterval = null;
19257
+ isPolling = false;
19258
+ // Cleanup state
19259
+ cleanupTimers = /* @__PURE__ */ new Map();
19260
+ constructor(client2, directory, store, concurrency, sessionPool2) {
19261
+ this.client = client2;
19262
+ this.directory = directory;
19263
+ this.store = store;
19264
+ this.concurrency = concurrency;
19265
+ this.sessionPool = sessionPool2;
19266
+ }
19267
+ // ========================================================================
19268
+ // LAUNCH: Task Creation and Execution
19269
+ // ========================================================================
19270
+ /**
19271
+ * Launch a new parallel task
19272
+ */
19273
+ async launch(input) {
19274
+ const depth = input.depth ?? 1;
19275
+ if (depth >= PARALLEL_TASK.MAX_DEPTH) {
19276
+ throw new Error(`Max depth ${PARALLEL_TASK.MAX_DEPTH} exceeded (current: ${depth})`);
19277
+ }
19278
+ const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
19279
+ const concurrencyKey = input.agent;
19280
+ await this.concurrency.acquire(concurrencyKey);
19281
+ let sessionID;
19282
+ try {
19283
+ const session = await this.sessionPool.acquire(
19284
+ input.agent,
19285
+ input.parentSessionID,
19286
+ input.description
19287
+ );
19288
+ sessionID = session.id;
19289
+ const task = {
19290
+ id: taskId,
19291
+ sessionID,
19292
+ parentSessionID: input.parentSessionID,
19293
+ description: input.description,
19294
+ prompt: input.prompt,
19295
+ agent: input.agent,
19296
+ status: TASK_STATUS.RUNNING,
19297
+ startedAt: /* @__PURE__ */ new Date(),
19298
+ depth,
19299
+ concurrencyKey,
19300
+ mode: input.mode,
19301
+ groupID: input.groupID,
19302
+ progress: {
19303
+ toolCalls: 0,
19304
+ lastTool: void 0,
19305
+ lastUpdate: /* @__PURE__ */ new Date()
19306
+ }
19307
+ };
19308
+ this.store.set(taskId, task);
19309
+ this.store.trackPending(input.parentSessionID, taskId);
19310
+ await taskWAL.log(WAL_ACTIONS.LAUNCH, task);
19311
+ log(`[UnifiedExecutor] Task launched: ${taskId} (agent: ${input.agent}, depth: ${input.depth})`);
19312
+ const toastManager = getTaskToastManager();
19313
+ if (toastManager) {
19314
+ toastManager.addTask({
19315
+ id: taskId,
19316
+ description: input.description,
19317
+ agent: input.agent,
19318
+ isBackground: false,
19319
+ parentSessionID: input.parentSessionID,
19320
+ sessionID,
19321
+ status: TASK_STATUS.RUNNING
19322
+ });
19323
+ }
19324
+ await this.client.session.prompt({
19325
+ path: { id: sessionID },
19326
+ body: {
19327
+ agent: input.agent,
19328
+ tools: {},
19329
+ parts: [
19330
+ {
19331
+ type: PART_TYPES.TEXT,
19332
+ text: input.prompt
19333
+ }
19334
+ ]
19335
+ }
19336
+ });
19337
+ this.startPolling();
19338
+ return task;
19339
+ } catch (error92) {
19340
+ if (sessionID) {
19341
+ await this.sessionPool.release(sessionID);
19342
+ }
19343
+ this.concurrency.release(concurrencyKey);
19344
+ log(`[UnifiedExecutor] Launch failed: ${error92}`);
19345
+ throw error92;
19289
19346
  }
19290
- },
19291
- // Context overflow
19292
- {
19293
- pattern: /context.?length|token.?limit|maximum.?context/i,
19294
- category: "context_overflow",
19295
- handler: () => {
19296
- presets_exports.errorRecovery("Compacting context");
19297
- return { type: "compact", reason: "Context limit reached" };
19347
+ }
19348
+ // ========================================================================
19349
+ // POLLING: Monitor Task Execution
19350
+ // ========================================================================
19351
+ /**
19352
+ * Start polling for task updates
19353
+ */
19354
+ startPolling() {
19355
+ if (this.isPolling) return;
19356
+ this.isPolling = true;
19357
+ this.pollInterval = setInterval(() => {
19358
+ this.pollTasks().catch((err) => {
19359
+ log(`[UnifiedExecutor] Poll error: ${err}`);
19360
+ });
19361
+ }, PARALLEL_TASK.POLL_INTERVAL_MS);
19362
+ this.pollInterval.unref?.();
19363
+ }
19364
+ /**
19365
+ * Stop polling
19366
+ */
19367
+ stopPolling() {
19368
+ if (this.pollInterval) {
19369
+ clearInterval(this.pollInterval);
19370
+ this.pollInterval = null;
19298
19371
  }
19299
- },
19300
- // Network errors
19301
- {
19302
- pattern: /ECONNREFUSED|ETIMEDOUT|network|fetch.?failed/i,
19303
- category: "network",
19304
- handler: (ctx) => {
19305
- if (ctx.attempt >= MAX_RETRIES) {
19306
- return { type: "abort", reason: "Network unavailable after retries" };
19372
+ this.isPolling = false;
19373
+ }
19374
+ /**
19375
+ * Poll all running tasks
19376
+ */
19377
+ async pollTasks() {
19378
+ const runningTasks = this.store.getRunning();
19379
+ if (runningTasks.length === 0) {
19380
+ this.stopPolling();
19381
+ return;
19382
+ }
19383
+ for (const task of runningTasks) {
19384
+ await this.pollTask(task);
19385
+ }
19386
+ this.pruneExpiredTasks();
19387
+ }
19388
+ /**
19389
+ * Poll a single task
19390
+ */
19391
+ async pollTask(task) {
19392
+ try {
19393
+ const result = await this.client.session.get({
19394
+ path: { id: task.sessionID }
19395
+ });
19396
+ if ("error" in result && result.error) {
19397
+ const errorMessage = "Session error";
19398
+ await this.handleTaskError(task.id, new Error(errorMessage));
19399
+ return;
19307
19400
  }
19308
- return { type: "retry", delay: BASE_DELAY * (ctx.attempt + 1), attempt: ctx.attempt + 1 };
19401
+ const session = result.data;
19402
+ if (!session) {
19403
+ await this.handleTaskError(task.id, new Error("Session not found"));
19404
+ return;
19405
+ }
19406
+ const messageCount = session.messages?.length || 0;
19407
+ if (task.progress) {
19408
+ task.progress.lastUpdate = /* @__PURE__ */ new Date();
19409
+ }
19410
+ const isBusy = session.status === "busy" || session.busy;
19411
+ const hasOutput = messageCount > 1;
19412
+ if (!isBusy && hasOutput) {
19413
+ if (task.lastMsgCount === messageCount) {
19414
+ task.stablePolls = (task.stablePolls || 0) + 1;
19415
+ } else {
19416
+ task.stablePolls = 0;
19417
+ task.lastMsgCount = messageCount;
19418
+ }
19419
+ if (task.stablePolls >= PARALLEL_TASK.STABLE_POLLS_REQUIRED) {
19420
+ await this.handleTaskComplete(task);
19421
+ }
19422
+ } else {
19423
+ task.stablePolls = 0;
19424
+ }
19425
+ } catch (error92) {
19426
+ log(`[UnifiedExecutor] Poll task ${task.id} error: ${error92}`);
19309
19427
  }
19310
- },
19311
- // Session errors
19312
- {
19313
- pattern: /session.?not.?found|session.?expired/i,
19314
- category: "session",
19315
- handler: () => {
19316
- return { type: "abort", reason: "Session no longer available" };
19428
+ }
19429
+ // ========================================================================
19430
+ // COMPLETION: Handle Task Completion
19431
+ // ========================================================================
19432
+ /**
19433
+ * Handle task completion
19434
+ */
19435
+ async handleTaskComplete(task) {
19436
+ log(`[UnifiedExecutor] Task complete: ${task.id}`);
19437
+ task.status = TASK_STATUS.COMPLETED;
19438
+ task.completedAt = /* @__PURE__ */ new Date();
19439
+ await taskWAL.log(WAL_ACTIONS.UPDATE, task);
19440
+ await this.sessionPool.release(task.sessionID);
19441
+ this.concurrency.release(task.concurrencyKey || task.agent);
19442
+ this.concurrency.reportResult(task.concurrencyKey || task.agent, true);
19443
+ this.store.untrackPending(task.parentSessionID, task.id);
19444
+ const toastManager = getTaskToastManager();
19445
+ if (toastManager) {
19446
+ toastManager.updateTask(task.id, TASK_STATUS.COMPLETED);
19317
19447
  }
19318
- },
19319
- // Tool errors
19320
- {
19321
- pattern: /tool.?not.?found|unknown.?tool/i,
19322
- category: "tool",
19323
- handler: (ctx) => {
19324
- return { type: "escalate", to: "Reviewer", reason: `Unknown tool used by ${ctx.agent}` };
19448
+ this.scheduleCleanup(task.id);
19449
+ await this.notifyParentIfAllComplete(task.parentSessionID);
19450
+ }
19451
+ /**
19452
+ * Handle task error
19453
+ */
19454
+ async handleTaskError(taskId, error92) {
19455
+ const task = this.store.get(taskId);
19456
+ if (!task) return;
19457
+ log(`[UnifiedExecutor] Task error: ${taskId} - ${error92.message}`);
19458
+ task.status = TASK_STATUS.ERROR;
19459
+ task.completedAt = /* @__PURE__ */ new Date();
19460
+ task.error = error92.message;
19461
+ await taskWAL.log(WAL_ACTIONS.UPDATE, task);
19462
+ await this.sessionPool.release(task.sessionID);
19463
+ this.concurrency.release(task.concurrencyKey || task.agent);
19464
+ this.concurrency.reportResult(task.concurrencyKey || task.agent, false);
19465
+ this.store.untrackPending(task.parentSessionID, task.id);
19466
+ const toastManager = getTaskToastManager();
19467
+ if (toastManager) {
19468
+ toastManager.updateTask(task.id, TASK_STATUS.ERROR);
19325
19469
  }
19326
- },
19327
- // Parse errors
19328
- {
19329
- pattern: /parse.?error|invalid.?json|syntax.?error/i,
19330
- category: "parse",
19331
- handler: (ctx) => {
19332
- if (ctx.attempt >= 2) {
19333
- return { type: "skip", reason: "Persistent parse error" };
19470
+ this.scheduleCleanup(task.id);
19471
+ await this.notifyParentIfAllComplete(task.parentSessionID);
19472
+ }
19473
+ // ========================================================================
19474
+ // CLEANUP: Task Cleanup and GC
19475
+ // ========================================================================
19476
+ /**
19477
+ * Schedule cleanup for a task
19478
+ */
19479
+ scheduleCleanup(taskId) {
19480
+ const timer = setTimeout(() => {
19481
+ this.cleanupTask(taskId);
19482
+ this.cleanupTimers.delete(taskId);
19483
+ }, PARALLEL_TASK.CLEANUP_DELAY_MS);
19484
+ this.cleanupTimers.set(taskId, timer);
19485
+ }
19486
+ /**
19487
+ * Cleanup a task
19488
+ */
19489
+ async cleanupTask(taskId) {
19490
+ const task = this.store.get(taskId);
19491
+ if (!task) return;
19492
+ log(`[UnifiedExecutor] Cleaning up task: ${taskId}`);
19493
+ this.store.delete(taskId);
19494
+ if (task) {
19495
+ await taskWAL.log(WAL_ACTIONS.DELETE, task);
19496
+ }
19497
+ const toastManager = getTaskToastManager();
19498
+ if (toastManager) {
19499
+ toastManager.removeTask(taskId);
19500
+ }
19501
+ }
19502
+ /**
19503
+ * Prune expired tasks
19504
+ */
19505
+ pruneExpiredTasks() {
19506
+ const now = Date.now();
19507
+ const tasks = this.store.getAll();
19508
+ for (const task of tasks) {
19509
+ const age = now - task.startedAt.getTime();
19510
+ if (age > PARALLEL_TASK.TTL_MS) {
19511
+ log(`[UnifiedExecutor] Pruning expired task: ${task.id} (age: ${age}ms)`);
19512
+ this.handleTaskError(task.id, new Error("Task timeout"));
19334
19513
  }
19335
- return { type: "retry", delay: 500, attempt: ctx.attempt + 1 };
19336
19514
  }
19337
- },
19338
- // Gibberish / hallucination
19339
- {
19340
- pattern: /gibberish|hallucination|mixed.?language/i,
19341
- category: "gibberish",
19342
- handler: () => {
19343
- presets_exports.errorRecovery("Retrying with clean context");
19344
- return { type: "retry", delay: 1e3, attempt: 1 };
19515
+ }
19516
+ // ========================================================================
19517
+ // NOTIFICATION: Parent Notification
19518
+ // ========================================================================
19519
+ /**
19520
+ * Notify parent if all tasks complete
19521
+ */
19522
+ async notifyParentIfAllComplete(parentSessionID) {
19523
+ const pendingCount = this.store.getPendingCount(parentSessionID);
19524
+ if (pendingCount === 0) {
19525
+ log(`[UnifiedExecutor] All tasks complete for parent: ${parentSessionID}`);
19526
+ const tasks = this.store.getByParent(parentSessionID);
19527
+ const summary = tasks.map(
19528
+ (t) => `- ${t.status === TASK_STATUS.COMPLETED ? "\u2705" : "\u274C"} ${t.description}`
19529
+ ).join("\n");
19530
+ try {
19531
+ await this.client.session.prompt({
19532
+ path: { id: parentSessionID },
19533
+ body: {
19534
+ agent: "",
19535
+ tools: {},
19536
+ parts: [{
19537
+ type: PART_TYPES.TEXT,
19538
+ text: `All delegated tasks complete:
19539
+
19540
+ ${summary}`
19541
+ }]
19542
+ }
19543
+ });
19544
+ } catch (error92) {
19545
+ log(`[UnifiedExecutor] Failed to notify parent: ${error92}`);
19546
+ }
19547
+ this.store.clearNotifications(parentSessionID);
19345
19548
  }
19346
- },
19347
- // LSP specific errors
19348
- {
19349
- pattern: /lsp.?diagnostics|tsc.?error|eslint.?error/i,
19350
- category: "lsp",
19351
- handler: (ctx) => {
19352
- return { type: "retry", delay: 2e3, attempt: ctx.attempt + 1, modifyPrompt: "Note: Previous attempt had LSP issues. Ensure code quality and consider simpler implementation." };
19549
+ }
19550
+ // ========================================================================
19551
+ // RESUME: Task Recovery
19552
+ // ========================================================================
19553
+ /**
19554
+ * Resume a task from disk
19555
+ */
19556
+ async resume(input) {
19557
+ log(`[UnifiedExecutor] Resuming task: ${input.sessionId}`);
19558
+ const tasks = await taskWAL.readAll();
19559
+ const task = tasks.get(input.sessionId);
19560
+ if (!task) {
19561
+ log(`[UnifiedExecutor] No task found to resume: ${input.sessionId}`);
19562
+ return null;
19353
19563
  }
19354
- },
19355
- // execution timeout
19356
- {
19357
- pattern: /execution.?timed.?out|timeout/i,
19358
- category: "timeout",
19359
- handler: (ctx) => {
19360
- return { type: "retry", delay: 5e3, attempt: ctx.attempt + 1, modifyPrompt: "Note: Previous attempt timed out. Break down the task into smaller sub-tasks if necessary." };
19564
+ this.store.set(task.id, task);
19565
+ this.store.trackPending(task.parentSessionID, task.id);
19566
+ this.startPolling();
19567
+ log(`[UnifiedExecutor] Task resumed: ${task.id}`);
19568
+ return task;
19569
+ }
19570
+ /**
19571
+ * Recover all active tasks from disk
19572
+ */
19573
+ async recoverAll() {
19574
+ log("[UnifiedExecutor] Recovering tasks from WAL...");
19575
+ const tasks = await taskWAL.readAll();
19576
+ let recovered = 0;
19577
+ for (const [sessionID, task] of tasks) {
19578
+ if (task.status === TASK_STATUS.RUNNING || task.status === TASK_STATUS.PENDING) {
19579
+ try {
19580
+ const result = await this.client.session.get({ path: { id: sessionID } });
19581
+ if (result.data) {
19582
+ this.store.set(task.id, task);
19583
+ this.store.trackPending(task.parentSessionID, task.id);
19584
+ recovered++;
19585
+ }
19586
+ } catch {
19587
+ }
19588
+ }
19361
19589
  }
19590
+ if (recovered > 0) {
19591
+ log(`[UnifiedExecutor] Recovered ${recovered} tasks`);
19592
+ this.startPolling();
19593
+ }
19594
+ return recovered;
19362
19595
  }
19363
- ];
19596
+ // ========================================================================
19597
+ // UTILITIES
19598
+ // ========================================================================
19599
+ /**
19600
+ * Cancel a task
19601
+ */
19602
+ async cancel(taskId) {
19603
+ const task = this.store.get(taskId);
19604
+ if (!task) return false;
19605
+ log(`[UnifiedExecutor] Cancelling task: ${taskId}`);
19606
+ try {
19607
+ await this.client.session.delete({ path: { id: task.sessionID } });
19608
+ } catch {
19609
+ }
19610
+ await this.handleTaskError(taskId, new Error("Cancelled by user"));
19611
+ return true;
19612
+ }
19613
+ /**
19614
+ * Get task by ID
19615
+ */
19616
+ getTask(taskId) {
19617
+ return this.store.get(taskId);
19618
+ }
19619
+ /**
19620
+ * Get all tasks
19621
+ */
19622
+ getAllTasks() {
19623
+ return this.store.getAll();
19624
+ }
19625
+ /**
19626
+ * Get tasks by parent
19627
+ */
19628
+ getTasksByParent(parentSessionID) {
19629
+ return this.store.getByParent(parentSessionID);
19630
+ }
19631
+ /**
19632
+ * Cleanup all resources
19633
+ */
19634
+ cleanup() {
19635
+ log("[UnifiedExecutor] Cleaning up...");
19636
+ this.stopPolling();
19637
+ for (const timer of this.cleanupTimers.values()) {
19638
+ clearTimeout(timer);
19639
+ }
19640
+ this.cleanupTimers.clear();
19641
+ this.pruneExpiredTasks();
19642
+ }
19643
+ };
19364
19644
 
19365
- // src/core/recovery/handler.ts
19366
- var recoveryHistory = [];
19367
- function handleError(context) {
19368
- const errorMessage = context.error.message || String(context.error);
19369
- for (const pattern of errorPatterns) {
19370
- const matches = typeof pattern.pattern === "string" ? errorMessage.includes(pattern.pattern) : pattern.pattern.test(errorMessage);
19371
- if (matches) {
19372
- const action = pattern.handler(context);
19373
- recoveryHistory.push({
19374
- context,
19375
- action,
19376
- timestamp: /* @__PURE__ */ new Date()
19377
- });
19378
- if (recoveryHistory.length > MAX_HISTORY) {
19379
- recoveryHistory.shift();
19645
+ // src/core/agents/session-pool.ts
19646
+ init_shared();
19647
+ var DEFAULT_CONFIG = {
19648
+ maxPoolSizePerAgent: 5,
19649
+ idleTimeoutMs: 3e5,
19650
+ // 5 minutes
19651
+ maxReuseCount: 10,
19652
+ healthCheckIntervalMs: 6e4
19653
+ // 1 minute
19654
+ };
19655
+ var SessionPool = class _SessionPool {
19656
+ static _instance;
19657
+ pool = /* @__PURE__ */ new Map();
19658
+ // key: agentName
19659
+ sessionsById = /* @__PURE__ */ new Map();
19660
+ config;
19661
+ client;
19662
+ directory;
19663
+ healthCheckInterval = null;
19664
+ // Statistics
19665
+ stats = {
19666
+ reuseHits: 0,
19667
+ creationMisses: 0
19668
+ };
19669
+ constructor(client2, directory, config3 = {}) {
19670
+ this.client = client2;
19671
+ this.directory = directory;
19672
+ this.config = { ...DEFAULT_CONFIG, ...config3 };
19673
+ this.startHealthCheck();
19674
+ }
19675
+ static getInstance(client2, directory, config3) {
19676
+ if (!_SessionPool._instance) {
19677
+ if (!client2 || !directory) {
19678
+ throw new Error("SessionPool requires client and directory on first call");
19380
19679
  }
19381
- return action;
19680
+ _SessionPool._instance = new _SessionPool(client2, directory, config3);
19382
19681
  }
19682
+ return _SessionPool._instance;
19383
19683
  }
19384
- if (context.attempt < MAX_RETRIES) {
19684
+ /**
19685
+ * Acquire a session from the pool or create a new one.
19686
+ * Sessions are validated before reuse to ensure health.
19687
+ */
19688
+ async acquire(agentName, parentSessionID, description) {
19689
+ const poolKey = this.getPoolKey(agentName);
19690
+ const agentPool = this.pool.get(poolKey) || [];
19691
+ const candidates = agentPool.filter((s) => !s.inUse && s.reuseCount < this.config.maxReuseCount);
19692
+ for (const candidate of candidates) {
19693
+ const isHealthy = await this.validateSessionHealth(candidate.id);
19694
+ if (isHealthy) {
19695
+ candidate.inUse = true;
19696
+ candidate.lastUsedAt = /* @__PURE__ */ new Date();
19697
+ candidate.reuseCount++;
19698
+ candidate.health = "healthy";
19699
+ this.stats.reuseHits++;
19700
+ log(`[SessionPool] Reusing session ${candidate.id.slice(0, 8)}... for ${agentName} (reuse #${candidate.reuseCount})`);
19701
+ return candidate;
19702
+ } else {
19703
+ log(`[SessionPool] Session ${candidate.id.slice(0, 8)}... failed health check, removing`);
19704
+ await this.deleteSession(candidate.id);
19705
+ }
19706
+ }
19707
+ this.stats.creationMisses++;
19708
+ return this.createSession(agentName, parentSessionID, description);
19709
+ }
19710
+ /**
19711
+ * Release a session back to the pool for reuse.
19712
+ */
19713
+ async release(sessionId) {
19714
+ const session = this.sessionsById.get(sessionId);
19715
+ if (!session) {
19716
+ log(`[SessionPool] Session ${sessionId.slice(0, 8)}... not found in pool`);
19717
+ return;
19718
+ }
19719
+ const now = Date.now();
19720
+ const age = now - session.createdAt.getTime();
19721
+ const idle = now - session.lastUsedAt.getTime();
19722
+ if (session.reuseCount >= this.config.maxReuseCount || age > this.config.idleTimeoutMs * 2) {
19723
+ await this.invalidate(sessionId);
19724
+ return;
19725
+ }
19726
+ const poolKey = this.getPoolKey(session.agentName);
19727
+ const agentPool = this.pool.get(poolKey) || [];
19728
+ const availableCount = agentPool.filter((s) => !s.inUse).length;
19729
+ if (availableCount >= this.config.maxPoolSizePerAgent) {
19730
+ const oldest = agentPool.filter((s) => !s.inUse).sort((a, b) => a.lastUsedAt.getTime() - b.lastUsedAt.getTime())[0];
19731
+ if (oldest) {
19732
+ await this.deleteSession(oldest.id);
19733
+ }
19734
+ }
19735
+ await this.resetSession(sessionId);
19736
+ session.inUse = false;
19737
+ log(`[SessionPool] Released session ${sessionId.slice(0, 8)}... to pool`);
19738
+ }
19739
+ /**
19740
+ * Invalidate a session (remove from pool and delete).
19741
+ */
19742
+ async invalidate(sessionId) {
19743
+ const session = this.sessionsById.get(sessionId);
19744
+ if (!session) return;
19745
+ await this.deleteSession(sessionId);
19746
+ log(`[SessionPool] Invalidated session ${sessionId.slice(0, 8)}...`);
19747
+ }
19748
+ /**
19749
+ * Get current pool statistics.
19750
+ */
19751
+ getStats() {
19752
+ const byAgent = {};
19753
+ for (const [agentName, sessions] of this.pool.entries()) {
19754
+ const inUse = sessions.filter((s) => s.inUse).length;
19755
+ byAgent[agentName] = {
19756
+ total: sessions.length,
19757
+ inUse,
19758
+ available: sessions.length - inUse
19759
+ };
19760
+ }
19761
+ const allSessions = Array.from(this.sessionsById.values());
19762
+ const inUseCount = allSessions.filter((s) => s.inUse).length;
19385
19763
  return {
19386
- type: "retry",
19387
- delay: BASE_DELAY * Math.pow(2, context.attempt),
19388
- attempt: context.attempt + 1
19764
+ totalSessions: allSessions.length,
19765
+ sessionsInUse: inUseCount,
19766
+ availableSessions: allSessions.length - inUseCount,
19767
+ reuseHits: this.stats.reuseHits,
19768
+ creationMisses: this.stats.creationMisses,
19769
+ byAgent
19389
19770
  };
19390
19771
  }
19391
- return { type: "abort", reason: `Unknown error after ${MAX_RETRIES} retries` };
19392
- }
19772
+ /**
19773
+ * Cleanup stale sessions.
19774
+ */
19775
+ async cleanup() {
19776
+ const now = Date.now();
19777
+ let cleanedCount = 0;
19778
+ for (const [sessionId, session] of this.sessionsById.entries()) {
19779
+ if (session.inUse) continue;
19780
+ const idle = now - session.lastUsedAt.getTime();
19781
+ if (idle > this.config.idleTimeoutMs) {
19782
+ await this.deleteSession(sessionId);
19783
+ cleanedCount++;
19784
+ }
19785
+ }
19786
+ if (cleanedCount > 0) {
19787
+ log(`[SessionPool] Cleaned up ${cleanedCount} stale sessions`);
19788
+ }
19789
+ return cleanedCount;
19790
+ }
19791
+ /**
19792
+ * Shutdown the pool.
19793
+ */
19794
+ async shutdown() {
19795
+ log("[SessionPool] Shutting down...");
19796
+ if (this.healthCheckInterval) {
19797
+ clearInterval(this.healthCheckInterval);
19798
+ this.healthCheckInterval = null;
19799
+ }
19800
+ const deletePromises = Array.from(this.sessionsById.keys()).map(
19801
+ (id) => this.deleteSession(id).catch(() => {
19802
+ })
19803
+ );
19804
+ await Promise.all(deletePromises);
19805
+ this.pool.clear();
19806
+ this.sessionsById.clear();
19807
+ log("[SessionPool] Shutdown complete");
19808
+ }
19809
+ // =========================================================================
19810
+ // Private Methods
19811
+ // =========================================================================
19812
+ /**
19813
+ * Reset/Compact a session to clear context for next reuse.
19814
+ */
19815
+ async resetSession(sessionId) {
19816
+ const session = this.sessionsById.get(sessionId);
19817
+ if (!session) return;
19818
+ log(`[SessionPool] Resetting session ${sessionId.slice(0, 8)}...`);
19819
+ try {
19820
+ await this.client.session.compact?.({ path: { id: sessionId } });
19821
+ session.lastResetAt = /* @__PURE__ */ new Date();
19822
+ session.health = "healthy";
19823
+ } catch (error92) {
19824
+ log(`[SessionPool] Failed to reset session ${sessionId.slice(0, 8)}: ${error92}`);
19825
+ session.health = "degraded";
19826
+ }
19827
+ }
19828
+ getPoolKey(agentName) {
19829
+ return agentName;
19830
+ }
19831
+ async createSession(agentName, parentSessionID, description) {
19832
+ log(`[SessionPool] Creating new session for ${agentName}`);
19833
+ const result = await Promise.race([
19834
+ this.client.session.create({
19835
+ body: {
19836
+ parentID: parentSessionID,
19837
+ title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${description}`
19838
+ },
19839
+ query: { directory: this.directory }
19840
+ }),
19841
+ new Promise(
19842
+ (_, reject) => setTimeout(() => reject(new Error("Session creation timed out after 60s")), 6e4)
19843
+ )
19844
+ ]);
19845
+ if (result.error || !result.data?.id) {
19846
+ throw new Error(`Session creation failed: ${result.error || "No ID"}`);
19847
+ }
19848
+ const session = {
19849
+ id: result.data.id,
19850
+ agentName,
19851
+ projectDirectory: this.directory,
19852
+ createdAt: /* @__PURE__ */ new Date(),
19853
+ lastUsedAt: /* @__PURE__ */ new Date(),
19854
+ reuseCount: 0,
19855
+ inUse: true,
19856
+ health: "healthy",
19857
+ lastResetAt: /* @__PURE__ */ new Date()
19858
+ };
19859
+ const poolKey = this.getPoolKey(agentName);
19860
+ const agentPool = this.pool.get(poolKey) || [];
19861
+ agentPool.push(session);
19862
+ this.pool.set(poolKey, agentPool);
19863
+ this.sessionsById.set(session.id, session);
19864
+ return session;
19865
+ }
19866
+ async deleteSession(sessionId) {
19867
+ const session = this.sessionsById.get(sessionId);
19868
+ if (!session) return;
19869
+ this.sessionsById.delete(sessionId);
19870
+ const poolKey = this.getPoolKey(session.agentName);
19871
+ const agentPool = this.pool.get(poolKey);
19872
+ if (agentPool) {
19873
+ const idx = agentPool.findIndex((s) => s.id === sessionId);
19874
+ if (idx !== -1) {
19875
+ agentPool.splice(idx, 1);
19876
+ }
19877
+ if (agentPool.length === 0) {
19878
+ this.pool.delete(poolKey);
19879
+ }
19880
+ }
19881
+ try {
19882
+ await this.client.session.delete({ path: { id: sessionId } });
19883
+ } catch {
19884
+ }
19885
+ }
19886
+ /**
19887
+ * Validate session health by checking if it exists on the server.
19888
+ */
19889
+ async validateSessionHealth(sessionId) {
19890
+ try {
19891
+ const result = await Promise.race([
19892
+ this.client.session.get({ path: { id: sessionId } }),
19893
+ new Promise(
19894
+ (_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
19895
+ )
19896
+ ]);
19897
+ if (result.error || !result.data) {
19898
+ return false;
19899
+ }
19900
+ return true;
19901
+ } catch (error92) {
19902
+ log(`[SessionPool] Health check failed for session ${sessionId.slice(0, 8)}: ${error92}`);
19903
+ return false;
19904
+ }
19905
+ }
19906
+ startHealthCheck() {
19907
+ this.healthCheckInterval = setInterval(() => {
19908
+ this.cleanup().catch(() => {
19909
+ });
19910
+ }, this.config.healthCheckIntervalMs);
19911
+ this.healthCheckInterval.unref?.();
19912
+ }
19913
+ };
19914
+ var sessionPool = {
19915
+ getInstance: SessionPool.getInstance.bind(SessionPool)
19916
+ };
19917
+
19918
+ // src/core/progress/state-broadcaster.ts
19919
+ var StateBroadcaster = class _StateBroadcaster {
19920
+ static _instance;
19921
+ listeners = /* @__PURE__ */ new Set();
19922
+ currentState = null;
19923
+ constructor() {
19924
+ }
19925
+ static getInstance() {
19926
+ if (!_StateBroadcaster._instance) {
19927
+ _StateBroadcaster._instance = new _StateBroadcaster();
19928
+ }
19929
+ return _StateBroadcaster._instance;
19930
+ }
19931
+ subscribe(listener) {
19932
+ this.listeners.add(listener);
19933
+ if (this.currentState) {
19934
+ listener(this.currentState);
19935
+ }
19936
+ return () => this.listeners.delete(listener);
19937
+ }
19938
+ broadcast(state2) {
19939
+ this.currentState = state2;
19940
+ this.listeners.forEach((listener) => {
19941
+ try {
19942
+ listener(state2);
19943
+ } catch (error92) {
19944
+ }
19945
+ });
19946
+ }
19947
+ getCurrentState() {
19948
+ return this.currentState;
19949
+ }
19950
+ };
19951
+ var stateBroadcaster = StateBroadcaster.getInstance();
19952
+
19953
+ // src/core/progress/progress-notifier.ts
19954
+ init_shared();
19955
+ var ProgressNotifier = class _ProgressNotifier {
19956
+ static _instance;
19957
+ manager = null;
19958
+ constructor() {
19959
+ stateBroadcaster.subscribe(this.handleStateChange.bind(this));
19960
+ }
19961
+ static getInstance() {
19962
+ if (!_ProgressNotifier._instance) {
19963
+ _ProgressNotifier._instance = new _ProgressNotifier();
19964
+ }
19965
+ return _ProgressNotifier._instance;
19966
+ }
19967
+ setManager(manager) {
19968
+ this.manager = manager;
19969
+ }
19970
+ /**
19971
+ * Poll current status from ParallelAgentManager and broadcast it
19972
+ */
19973
+ update() {
19974
+ if (!this.manager) return;
19975
+ const tasks = this.manager.getAllTasks();
19976
+ const running = tasks.filter((t) => t.status === TASK_STATUS.RUNNING);
19977
+ const completed = tasks.filter((t) => t.status === TASK_STATUS.COMPLETED);
19978
+ const total = tasks.length;
19979
+ const percentage = total > 0 ? Math.round(completed.length / total * 100) : 0;
19980
+ const state2 = {
19981
+ missionId: "current-mission",
19982
+ // Could be dynamic
19983
+ status: percentage === 100 ? "completed" : "executing",
19984
+ progress: {
19985
+ totalTasks: total,
19986
+ completedTasks: completed.length,
19987
+ percentage
19988
+ },
19989
+ activeAgents: running.map((t) => ({
19990
+ id: t.id,
19991
+ type: t.agent,
19992
+ status: t.status,
19993
+ currentTask: t.description
19994
+ })),
19995
+ todo: [],
19996
+ // Need to fetch from TodoEnforcer if possible
19997
+ lastUpdated: /* @__PURE__ */ new Date()
19998
+ };
19999
+ stateBroadcaster.broadcast(state2);
20000
+ }
20001
+ handleStateChange(state2) {
20002
+ if (state2.progress.percentage > 0 && state2.progress.percentage % 25 === 0) {
20003
+ const toastManager = getTaskToastManager();
20004
+ if (toastManager) {
20005
+ }
20006
+ }
20007
+ }
20008
+ };
20009
+ var progressNotifier = ProgressNotifier.getInstance();
19393
20010
 
19394
20011
  // src/core/memory/interfaces.ts
19395
20012
  var MemoryLevel = /* @__PURE__ */ ((MemoryLevel2) => {
@@ -19523,6 +20140,9 @@ var MemoryManager = class _MemoryManager {
19523
20140
  }
19524
20141
  };
19525
20142
 
20143
+ // src/core/agents/manager.ts
20144
+ init_core2();
20145
+
19526
20146
  // src/core/agents/agent-registry.ts
19527
20147
  init_shared();
19528
20148
  import * as fs4 from "fs/promises";
@@ -33175,1213 +33795,214 @@ function convertBaseSchema(schema, ctx) {
33175
33795
  arraySchema = arraySchema.min(schema.minItems);
33176
33796
  }
33177
33797
  if (typeof schema.maxItems === "number") {
33178
- arraySchema = arraySchema.max(schema.maxItems);
33179
- }
33180
- zodSchema = arraySchema;
33181
- } else {
33182
- zodSchema = z.array(z.any());
33183
- }
33184
- break;
33185
- }
33186
- default:
33187
- throw new Error(`Unsupported type: ${type}`);
33188
- }
33189
- if (schema.description) {
33190
- zodSchema = zodSchema.describe(schema.description);
33191
- }
33192
- if (schema.default !== void 0) {
33193
- zodSchema = zodSchema.default(schema.default);
33194
- }
33195
- return zodSchema;
33196
- }
33197
- function convertSchema(schema, ctx) {
33198
- if (typeof schema === "boolean") {
33199
- return schema ? z.any() : z.never();
33200
- }
33201
- let baseSchema = convertBaseSchema(schema, ctx);
33202
- const hasExplicitType = schema.type || schema.enum !== void 0 || schema.const !== void 0;
33203
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
33204
- const options = schema.anyOf.map((s) => convertSchema(s, ctx));
33205
- const anyOfUnion = z.union(options);
33206
- baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
33207
- }
33208
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
33209
- const options = schema.oneOf.map((s) => convertSchema(s, ctx));
33210
- const oneOfUnion = z.xor(options);
33211
- baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
33212
- }
33213
- if (schema.allOf && Array.isArray(schema.allOf)) {
33214
- if (schema.allOf.length === 0) {
33215
- baseSchema = hasExplicitType ? baseSchema : z.any();
33216
- } else {
33217
- let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx);
33218
- const startIdx = hasExplicitType ? 0 : 1;
33219
- for (let i = startIdx; i < schema.allOf.length; i++) {
33220
- result = z.intersection(result, convertSchema(schema.allOf[i], ctx));
33221
- }
33222
- baseSchema = result;
33223
- }
33224
- }
33225
- if (schema.nullable === true && ctx.version === "openapi-3.0") {
33226
- baseSchema = z.nullable(baseSchema);
33227
- }
33228
- if (schema.readOnly === true) {
33229
- baseSchema = z.readonly(baseSchema);
33230
- }
33231
- const extraMeta = {};
33232
- const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
33233
- for (const key of coreMetadataKeys) {
33234
- if (key in schema) {
33235
- extraMeta[key] = schema[key];
33236
- }
33237
- }
33238
- const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
33239
- for (const key of contentMetadataKeys) {
33240
- if (key in schema) {
33241
- extraMeta[key] = schema[key];
33242
- }
33243
- }
33244
- for (const key of Object.keys(schema)) {
33245
- if (!RECOGNIZED_KEYS.has(key)) {
33246
- extraMeta[key] = schema[key];
33247
- }
33248
- }
33249
- if (Object.keys(extraMeta).length > 0) {
33250
- ctx.registry.add(baseSchema, extraMeta);
33251
- }
33252
- return baseSchema;
33253
- }
33254
- function fromJSONSchema(schema, params) {
33255
- if (typeof schema === "boolean") {
33256
- return schema ? z.any() : z.never();
33257
- }
33258
- const version3 = detectVersion(schema, params?.defaultTarget);
33259
- const defs = schema.$defs || schema.definitions || {};
33260
- const ctx = {
33261
- version: version3,
33262
- defs,
33263
- refs: /* @__PURE__ */ new Map(),
33264
- processing: /* @__PURE__ */ new Set(),
33265
- rootSchema: schema,
33266
- registry: params?.registry ?? globalRegistry2
33267
- };
33268
- return convertSchema(schema, ctx);
33269
- }
33270
-
33271
- // node_modules/zod/v4/classic/coerce.js
33272
- var coerce_exports2 = {};
33273
- __export(coerce_exports2, {
33274
- bigint: () => bigint6,
33275
- boolean: () => boolean6,
33276
- date: () => date8,
33277
- number: () => number6,
33278
- string: () => string6
33279
- });
33280
- function string6(params) {
33281
- return _coercedString2(ZodString2, params);
33282
- }
33283
- function number6(params) {
33284
- return _coercedNumber2(ZodNumber2, params);
33285
- }
33286
- function boolean6(params) {
33287
- return _coercedBoolean2(ZodBoolean2, params);
33288
- }
33289
- function bigint6(params) {
33290
- return _coercedBigint2(ZodBigInt2, params);
33291
- }
33292
- function date8(params) {
33293
- return _coercedDate2(ZodDate2, params);
33294
- }
33295
-
33296
- // node_modules/zod/v4/classic/external.js
33297
- config2(en_default2());
33298
-
33299
- // src/core/agents/agent-registry.ts
33300
- var AgentDefinitionSchema = external_exports2.object({
33301
- id: external_exports2.string(),
33302
- // ID is required inside the definition object
33303
- description: external_exports2.string(),
33304
- systemPrompt: external_exports2.string(),
33305
- mode: external_exports2.enum(["primary", "subagent"]).optional(),
33306
- color: external_exports2.string().optional(),
33307
- hidden: external_exports2.boolean().optional(),
33308
- thinking: external_exports2.boolean().optional(),
33309
- maxTokens: external_exports2.number().optional(),
33310
- budgetTokens: external_exports2.number().optional(),
33311
- canWrite: external_exports2.boolean(),
33312
- // Required per interface
33313
- canBash: external_exports2.boolean()
33314
- // Required per interface
33315
- });
33316
- var AgentRegistry = class _AgentRegistry {
33317
- static instance;
33318
- agents = /* @__PURE__ */ new Map();
33319
- directory = "";
33320
- constructor() {
33321
- for (const [name, def] of Object.entries(AGENTS)) {
33322
- this.agents.set(name, def);
33323
- }
33324
- }
33325
- static getInstance() {
33326
- if (!_AgentRegistry.instance) {
33327
- _AgentRegistry.instance = new _AgentRegistry();
33328
- }
33329
- return _AgentRegistry.instance;
33330
- }
33331
- setDirectory(dir) {
33332
- this.directory = dir;
33333
- this.loadCustomAgents();
33334
- }
33335
- /**
33336
- * Get agent definition by name
33337
- */
33338
- getAgent(name) {
33339
- return this.agents.get(name);
33340
- }
33341
- /**
33342
- * List all available agent names
33343
- */
33344
- listAgents() {
33345
- return Array.from(this.agents.keys());
33346
- }
33347
- /**
33348
- * Add or update an agent definition
33349
- */
33350
- registerAgent(name, def) {
33351
- this.agents.set(name, def);
33352
- log(`[AgentRegistry] Registered agent: ${name}`);
33353
- }
33354
- /**
33355
- * Load custom agents from .opencode/agents.json
33356
- */
33357
- async loadCustomAgents() {
33358
- if (!this.directory) return;
33359
- const agentsConfigPath = path4.join(this.directory, PATHS.AGENTS_CONFIG);
33360
- try {
33361
- const content = await fs4.readFile(agentsConfigPath, "utf-8");
33362
- const customAgents = JSON.parse(content);
33363
- if (typeof customAgents === "object" && customAgents !== null) {
33364
- for (const [name, def] of Object.entries(customAgents)) {
33365
- const result = AgentDefinitionSchema.safeParse(def);
33366
- if (result.success) {
33367
- this.registerAgent(name, def);
33368
- } else {
33369
- log(`[AgentRegistry] Invalid custom agent definition for: ${name}. Errors: ${result.error.message}`);
33370
- }
33371
- }
33372
- }
33373
- } catch (error92) {
33374
- if (error92.code !== "ENOENT") {
33375
- log(`[AgentRegistry] Error loading custom agents: ${error92}`);
33376
- }
33377
- }
33378
- }
33379
- };
33380
-
33381
- // src/core/agents/manager/task-launcher.ts
33382
- var TaskLauncher = class {
33383
- constructor(client2, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
33384
- this.client = client2;
33385
- this.directory = directory;
33386
- this.store = store;
33387
- this.concurrency = concurrency;
33388
- this.sessionPool = sessionPool2;
33389
- this.onTaskError = onTaskError;
33390
- this.startPolling = startPolling;
33391
- }
33392
- /**
33393
- * Unified launch method - handles both single and multiple tasks efficiently.
33394
- * All session creations happen in parallel immediately.
33395
- * Concurrency acquisition and prompt firing happen in the background.
33396
- */
33397
- async launch(inputs) {
33398
- const isArray = Array.isArray(inputs);
33399
- const taskInputs = isArray ? inputs : [inputs];
33400
- if (taskInputs.length === 0) return isArray ? [] : null;
33401
- const tasks = await Promise.all(taskInputs.map(
33402
- (input) => this.prepareTask(input).catch(() => null)
33403
- ));
33404
- const successfulTasks = tasks.filter((t) => t !== null);
33405
- successfulTasks.forEach((task) => {
33406
- this.executeBackground(task).catch((error92) => {
33407
- this.onTaskError(task.id, error92);
33408
- });
33409
- });
33410
- if (successfulTasks.length > 0) {
33411
- this.startPolling();
33412
- }
33413
- return isArray ? successfulTasks : successfulTasks[0] || null;
33414
- }
33415
- /**
33416
- * Prepare task: Create session and registration without blocking on concurrency
33417
- */
33418
- async prepareTask(input) {
33419
- const currentDepth = input.depth ?? 0;
33420
- if (currentDepth >= PARALLEL_TASK.MAX_DEPTH) {
33421
- throw new Error(`Maximum task depth (${PARALLEL_TASK.MAX_DEPTH}) reached. To prevent infinite recursion, no further sub-tasks can be spawned.`);
33422
- }
33423
- const session = await this.sessionPool.acquire(
33424
- input.agent,
33425
- input.parentSessionID,
33426
- input.description
33427
- );
33428
- const sessionID = session.id;
33429
- const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
33430
- const task = {
33431
- id: taskId,
33432
- sessionID,
33433
- parentSessionID: input.parentSessionID,
33434
- description: input.description,
33435
- prompt: input.prompt,
33436
- agent: input.agent,
33437
- status: TASK_STATUS.PENDING,
33438
- // Start as PENDING
33439
- startedAt: /* @__PURE__ */ new Date(),
33440
- concurrencyKey: input.agent,
33441
- depth: (input.depth ?? 0) + 1,
33442
- mode: input.mode || "normal",
33443
- groupID: input.groupID
33444
- };
33445
- this.store.set(taskId, task);
33446
- this.store.trackPending(input.parentSessionID, taskId);
33447
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
33448
- });
33449
- const toastManager = getTaskToastManager();
33450
- if (toastManager) {
33451
- toastManager.addTask({
33452
- id: taskId,
33453
- description: input.description,
33454
- agent: input.agent,
33455
- isBackground: true,
33456
- parentSessionID: input.parentSessionID,
33457
- sessionID
33458
- });
33459
- }
33460
- presets_exports.sessionCreated(sessionID, input.agent);
33461
- return task;
33462
- }
33463
- /**
33464
- * Background execution: Acquire slot and fire prompt with auto-retry
33465
- */
33466
- async executeBackground(task) {
33467
- let attempt = 1;
33468
- while (true) {
33469
- try {
33470
- await this.concurrency.acquire(task.agent);
33471
- task.status = TASK_STATUS.RUNNING;
33472
- task.startedAt = /* @__PURE__ */ new Date();
33473
- this.store.set(task.id, task);
33474
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
33475
- });
33476
- const agentDef = AgentRegistry.getInstance().getAgent(task.agent);
33477
- let finalPrompt = task.prompt;
33478
- if (agentDef) {
33479
- finalPrompt = `### AGENT ROLE: ${agentDef.id}
33480
- ${agentDef.description}
33481
-
33482
- ${agentDef.systemPrompt}
33483
-
33484
- ${finalPrompt}`;
33485
- }
33486
- const memory = MemoryManager.getInstance().getContext(finalPrompt);
33487
- const injectedPrompt = memory ? `${memory}
33488
-
33489
- ${finalPrompt}` : finalPrompt;
33490
- const wireAgent = Object.values(AGENT_NAMES).includes(task.agent) ? task.agent : AGENT_NAMES.COMMANDER;
33491
- const promptPromise = this.client.session.prompt({
33492
- path: { id: task.sessionID },
33493
- body: {
33494
- agent: wireAgent,
33495
- tools: {
33496
- [TOOL_NAMES.DELEGATE_TASK]: true,
33497
- [TOOL_NAMES.GET_TASK_RESULT]: true,
33498
- [TOOL_NAMES.LIST_TASKS]: true,
33499
- [TOOL_NAMES.CANCEL_TASK]: true,
33500
- [TOOL_NAMES.SKILL]: true,
33501
- [TOOL_NAMES.RUN_COMMAND]: true
33502
- },
33503
- parts: [{ type: PART_TYPES.TEXT, text: injectedPrompt }]
33504
- }
33505
- });
33506
- await Promise.race([
33507
- promptPromise,
33508
- new Promise(
33509
- (_, reject) => setTimeout(() => reject(new Error("Session prompt execution timed out after 600s")), 6e5)
33510
- )
33511
- ]);
33512
- return;
33513
- } catch (error92) {
33514
- this.concurrency.release(task.agent);
33515
- const context = {
33516
- sessionId: task.sessionID,
33517
- taskId: task.id,
33518
- agent: task.agent,
33519
- error: error92 instanceof Error ? error92 : new Error(String(error92)),
33520
- attempt,
33521
- timestamp: /* @__PURE__ */ new Date()
33522
- };
33523
- const action = handleError(context);
33524
- if (action.type === "retry") {
33525
- log(`[AutoRetry] Task ${task.id} failed (attempt ${attempt}). Retrying in ${action.delay}ms...`);
33526
- if (action.modifyPrompt) {
33527
- task.prompt += `
33528
-
33529
- ${action.modifyPrompt}`;
33530
- }
33531
- await new Promise((r) => setTimeout(r, action.delay));
33532
- attempt++;
33533
- continue;
33534
- }
33535
- throw error92;
33536
- }
33537
- }
33538
- }
33539
- };
33540
-
33541
- // src/core/agents/manager/task-resumer.ts
33542
- init_shared();
33543
- var TaskResumer = class {
33544
- constructor(client2, store, findBySession, startPolling, notifyParentIfAllComplete) {
33545
- this.client = client2;
33546
- this.store = store;
33547
- this.findBySession = findBySession;
33548
- this.startPolling = startPolling;
33549
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
33550
- }
33551
- async resume(input) {
33552
- const existingTask = this.findBySession(input.sessionId);
33553
- if (!existingTask) {
33554
- throw new Error(`Task not found for session: ${input.sessionId}`);
33555
- }
33556
- existingTask.status = TASK_STATUS.RUNNING;
33557
- existingTask.completedAt = void 0;
33558
- existingTask.error = void 0;
33559
- existingTask.result = void 0;
33560
- existingTask.parentSessionID = input.parentSessionID;
33561
- existingTask.startedAt = /* @__PURE__ */ new Date();
33562
- existingTask.stablePolls = 0;
33563
- this.store.trackPending(input.parentSessionID, existingTask.id);
33564
- this.startPolling();
33565
- taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
33566
- });
33567
- log(`Resuming task ${existingTask.id} in session ${existingTask.sessionID}`);
33568
- this.client.session.prompt({
33569
- path: { id: existingTask.sessionID },
33570
- body: {
33571
- agent: existingTask.agent,
33572
- parts: [{ type: PART_TYPES.TEXT, text: input.prompt }]
33573
- }
33574
- }).catch((error92) => {
33575
- log(`Resume prompt error for ${existingTask.id}:`, error92);
33576
- existingTask.status = TASK_STATUS.ERROR;
33577
- existingTask.error = error92 instanceof Error ? error92.message : String(error92);
33578
- existingTask.completedAt = /* @__PURE__ */ new Date();
33579
- this.store.untrackPending(input.parentSessionID, existingTask.id);
33580
- this.store.queueNotification(existingTask);
33581
- this.notifyParentIfAllComplete(input.parentSessionID).catch(() => {
33582
- });
33583
- taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
33584
- });
33585
- });
33586
- return existingTask;
33587
- }
33588
- };
33589
-
33590
- // src/core/agents/config.ts
33591
- init_shared();
33592
- var CONFIG = {
33593
- TASK_TTL_MS: PARALLEL_TASK.TTL_MS,
33594
- CLEANUP_DELAY_MS: PARALLEL_TASK.CLEANUP_DELAY_MS,
33595
- MIN_STABILITY_MS: PARALLEL_TASK.MIN_STABILITY_MS,
33596
- POLL_INTERVAL_MS: PARALLEL_TASK.POLL_INTERVAL_MS,
33597
- STABLE_POLLS_REQUIRED: PARALLEL_TASK.STABLE_POLLS_REQUIRED
33598
- };
33599
-
33600
- // src/core/agents/manager/task-poller.ts
33601
- init_shared();
33602
- init_shared();
33603
-
33604
- // src/core/progress/state-broadcaster.ts
33605
- var StateBroadcaster = class _StateBroadcaster {
33606
- static _instance;
33607
- listeners = /* @__PURE__ */ new Set();
33608
- currentState = null;
33609
- constructor() {
33610
- }
33611
- static getInstance() {
33612
- if (!_StateBroadcaster._instance) {
33613
- _StateBroadcaster._instance = new _StateBroadcaster();
33614
- }
33615
- return _StateBroadcaster._instance;
33616
- }
33617
- subscribe(listener) {
33618
- this.listeners.add(listener);
33619
- if (this.currentState) {
33620
- listener(this.currentState);
33621
- }
33622
- return () => this.listeners.delete(listener);
33623
- }
33624
- broadcast(state2) {
33625
- this.currentState = state2;
33626
- this.listeners.forEach((listener) => {
33627
- try {
33628
- listener(state2);
33629
- } catch (error92) {
33630
- }
33631
- });
33632
- }
33633
- getCurrentState() {
33634
- return this.currentState;
33635
- }
33636
- };
33637
- var stateBroadcaster = StateBroadcaster.getInstance();
33638
-
33639
- // src/core/progress/progress-notifier.ts
33640
- init_shared();
33641
- var ProgressNotifier = class _ProgressNotifier {
33642
- static _instance;
33643
- manager = null;
33644
- constructor() {
33645
- stateBroadcaster.subscribe(this.handleStateChange.bind(this));
33646
- }
33647
- static getInstance() {
33648
- if (!_ProgressNotifier._instance) {
33649
- _ProgressNotifier._instance = new _ProgressNotifier();
33650
- }
33651
- return _ProgressNotifier._instance;
33652
- }
33653
- setManager(manager) {
33654
- this.manager = manager;
33655
- }
33656
- /**
33657
- * Poll current status from ParallelAgentManager and broadcast it
33658
- */
33659
- update() {
33660
- if (!this.manager) return;
33661
- const tasks = this.manager.getAllTasks();
33662
- const running = tasks.filter((t) => t.status === TASK_STATUS.RUNNING);
33663
- const completed = tasks.filter((t) => t.status === TASK_STATUS.COMPLETED);
33664
- const total = tasks.length;
33665
- const percentage = total > 0 ? Math.round(completed.length / total * 100) : 0;
33666
- const state2 = {
33667
- missionId: "current-mission",
33668
- // Could be dynamic
33669
- status: percentage === 100 ? "completed" : "executing",
33670
- progress: {
33671
- totalTasks: total,
33672
- completedTasks: completed.length,
33673
- percentage
33674
- },
33675
- activeAgents: running.map((t) => ({
33676
- id: t.id,
33677
- type: t.agent,
33678
- status: t.status,
33679
- currentTask: t.description
33680
- })),
33681
- todo: [],
33682
- // Need to fetch from TodoEnforcer if possible
33683
- lastUpdated: /* @__PURE__ */ new Date()
33684
- };
33685
- stateBroadcaster.broadcast(state2);
33686
- }
33687
- handleStateChange(state2) {
33688
- if (state2.progress.percentage > 0 && state2.progress.percentage % 25 === 0) {
33689
- const toastManager = getTaskToastManager();
33690
- if (toastManager) {
33691
- }
33692
- }
33693
- }
33694
- };
33695
- var progressNotifier = ProgressNotifier.getInstance();
33696
-
33697
- // src/core/agents/manager/task-poller.ts
33698
- var MAX_TASK_DURATION_MS = 18e5;
33699
- var TaskPoller = class {
33700
- constructor(client2, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete, onTaskError) {
33701
- this.client = client2;
33702
- this.store = store;
33703
- this.concurrency = concurrency;
33704
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
33705
- this.scheduleCleanup = scheduleCleanup;
33706
- this.pruneExpiredTasks = pruneExpiredTasks;
33707
- this.onTaskComplete = onTaskComplete;
33708
- this.onTaskError = onTaskError;
33709
- }
33710
- pollTimeout;
33711
- messageCache = /* @__PURE__ */ new Map();
33712
- start() {
33713
- if (this.pollTimeout) return;
33714
- log("[task-poller.ts] start() - polling started");
33715
- this.scheduleNextPoll();
33716
- }
33717
- stop() {
33718
- if (this.pollTimeout) {
33719
- clearTimeout(this.pollTimeout);
33720
- this.pollTimeout = void 0;
33721
- }
33722
- }
33723
- isRunning() {
33724
- return !!this.pollTimeout;
33725
- }
33726
- scheduleNextPoll() {
33727
- if (this.pollTimeout) clearTimeout(this.pollTimeout);
33728
- this.pollTimeout = setTimeout(() => this.poll(), CONFIG.POLL_INTERVAL_MS);
33729
- this.pollTimeout.unref();
33730
- }
33731
- async poll() {
33732
- this.pruneExpiredTasks();
33733
- const running = this.store.getRunning();
33734
- if (running.length === 0) {
33735
- this.stop();
33736
- return;
33737
- }
33738
- log("[task-poller.ts] poll() checking", running.length, "running tasks");
33739
- try {
33740
- const statusResult = await this.client.session.status();
33741
- const allStatuses = statusResult.data ?? {};
33742
- for (const task of running) {
33743
- try {
33744
- const taskDuration = Date.now() - task.startedAt.getTime();
33745
- if (taskDuration > MAX_TASK_DURATION_MS) {
33746
- log(`[task-poller] Task ${task.id} exceeded max duration (${MAX_TASK_DURATION_MS}ms). Marking as error.`);
33747
- this.onTaskError?.(task.id, new Error("Task exceeded maximum execution time"));
33748
- continue;
33749
- }
33750
- if (task.status === TASK_STATUS.PENDING) continue;
33751
- const sessionStatus = allStatuses[task.sessionID];
33752
- if (sessionStatus?.type === SESSION_STATUS.IDLE) {
33753
- const elapsed2 = Date.now() - task.startedAt.getTime();
33754
- if (elapsed2 < CONFIG.MIN_STABILITY_MS) continue;
33755
- if (!task.hasStartedOutputting && !await this.validateSessionHasOutput(task.sessionID, task)) continue;
33756
- await this.completeTask(task);
33757
- continue;
33758
- }
33759
- await this.updateTaskProgress(task, sessionStatus);
33760
- const elapsed = Date.now() - task.startedAt.getTime();
33761
- if (elapsed >= CONFIG.MIN_STABILITY_MS && task.stablePolls && task.stablePolls >= CONFIG.STABLE_POLLS_REQUIRED) {
33762
- if (task.hasStartedOutputting || await this.validateSessionHasOutput(task.sessionID, task)) {
33763
- log(`Task ${task.id} stable for ${task.stablePolls} polls, completing...`);
33764
- await this.completeTask(task);
33765
- }
33766
- }
33767
- } catch (error92) {
33768
- log(`Poll error for task ${task.id}:`, error92);
33769
- }
33770
- }
33771
- progressNotifier.update();
33772
- } catch (error92) {
33773
- log("Polling error:", error92);
33774
- } finally {
33775
- if (this.store.getRunning().length > 0) {
33776
- this.scheduleNextPoll();
33777
- } else {
33778
- this.stop();
33779
- }
33780
- }
33781
- }
33782
- async validateSessionHasOutput(sessionID, task) {
33783
- try {
33784
- const response = await this.client.session.messages({ path: { id: sessionID } });
33785
- const messages = response.data ?? [];
33786
- const hasOutput = messages.some((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT && m.parts?.some((p) => p.type === PART_TYPES.TEXT && p.text?.trim() || p.type === PART_TYPES.TOOL));
33787
- if (hasOutput && task) {
33788
- task.hasStartedOutputting = true;
33789
- }
33790
- return hasOutput;
33791
- } catch {
33792
- return true;
33793
- }
33794
- }
33795
- async completeTask(task) {
33796
- log("[task-poller.ts] completeTask() called for", task.id, task.agent);
33797
- task.status = TASK_STATUS.COMPLETED;
33798
- task.completedAt = /* @__PURE__ */ new Date();
33799
- if (task.concurrencyKey) {
33800
- this.concurrency.release(task.concurrencyKey);
33801
- this.concurrency.reportResult(task.concurrencyKey, true);
33802
- task.concurrencyKey = void 0;
33803
- }
33804
- this.store.untrackPending(task.parentSessionID, task.id);
33805
- this.store.queueNotification(task);
33806
- await this.notifyParentIfAllComplete(task.parentSessionID);
33807
- this.scheduleCleanup(task.id);
33808
- taskWAL.log(WAL_ACTIONS.COMPLETE, task).catch(() => {
33809
- });
33810
- if (this.onTaskComplete) {
33811
- Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
33812
- }
33813
- const duration5 = formatDuration(task.startedAt, task.completedAt);
33814
- presets_exports.sessionCompleted(task.sessionID, duration5);
33815
- log(`Completed ${task.id} (${duration5})`);
33816
- progressNotifier.update();
33817
- }
33818
- async updateTaskProgress(task, sessionStatus) {
33819
- try {
33820
- const cached3 = this.messageCache.get(task.sessionID);
33821
- let currentMsgCount = sessionStatus?.messageCount;
33822
- if (currentMsgCount === void 0) {
33823
- const statusResult = await this.client.session.status();
33824
- const sessionInfo = statusResult.data?.[task.sessionID];
33825
- currentMsgCount = sessionInfo?.messageCount ?? 0;
33826
- }
33827
- if (cached3 && cached3.count === currentMsgCount) {
33828
- task.stablePolls = (task.stablePolls ?? 0) + 1;
33829
- return;
33830
- }
33831
- const result = await this.client.session.messages({ path: { id: task.sessionID } });
33832
- this.messageCache.set(task.sessionID, { count: currentMsgCount, lastChecked: /* @__PURE__ */ new Date() });
33833
- if (result.error) return;
33834
- const messages = result.data ?? [];
33835
- const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
33836
- let toolCalls = 0;
33837
- let lastTool;
33838
- let lastMessage;
33839
- for (const msg of assistantMsgs) {
33840
- for (const part of msg.parts ?? []) {
33841
- if (part.type === PART_TYPES.TOOL_USE || part.tool) {
33842
- toolCalls++;
33843
- lastTool = part.tool || part.name;
33844
- }
33845
- if (part.type === PART_TYPES.TEXT && part.text) {
33846
- lastMessage = part.text;
33847
- }
33848
- }
33849
- }
33850
- task.progress = {
33851
- toolCalls,
33852
- lastTool,
33853
- lastMessage: lastMessage?.slice(0, 100),
33854
- lastUpdate: /* @__PURE__ */ new Date()
33855
- };
33856
- if (task.lastMsgCount === currentMsgCount) {
33857
- task.stablePolls = 0;
33798
+ arraySchema = arraySchema.max(schema.maxItems);
33799
+ }
33800
+ zodSchema = arraySchema;
33858
33801
  } else {
33859
- task.stablePolls = 0;
33802
+ zodSchema = z.array(z.any());
33860
33803
  }
33861
- task.lastMsgCount = currentMsgCount;
33862
- } catch {
33804
+ break;
33863
33805
  }
33806
+ default:
33807
+ throw new Error(`Unsupported type: ${type}`);
33864
33808
  }
33865
- };
33866
-
33867
- // src/core/agents/manager/task-cleaner.ts
33868
- init_shared();
33869
- init_store();
33870
- var TaskCleaner = class {
33871
- constructor(client2, store, concurrency, sessionPool2) {
33872
- this.client = client2;
33873
- this.store = store;
33874
- this.concurrency = concurrency;
33875
- this.sessionPool = sessionPool2;
33809
+ if (schema.description) {
33810
+ zodSchema = zodSchema.describe(schema.description);
33876
33811
  }
33877
- pruneExpiredTasks() {
33878
- const now = Date.now();
33879
- for (const [taskId, task] of this.store.getAll().map((t) => [t.id, t])) {
33880
- const age = now - task.startedAt.getTime();
33881
- if (age <= CONFIG.TASK_TTL_MS) continue;
33882
- log(`Timeout: ${taskId}`);
33883
- if (task.status === TASK_STATUS.RUNNING) {
33884
- task.status = TASK_STATUS.TIMEOUT;
33885
- task.error = "Task exceeded 30 minute time limit";
33886
- task.completedAt = /* @__PURE__ */ new Date();
33887
- if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
33888
- this.store.untrackPending(task.parentSessionID, taskId);
33889
- const toastManager = getTaskToastManager();
33890
- if (toastManager) {
33891
- toastManager.showCompletionToast({
33892
- id: taskId,
33893
- description: task.description,
33894
- duration: formatDuration(task.startedAt, task.completedAt),
33895
- status: TASK_STATUS.ERROR,
33896
- error: task.error
33897
- });
33898
- }
33899
- }
33900
- this.sessionPool.release(task.sessionID).catch(() => {
33901
- });
33902
- clear2(task.sessionID);
33903
- this.store.delete(taskId);
33904
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
33905
- });
33906
- }
33907
- this.store.cleanEmptyNotifications();
33812
+ if (schema.default !== void 0) {
33813
+ zodSchema = zodSchema.default(schema.default);
33908
33814
  }
33909
- scheduleCleanup(taskId) {
33910
- const task = this.store.get(taskId);
33911
- const sessionID = task?.sessionID;
33912
- setTimeout(async () => {
33913
- if (sessionID) {
33914
- try {
33915
- await this.sessionPool.release(sessionID);
33916
- clear2(sessionID);
33917
- } catch {
33918
- }
33919
- }
33920
- this.store.delete(taskId);
33921
- if (task) taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
33922
- });
33923
- log(`Cleaned up ${taskId}`);
33924
- }, CONFIG.CLEANUP_DELAY_MS);
33815
+ return zodSchema;
33816
+ }
33817
+ function convertSchema(schema, ctx) {
33818
+ if (typeof schema === "boolean") {
33819
+ return schema ? z.any() : z.never();
33925
33820
  }
33926
- /**
33927
- * Notify parent session when task(s) complete.
33928
- * Uses noReply strategy:
33929
- * - Individual completion: noReply=true (silent notification, save tokens)
33930
- * - All complete: noReply=false (AI should process and report results)
33931
- */
33932
- async notifyParentIfAllComplete(parentSessionID) {
33933
- const pendingCount = this.store.getPendingCount(parentSessionID);
33934
- const notifications = this.store.getNotifications(parentSessionID);
33935
- if (notifications.length === 0) return;
33936
- const allComplete = pendingCount === 0;
33937
- const toastManager = getTaskToastManager();
33938
- const completionInfos = notifications.map((task) => ({
33939
- id: task.id,
33940
- description: task.description,
33941
- duration: formatDuration(task.startedAt, task.completedAt),
33942
- status: task.status,
33943
- error: task.error
33944
- }));
33945
- if (allComplete && completionInfos.length > 1 && toastManager) {
33946
- toastManager.showAllCompleteToast(parentSessionID, completionInfos);
33947
- } else if (toastManager) {
33948
- for (const info of completionInfos) {
33949
- toastManager.showCompletionToast(info);
33821
+ let baseSchema = convertBaseSchema(schema, ctx);
33822
+ const hasExplicitType = schema.type || schema.enum !== void 0 || schema.const !== void 0;
33823
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
33824
+ const options = schema.anyOf.map((s) => convertSchema(s, ctx));
33825
+ const anyOfUnion = z.union(options);
33826
+ baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
33827
+ }
33828
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
33829
+ const options = schema.oneOf.map((s) => convertSchema(s, ctx));
33830
+ const oneOfUnion = z.xor(options);
33831
+ baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
33832
+ }
33833
+ if (schema.allOf && Array.isArray(schema.allOf)) {
33834
+ if (schema.allOf.length === 0) {
33835
+ baseSchema = hasExplicitType ? baseSchema : z.any();
33836
+ } else {
33837
+ let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx);
33838
+ const startIdx = hasExplicitType ? 0 : 1;
33839
+ for (let i = startIdx; i < schema.allOf.length; i++) {
33840
+ result = z.intersection(result, convertSchema(schema.allOf[i], ctx));
33950
33841
  }
33842
+ baseSchema = result;
33951
33843
  }
33952
- let message;
33953
- if (allComplete) {
33954
- message = buildNotificationMessage(notifications);
33955
- message += `
33956
-
33957
- **ACTION REQUIRED:** All background tasks are complete. Use \`get_task_result(taskId)\` to retrieve outputs and continue with the mission.`;
33958
- } else {
33959
- const completedCount = notifications.length;
33960
- message = `[BACKGROUND UPDATE] ${completedCount} task(s) completed, ${pendingCount} still running.
33961
- Completed: ${notifications.map((t) => `\`${t.id}\``).join(", ")}
33962
- You will be notified when ALL tasks complete. Continue productive work.`;
33844
+ }
33845
+ if (schema.nullable === true && ctx.version === "openapi-3.0") {
33846
+ baseSchema = z.nullable(baseSchema);
33847
+ }
33848
+ if (schema.readOnly === true) {
33849
+ baseSchema = z.readonly(baseSchema);
33850
+ }
33851
+ const extraMeta = {};
33852
+ const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
33853
+ for (const key of coreMetadataKeys) {
33854
+ if (key in schema) {
33855
+ extraMeta[key] = schema[key];
33963
33856
  }
33964
- try {
33965
- await this.client.session.prompt({
33966
- path: { id: parentSessionID },
33967
- body: {
33968
- // Key optimization: only trigger AI response when ALL complete
33969
- noReply: !allComplete,
33970
- parts: [{ type: PART_TYPES.TEXT, text: message }]
33971
- }
33972
- });
33973
- log(`Notified parent ${parentSessionID} (allComplete=${allComplete}, noReply=${!allComplete})`);
33974
- } catch (error92) {
33975
- log("Notification error:", error92);
33857
+ }
33858
+ const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
33859
+ for (const key of contentMetadataKeys) {
33860
+ if (key in schema) {
33861
+ extraMeta[key] = schema[key];
33976
33862
  }
33977
- this.store.clearNotifications(parentSessionID);
33978
33863
  }
33979
- };
33980
-
33981
- // src/core/agents/manager/event-handler.ts
33982
- init_shared();
33983
-
33984
- // src/core/loop/continuation-lock.ts
33985
- var locks = /* @__PURE__ */ new Map();
33986
- var LOCK_TIMEOUT_MS = 12e4;
33987
- function tryAcquireContinuationLock(sessionID, source) {
33988
- const now = Date.now();
33989
- const existing = locks.get(sessionID);
33990
- if (existing?.acquired) {
33991
- const elapsed = now - existing.timestamp;
33992
- if (elapsed < LOCK_TIMEOUT_MS) {
33993
- log("[continuation-lock] Lock denied - already held", {
33994
- sessionID: sessionID.slice(0, 8),
33995
- heldBy: existing.source,
33996
- requestedBy: source,
33997
- elapsedMs: elapsed
33998
- });
33999
- return false;
33864
+ for (const key of Object.keys(schema)) {
33865
+ if (!RECOGNIZED_KEYS.has(key)) {
33866
+ extraMeta[key] = schema[key];
34000
33867
  }
34001
- log("[continuation-lock] Forcing stale lock release", {
34002
- sessionID: sessionID.slice(0, 8),
34003
- staleSource: existing.source,
34004
- elapsedMs: elapsed
34005
- });
34006
33868
  }
34007
- locks.set(sessionID, { acquired: true, timestamp: now, source });
34008
- log("[continuation-lock] Lock acquired", {
34009
- sessionID: sessionID.slice(0, 8),
34010
- source
34011
- });
34012
- return true;
34013
- }
34014
- function releaseContinuationLock(sessionID) {
34015
- const existing = locks.get(sessionID);
34016
- if (existing?.acquired) {
34017
- const duration5 = Date.now() - existing.timestamp;
34018
- log("[continuation-lock] Lock released", {
34019
- sessionID: sessionID.slice(0, 8),
34020
- source: existing.source,
34021
- heldMs: duration5
34022
- });
33869
+ if (Object.keys(extraMeta).length > 0) {
33870
+ ctx.registry.add(baseSchema, extraMeta);
34023
33871
  }
34024
- locks.delete(sessionID);
33872
+ return baseSchema;
34025
33873
  }
34026
- function hasContinuationLock(sessionID) {
34027
- const lock = locks.get(sessionID);
34028
- if (!lock?.acquired) return false;
34029
- if (Date.now() - lock.timestamp >= LOCK_TIMEOUT_MS) {
34030
- log("[continuation-lock] Stale lock detected during check", {
34031
- sessionID: sessionID.slice(0, 8)
34032
- });
34033
- locks.delete(sessionID);
34034
- return false;
33874
+ function fromJSONSchema(schema, params) {
33875
+ if (typeof schema === "boolean") {
33876
+ return schema ? z.any() : z.never();
34035
33877
  }
34036
- return true;
33878
+ const version3 = detectVersion(schema, params?.defaultTarget);
33879
+ const defs = schema.$defs || schema.definitions || {};
33880
+ const ctx = {
33881
+ version: version3,
33882
+ defs,
33883
+ refs: /* @__PURE__ */ new Map(),
33884
+ processing: /* @__PURE__ */ new Set(),
33885
+ rootSchema: schema,
33886
+ registry: params?.registry ?? globalRegistry2
33887
+ };
33888
+ return convertSchema(schema, ctx);
34037
33889
  }
34038
- function cleanupContinuationLock(sessionID) {
34039
- locks.delete(sessionID);
33890
+
33891
+ // node_modules/zod/v4/classic/coerce.js
33892
+ var coerce_exports2 = {};
33893
+ __export(coerce_exports2, {
33894
+ bigint: () => bigint6,
33895
+ boolean: () => boolean6,
33896
+ date: () => date8,
33897
+ number: () => number6,
33898
+ string: () => string6
33899
+ });
33900
+ function string6(params) {
33901
+ return _coercedString2(ZodString2, params);
33902
+ }
33903
+ function number6(params) {
33904
+ return _coercedNumber2(ZodNumber2, params);
33905
+ }
33906
+ function boolean6(params) {
33907
+ return _coercedBoolean2(ZodBoolean2, params);
33908
+ }
33909
+ function bigint6(params) {
33910
+ return _coercedBigint2(ZodBigInt2, params);
33911
+ }
33912
+ function date8(params) {
33913
+ return _coercedDate2(ZodDate2, params);
34040
33914
  }
34041
33915
 
34042
- // src/core/agents/manager/event-handler.ts
34043
- var EventHandler = class {
34044
- constructor(client2, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
34045
- this.client = client2;
34046
- this.store = store;
34047
- this.concurrency = concurrency;
34048
- this.findBySession = findBySession;
34049
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
34050
- this.scheduleCleanup = scheduleCleanup;
34051
- this.validateSessionHasOutput = validateSessionHasOutput2;
34052
- this.onTaskComplete = onTaskComplete;
34053
- }
34054
- /**
34055
- * Handle OpenCode session events for proper resource cleanup.
34056
- * Call this from your plugin's event hook.
34057
- */
34058
- handle(event) {
34059
- const props = event.properties;
34060
- if (event.type === SESSION_EVENTS.IDLE) {
34061
- const sessionID = props?.sessionID;
34062
- if (!sessionID) return;
34063
- recordSessionResponse(sessionID);
34064
- const task = this.findBySession(sessionID);
34065
- if (!task || task.status !== TASK_STATUS.RUNNING) return;
34066
- this.handleSessionIdle(task).catch((err) => {
34067
- log("Error handling session.idle:", err);
34068
- });
34069
- }
34070
- if (event.type === SESSION_EVENTS.DELETED) {
34071
- const sessionID = props?.info?.id ?? props?.sessionID;
34072
- if (!sessionID) return;
34073
- const task = this.findBySession(sessionID);
34074
- if (!task) return;
34075
- this.handleSessionDeleted(task);
34076
- }
34077
- }
34078
- async handleSessionIdle(task) {
34079
- const elapsed = Date.now() - task.startedAt.getTime();
34080
- if (elapsed < CONFIG.MIN_STABILITY_MS) {
34081
- log(`Session idle but too early for ${task.id}, waiting...`);
34082
- return;
34083
- }
34084
- const hasOutput = await this.validateSessionHasOutput(task.sessionID);
34085
- if (!hasOutput) {
34086
- log(`Session idle but no output for ${task.id}, waiting...`);
34087
- return;
34088
- }
34089
- task.status = TASK_STATUS.COMPLETED;
34090
- task.completedAt = /* @__PURE__ */ new Date();
34091
- if (task.concurrencyKey) {
34092
- this.concurrency.release(task.concurrencyKey);
34093
- this.concurrency.reportResult(task.concurrencyKey, true);
34094
- task.concurrencyKey = void 0;
34095
- }
34096
- this.store.untrackPending(task.parentSessionID, task.id);
34097
- this.store.queueNotification(task);
34098
- await this.notifyParentIfAllComplete(task.parentSessionID);
34099
- this.scheduleCleanup(task.id);
34100
- taskWAL.log(WAL_ACTIONS.COMPLETE, task).catch(() => {
34101
- });
34102
- if (this.onTaskComplete) {
34103
- Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
34104
- }
34105
- progressNotifier.update();
34106
- log(`Task ${task.id} completed via session.idle event (${formatDuration(task.startedAt, task.completedAt)})`);
34107
- }
34108
- handleSessionDeleted(task) {
34109
- log(`Session deleted event for task ${task.id}`);
34110
- if (task.status === TASK_STATUS.RUNNING) {
34111
- task.status = TASK_STATUS.ERROR;
34112
- task.error = "Session deleted";
34113
- task.completedAt = /* @__PURE__ */ new Date();
34114
- }
34115
- if (task.concurrencyKey) {
34116
- this.concurrency.release(task.concurrencyKey);
34117
- this.concurrency.reportResult(task.concurrencyKey, false);
34118
- task.concurrencyKey = void 0;
34119
- }
34120
- this.store.untrackPending(task.parentSessionID, task.id);
34121
- this.store.clearNotificationsForTask(task.id);
34122
- this.store.delete(task.id);
34123
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
34124
- });
34125
- cleanupSessionHealth(task.sessionID);
34126
- cleanupContinuationLock(task.sessionID);
34127
- progressNotifier.update();
34128
- log(`Cleaned up deleted session task: ${task.id}`);
34129
- }
34130
- };
33916
+ // node_modules/zod/v4/classic/external.js
33917
+ config2(en_default2());
34131
33918
 
34132
- // src/core/agents/session-pool.ts
34133
- init_shared();
34134
- var DEFAULT_CONFIG = {
34135
- maxPoolSizePerAgent: 5,
34136
- idleTimeoutMs: 3e5,
34137
- // 5 minutes
34138
- maxReuseCount: 10,
34139
- healthCheckIntervalMs: 6e4
34140
- // 1 minute
34141
- };
34142
- var SessionPool = class _SessionPool {
34143
- static _instance;
34144
- pool = /* @__PURE__ */ new Map();
34145
- // key: agentName
34146
- sessionsById = /* @__PURE__ */ new Map();
34147
- config;
34148
- client;
34149
- directory;
34150
- healthCheckInterval = null;
34151
- // Statistics
34152
- stats = {
34153
- reuseHits: 0,
34154
- creationMisses: 0
34155
- };
34156
- constructor(client2, directory, config3 = {}) {
34157
- this.client = client2;
34158
- this.directory = directory;
34159
- this.config = { ...DEFAULT_CONFIG, ...config3 };
34160
- this.startHealthCheck();
34161
- }
34162
- static getInstance(client2, directory, config3) {
34163
- if (!_SessionPool._instance) {
34164
- if (!client2 || !directory) {
34165
- throw new Error("SessionPool requires client and directory on first call");
34166
- }
34167
- _SessionPool._instance = new _SessionPool(client2, directory, config3);
34168
- }
34169
- return _SessionPool._instance;
34170
- }
34171
- /**
34172
- * Acquire a session from the pool or create a new one.
34173
- */
34174
- async acquire(agentName, parentSessionID, description) {
34175
- const poolKey = this.getPoolKey(agentName);
34176
- const agentPool = this.pool.get(poolKey) || [];
34177
- const available = agentPool.find((s) => !s.inUse && s.reuseCount < this.config.maxReuseCount);
34178
- if (available) {
34179
- available.inUse = true;
34180
- available.lastUsedAt = /* @__PURE__ */ new Date();
34181
- available.reuseCount++;
34182
- this.stats.reuseHits++;
34183
- log(`[SessionPool] Reusing session ${available.id.slice(0, 8)}... for ${agentName} (reuse #${available.reuseCount})`);
34184
- return available;
33919
+ // src/core/agents/agent-registry.ts
33920
+ var AgentDefinitionSchema = external_exports2.object({
33921
+ id: external_exports2.string(),
33922
+ // ID is required inside the definition object
33923
+ description: external_exports2.string(),
33924
+ systemPrompt: external_exports2.string(),
33925
+ mode: external_exports2.enum(["primary", "subagent"]).optional(),
33926
+ color: external_exports2.string().optional(),
33927
+ hidden: external_exports2.boolean().optional(),
33928
+ thinking: external_exports2.boolean().optional(),
33929
+ maxTokens: external_exports2.number().optional(),
33930
+ budgetTokens: external_exports2.number().optional(),
33931
+ canWrite: external_exports2.boolean(),
33932
+ // Required per interface
33933
+ canBash: external_exports2.boolean()
33934
+ // Required per interface
33935
+ });
33936
+ var AgentRegistry = class _AgentRegistry {
33937
+ static instance;
33938
+ agents = /* @__PURE__ */ new Map();
33939
+ directory = "";
33940
+ constructor() {
33941
+ for (const [name, def] of Object.entries(AGENTS)) {
33942
+ this.agents.set(name, def);
34185
33943
  }
34186
- this.stats.creationMisses++;
34187
- return this.createSession(agentName, parentSessionID, description);
34188
33944
  }
34189
- /**
34190
- * Release a session back to the pool for reuse.
34191
- */
34192
- async release(sessionId) {
34193
- const session = this.sessionsById.get(sessionId);
34194
- if (!session) {
34195
- log(`[SessionPool] Session ${sessionId.slice(0, 8)}... not found in pool`);
34196
- return;
34197
- }
34198
- const now = Date.now();
34199
- const age = now - session.createdAt.getTime();
34200
- const idle = now - session.lastUsedAt.getTime();
34201
- if (session.reuseCount >= this.config.maxReuseCount || age > this.config.idleTimeoutMs * 2) {
34202
- await this.invalidate(sessionId);
34203
- return;
34204
- }
34205
- const poolKey = this.getPoolKey(session.agentName);
34206
- const agentPool = this.pool.get(poolKey) || [];
34207
- const availableCount = agentPool.filter((s) => !s.inUse).length;
34208
- if (availableCount >= this.config.maxPoolSizePerAgent) {
34209
- const oldest = agentPool.filter((s) => !s.inUse).sort((a, b) => a.lastUsedAt.getTime() - b.lastUsedAt.getTime())[0];
34210
- if (oldest) {
34211
- await this.deleteSession(oldest.id);
34212
- }
33945
+ static getInstance() {
33946
+ if (!_AgentRegistry.instance) {
33947
+ _AgentRegistry.instance = new _AgentRegistry();
34213
33948
  }
34214
- await this.resetSession(sessionId);
34215
- session.inUse = false;
34216
- log(`[SessionPool] Released session ${sessionId.slice(0, 8)}... to pool`);
33949
+ return _AgentRegistry.instance;
34217
33950
  }
34218
- /**
34219
- * Invalidate a session (remove from pool and delete).
34220
- */
34221
- async invalidate(sessionId) {
34222
- const session = this.sessionsById.get(sessionId);
34223
- if (!session) return;
34224
- await this.deleteSession(sessionId);
34225
- log(`[SessionPool] Invalidated session ${sessionId.slice(0, 8)}...`);
33951
+ setDirectory(dir) {
33952
+ this.directory = dir;
33953
+ this.loadCustomAgents();
34226
33954
  }
34227
33955
  /**
34228
- * Get current pool statistics.
33956
+ * Get agent definition by name
34229
33957
  */
34230
- getStats() {
34231
- const byAgent = {};
34232
- for (const [agentName, sessions] of this.pool.entries()) {
34233
- const inUse = sessions.filter((s) => s.inUse).length;
34234
- byAgent[agentName] = {
34235
- total: sessions.length,
34236
- inUse,
34237
- available: sessions.length - inUse
34238
- };
34239
- }
34240
- const allSessions = Array.from(this.sessionsById.values());
34241
- const inUseCount = allSessions.filter((s) => s.inUse).length;
34242
- return {
34243
- totalSessions: allSessions.length,
34244
- sessionsInUse: inUseCount,
34245
- availableSessions: allSessions.length - inUseCount,
34246
- reuseHits: this.stats.reuseHits,
34247
- creationMisses: this.stats.creationMisses,
34248
- byAgent
34249
- };
33958
+ getAgent(name) {
33959
+ return this.agents.get(name);
34250
33960
  }
34251
33961
  /**
34252
- * Cleanup stale sessions.
33962
+ * List all available agent names
34253
33963
  */
34254
- async cleanup() {
34255
- const now = Date.now();
34256
- let cleanedCount = 0;
34257
- for (const [sessionId, session] of this.sessionsById.entries()) {
34258
- if (session.inUse) continue;
34259
- const idle = now - session.lastUsedAt.getTime();
34260
- if (idle > this.config.idleTimeoutMs) {
34261
- await this.deleteSession(sessionId);
34262
- cleanedCount++;
34263
- }
34264
- }
34265
- if (cleanedCount > 0) {
34266
- log(`[SessionPool] Cleaned up ${cleanedCount} stale sessions`);
34267
- }
34268
- return cleanedCount;
33964
+ listAgents() {
33965
+ return Array.from(this.agents.keys());
34269
33966
  }
34270
33967
  /**
34271
- * Shutdown the pool.
33968
+ * Add or update an agent definition
34272
33969
  */
34273
- async shutdown() {
34274
- log("[SessionPool] Shutting down...");
34275
- if (this.healthCheckInterval) {
34276
- clearInterval(this.healthCheckInterval);
34277
- this.healthCheckInterval = null;
34278
- }
34279
- const deletePromises = Array.from(this.sessionsById.keys()).map(
34280
- (id) => this.deleteSession(id).catch(() => {
34281
- })
34282
- );
34283
- await Promise.all(deletePromises);
34284
- this.pool.clear();
34285
- this.sessionsById.clear();
34286
- log("[SessionPool] Shutdown complete");
33970
+ registerAgent(name, def) {
33971
+ this.agents.set(name, def);
33972
+ log(`[AgentRegistry] Registered agent: ${name}`);
34287
33973
  }
34288
- // =========================================================================
34289
- // Private Methods
34290
- // =========================================================================
34291
33974
  /**
34292
- * Reset/Compact a session to clear context for next reuse.
33975
+ * Load custom agents from .opencode/agents.json
34293
33976
  */
34294
- async resetSession(sessionId) {
34295
- const session = this.sessionsById.get(sessionId);
34296
- if (!session) return;
34297
- log(`[SessionPool] Resetting session ${sessionId.slice(0, 8)}...`);
33977
+ async loadCustomAgents() {
33978
+ if (!this.directory) return;
33979
+ const agentsConfigPath = path4.join(this.directory, PATHS.AGENTS_CONFIG);
34298
33980
  try {
34299
- await this.client.session.compact?.({ path: { id: sessionId } });
34300
- session.lastResetAt = /* @__PURE__ */ new Date();
34301
- session.health = "healthy";
34302
- } catch (error92) {
34303
- log(`[SessionPool] Failed to reset session ${sessionId.slice(0, 8)}: ${error92}`);
34304
- session.health = "degraded";
34305
- }
34306
- }
34307
- getPoolKey(agentName) {
34308
- return agentName;
34309
- }
34310
- async createSession(agentName, parentSessionID, description) {
34311
- log(`[SessionPool] Creating new session for ${agentName}`);
34312
- const result = await Promise.race([
34313
- this.client.session.create({
34314
- body: {
34315
- parentID: parentSessionID,
34316
- title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${description}`
34317
- },
34318
- query: { directory: this.directory }
34319
- }),
34320
- new Promise(
34321
- (_, reject) => setTimeout(() => reject(new Error("Session creation timed out after 60s")), 6e4)
34322
- )
34323
- ]);
34324
- if (result.error || !result.data?.id) {
34325
- throw new Error(`Session creation failed: ${result.error || "No ID"}`);
34326
- }
34327
- const session = {
34328
- id: result.data.id,
34329
- agentName,
34330
- projectDirectory: this.directory,
34331
- createdAt: /* @__PURE__ */ new Date(),
34332
- lastUsedAt: /* @__PURE__ */ new Date(),
34333
- reuseCount: 0,
34334
- inUse: true,
34335
- health: "healthy",
34336
- lastResetAt: /* @__PURE__ */ new Date()
34337
- };
34338
- const poolKey = this.getPoolKey(agentName);
34339
- const agentPool = this.pool.get(poolKey) || [];
34340
- agentPool.push(session);
34341
- this.pool.set(poolKey, agentPool);
34342
- this.sessionsById.set(session.id, session);
34343
- return session;
34344
- }
34345
- async deleteSession(sessionId) {
34346
- const session = this.sessionsById.get(sessionId);
34347
- if (!session) return;
34348
- this.sessionsById.delete(sessionId);
34349
- const poolKey = this.getPoolKey(session.agentName);
34350
- const agentPool = this.pool.get(poolKey);
34351
- if (agentPool) {
34352
- const idx = agentPool.findIndex((s) => s.id === sessionId);
34353
- if (idx !== -1) {
34354
- agentPool.splice(idx, 1);
33981
+ const content = await fs4.readFile(agentsConfigPath, "utf-8");
33982
+ const customAgents = JSON.parse(content);
33983
+ if (typeof customAgents === "object" && customAgents !== null) {
33984
+ for (const [name, def] of Object.entries(customAgents)) {
33985
+ const result = AgentDefinitionSchema.safeParse(def);
33986
+ if (result.success) {
33987
+ this.registerAgent(name, def);
33988
+ } else {
33989
+ log(`[AgentRegistry] Invalid custom agent definition for: ${name}. Errors: ${result.error.message}`);
33990
+ }
33991
+ }
34355
33992
  }
34356
- if (agentPool.length === 0) {
34357
- this.pool.delete(poolKey);
33993
+ } catch (error92) {
33994
+ if (error92.code !== "ENOENT") {
33995
+ log(`[AgentRegistry] Error loading custom agents: ${error92}`);
34358
33996
  }
34359
33997
  }
34360
- try {
34361
- await this.client.session.delete({ path: { id: sessionId } });
34362
- } catch {
34363
- }
34364
- }
34365
- startHealthCheck() {
34366
- this.healthCheckInterval = setInterval(() => {
34367
- this.cleanup().catch(() => {
34368
- });
34369
- }, this.config.healthCheckIntervalMs);
34370
- this.healthCheckInterval.unref?.();
34371
33998
  }
34372
33999
  };
34373
- var sessionPool = {
34374
- getInstance: SessionPool.getInstance.bind(SessionPool)
34375
- };
34376
-
34377
- // src/core/agents/manager.ts
34378
- init_core2();
34379
34000
 
34380
34001
  // src/core/todo/todo-manager.ts
34381
34002
  init_shared();
34382
34003
  import * as fs5 from "node:fs";
34383
34004
  import * as path5 from "node:path";
34384
- import * as crypto2 from "node:crypto";
34005
+ import * as crypto from "node:crypto";
34385
34006
  var TodoManager = class _TodoManager {
34386
34007
  static _instance;
34387
34008
  directory = "";
@@ -34451,13 +34072,22 @@ var TodoManager = class _TodoManager {
34451
34072
  });
34452
34073
  }
34453
34074
  async _internalUpdate(expectedVersion, updater, author) {
34454
- const MAX_RETRIES2 = 3;
34455
- const RETRY_DELAY = 50;
34075
+ const MAX_RETRIES2 = 5;
34076
+ const BASE_DELAY_MS = 50;
34456
34077
  for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
34457
34078
  try {
34458
34079
  const current = await this.readWithVersion();
34459
34080
  if (current.version.version !== expectedVersion) {
34460
34081
  log(`[TodoManager] Conflict: expected v${expectedVersion}, found v${current.version.version}`);
34082
+ if (attempt < MAX_RETRIES2 - 1) {
34083
+ const backoffDelay = BASE_DELAY_MS * Math.pow(2, attempt);
34084
+ const jitter = Math.random() * 50;
34085
+ const totalDelay = backoffDelay + jitter;
34086
+ log(`[TodoManager] Retrying in ${Math.round(totalDelay)}ms (attempt ${attempt + 1}/${MAX_RETRIES2})`);
34087
+ await new Promise((r) => setTimeout(r, totalDelay));
34088
+ expectedVersion = current.version.version;
34089
+ continue;
34090
+ }
34461
34091
  return {
34462
34092
  success: false,
34463
34093
  currentVersion: current.version.version,
@@ -34480,18 +34110,21 @@ var TodoManager = class _TodoManager {
34480
34110
  await fs5.promises.rename(tmpPath, this.todoPath);
34481
34111
  this.logChange(newVersion, newContent, author).catch(() => {
34482
34112
  });
34483
- log(`[TodoManager] Updated TODO to v${newVersion} by ${author}`);
34113
+ log(`[TodoManager] Updated TODO to v${newVersion} by ${author}${attempt > 0 ? ` (after ${attempt} retries)` : ""}`);
34484
34114
  return { success: true, currentVersion: newVersion };
34485
34115
  } catch (error92) {
34486
34116
  if (attempt === MAX_RETRIES2 - 1) throw error92;
34487
- await new Promise((r) => setTimeout(r, RETRY_DELAY));
34117
+ const backoffDelay = BASE_DELAY_MS * Math.pow(2, attempt);
34118
+ const jitter = Math.random() * 50;
34119
+ await new Promise((r) => setTimeout(r, backoffDelay + jitter));
34488
34120
  }
34489
34121
  }
34490
- throw new Error("Failed to update TODO");
34122
+ throw new Error("Failed to update TODO after max retries");
34491
34123
  }
34492
34124
  async updateItem(searchText, newStatus, author = "system") {
34493
- let retries = 5;
34494
- while (retries-- > 0) {
34125
+ const MAX_RETRIES2 = 5;
34126
+ const BASE_DELAY_MS = 50;
34127
+ for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
34495
34128
  const data = await this.readWithVersion();
34496
34129
  const statusMap = {
34497
34130
  [TODO_CONSTANTS.STATUS.PENDING]: TODO_CONSTANTS.MARKERS.PENDING,
@@ -34514,13 +34147,18 @@ var TodoManager = class _TodoManager {
34514
34147
  }, author);
34515
34148
  if (result.success) return true;
34516
34149
  if (!result.conflict) return false;
34517
- await new Promise((r) => setTimeout(r, 50));
34150
+ if (attempt < MAX_RETRIES2 - 1) {
34151
+ const backoffDelay = BASE_DELAY_MS * Math.pow(2, attempt);
34152
+ const jitter = Math.random() * 50;
34153
+ await new Promise((r) => setTimeout(r, backoffDelay + jitter));
34154
+ }
34518
34155
  }
34519
34156
  return false;
34520
34157
  }
34521
34158
  async addSubTask(parentText, subTaskText, author = "system") {
34522
- let retries = 5;
34523
- while (retries-- > 0) {
34159
+ const MAX_RETRIES2 = 5;
34160
+ const BASE_DELAY_MS = 50;
34161
+ for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
34524
34162
  const data = await this.readWithVersion();
34525
34163
  const result = await this.update(data.version.version, (content) => {
34526
34164
  const lines = content.split("\n");
@@ -34544,7 +34182,11 @@ var TodoManager = class _TodoManager {
34544
34182
  }, author);
34545
34183
  if (result.success) return true;
34546
34184
  if (!result.conflict) return false;
34547
- await new Promise((r) => setTimeout(r, 50));
34185
+ if (attempt < MAX_RETRIES2 - 1) {
34186
+ const backoffDelay = BASE_DELAY_MS * Math.pow(2, attempt);
34187
+ const jitter = Math.random() * 50;
34188
+ await new Promise((r) => setTimeout(r, backoffDelay + jitter));
34189
+ }
34548
34190
  }
34549
34191
  return false;
34550
34192
  }
@@ -34553,7 +34195,7 @@ var TodoManager = class _TodoManager {
34553
34195
  version: version3,
34554
34196
  timestamp: Date.now(),
34555
34197
  author,
34556
- contentHash: crypto2.createHash("sha256").update(content).digest("hex"),
34198
+ contentHash: crypto.createHash("sha256").update(content).digest("hex"),
34557
34199
  size: content.length
34558
34200
  };
34559
34201
  await fs5.promises.appendFile(this.historyPath, JSON.stringify(entry) + "\n", "utf-8");
@@ -34568,12 +34210,8 @@ var ParallelAgentManager = class _ParallelAgentManager {
34568
34210
  directory;
34569
34211
  concurrency = new ConcurrencyController();
34570
34212
  sessionPool;
34571
- // Composed components
34572
- launcher;
34573
- resumer;
34574
- poller;
34575
- cleaner;
34576
- eventHandler;
34213
+ // Unified executor (replaces 5 separate components)
34214
+ executor;
34577
34215
  constructor(client2, directory) {
34578
34216
  this.client = client2;
34579
34217
  this.directory = directory;
@@ -34584,42 +34222,12 @@ var ParallelAgentManager = class _ParallelAgentManager {
34584
34222
  AgentRegistry.getInstance().setDirectory(directory);
34585
34223
  TodoManager.getInstance().setDirectory(directory);
34586
34224
  this.sessionPool = SessionPool.getInstance(client2, directory);
34587
- this.cleaner = new TaskCleaner(client2, this.store, this.concurrency, this.sessionPool);
34588
- this.poller = new TaskPoller(
34589
- client2,
34590
- this.store,
34591
- this.concurrency,
34592
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
34593
- (taskId) => this.cleaner.scheduleCleanup(taskId),
34594
- () => this.cleaner.pruneExpiredTasks(),
34595
- (task) => this.handleTaskComplete(task),
34596
- (taskId, error92) => this.handleTaskError(taskId, error92)
34597
- );
34598
- this.launcher = new TaskLauncher(
34225
+ this.executor = new UnifiedTaskExecutor(
34599
34226
  client2,
34600
34227
  directory,
34601
34228
  this.store,
34602
34229
  this.concurrency,
34603
- this.sessionPool,
34604
- (taskId, error92) => this.handleTaskError(taskId, error92),
34605
- () => this.poller.start()
34606
- );
34607
- this.resumer = new TaskResumer(
34608
- client2,
34609
- this.store,
34610
- (sessionID) => this.findBySession(sessionID),
34611
- () => this.poller.start(),
34612
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
34613
- );
34614
- this.eventHandler = new EventHandler(
34615
- client2,
34616
- this.store,
34617
- this.concurrency,
34618
- (sessionID) => this.findBySession(sessionID),
34619
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
34620
- (taskId) => this.cleaner.scheduleCleanup(taskId),
34621
- (sessionID) => this.poller.validateSessionHasOutput(sessionID),
34622
- (task) => this.handleTaskComplete(task)
34230
+ this.sessionPool
34623
34231
  );
34624
34232
  progressNotifier.setManager(this);
34625
34233
  this.recoverActiveTasks().catch((err) => {
@@ -34639,13 +34247,22 @@ var ParallelAgentManager = class _ParallelAgentManager {
34639
34247
  // Public API
34640
34248
  // ========================================================================
34641
34249
  async launch(inputs) {
34642
- this.cleaner.pruneExpiredTasks();
34643
- const result = await this.launcher.launch(inputs);
34644
- progressNotifier.update();
34645
- return result;
34250
+ if (Array.isArray(inputs)) {
34251
+ const results = await Promise.all(inputs.map((input) => this.executor.launch(input)));
34252
+ progressNotifier.update();
34253
+ return results;
34254
+ } else {
34255
+ const result = await this.executor.launch(inputs);
34256
+ progressNotifier.update();
34257
+ return result;
34258
+ }
34646
34259
  }
34647
34260
  async resume(input) {
34648
- return this.resumer.resume(input);
34261
+ const task = await this.executor.resume(input);
34262
+ if (!task) {
34263
+ throw new Error(`Task not found: ${input.sessionId}`);
34264
+ }
34265
+ return task;
34649
34266
  }
34650
34267
  getTask(id) {
34651
34268
  return this.store.get(id);
@@ -34660,25 +34277,11 @@ var ParallelAgentManager = class _ParallelAgentManager {
34660
34277
  return this.store.getByParent(parentSessionID);
34661
34278
  }
34662
34279
  async cancelTask(taskId) {
34663
- const task = this.store.get(taskId);
34664
- if (!task || task.status !== TASK_STATUS.RUNNING) return false;
34665
- task.status = TASK_STATUS.ERROR;
34666
- task.error = "Cancelled by user";
34667
- task.completedAt = /* @__PURE__ */ new Date();
34668
- if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
34669
- this.store.untrackPending(task.parentSessionID, taskId);
34670
- try {
34671
- await this.client.session.delete({ path: { id: task.sessionID } });
34672
- log(`Session ${task.sessionID.slice(0, 8)}... deleted`);
34673
- } catch {
34674
- log(`Session ${task.sessionID.slice(0, 8)}... already gone`);
34280
+ const result = await this.executor.cancel(taskId);
34281
+ if (result) {
34282
+ progressNotifier.update();
34675
34283
  }
34676
- this.cleaner.scheduleCleanup(taskId);
34677
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
34678
- });
34679
- progressNotifier.update();
34680
- log(`Cancelled ${taskId}`);
34681
- return true;
34284
+ return result;
34682
34285
  }
34683
34286
  async getResult(taskId) {
34684
34287
  const task = this.store.get(taskId);
@@ -34709,7 +34312,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
34709
34312
  return this.concurrency;
34710
34313
  }
34711
34314
  cleanup() {
34712
- this.poller.stop();
34315
+ this.executor.cleanup();
34713
34316
  stopHealthCheck();
34714
34317
  this.store.clear();
34715
34318
  MemoryManager.getInstance().clearTaskMemory();
@@ -34721,7 +34324,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
34721
34324
  // Event Handling
34722
34325
  // ========================================================================
34723
34326
  handleEvent(event) {
34724
- this.eventHandler.handle(event);
34327
+ log("[ParallelAgentManager] Event received:", event.type);
34725
34328
  }
34726
34329
  // ========================================================================
34727
34330
  // Private Helpers
@@ -34730,87 +34333,17 @@ var ParallelAgentManager = class _ParallelAgentManager {
34730
34333
  return this.store.getAll().find((t) => t.sessionID === sessionID);
34731
34334
  }
34732
34335
  handleTaskError(taskId, error92) {
34733
- const task = this.store.get(taskId);
34734
- if (!task) return;
34735
- task.status = TASK_STATUS.ERROR;
34736
- task.error = error92 instanceof Error ? error92.message : String(error92);
34737
- task.completedAt = /* @__PURE__ */ new Date();
34738
- if (task.concurrencyKey) {
34739
- this.concurrency.release(task.concurrencyKey);
34740
- this.concurrency.reportResult(task.concurrencyKey, false);
34741
- }
34742
- this.store.untrackPending(task.parentSessionID, taskId);
34743
- this.cleaner.notifyParentIfAllComplete(task.parentSessionID);
34744
- this.cleaner.scheduleCleanup(taskId);
34745
- progressNotifier.update();
34746
- taskWAL.log(WAL_ACTIONS.UPDATE, task).catch(() => {
34747
- });
34336
+ log(`[ParallelAgentManager] Delegating error handling to executor for task ${taskId}`);
34748
34337
  }
34749
34338
  async handleTaskComplete(task) {
34750
- if (task.agent === AGENT_NAMES.WORKER && task.mode !== "race") {
34751
- log(`[MSVP] Triggering Unit Review for task ${task.id}`);
34752
- try {
34753
- await this.launch({
34754
- agent: AGENT_NAMES.REVIEWER,
34755
- description: `Unit Review: ${task.description}`,
34756
- prompt: `Perform a Unit Review (verification) for the completed task (\`${task.description}\`).
34757
- Key Checklist:
34758
- 1. Verify if unit test code for the module is written and passes.
34759
- 2. Check for code quality and modularity compliance.
34760
- 3. Instruct immediate correction of found defects or report them.
34761
-
34762
- This task ensures the completeness of the unit before global integration.`,
34763
- parentSessionID: task.parentSessionID,
34764
- depth: task.depth,
34765
- groupID: task.groupID || task.id
34766
- // Group reviews with their origins
34767
- });
34768
- } catch (error92) {
34769
- log(`[MSVP] Failed to trigger review for ${task.id}:`, error92);
34770
- }
34771
- }
34339
+ log(`[ParallelAgentManager] Task ${task.id} completed`);
34772
34340
  progressNotifier.update();
34773
34341
  }
34774
34342
  async recoverActiveTasks() {
34775
- const tasksMap = await taskWAL.readAll();
34776
- if (tasksMap.size === 0) return;
34777
- const tasks = Array.from(tasksMap.values());
34778
- log(`Attempting to recover ${tasks.length} tasks from WAL in parallel...`);
34779
- let recoveredCount = 0;
34780
- const chunks = [];
34781
- const chunkSize = 10;
34782
- for (let i = 0; i < tasks.length; i += chunkSize) {
34783
- chunks.push(tasks.slice(i, i + chunkSize));
34784
- }
34785
- for (const chunk of chunks) {
34786
- await Promise.all(chunk.map(async (task) => {
34787
- if (task.status === TASK_STATUS.RUNNING) {
34788
- try {
34789
- const status = await this.client.session.get({ path: { id: task.sessionID } });
34790
- if (!status.error) {
34791
- this.store.set(task.id, task);
34792
- this.store.trackPending(task.parentSessionID, task.id);
34793
- const toastManager = getTaskToastManager();
34794
- if (toastManager) {
34795
- toastManager.addTask({
34796
- id: task.id,
34797
- description: task.description,
34798
- agent: task.agent,
34799
- isBackground: true,
34800
- parentSessionID: task.parentSessionID,
34801
- sessionID: task.sessionID
34802
- });
34803
- }
34804
- recoveredCount++;
34805
- }
34806
- } catch {
34807
- }
34808
- }
34809
- }));
34810
- }
34343
+ const recoveredCount = await this.executor.recoverAll();
34811
34344
  if (recoveredCount > 0) {
34812
- log(`Recovered ${recoveredCount} active tasks.`);
34813
- this.poller.start();
34345
+ log(`Recovered ${recoveredCount} active tasks via UnifiedTaskExecutor.`);
34346
+ progressNotifier.update();
34814
34347
  }
34815
34348
  }
34816
34349
  };
@@ -35504,7 +35037,7 @@ async function list() {
35504
35037
  expired: new Date(entry.expiresAt) < now
35505
35038
  }));
35506
35039
  }
35507
- async function clear3() {
35040
+ async function clear2() {
35508
35041
  const metadata = await readMetadata();
35509
35042
  const count = Object.keys(metadata.documents).length;
35510
35043
  for (const filename of Object.keys(metadata.documents)) {
@@ -35990,7 +35523,7 @@ Cached: ${doc.fetchedAt}
35990
35523
  ${doc.content}`;
35991
35524
  }
35992
35525
  case CACHE_ACTIONS.CLEAR: {
35993
- const count = await clear3();
35526
+ const count = await clear2();
35994
35527
  return `Cleared ${count} cached documents`;
35995
35528
  }
35996
35529
  case CACHE_ACTIONS.STATS: {
@@ -36258,6 +35791,10 @@ Runs TypeScript compiler and/or ESLint to find issues.
36258
35791
  // src/index.ts
36259
35792
  init_shared();
36260
35793
 
35794
+ // src/core/notification/toast.ts
35795
+ init_toast_core();
35796
+ init_shared();
35797
+
36261
35798
  // src/hooks/constants.ts
36262
35799
  init_shared();
36263
35800
  var HOOK_ACTIONS = {
@@ -36913,7 +36450,7 @@ function getLatest(sessionId) {
36913
36450
  const history = progressHistory.get(sessionId);
36914
36451
  return history?.[history.length - 1];
36915
36452
  }
36916
- function clearSession2(sessionId) {
36453
+ function clearSession(sessionId) {
36917
36454
  progressHistory.delete(sessionId);
36918
36455
  sessionStartTimes.delete(sessionId);
36919
36456
  }
@@ -37244,15 +36781,29 @@ function verifyMissionCompletionSync(directory) {
37244
36781
  }
37245
36782
  if (hasChecklist) {
37246
36783
  result.passed = result.checklistComplete && result.syncIssuesEmpty;
36784
+ if (!result.passed && result.errors.length === 0) {
36785
+ result.errors.push("Checklist verification failed - complete all items");
36786
+ }
37247
36787
  } else {
37248
36788
  result.passed = result.todoComplete && result.syncIssuesEmpty;
36789
+ if (!result.passed && result.errors.length === 0) {
36790
+ if (!result.todoComplete) {
36791
+ result.errors.push("TODO verification failed - complete all items");
36792
+ }
36793
+ if (!result.syncIssuesEmpty) {
36794
+ result.errors.push("Sync issues must be resolved");
36795
+ }
36796
+ }
37249
36797
  }
37250
36798
  log("[verification] Mission verification result", {
37251
36799
  passed: result.passed,
37252
36800
  hasChecklist,
37253
36801
  checklistProgress: result.checklistProgress,
37254
36802
  todoProgress: result.todoProgress,
36803
+ todoComplete: result.todoComplete,
37255
36804
  syncIssuesEmpty: result.syncIssuesEmpty,
36805
+ syncIssuesCount: result.syncIssuesCount,
36806
+ errorCount: result.errors.length,
37256
36807
  errors: result.errors.length > 0 ? result.errors : void 0
37257
36808
  });
37258
36809
  return result;
@@ -37325,8 +36876,19 @@ async function verifyMissionCompletionAsync(directory) {
37325
36876
  }
37326
36877
  if (hasChecklist) {
37327
36878
  result.passed = result.checklistComplete && result.syncIssuesEmpty;
36879
+ if (!result.passed && result.errors.length === 0) {
36880
+ result.errors.push("Checklist verification failed - complete all items");
36881
+ }
37328
36882
  } else {
37329
36883
  result.passed = result.todoComplete && result.syncIssuesEmpty;
36884
+ if (!result.passed && result.errors.length === 0) {
36885
+ if (!result.todoComplete) {
36886
+ result.errors.push("TODO verification failed - complete all items");
36887
+ }
36888
+ if (!result.syncIssuesEmpty) {
36889
+ result.errors.push("Sync issues must be resolved");
36890
+ }
36891
+ }
37330
36892
  }
37331
36893
  lastVerificationResult.set(directory, { result, timestamp: Date.now() });
37332
36894
  return result;
@@ -37991,7 +37553,7 @@ function getNextPending(todos) {
37991
37553
  pending2.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
37992
37554
  return pending2[0];
37993
37555
  }
37994
- function getStats3(todos) {
37556
+ function getStats2(todos) {
37995
37557
  const stats2 = {
37996
37558
  total: todos.length,
37997
37559
  pending: todos.filter((t) => t.status === TODO_STATUS2.PENDING).length,
@@ -38011,7 +37573,7 @@ function getStats3(todos) {
38011
37573
  // src/core/loop/formatters.ts
38012
37574
  init_shared();
38013
37575
  function formatProgress(todos) {
38014
- const stats2 = getStats3(todos);
37576
+ const stats2 = getStats2(todos);
38015
37577
  const done = stats2.completed + stats2.cancelled;
38016
37578
  return `${done}/${stats2.total} (${stats2.percentComplete}%)`;
38017
37579
  }
@@ -38078,6 +37640,129 @@ ${LOOP_LABELS.ACTION_DONT_STOP}
38078
37640
  // src/core/recovery/session-recovery.ts
38079
37641
  init_shared();
38080
37642
  init_shared();
37643
+
37644
+ // src/core/recovery/constants.ts
37645
+ init_shared();
37646
+ var MAX_RETRIES = RECOVERY.MAX_ATTEMPTS;
37647
+ var BASE_DELAY = RECOVERY.BASE_DELAY_MS;
37648
+ var MAX_HISTORY = HISTORY.MAX_RECOVERY;
37649
+
37650
+ // src/core/recovery/patterns.ts
37651
+ var errorPatterns = [
37652
+ // Rate limiting
37653
+ {
37654
+ pattern: /rate.?limit|too.?many.?requests|429/i,
37655
+ category: "rate_limit",
37656
+ handler: (ctx) => {
37657
+ const delay = BASE_DELAY * Math.pow(2, ctx.attempt);
37658
+ presets_exports.warningRateLimited();
37659
+ return { type: "retry", delay, attempt: ctx.attempt + 1 };
37660
+ }
37661
+ },
37662
+ // Context overflow
37663
+ {
37664
+ pattern: /context.?length|token.?limit|maximum.?context/i,
37665
+ category: "context_overflow",
37666
+ handler: () => {
37667
+ presets_exports.errorRecovery("Compacting context");
37668
+ return { type: "compact", reason: "Context limit reached" };
37669
+ }
37670
+ },
37671
+ // Network errors
37672
+ {
37673
+ pattern: /ECONNREFUSED|ETIMEDOUT|network|fetch.?failed/i,
37674
+ category: "network",
37675
+ handler: (ctx) => {
37676
+ if (ctx.attempt >= MAX_RETRIES) {
37677
+ return { type: "abort", reason: "Network unavailable after retries" };
37678
+ }
37679
+ return { type: "retry", delay: BASE_DELAY * (ctx.attempt + 1), attempt: ctx.attempt + 1 };
37680
+ }
37681
+ },
37682
+ // Session errors
37683
+ {
37684
+ pattern: /session.?not.?found|session.?expired/i,
37685
+ category: "session",
37686
+ handler: () => {
37687
+ return { type: "abort", reason: "Session no longer available" };
37688
+ }
37689
+ },
37690
+ // Tool errors
37691
+ {
37692
+ pattern: /tool.?not.?found|unknown.?tool/i,
37693
+ category: "tool",
37694
+ handler: (ctx) => {
37695
+ return { type: "escalate", to: "Reviewer", reason: `Unknown tool used by ${ctx.agent}` };
37696
+ }
37697
+ },
37698
+ // Parse errors
37699
+ {
37700
+ pattern: /parse.?error|invalid.?json|syntax.?error/i,
37701
+ category: "parse",
37702
+ handler: (ctx) => {
37703
+ if (ctx.attempt >= 2) {
37704
+ return { type: "skip", reason: "Persistent parse error" };
37705
+ }
37706
+ return { type: "retry", delay: 500, attempt: ctx.attempt + 1 };
37707
+ }
37708
+ },
37709
+ // Gibberish / hallucination
37710
+ {
37711
+ pattern: /gibberish|hallucination|mixed.?language/i,
37712
+ category: "gibberish",
37713
+ handler: () => {
37714
+ presets_exports.errorRecovery("Retrying with clean context");
37715
+ return { type: "retry", delay: 1e3, attempt: 1 };
37716
+ }
37717
+ },
37718
+ // LSP specific errors
37719
+ {
37720
+ pattern: /lsp.?diagnostics|tsc.?error|eslint.?error/i,
37721
+ category: "lsp",
37722
+ handler: (ctx) => {
37723
+ return { type: "retry", delay: 2e3, attempt: ctx.attempt + 1, modifyPrompt: "Note: Previous attempt had LSP issues. Ensure code quality and consider simpler implementation." };
37724
+ }
37725
+ },
37726
+ // execution timeout
37727
+ {
37728
+ pattern: /execution.?timed.?out|timeout/i,
37729
+ category: "timeout",
37730
+ handler: (ctx) => {
37731
+ return { type: "retry", delay: 5e3, attempt: ctx.attempt + 1, modifyPrompt: "Note: Previous attempt timed out. Break down the task into smaller sub-tasks if necessary." };
37732
+ }
37733
+ }
37734
+ ];
37735
+
37736
+ // src/core/recovery/handler.ts
37737
+ var recoveryHistory = [];
37738
+ function handleError(context) {
37739
+ const errorMessage = context.error.message || String(context.error);
37740
+ for (const pattern of errorPatterns) {
37741
+ const matches = typeof pattern.pattern === "string" ? errorMessage.includes(pattern.pattern) : pattern.pattern.test(errorMessage);
37742
+ if (matches) {
37743
+ const action = pattern.handler(context);
37744
+ recoveryHistory.push({
37745
+ context,
37746
+ action,
37747
+ timestamp: /* @__PURE__ */ new Date()
37748
+ });
37749
+ if (recoveryHistory.length > MAX_HISTORY) {
37750
+ recoveryHistory.shift();
37751
+ }
37752
+ return action;
37753
+ }
37754
+ }
37755
+ if (context.attempt < MAX_RETRIES) {
37756
+ return {
37757
+ type: "retry",
37758
+ delay: BASE_DELAY * Math.pow(2, context.attempt),
37759
+ attempt: context.attempt + 1
37760
+ };
37761
+ }
37762
+ return { type: "abort", reason: `Unknown error after ${MAX_RETRIES} retries` };
37763
+ }
37764
+
37765
+ // src/core/recovery/session-recovery.ts
38081
37766
  var recoveryState = /* @__PURE__ */ new Map();
38082
37767
  function getState2(sessionID) {
38083
37768
  let state2 = recoveryState.get(sessionID);
@@ -38212,6 +37897,61 @@ function isSessionRecovering(sessionID) {
38212
37897
  return recoveryState.get(sessionID)?.isRecovering ?? false;
38213
37898
  }
38214
37899
 
37900
+ // src/core/loop/continuation-lock.ts
37901
+ var locks = /* @__PURE__ */ new Map();
37902
+ var LOCK_TIMEOUT_MS = 12e4;
37903
+ function tryAcquireContinuationLock(sessionID, source) {
37904
+ const now = Date.now();
37905
+ const existing = locks.get(sessionID);
37906
+ if (existing?.acquired) {
37907
+ const elapsed = now - existing.timestamp;
37908
+ if (elapsed < LOCK_TIMEOUT_MS) {
37909
+ log("[continuation-lock] Lock denied - already held", {
37910
+ sessionID: sessionID.slice(0, 8),
37911
+ heldBy: existing.source,
37912
+ requestedBy: source,
37913
+ elapsedMs: elapsed
37914
+ });
37915
+ return false;
37916
+ }
37917
+ log("[continuation-lock] Forcing stale lock release", {
37918
+ sessionID: sessionID.slice(0, 8),
37919
+ staleSource: existing.source,
37920
+ elapsedMs: elapsed
37921
+ });
37922
+ }
37923
+ locks.set(sessionID, { acquired: true, timestamp: now, source });
37924
+ log("[continuation-lock] Lock acquired", {
37925
+ sessionID: sessionID.slice(0, 8),
37926
+ source
37927
+ });
37928
+ return true;
37929
+ }
37930
+ function releaseContinuationLock(sessionID) {
37931
+ const existing = locks.get(sessionID);
37932
+ if (existing?.acquired) {
37933
+ const duration5 = Date.now() - existing.timestamp;
37934
+ log("[continuation-lock] Lock released", {
37935
+ sessionID: sessionID.slice(0, 8),
37936
+ source: existing.source,
37937
+ heldMs: duration5
37938
+ });
37939
+ }
37940
+ locks.delete(sessionID);
37941
+ }
37942
+ function hasContinuationLock(sessionID) {
37943
+ const lock = locks.get(sessionID);
37944
+ if (!lock?.acquired) return false;
37945
+ if (Date.now() - lock.timestamp >= LOCK_TIMEOUT_MS) {
37946
+ log("[continuation-lock] Stale lock detected during check", {
37947
+ sessionID: sessionID.slice(0, 8)
37948
+ });
37949
+ locks.delete(sessionID);
37950
+ return false;
37951
+ }
37952
+ return true;
37953
+ }
37954
+
38215
37955
  // src/core/loop/todo-continuation.ts
38216
37956
  var sessionStates2 = /* @__PURE__ */ new Map();
38217
37957
  var COUNTDOWN_SECONDS = 2;
@@ -38755,6 +38495,11 @@ var TodoSyncService = class {
38755
38495
  taskTodos = /* @__PURE__ */ new Map();
38756
38496
  updateTimeout = null;
38757
38497
  watcher = null;
38498
+ // Batching support
38499
+ pendingUpdates = /* @__PURE__ */ new Set();
38500
+ batchTimer = null;
38501
+ BATCH_WINDOW_MS = 100;
38502
+ // 100ms batch window
38758
38503
  activeSessions = /* @__PURE__ */ new Set();
38759
38504
  constructor(client2, directory) {
38760
38505
  this.client = client2;
@@ -38763,6 +38508,7 @@ var TodoSyncService = class {
38763
38508
  }
38764
38509
  async start() {
38765
38510
  await this.reloadFileTodos();
38511
+ this.broadcastUpdate();
38766
38512
  if (fs10.existsSync(this.todoPath)) {
38767
38513
  let timer;
38768
38514
  this.watcher = fs10.watch(this.todoPath, (eventType) => {
@@ -38821,8 +38567,29 @@ var TodoSyncService = class {
38821
38567
  }
38822
38568
  }
38823
38569
  scheduleUpdate(sessionID) {
38824
- this.sendTodosToSession(sessionID).catch((err) => {
38825
- });
38570
+ this.pendingUpdates.add(sessionID);
38571
+ if (!this.batchTimer) {
38572
+ this.batchTimer = setTimeout(() => {
38573
+ this.flushBatchedUpdates();
38574
+ }, this.BATCH_WINDOW_MS);
38575
+ }
38576
+ }
38577
+ /**
38578
+ * Flush all pending updates in parallel batches
38579
+ */
38580
+ async flushBatchedUpdates() {
38581
+ const sessions = Array.from(this.pendingUpdates);
38582
+ this.pendingUpdates.clear();
38583
+ this.batchTimer = null;
38584
+ if (sessions.length === 0) return;
38585
+ log(`[TodoSync] Flushing ${sessions.length} batched updates`);
38586
+ const CHUNK_SIZE = 5;
38587
+ for (let i = 0; i < sessions.length; i += CHUNK_SIZE) {
38588
+ const chunk = sessions.slice(i, i + CHUNK_SIZE);
38589
+ await Promise.allSettled(
38590
+ chunk.map((sessionID) => this.sendTodosToSession(sessionID))
38591
+ );
38592
+ }
38826
38593
  }
38827
38594
  async sendTodosToSession(sessionID) {
38828
38595
  const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
@@ -38864,6 +38631,11 @@ var TodoSyncService = class {
38864
38631
  if (this.watcher) {
38865
38632
  this.watcher.close();
38866
38633
  }
38634
+ if (this.batchTimer) {
38635
+ clearTimeout(this.batchTimer);
38636
+ this.flushBatchedUpdates().catch(() => {
38637
+ });
38638
+ }
38867
38639
  }
38868
38640
  };
38869
38641
 
@@ -39331,7 +39103,7 @@ function createEventHandler(ctx) {
39331
39103
  const duration5 = totalTime < 6e4 ? `${Math.round(totalTime / 1e3)}s` : `${Math.round(totalTime / 6e4)}m`;
39332
39104
  sessions.delete(sessionID);
39333
39105
  state2.sessions.delete(sessionID);
39334
- clearSession2(sessionID);
39106
+ clearSession(sessionID);
39335
39107
  cleanupSessionRecovery(sessionID);
39336
39108
  cleanupSession2(sessionID);
39337
39109
  cleanupSession3(sessionID);
@@ -39570,6 +39342,8 @@ Wait for these tasks to complete before concluding the mission.
39570
39342
 
39571
39343
  // src/plugin-handlers/system-transform-handler.ts
39572
39344
  init_shared();
39345
+ import { existsSync as existsSync9, readFileSync as readFileSync3 } from "node:fs";
39346
+ import { join as join12 } from "node:path";
39573
39347
  function createSystemTransformHandler(ctx) {
39574
39348
  const { directory, sessions, state: state2 } = ctx;
39575
39349
  return async (input, output) => {
@@ -39599,6 +39373,17 @@ function createSystemTransformHandler(ctx) {
39599
39373
  }
39600
39374
  } catch {
39601
39375
  }
39376
+ const todoPath = join12(directory, PATHS.TODO);
39377
+ if (existsSync9(todoPath)) {
39378
+ try {
39379
+ const todoContent = readFileSync3(todoPath, "utf-8");
39380
+ const todos = parseTodoMd(todoContent);
39381
+ if (todos.length > 0) {
39382
+ systemAdditions.push(buildTodoPrompt(todos));
39383
+ }
39384
+ } catch {
39385
+ }
39386
+ }
39602
39387
  if (systemAdditions.length > 0) {
39603
39388
  output.system.unshift(...systemAdditions);
39604
39389
  }
@@ -39636,6 +39421,27 @@ Use \`get_task_result\` to check completed tasks.
39636
39421
  Use \`delegate_task\` with background=true for parallel work.
39637
39422
  </orchestrator_background_tasks>`;
39638
39423
  }
39424
+ function buildTodoPrompt(todos) {
39425
+ const incompleteTodos = todos.filter((t) => t.status !== "completed");
39426
+ const totalCount = todos.length;
39427
+ const completeCount = totalCount - incompleteTodos.length;
39428
+ if (incompleteTodos.length === 0) {
39429
+ return `<orchestrator_todos>
39430
+ \u2705 All TODOs Complete (${completeCount}/${totalCount})
39431
+
39432
+ All tasks in .opencode/todo.md are finished.
39433
+ </orchestrator_todos>`;
39434
+ }
39435
+ const todoList = incompleteTodos.map((t) => ` - [${t.status === "in_progress" ? "~" : " "}] ${t.content} (${t.priority})`).join("\n");
39436
+ return `<orchestrator_todos>
39437
+ \u{1F4DD} TODO List (${completeCount}/${totalCount} complete):
39438
+
39439
+ ${todoList}
39440
+
39441
+ Use the built-in \`todowrite\` tool to update TODO status as you complete tasks.
39442
+ When you finish a task, mark it as completed in the TODO list.
39443
+ </orchestrator_todos>`;
39444
+ }
39639
39445
 
39640
39446
  // src/index.ts
39641
39447
  var require2 = createRequire(import.meta.url);