opencode-orchestrator 1.2.15 → 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,22 +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: 10,
505
- MAX_CONCURRENCY: 50,
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,
508
+ MAX_CONCURRENCY: 5,
506
509
  // Sync polling (for delegate_task sync mode)
507
- // Optimized: Reduced polling frequency while relying more on events
508
- SYNC_TIMEOUT_MS: 5 * TIME.MINUTE,
509
- POLL_INTERVAL_MS: 2e3,
510
- // 500 → 2000ms (75% less API calls)
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)
511
515
  MIN_IDLE_TIME_MS: 3 * TIME.SECOND,
512
- // 5s → 3s (faster detection)
513
- MIN_STABILITY_MS: 2 * TIME.SECOND,
514
- // 3s2s (faster stability)
516
+ // 5s → 3s (quicker detection)
517
+ MIN_STABILITY_MS: 3 * TIME.SECOND,
518
+ // 5s3s (faster completion)
515
519
  STABLE_POLLS_REQUIRED: 2,
516
- // 3 2 (faster completion)
517
- MAX_POLL_COUNT: 150,
518
- // 600150 (adjusted for 2s interval)
520
+ // Keep at 2 for reliability
521
+ MAX_POLL_COUNT: 600,
522
+ // 150600 (30min = 600 * 3s)
519
523
  // Session naming
520
524
  SESSION_TITLE_PREFIX: "Parallel",
521
525
  // Labels for output
@@ -637,7 +641,7 @@ var init_mission_control = __esm({
637
641
  init_limits();
638
642
  MISSION_CONTROL = {
639
643
  DEFAULT_MAX_ITERATIONS: LIMITS.MAX_ITERATIONS,
640
- DEFAULT_COUNTDOWN_SECONDS: 3,
644
+ DEFAULT_COUNTDOWN_SECONDS: 10,
641
645
  STATE_FILE: "loop-state.json",
642
646
  STOP_COMMAND: "/stop",
643
647
  CANCEL_COMMAND: "/cancel",
@@ -1375,6 +1379,20 @@ var init_special_events = __esm({
1375
1379
  }
1376
1380
  });
1377
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
+
1378
1396
  // src/shared/session/constants/events/index.ts
1379
1397
  var EVENT_TYPES;
1380
1398
  var init_events = __esm({
@@ -1387,6 +1405,7 @@ var init_events = __esm({
1387
1405
  init_mission_events();
1388
1406
  init_message_events();
1389
1407
  init_special_events();
1408
+ init_hook_events();
1390
1409
  init_task_events();
1391
1410
  init_todo_events();
1392
1411
  init_session_events();
@@ -1394,6 +1413,7 @@ var init_events = __esm({
1394
1413
  init_mission_events();
1395
1414
  init_message_events();
1396
1415
  init_special_events();
1416
+ init_hook_events();
1397
1417
  EVENT_TYPES = {
1398
1418
  ...TASK_EVENTS,
1399
1419
  ...TODO_EVENTS,
@@ -1401,7 +1421,8 @@ var init_events = __esm({
1401
1421
  ...DOCUMENT_EVENTS,
1402
1422
  ...MISSION_EVENTS,
1403
1423
  ...MESSAGE_EVENTS,
1404
- ...SPECIAL_EVENTS
1424
+ ...SPECIAL_EVENTS,
1425
+ ...HOOK_EVENTS
1405
1426
  };
1406
1427
  }
1407
1428
  });
@@ -5126,13 +5147,13 @@ __export(store_exports, {
5126
5147
  addDecision: () => addDecision,
5127
5148
  addDocument: () => addDocument,
5128
5149
  addFinding: () => addFinding,
5129
- clear: () => clear2,
5150
+ clear: () => clear,
5130
5151
  clearAll: () => clearAll,
5131
5152
  create: () => create,
5132
5153
  get: () => get,
5133
5154
  getChildren: () => getChildren,
5134
5155
  getMerged: () => getMerged,
5135
- getStats: () => getStats2
5156
+ getStats: () => getStats
5136
5157
  });
5137
5158
  function create(sessionId, parentId) {
5138
5159
  const context = {
@@ -5200,7 +5221,7 @@ function addDecision(sessionId, decision) {
5200
5221
  function getChildren(parentId) {
5201
5222
  return Array.from(parentChildMap.get(parentId) || []);
5202
5223
  }
5203
- function clear2(sessionId) {
5224
+ function clear(sessionId) {
5204
5225
  const context = contexts.get(sessionId);
5205
5226
  if (context?.parentId) {
5206
5227
  parentChildMap.get(context.parentId)?.delete(sessionId);
@@ -5211,7 +5232,7 @@ function clearAll() {
5211
5232
  contexts.clear();
5212
5233
  parentChildMap.clear();
5213
5234
  }
5214
- function getStats2() {
5235
+ function getStats() {
5215
5236
  let totalDocuments = 0;
5216
5237
  let totalFindings = 0;
5217
5238
  let totalDecisions = 0;
@@ -18579,34 +18600,20 @@ var ConcurrencyController = class {
18579
18600
  }
18580
18601
  }
18581
18602
  /**
18582
- * 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.
18583
18607
  */
18584
18608
  reportResult(key, success3) {
18585
18609
  if (success3) {
18586
18610
  const streak = (this.successStreak.get(key) ?? 0) + 1;
18587
18611
  this.successStreak.set(key, streak);
18588
18612
  this.failureCount.set(key, 0);
18589
- if (streak >= 3) {
18590
- const currentLimit = this.getConcurrencyLimit(key);
18591
- if (currentLimit < PARALLEL_TASK.MAX_CONCURRENCY) {
18592
- this.setLimit(key, currentLimit + 1);
18593
- this.successStreak.set(key, 0);
18594
- log(`[concurrency] Auto-scaling UP for ${key}: ${currentLimit + 1}`);
18595
- }
18596
- }
18597
18613
  } else {
18598
18614
  const failures = (this.failureCount.get(key) ?? 0) + 1;
18599
18615
  this.failureCount.set(key, failures);
18600
18616
  this.successStreak.set(key, 0);
18601
- if (failures >= 2) {
18602
- const currentLimit = this.getConcurrencyLimit(key);
18603
- const minLimit = 1;
18604
- if (currentLimit > minLimit) {
18605
- this.setLimit(key, currentLimit - 1);
18606
- this.failureCount.set(key, 0);
18607
- log(`[concurrency] Auto-scaling DOWN for ${key}: ${currentLimit - 1} (due to ${failures} failures)`);
18608
- }
18609
- }
18610
18617
  }
18611
18618
  }
18612
18619
  getQueueLength(key) {
@@ -18809,39 +18816,14 @@ function formatDuration(start, end) {
18809
18816
  if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
18810
18817
  return `${seconds}s`;
18811
18818
  }
18812
- function buildNotificationMessage(tasks) {
18813
- const summary = tasks.map((t) => {
18814
- const status = t.status === TASK_STATUS.COMPLETED ? "\u2705" : "\u274C";
18815
- return `${status} \`${t.id}\`: ${t.description}`;
18816
- }).join("\n");
18817
- return `<system-notification>
18818
- **All Parallel Tasks Complete**
18819
-
18820
- ${summary}
18821
-
18822
- Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
18823
- </system-notification>`;
18824
- }
18825
18819
 
18826
18820
  // src/core/session/session-health.ts
18827
18821
  var sessionHealth = /* @__PURE__ */ new Map();
18828
- var STALE_THRESHOLD_MS = 12e4;
18829
- var HEALTH_CHECK_INTERVAL_MS = 3e4;
18830
- var WARNING_THRESHOLD_MS = 6e4;
18822
+ var STALE_THRESHOLD_MS = 6e5;
18823
+ var HEALTH_CHECK_INTERVAL_MS = 6e4;
18824
+ var WARNING_THRESHOLD_MS = 3e5;
18831
18825
  var healthCheckTimer;
18832
18826
  var client;
18833
- function recordSessionResponse(sessionID) {
18834
- const health = sessionHealth.get(sessionID);
18835
- if (health) {
18836
- health.lastResponseTime = Date.now();
18837
- health.isStale = false;
18838
- }
18839
- }
18840
- function isSessionStale(sessionID) {
18841
- const health = sessionHealth.get(sessionID);
18842
- if (!health) return false;
18843
- return health.isStale;
18844
- }
18845
18827
  function startHealthCheck(opencodeClient) {
18846
18828
  if (healthCheckTimer) {
18847
18829
  log("[session-health] Health check already running");
@@ -18888,292 +18870,6 @@ function performHealthCheck() {
18888
18870
  });
18889
18871
  }
18890
18872
  }
18891
- function cleanupSessionHealth(sessionID) {
18892
- sessionHealth.delete(sessionID);
18893
- }
18894
-
18895
- // src/core/agents/manager/task-launcher.ts
18896
- init_shared();
18897
- init_shared();
18898
-
18899
- // src/core/notification/task-toast-manager.ts
18900
- init_shared();
18901
- var TaskToastManager = class {
18902
- tasks = /* @__PURE__ */ new Map();
18903
- client = null;
18904
- concurrency = null;
18905
- todoSync = null;
18906
- /**
18907
- * Initialize the manager with OpenCode client
18908
- */
18909
- init(client2, concurrency) {
18910
- this.client = client2;
18911
- this.concurrency = concurrency ?? null;
18912
- }
18913
- /**
18914
- * Set concurrency controller (can be set after init)
18915
- */
18916
- setConcurrencyController(concurrency) {
18917
- this.concurrency = concurrency;
18918
- }
18919
- /**
18920
- * Set TodoSyncService for TUI status synchronization
18921
- */
18922
- setTodoSync(service) {
18923
- this.todoSync = service;
18924
- }
18925
- /**
18926
- * Add a new task and show consolidated toast
18927
- */
18928
- addTask(task) {
18929
- const trackedTask = {
18930
- id: task.id,
18931
- description: task.description,
18932
- agent: task.agent,
18933
- status: task.status ?? STATUS_LABEL.RUNNING,
18934
- startedAt: /* @__PURE__ */ new Date(),
18935
- isBackground: task.isBackground,
18936
- parentSessionID: task.parentSessionID,
18937
- sessionID: task.sessionID
18938
- };
18939
- this.tasks.set(task.id, trackedTask);
18940
- this.todoSync?.updateTaskStatus(trackedTask);
18941
- this.showTaskListToast(trackedTask);
18942
- }
18943
- /**
18944
- * Update task status
18945
- */
18946
- updateTask(id, status) {
18947
- const task = this.tasks.get(id);
18948
- if (task) {
18949
- task.status = status;
18950
- this.todoSync?.updateTaskStatus(task);
18951
- }
18952
- }
18953
- /**
18954
- * Remove a task
18955
- */
18956
- removeTask(id) {
18957
- this.tasks.delete(id);
18958
- this.todoSync?.removeTask(id);
18959
- }
18960
- /**
18961
- * Get all running tasks (newest first)
18962
- */
18963
- getRunningTasks() {
18964
- return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.RUNNING).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
18965
- }
18966
- /**
18967
- * Get all queued tasks (oldest first - FIFO)
18968
- */
18969
- getQueuedTasks() {
18970
- return Array.from(this.tasks.values()).filter((t) => t.status === STATUS_LABEL.QUEUED).sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
18971
- }
18972
- /**
18973
- * Get tasks by parent session
18974
- */
18975
- getTasksByParent(parentSessionID) {
18976
- return Array.from(this.tasks.values()).filter((t) => t.parentSessionID === parentSessionID);
18977
- }
18978
- /**
18979
- * Format duration since task started
18980
- */
18981
- formatDuration(startedAt) {
18982
- const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1e3);
18983
- if (seconds < 60) return `${seconds}s`;
18984
- const minutes = Math.floor(seconds / 60);
18985
- if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
18986
- const hours = Math.floor(minutes / 60);
18987
- return `${hours}h ${minutes % 60}m`;
18988
- }
18989
- /**
18990
- * Get concurrency info string (e.g., " [2/5]")
18991
- */
18992
- /**
18993
- * Get concurrency info string (e.g., " [2/5]")
18994
- */
18995
- getConcurrencyInfo() {
18996
- if (!this.concurrency) return "";
18997
- const running = this.getRunningTasks();
18998
- const queued = this.getQueuedTasks();
18999
- const total = running.length;
19000
- const limit = this.concurrency.getConcurrencyLimit("default");
19001
- if (limit === Infinity) return "";
19002
- const filled = TUI_BLOCKS.FILLED.repeat(total);
19003
- const empty = TUI_BLOCKS.EMPTY.repeat(Math.max(0, limit - total));
19004
- return ` [${filled}${empty} ${total}/${limit}]`;
19005
- }
19006
- /**
19007
- * Build consolidated task list message
19008
- */
19009
- buildTaskListMessage(newTask) {
19010
- const running = this.getRunningTasks();
19011
- const queued = this.getQueuedTasks();
19012
- const concurrencyInfo = this.getConcurrencyInfo();
19013
- const lines = [];
19014
- if (running.length > 0) {
19015
- lines.push(`${TUI_ICONS.RUNNING} Running (${running.length}) ${concurrencyInfo}`);
19016
- for (const task of running) {
19017
- const duration5 = this.formatDuration(task.startedAt);
19018
- const bgTag = task.isBackground ? TUI_TAGS.BACKGROUND : TUI_TAGS.FOREGROUND;
19019
- const isNew = newTask && task.id === newTask.id ? TUI_ICONS.NEW : "";
19020
- lines.push(`${bgTag} ${task.description} (${task.agent}) - ${duration5}${isNew}`);
19021
- }
19022
- }
19023
- if (queued.length > 0) {
19024
- if (lines.length > 0) lines.push("");
19025
- lines.push(`${TUI_ICONS.QUEUED} Queued (${queued.length}):`);
19026
- for (const task of queued) {
19027
- const bgTag = task.isBackground ? TUI_TAGS.WAITING : TUI_TAGS.PENDING;
19028
- lines.push(`${bgTag} ${task.description} (${task.agent})`);
19029
- }
19030
- }
19031
- return lines.join("\n");
19032
- }
19033
- /**
19034
- * Show consolidated toast with all running/queued tasks
19035
- */
19036
- showTaskListToast(newTask) {
19037
- if (!this.client || !this.client.tui) return;
19038
- const message = this.buildTaskListMessage(newTask);
19039
- const running = this.getRunningTasks();
19040
- const queued = this.getQueuedTasks();
19041
- const title = newTask.isBackground ? `Background Task Started` : `Task Started`;
19042
- this.client.tui.showToast({
19043
- body: {
19044
- title,
19045
- message: message || `${newTask.description} (${newTask.agent})`,
19046
- variant: STATUS_LABEL.INFO,
19047
- duration: running.length + queued.length > 2 ? 5e3 : 3e3
19048
- }
19049
- }).catch(() => {
19050
- });
19051
- }
19052
- /**
19053
- * Show task completion toast
19054
- */
19055
- showCompletionToast(info) {
19056
- if (!this.client || !this.client.tui) return;
19057
- this.removeTask(info.id);
19058
- const remaining = this.getRunningTasks();
19059
- const queued = this.getQueuedTasks();
19060
- let message;
19061
- let title;
19062
- let variant;
19063
- if (info.status === STATUS_LABEL.ERROR || info.status === STATUS_LABEL.CANCELLED || info.status === STATUS_LABEL.FAILED) {
19064
- title = info.status === STATUS_LABEL.ERROR ? "Task Failed" : "Task Cancelled";
19065
- message = `[FAIL] "${info.description}" ${info.status}
19066
- ${info.error || ""}`;
19067
- variant = STATUS_LABEL.ERROR;
19068
- } else {
19069
- title = "Task Completed";
19070
- message = `[DONE] "${info.description}" finished in ${info.duration}`;
19071
- variant = STATUS_LABEL.SUCCESS;
19072
- }
19073
- if (remaining.length > 0 || queued.length > 0) {
19074
- message += `
19075
-
19076
- Still running: ${remaining.length} | Queued: ${queued.length}`;
19077
- }
19078
- this.client.tui.showToast({
19079
- body: {
19080
- title,
19081
- message,
19082
- variant,
19083
- duration: 5e3
19084
- }
19085
- }).catch(() => {
19086
- });
19087
- }
19088
- /**
19089
- * Show all-tasks-complete summary toast
19090
- */
19091
- showAllCompleteToast(parentSessionID, completedTasks) {
19092
- if (!this.client || !this.client.tui) return;
19093
- const successCount = completedTasks.filter((t) => t.status === STATUS_LABEL.COMPLETED).length;
19094
- const failCount = completedTasks.filter((t) => t.status === STATUS_LABEL.ERROR || t.status === STATUS_LABEL.CANCELLED || t.status === STATUS_LABEL.FAILED).length;
19095
- const taskList = completedTasks.map((t) => `- [${t.status === STATUS_LABEL.COMPLETED ? "OK" : "FAIL"}] ${t.description} (${t.duration})`).join("\n");
19096
- this.client.tui.showToast({
19097
- body: {
19098
- title: "All Tasks Completed",
19099
- message: `${successCount} succeeded, ${failCount} failed
19100
-
19101
- ${taskList}`,
19102
- variant: failCount > 0 ? STATUS_LABEL.WARNING : STATUS_LABEL.SUCCESS,
19103
- duration: 7e3
19104
- }
19105
- }).catch(() => {
19106
- });
19107
- }
19108
- /**
19109
- * Show Mission Complete toast (Grand Finale)
19110
- */
19111
- showMissionCompleteToast(title = "Mission Complete", message = "All tasks completed successfully.") {
19112
- if (!this.client || !this.client.tui) return;
19113
- const decoratedMessage = `
19114
- ${TUI_ICONS.MISSION_COMPLETE} ${TUI_MESSAGES.MISSION_COMPLETE_TITLE} ${TUI_ICONS.MISSION_COMPLETE}
19115
- \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
19116
- ${message}
19117
- \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
19118
- ${TUI_MESSAGES.MISSION_COMPLETE_SUBTITLE}
19119
- `.trim();
19120
- this.client.tui.showToast({
19121
- body: {
19122
- title: `${TUI_ICONS.SHIELD} ${title}`,
19123
- message: decoratedMessage,
19124
- variant: STATUS_LABEL.SUCCESS,
19125
- duration: 1e4
19126
- // Longer duration for the finale
19127
- }
19128
- }).catch(() => {
19129
- });
19130
- }
19131
- /**
19132
- * Show progress toast (for long-running tasks)
19133
- */
19134
- showProgressToast(taskId, progress) {
19135
- if (!this.client || !this.client.tui) return;
19136
- const task = this.tasks.get(taskId);
19137
- if (!task) return;
19138
- const percentage = Math.round(progress.current / progress.total * 100);
19139
- const progressBar = `[${"#".repeat(Math.floor(percentage / 10))}${"-".repeat(10 - Math.floor(percentage / 10))}]`;
19140
- this.client.tui.showToast({
19141
- body: {
19142
- title: `Task Progress: ${task.description}`,
19143
- message: `${progressBar} ${percentage}%
19144
- ${progress.message || ""}`,
19145
- variant: STATUS_LABEL.INFO,
19146
- duration: 2e3
19147
- }
19148
- }).catch(() => {
19149
- });
19150
- }
19151
- /**
19152
- * Clear all tracked tasks
19153
- */
19154
- clear() {
19155
- this.tasks.clear();
19156
- }
19157
- /**
19158
- * Get task count stats
19159
- */
19160
- getStats() {
19161
- const running = this.getRunningTasks().length;
19162
- const queued = this.getQueuedTasks().length;
19163
- return { running, queued, total: this.tasks.size };
19164
- }
19165
- };
19166
- var instance = null;
19167
- function getTaskToastManager() {
19168
- return instance;
19169
- }
19170
- function initTaskToastManager(client2, concurrency) {
19171
- if (!instance) {
19172
- instance = new TaskToastManager();
19173
- }
19174
- instance.init(client2, concurrency);
19175
- return instance;
19176
- }
19177
18873
 
19178
18874
  // src/core/agents/persistence/task-wal.ts
19179
18875
  init_shared();
@@ -19269,130 +18965,1048 @@ var TaskWAL = class {
19269
18965
  };
19270
18966
  var taskWAL = new TaskWAL();
19271
18967
 
19272
- // src/core/recovery/constants.ts
18968
+ // src/core/notification/task-toast-manager.ts
19273
18969
  init_shared();
19274
- var MAX_RETRIES = RECOVERY.MAX_ATTEMPTS;
19275
- var BASE_DELAY = RECOVERY.BASE_DELAY_MS;
19276
- 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 += `
19277
19144
 
19278
- // src/core/notification/toast.ts
19279
- init_toast_core();
19280
- 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
19281
19169
 
19282
- // src/core/recovery/patterns.ts
19283
- var errorPatterns = [
19284
- // Rate limiting
19285
- {
19286
- pattern: /rate.?limit|too.?many.?requests|429/i,
19287
- category: "rate_limit",
19288
- handler: (ctx) => {
19289
- const delay = BASE_DELAY * Math.pow(2, ctx.attempt);
19290
- presets_exports.warningRateLimited();
19291
- 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;
19292
19346
  }
19293
- },
19294
- // Context overflow
19295
- {
19296
- pattern: /context.?length|token.?limit|maximum.?context/i,
19297
- category: "context_overflow",
19298
- handler: () => {
19299
- presets_exports.errorRecovery("Compacting context");
19300
- 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;
19301
19371
  }
19302
- },
19303
- // Network errors
19304
- {
19305
- pattern: /ECONNREFUSED|ETIMEDOUT|network|fetch.?failed/i,
19306
- category: "network",
19307
- handler: (ctx) => {
19308
- if (ctx.attempt >= MAX_RETRIES) {
19309
- 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;
19310
19400
  }
19311
- 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}`);
19312
19427
  }
19313
- },
19314
- // Session errors
19315
- {
19316
- pattern: /session.?not.?found|session.?expired/i,
19317
- category: "session",
19318
- handler: () => {
19319
- 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);
19320
19447
  }
19321
- },
19322
- // Tool errors
19323
- {
19324
- pattern: /tool.?not.?found|unknown.?tool/i,
19325
- category: "tool",
19326
- handler: (ctx) => {
19327
- 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);
19328
19469
  }
19329
- },
19330
- // Parse errors
19331
- {
19332
- pattern: /parse.?error|invalid.?json|syntax.?error/i,
19333
- category: "parse",
19334
- handler: (ctx) => {
19335
- if (ctx.attempt >= 2) {
19336
- 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"));
19337
19513
  }
19338
- return { type: "retry", delay: 500, attempt: ctx.attempt + 1 };
19339
19514
  }
19340
- },
19341
- // Gibberish / hallucination
19342
- {
19343
- pattern: /gibberish|hallucination|mixed.?language/i,
19344
- category: "gibberish",
19345
- handler: () => {
19346
- presets_exports.errorRecovery("Retrying with clean context");
19347
- 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);
19348
19548
  }
19349
- },
19350
- // LSP specific errors
19351
- {
19352
- pattern: /lsp.?diagnostics|tsc.?error|eslint.?error/i,
19353
- category: "lsp",
19354
- handler: (ctx) => {
19355
- 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;
19356
19563
  }
19357
- },
19358
- // execution timeout
19359
- {
19360
- pattern: /execution.?timed.?out|timeout/i,
19361
- category: "timeout",
19362
- handler: (ctx) => {
19363
- 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
+ }
19364
19589
  }
19590
+ if (recovered > 0) {
19591
+ log(`[UnifiedExecutor] Recovered ${recovered} tasks`);
19592
+ this.startPolling();
19593
+ }
19594
+ return recovered;
19365
19595
  }
19366
- ];
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
+ };
19367
19644
 
19368
- // src/core/recovery/handler.ts
19369
- var recoveryHistory = [];
19370
- function handleError(context) {
19371
- const errorMessage = context.error.message || String(context.error);
19372
- for (const pattern of errorPatterns) {
19373
- const matches = typeof pattern.pattern === "string" ? errorMessage.includes(pattern.pattern) : pattern.pattern.test(errorMessage);
19374
- if (matches) {
19375
- const action = pattern.handler(context);
19376
- recoveryHistory.push({
19377
- context,
19378
- action,
19379
- timestamp: /* @__PURE__ */ new Date()
19380
- });
19381
- if (recoveryHistory.length > MAX_HISTORY) {
19382
- 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");
19383
19679
  }
19384
- return action;
19680
+ _SessionPool._instance = new _SessionPool(client2, directory, config3);
19385
19681
  }
19682
+ return _SessionPool._instance;
19386
19683
  }
19387
- 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;
19388
19763
  return {
19389
- type: "retry",
19390
- delay: BASE_DELAY * Math.pow(2, context.attempt),
19391
- 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
19392
19770
  };
19393
19771
  }
19394
- return { type: "abort", reason: `Unknown error after ${MAX_RETRIES} retries` };
19395
- }
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();
19396
20010
 
19397
20011
  // src/core/memory/interfaces.ts
19398
20012
  var MemoryLevel = /* @__PURE__ */ ((MemoryLevel2) => {
@@ -19526,6 +20140,9 @@ var MemoryManager = class _MemoryManager {
19526
20140
  }
19527
20141
  };
19528
20142
 
20143
+ // src/core/agents/manager.ts
20144
+ init_core2();
20145
+
19529
20146
  // src/core/agents/agent-registry.ts
19530
20147
  init_shared();
19531
20148
  import * as fs4 from "fs/promises";
@@ -33180,1202 +33797,212 @@ function convertBaseSchema(schema, ctx) {
33180
33797
  if (typeof schema.maxItems === "number") {
33181
33798
  arraySchema = arraySchema.max(schema.maxItems);
33182
33799
  }
33183
- zodSchema = arraySchema;
33184
- } else {
33185
- zodSchema = z.array(z.any());
33186
- }
33187
- break;
33188
- }
33189
- default:
33190
- throw new Error(`Unsupported type: ${type}`);
33191
- }
33192
- if (schema.description) {
33193
- zodSchema = zodSchema.describe(schema.description);
33194
- }
33195
- if (schema.default !== void 0) {
33196
- zodSchema = zodSchema.default(schema.default);
33197
- }
33198
- return zodSchema;
33199
- }
33200
- function convertSchema(schema, ctx) {
33201
- if (typeof schema === "boolean") {
33202
- return schema ? z.any() : z.never();
33203
- }
33204
- let baseSchema = convertBaseSchema(schema, ctx);
33205
- const hasExplicitType = schema.type || schema.enum !== void 0 || schema.const !== void 0;
33206
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
33207
- const options = schema.anyOf.map((s) => convertSchema(s, ctx));
33208
- const anyOfUnion = z.union(options);
33209
- baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
33210
- }
33211
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
33212
- const options = schema.oneOf.map((s) => convertSchema(s, ctx));
33213
- const oneOfUnion = z.xor(options);
33214
- baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
33215
- }
33216
- if (schema.allOf && Array.isArray(schema.allOf)) {
33217
- if (schema.allOf.length === 0) {
33218
- baseSchema = hasExplicitType ? baseSchema : z.any();
33219
- } else {
33220
- let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx);
33221
- const startIdx = hasExplicitType ? 0 : 1;
33222
- for (let i = startIdx; i < schema.allOf.length; i++) {
33223
- result = z.intersection(result, convertSchema(schema.allOf[i], ctx));
33224
- }
33225
- baseSchema = result;
33226
- }
33227
- }
33228
- if (schema.nullable === true && ctx.version === "openapi-3.0") {
33229
- baseSchema = z.nullable(baseSchema);
33230
- }
33231
- if (schema.readOnly === true) {
33232
- baseSchema = z.readonly(baseSchema);
33233
- }
33234
- const extraMeta = {};
33235
- const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
33236
- for (const key of coreMetadataKeys) {
33237
- if (key in schema) {
33238
- extraMeta[key] = schema[key];
33239
- }
33240
- }
33241
- const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
33242
- for (const key of contentMetadataKeys) {
33243
- if (key in schema) {
33244
- extraMeta[key] = schema[key];
33245
- }
33246
- }
33247
- for (const key of Object.keys(schema)) {
33248
- if (!RECOGNIZED_KEYS.has(key)) {
33249
- extraMeta[key] = schema[key];
33250
- }
33251
- }
33252
- if (Object.keys(extraMeta).length > 0) {
33253
- ctx.registry.add(baseSchema, extraMeta);
33254
- }
33255
- return baseSchema;
33256
- }
33257
- function fromJSONSchema(schema, params) {
33258
- if (typeof schema === "boolean") {
33259
- return schema ? z.any() : z.never();
33260
- }
33261
- const version3 = detectVersion(schema, params?.defaultTarget);
33262
- const defs = schema.$defs || schema.definitions || {};
33263
- const ctx = {
33264
- version: version3,
33265
- defs,
33266
- refs: /* @__PURE__ */ new Map(),
33267
- processing: /* @__PURE__ */ new Set(),
33268
- rootSchema: schema,
33269
- registry: params?.registry ?? globalRegistry2
33270
- };
33271
- return convertSchema(schema, ctx);
33272
- }
33273
-
33274
- // node_modules/zod/v4/classic/coerce.js
33275
- var coerce_exports2 = {};
33276
- __export(coerce_exports2, {
33277
- bigint: () => bigint6,
33278
- boolean: () => boolean6,
33279
- date: () => date8,
33280
- number: () => number6,
33281
- string: () => string6
33282
- });
33283
- function string6(params) {
33284
- return _coercedString2(ZodString2, params);
33285
- }
33286
- function number6(params) {
33287
- return _coercedNumber2(ZodNumber2, params);
33288
- }
33289
- function boolean6(params) {
33290
- return _coercedBoolean2(ZodBoolean2, params);
33291
- }
33292
- function bigint6(params) {
33293
- return _coercedBigint2(ZodBigInt2, params);
33294
- }
33295
- function date8(params) {
33296
- return _coercedDate2(ZodDate2, params);
33297
- }
33298
-
33299
- // node_modules/zod/v4/classic/external.js
33300
- config2(en_default2());
33301
-
33302
- // src/core/agents/agent-registry.ts
33303
- var AgentDefinitionSchema = external_exports2.object({
33304
- id: external_exports2.string(),
33305
- // ID is required inside the definition object
33306
- description: external_exports2.string(),
33307
- systemPrompt: external_exports2.string(),
33308
- mode: external_exports2.enum(["primary", "subagent"]).optional(),
33309
- color: external_exports2.string().optional(),
33310
- hidden: external_exports2.boolean().optional(),
33311
- thinking: external_exports2.boolean().optional(),
33312
- maxTokens: external_exports2.number().optional(),
33313
- budgetTokens: external_exports2.number().optional(),
33314
- canWrite: external_exports2.boolean(),
33315
- // Required per interface
33316
- canBash: external_exports2.boolean()
33317
- // Required per interface
33318
- });
33319
- var AgentRegistry = class _AgentRegistry {
33320
- static instance;
33321
- agents = /* @__PURE__ */ new Map();
33322
- directory = "";
33323
- constructor() {
33324
- for (const [name, def] of Object.entries(AGENTS)) {
33325
- this.agents.set(name, def);
33326
- }
33327
- }
33328
- static getInstance() {
33329
- if (!_AgentRegistry.instance) {
33330
- _AgentRegistry.instance = new _AgentRegistry();
33331
- }
33332
- return _AgentRegistry.instance;
33333
- }
33334
- setDirectory(dir) {
33335
- this.directory = dir;
33336
- this.loadCustomAgents();
33337
- }
33338
- /**
33339
- * Get agent definition by name
33340
- */
33341
- getAgent(name) {
33342
- return this.agents.get(name);
33343
- }
33344
- /**
33345
- * List all available agent names
33346
- */
33347
- listAgents() {
33348
- return Array.from(this.agents.keys());
33349
- }
33350
- /**
33351
- * Add or update an agent definition
33352
- */
33353
- registerAgent(name, def) {
33354
- this.agents.set(name, def);
33355
- log(`[AgentRegistry] Registered agent: ${name}`);
33356
- }
33357
- /**
33358
- * Load custom agents from .opencode/agents.json
33359
- */
33360
- async loadCustomAgents() {
33361
- if (!this.directory) return;
33362
- const agentsConfigPath = path4.join(this.directory, PATHS.AGENTS_CONFIG);
33363
- try {
33364
- const content = await fs4.readFile(agentsConfigPath, "utf-8");
33365
- const customAgents = JSON.parse(content);
33366
- if (typeof customAgents === "object" && customAgents !== null) {
33367
- for (const [name, def] of Object.entries(customAgents)) {
33368
- const result = AgentDefinitionSchema.safeParse(def);
33369
- if (result.success) {
33370
- this.registerAgent(name, def);
33371
- } else {
33372
- log(`[AgentRegistry] Invalid custom agent definition for: ${name}. Errors: ${result.error.message}`);
33373
- }
33374
- }
33375
- }
33376
- } catch (error92) {
33377
- if (error92.code !== "ENOENT") {
33378
- log(`[AgentRegistry] Error loading custom agents: ${error92}`);
33379
- }
33380
- }
33381
- }
33382
- };
33383
-
33384
- // src/core/agents/manager/task-launcher.ts
33385
- var TaskLauncher = class {
33386
- constructor(client2, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
33387
- this.client = client2;
33388
- this.directory = directory;
33389
- this.store = store;
33390
- this.concurrency = concurrency;
33391
- this.sessionPool = sessionPool2;
33392
- this.onTaskError = onTaskError;
33393
- this.startPolling = startPolling;
33394
- }
33395
- /**
33396
- * Unified launch method - handles both single and multiple tasks efficiently.
33397
- * All session creations happen in parallel immediately.
33398
- * Concurrency acquisition and prompt firing happen in the background.
33399
- */
33400
- async launch(inputs) {
33401
- const isArray = Array.isArray(inputs);
33402
- const taskInputs = isArray ? inputs : [inputs];
33403
- if (taskInputs.length === 0) return isArray ? [] : null;
33404
- const tasks = await Promise.all(taskInputs.map(
33405
- (input) => this.prepareTask(input).catch(() => null)
33406
- ));
33407
- const successfulTasks = tasks.filter((t) => t !== null);
33408
- successfulTasks.forEach((task) => {
33409
- this.executeBackground(task).catch((error92) => {
33410
- this.onTaskError(task.id, error92);
33411
- });
33412
- });
33413
- if (successfulTasks.length > 0) {
33414
- this.startPolling();
33415
- }
33416
- return isArray ? successfulTasks : successfulTasks[0] || null;
33417
- }
33418
- /**
33419
- * Prepare task: Create session and registration without blocking on concurrency
33420
- */
33421
- async prepareTask(input) {
33422
- const currentDepth = input.depth ?? 0;
33423
- if (currentDepth >= PARALLEL_TASK.MAX_DEPTH) {
33424
- throw new Error(`Maximum task depth (${PARALLEL_TASK.MAX_DEPTH}) reached. To prevent infinite recursion, no further sub-tasks can be spawned.`);
33425
- }
33426
- const session = await this.sessionPool.acquire(
33427
- input.agent,
33428
- input.parentSessionID,
33429
- input.description
33430
- );
33431
- const sessionID = session.id;
33432
- const taskId = `${ID_PREFIX.TASK}${crypto.randomUUID().slice(0, 8)}`;
33433
- const task = {
33434
- id: taskId,
33435
- sessionID,
33436
- parentSessionID: input.parentSessionID,
33437
- description: input.description,
33438
- prompt: input.prompt,
33439
- agent: input.agent,
33440
- status: TASK_STATUS.PENDING,
33441
- // Start as PENDING
33442
- startedAt: /* @__PURE__ */ new Date(),
33443
- concurrencyKey: input.agent,
33444
- depth: (input.depth ?? 0) + 1,
33445
- mode: input.mode || "normal",
33446
- groupID: input.groupID
33447
- };
33448
- this.store.set(taskId, task);
33449
- this.store.trackPending(input.parentSessionID, taskId);
33450
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
33451
- });
33452
- const toastManager = getTaskToastManager();
33453
- if (toastManager) {
33454
- toastManager.addTask({
33455
- id: taskId,
33456
- description: input.description,
33457
- agent: input.agent,
33458
- isBackground: true,
33459
- parentSessionID: input.parentSessionID,
33460
- sessionID
33461
- });
33462
- }
33463
- presets_exports.sessionCreated(sessionID, input.agent);
33464
- return task;
33465
- }
33466
- /**
33467
- * Background execution: Acquire slot and fire prompt with auto-retry
33468
- */
33469
- async executeBackground(task) {
33470
- let attempt = 1;
33471
- while (true) {
33472
- try {
33473
- await this.concurrency.acquire(task.agent);
33474
- task.status = TASK_STATUS.RUNNING;
33475
- task.startedAt = /* @__PURE__ */ new Date();
33476
- this.store.set(task.id, task);
33477
- taskWAL.log(WAL_ACTIONS.LAUNCH, task).catch(() => {
33478
- });
33479
- const agentDef = AgentRegistry.getInstance().getAgent(task.agent);
33480
- let finalPrompt = task.prompt;
33481
- if (agentDef) {
33482
- finalPrompt = `### AGENT ROLE: ${agentDef.id}
33483
- ${agentDef.description}
33484
-
33485
- ${agentDef.systemPrompt}
33486
-
33487
- ${finalPrompt}`;
33488
- }
33489
- const memory = MemoryManager.getInstance().getContext(finalPrompt);
33490
- const injectedPrompt = memory ? `${memory}
33491
-
33492
- ${finalPrompt}` : finalPrompt;
33493
- const wireAgent = Object.values(AGENT_NAMES).includes(task.agent) ? task.agent : AGENT_NAMES.COMMANDER;
33494
- const promptPromise = this.client.session.prompt({
33495
- path: { id: task.sessionID },
33496
- body: {
33497
- agent: wireAgent,
33498
- tools: {
33499
- [TOOL_NAMES.DELEGATE_TASK]: true,
33500
- [TOOL_NAMES.GET_TASK_RESULT]: true,
33501
- [TOOL_NAMES.LIST_TASKS]: true,
33502
- [TOOL_NAMES.CANCEL_TASK]: true,
33503
- [TOOL_NAMES.SKILL]: true,
33504
- [TOOL_NAMES.RUN_COMMAND]: true
33505
- },
33506
- parts: [{ type: PART_TYPES.TEXT, text: injectedPrompt }]
33507
- }
33508
- });
33509
- await Promise.race([
33510
- promptPromise,
33511
- new Promise(
33512
- (_, reject) => setTimeout(() => reject(new Error("Session prompt execution timed out after 600s")), 6e5)
33513
- )
33514
- ]);
33515
- return;
33516
- } catch (error92) {
33517
- this.concurrency.release(task.agent);
33518
- const context = {
33519
- sessionId: task.sessionID,
33520
- taskId: task.id,
33521
- agent: task.agent,
33522
- error: error92 instanceof Error ? error92 : new Error(String(error92)),
33523
- attempt,
33524
- timestamp: /* @__PURE__ */ new Date()
33525
- };
33526
- const action = handleError(context);
33527
- if (action.type === "retry") {
33528
- log(`[AutoRetry] Task ${task.id} failed (attempt ${attempt}). Retrying in ${action.delay}ms...`);
33529
- if (action.modifyPrompt) {
33530
- task.prompt += `
33531
-
33532
- ${action.modifyPrompt}`;
33533
- }
33534
- await new Promise((r) => setTimeout(r, action.delay));
33535
- attempt++;
33536
- continue;
33537
- }
33538
- throw error92;
33539
- }
33540
- }
33541
- }
33542
- };
33543
-
33544
- // src/core/agents/manager/task-resumer.ts
33545
- init_shared();
33546
- var TaskResumer = class {
33547
- constructor(client2, store, findBySession, startPolling, notifyParentIfAllComplete) {
33548
- this.client = client2;
33549
- this.store = store;
33550
- this.findBySession = findBySession;
33551
- this.startPolling = startPolling;
33552
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
33553
- }
33554
- async resume(input) {
33555
- const existingTask = this.findBySession(input.sessionId);
33556
- if (!existingTask) {
33557
- throw new Error(`Task not found for session: ${input.sessionId}`);
33558
- }
33559
- existingTask.status = TASK_STATUS.RUNNING;
33560
- existingTask.completedAt = void 0;
33561
- existingTask.error = void 0;
33562
- existingTask.result = void 0;
33563
- existingTask.parentSessionID = input.parentSessionID;
33564
- existingTask.startedAt = /* @__PURE__ */ new Date();
33565
- existingTask.stablePolls = 0;
33566
- this.store.trackPending(input.parentSessionID, existingTask.id);
33567
- this.startPolling();
33568
- taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
33569
- });
33570
- log(`Resuming task ${existingTask.id} in session ${existingTask.sessionID}`);
33571
- this.client.session.prompt({
33572
- path: { id: existingTask.sessionID },
33573
- body: {
33574
- agent: existingTask.agent,
33575
- parts: [{ type: PART_TYPES.TEXT, text: input.prompt }]
33576
- }
33577
- }).catch((error92) => {
33578
- log(`Resume prompt error for ${existingTask.id}:`, error92);
33579
- existingTask.status = TASK_STATUS.ERROR;
33580
- existingTask.error = error92 instanceof Error ? error92.message : String(error92);
33581
- existingTask.completedAt = /* @__PURE__ */ new Date();
33582
- this.store.untrackPending(input.parentSessionID, existingTask.id);
33583
- this.store.queueNotification(existingTask);
33584
- this.notifyParentIfAllComplete(input.parentSessionID).catch(() => {
33585
- });
33586
- taskWAL.log(WAL_ACTIONS.UPDATE, existingTask).catch(() => {
33587
- });
33588
- });
33589
- return existingTask;
33590
- }
33591
- };
33592
-
33593
- // src/core/agents/config.ts
33594
- init_shared();
33595
- var CONFIG = {
33596
- TASK_TTL_MS: PARALLEL_TASK.TTL_MS,
33597
- CLEANUP_DELAY_MS: PARALLEL_TASK.CLEANUP_DELAY_MS,
33598
- MIN_STABILITY_MS: PARALLEL_TASK.MIN_STABILITY_MS,
33599
- POLL_INTERVAL_MS: PARALLEL_TASK.POLL_INTERVAL_MS
33600
- };
33601
-
33602
- // src/core/agents/manager/task-poller.ts
33603
- init_shared();
33604
- init_shared();
33605
-
33606
- // src/core/progress/state-broadcaster.ts
33607
- var StateBroadcaster = class _StateBroadcaster {
33608
- static _instance;
33609
- listeners = /* @__PURE__ */ new Set();
33610
- currentState = null;
33611
- constructor() {
33612
- }
33613
- static getInstance() {
33614
- if (!_StateBroadcaster._instance) {
33615
- _StateBroadcaster._instance = new _StateBroadcaster();
33616
- }
33617
- return _StateBroadcaster._instance;
33618
- }
33619
- subscribe(listener) {
33620
- this.listeners.add(listener);
33621
- if (this.currentState) {
33622
- listener(this.currentState);
33623
- }
33624
- return () => this.listeners.delete(listener);
33625
- }
33626
- broadcast(state2) {
33627
- this.currentState = state2;
33628
- this.listeners.forEach((listener) => {
33629
- try {
33630
- listener(state2);
33631
- } catch (error92) {
33632
- }
33633
- });
33634
- }
33635
- getCurrentState() {
33636
- return this.currentState;
33637
- }
33638
- };
33639
- var stateBroadcaster = StateBroadcaster.getInstance();
33640
-
33641
- // src/core/progress/progress-notifier.ts
33642
- init_shared();
33643
- var ProgressNotifier = class _ProgressNotifier {
33644
- static _instance;
33645
- manager = null;
33646
- constructor() {
33647
- stateBroadcaster.subscribe(this.handleStateChange.bind(this));
33648
- }
33649
- static getInstance() {
33650
- if (!_ProgressNotifier._instance) {
33651
- _ProgressNotifier._instance = new _ProgressNotifier();
33652
- }
33653
- return _ProgressNotifier._instance;
33654
- }
33655
- setManager(manager) {
33656
- this.manager = manager;
33657
- }
33658
- /**
33659
- * Poll current status from ParallelAgentManager and broadcast it
33660
- */
33661
- update() {
33662
- if (!this.manager) return;
33663
- const tasks = this.manager.getAllTasks();
33664
- const running = tasks.filter((t) => t.status === TASK_STATUS.RUNNING);
33665
- const completed = tasks.filter((t) => t.status === TASK_STATUS.COMPLETED);
33666
- const total = tasks.length;
33667
- const percentage = total > 0 ? Math.round(completed.length / total * 100) : 0;
33668
- const state2 = {
33669
- missionId: "current-mission",
33670
- // Could be dynamic
33671
- status: percentage === 100 ? "completed" : "executing",
33672
- progress: {
33673
- totalTasks: total,
33674
- completedTasks: completed.length,
33675
- percentage
33676
- },
33677
- activeAgents: running.map((t) => ({
33678
- id: t.id,
33679
- type: t.agent,
33680
- status: t.status,
33681
- currentTask: t.description
33682
- })),
33683
- todo: [],
33684
- // Need to fetch from TodoEnforcer if possible
33685
- lastUpdated: /* @__PURE__ */ new Date()
33686
- };
33687
- stateBroadcaster.broadcast(state2);
33688
- }
33689
- handleStateChange(state2) {
33690
- if (state2.progress.percentage > 0 && state2.progress.percentage % 25 === 0) {
33691
- const toastManager = getTaskToastManager();
33692
- if (toastManager) {
33693
- }
33694
- }
33695
- }
33696
- };
33697
- var progressNotifier = ProgressNotifier.getInstance();
33698
-
33699
- // src/core/agents/manager/task-poller.ts
33700
- var MAX_TASK_DURATION_MS = 6e5;
33701
- var TaskPoller = class {
33702
- constructor(client2, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete, onTaskError) {
33703
- this.client = client2;
33704
- this.store = store;
33705
- this.concurrency = concurrency;
33706
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
33707
- this.scheduleCleanup = scheduleCleanup;
33708
- this.pruneExpiredTasks = pruneExpiredTasks;
33709
- this.onTaskComplete = onTaskComplete;
33710
- this.onTaskError = onTaskError;
33711
- }
33712
- pollingInterval;
33713
- messageCache = /* @__PURE__ */ new Map();
33714
- start() {
33715
- if (this.pollingInterval) return;
33716
- log("[task-poller.ts] start() - polling started");
33717
- this.pollingInterval = setInterval(() => this.poll(), CONFIG.POLL_INTERVAL_MS);
33718
- this.pollingInterval.unref();
33719
- }
33720
- stop() {
33721
- if (this.pollingInterval) {
33722
- clearInterval(this.pollingInterval);
33723
- this.pollingInterval = void 0;
33724
- }
33725
- }
33726
- isRunning() {
33727
- return !!this.pollingInterval;
33728
- }
33729
- async poll() {
33730
- this.pruneExpiredTasks();
33731
- const running = this.store.getRunning();
33732
- if (running.length === 0) {
33733
- this.stop();
33734
- return;
33735
- }
33736
- log("[task-poller.ts] poll() checking", running.length, "running tasks");
33737
- try {
33738
- const statusResult = await this.client.session.status();
33739
- const allStatuses = statusResult.data ?? {};
33740
- for (const task of running) {
33741
- try {
33742
- const taskDuration = Date.now() - task.startedAt.getTime();
33743
- if (isSessionStale(task.sessionID)) {
33744
- log(`[task-poller] Task ${task.id} session is stale. Marking as error.`);
33745
- this.onTaskError?.(task.id, new Error("Session became stale (no response from agent)"));
33746
- continue;
33747
- }
33748
- if (taskDuration > MAX_TASK_DURATION_MS) {
33749
- log(`[task-poller] Task ${task.id} exceeded max duration (${MAX_TASK_DURATION_MS}ms). Marking as error.`);
33750
- this.onTaskError?.(task.id, new Error("Task exceeded maximum execution time"));
33751
- continue;
33752
- }
33753
- if (task.status === TASK_STATUS.PENDING) continue;
33754
- const sessionStatus = allStatuses[task.sessionID];
33755
- if (sessionStatus?.type === SESSION_STATUS.IDLE) {
33756
- const elapsed2 = Date.now() - task.startedAt.getTime();
33757
- if (elapsed2 < CONFIG.MIN_STABILITY_MS) continue;
33758
- if (!task.hasStartedOutputting && !await this.validateSessionHasOutput(task.sessionID, task)) continue;
33759
- await this.completeTask(task);
33760
- continue;
33761
- }
33762
- await this.updateTaskProgress(task);
33763
- const elapsed = Date.now() - task.startedAt.getTime();
33764
- if (elapsed >= CONFIG.MIN_STABILITY_MS && task.stablePolls && task.stablePolls >= 3) {
33765
- if (task.hasStartedOutputting || await this.validateSessionHasOutput(task.sessionID, task)) {
33766
- log(`Task ${task.id} stable for 3 polls, completing...`);
33767
- await this.completeTask(task);
33768
- }
33769
- }
33770
- } catch (error92) {
33771
- log(`Poll error for task ${task.id}:`, error92);
33772
- }
33773
- }
33774
- progressNotifier.update();
33775
- } catch (error92) {
33776
- log("Polling error:", error92);
33777
- }
33778
- }
33779
- async validateSessionHasOutput(sessionID, task) {
33780
- try {
33781
- const response = await this.client.session.messages({ path: { id: sessionID } });
33782
- const messages = response.data ?? [];
33783
- 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));
33784
- if (hasOutput && task) {
33785
- task.hasStartedOutputting = true;
33786
- }
33787
- return hasOutput;
33788
- } catch {
33789
- return true;
33790
- }
33791
- }
33792
- async completeTask(task) {
33793
- log("[task-poller.ts] completeTask() called for", task.id, task.agent);
33794
- task.status = TASK_STATUS.COMPLETED;
33795
- task.completedAt = /* @__PURE__ */ new Date();
33796
- if (task.concurrencyKey) {
33797
- this.concurrency.release(task.concurrencyKey);
33798
- this.concurrency.reportResult(task.concurrencyKey, true);
33799
- task.concurrencyKey = void 0;
33800
- }
33801
- this.store.untrackPending(task.parentSessionID, task.id);
33802
- this.store.queueNotification(task);
33803
- await this.notifyParentIfAllComplete(task.parentSessionID);
33804
- this.scheduleCleanup(task.id);
33805
- taskWAL.log(WAL_ACTIONS.COMPLETE, task).catch(() => {
33806
- });
33807
- if (this.onTaskComplete) {
33808
- Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
33809
- }
33810
- const duration5 = formatDuration(task.startedAt, task.completedAt);
33811
- presets_exports.sessionCompleted(task.sessionID, duration5);
33812
- log(`Completed ${task.id} (${duration5})`);
33813
- progressNotifier.update();
33814
- }
33815
- async updateTaskProgress(task) {
33816
- try {
33817
- const cached3 = this.messageCache.get(task.sessionID);
33818
- const statusResult = await this.client.session.status();
33819
- const sessionInfo = statusResult.data?.[task.sessionID];
33820
- const currentMsgCount = sessionInfo?.messageCount ?? 0;
33821
- if (cached3 && cached3.count === currentMsgCount) {
33822
- task.stablePolls = (task.stablePolls ?? 0) + 1;
33823
- return;
33824
- }
33825
- const result = await this.client.session.messages({ path: { id: task.sessionID } });
33826
- this.messageCache.set(task.sessionID, { count: currentMsgCount, lastChecked: /* @__PURE__ */ new Date() });
33827
- if (result.error) return;
33828
- const messages = result.data ?? [];
33829
- const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
33830
- let toolCalls = 0;
33831
- let lastTool;
33832
- let lastMessage;
33833
- for (const msg of assistantMsgs) {
33834
- for (const part of msg.parts ?? []) {
33835
- if (part.type === PART_TYPES.TOOL_USE || part.tool) {
33836
- toolCalls++;
33837
- lastTool = part.tool || part.name;
33838
- }
33839
- if (part.type === PART_TYPES.TEXT && part.text) {
33840
- lastMessage = part.text;
33841
- }
33842
- }
33843
- }
33844
- task.progress = {
33845
- toolCalls,
33846
- lastTool,
33847
- lastMessage: lastMessage?.slice(0, 100),
33848
- lastUpdate: /* @__PURE__ */ new Date()
33849
- };
33850
- if (task.lastMsgCount === currentMsgCount) {
33851
- task.stablePolls = 0;
33800
+ zodSchema = arraySchema;
33852
33801
  } else {
33853
- task.stablePolls = 0;
33802
+ zodSchema = z.array(z.any());
33854
33803
  }
33855
- task.lastMsgCount = currentMsgCount;
33856
- } catch {
33804
+ break;
33857
33805
  }
33806
+ default:
33807
+ throw new Error(`Unsupported type: ${type}`);
33858
33808
  }
33859
- };
33860
-
33861
- // src/core/agents/manager/task-cleaner.ts
33862
- init_shared();
33863
- init_store();
33864
- var TaskCleaner = class {
33865
- constructor(client2, store, concurrency, sessionPool2) {
33866
- this.client = client2;
33867
- this.store = store;
33868
- this.concurrency = concurrency;
33869
- this.sessionPool = sessionPool2;
33809
+ if (schema.description) {
33810
+ zodSchema = zodSchema.describe(schema.description);
33870
33811
  }
33871
- pruneExpiredTasks() {
33872
- const now = Date.now();
33873
- for (const [taskId, task] of this.store.getAll().map((t) => [t.id, t])) {
33874
- const age = now - task.startedAt.getTime();
33875
- if (age <= CONFIG.TASK_TTL_MS) continue;
33876
- log(`Timeout: ${taskId}`);
33877
- if (task.status === TASK_STATUS.RUNNING) {
33878
- task.status = TASK_STATUS.TIMEOUT;
33879
- task.error = "Task exceeded 30 minute time limit";
33880
- task.completedAt = /* @__PURE__ */ new Date();
33881
- if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
33882
- this.store.untrackPending(task.parentSessionID, taskId);
33883
- const toastManager = getTaskToastManager();
33884
- if (toastManager) {
33885
- toastManager.showCompletionToast({
33886
- id: taskId,
33887
- description: task.description,
33888
- duration: formatDuration(task.startedAt, task.completedAt),
33889
- status: TASK_STATUS.ERROR,
33890
- error: task.error
33891
- });
33892
- }
33893
- }
33894
- this.sessionPool.release(task.sessionID).catch(() => {
33895
- });
33896
- clear2(task.sessionID);
33897
- this.store.delete(taskId);
33898
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
33899
- });
33900
- }
33901
- this.store.cleanEmptyNotifications();
33812
+ if (schema.default !== void 0) {
33813
+ zodSchema = zodSchema.default(schema.default);
33902
33814
  }
33903
- scheduleCleanup(taskId) {
33904
- const task = this.store.get(taskId);
33905
- const sessionID = task?.sessionID;
33906
- setTimeout(async () => {
33907
- if (sessionID) {
33908
- try {
33909
- await this.sessionPool.release(sessionID);
33910
- clear2(sessionID);
33911
- } catch {
33912
- }
33913
- }
33914
- this.store.delete(taskId);
33915
- if (task) taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
33916
- });
33917
- log(`Cleaned up ${taskId}`);
33918
- }, 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();
33919
33820
  }
33920
- /**
33921
- * Notify parent session when task(s) complete.
33922
- * Uses noReply strategy:
33923
- * - Individual completion: noReply=true (silent notification, save tokens)
33924
- * - All complete: noReply=false (AI should process and report results)
33925
- */
33926
- async notifyParentIfAllComplete(parentSessionID) {
33927
- const pendingCount = this.store.getPendingCount(parentSessionID);
33928
- const notifications = this.store.getNotifications(parentSessionID);
33929
- if (notifications.length === 0) return;
33930
- const allComplete = pendingCount === 0;
33931
- const toastManager = getTaskToastManager();
33932
- const completionInfos = notifications.map((task) => ({
33933
- id: task.id,
33934
- description: task.description,
33935
- duration: formatDuration(task.startedAt, task.completedAt),
33936
- status: task.status,
33937
- error: task.error
33938
- }));
33939
- if (allComplete && completionInfos.length > 1 && toastManager) {
33940
- toastManager.showAllCompleteToast(parentSessionID, completionInfos);
33941
- } else if (toastManager) {
33942
- for (const info of completionInfos) {
33943
- 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));
33944
33841
  }
33842
+ baseSchema = result;
33945
33843
  }
33946
- let message;
33947
- if (allComplete) {
33948
- message = buildNotificationMessage(notifications);
33949
- message += `
33950
-
33951
- **ACTION REQUIRED:** All background tasks are complete. Use \`get_task_result(taskId)\` to retrieve outputs and continue with the mission.`;
33952
- } else {
33953
- const completedCount = notifications.length;
33954
- message = `[BACKGROUND UPDATE] ${completedCount} task(s) completed, ${pendingCount} still running.
33955
- Completed: ${notifications.map((t) => `\`${t.id}\``).join(", ")}
33956
- 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];
33957
33856
  }
33958
- try {
33959
- await this.client.session.prompt({
33960
- path: { id: parentSessionID },
33961
- body: {
33962
- // Key optimization: only trigger AI response when ALL complete
33963
- noReply: !allComplete,
33964
- parts: [{ type: PART_TYPES.TEXT, text: message }]
33965
- }
33966
- });
33967
- log(`Notified parent ${parentSessionID} (allComplete=${allComplete}, noReply=${!allComplete})`);
33968
- } catch (error92) {
33969
- 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];
33970
33862
  }
33971
- this.store.clearNotifications(parentSessionID);
33972
33863
  }
33973
- };
33974
-
33975
- // src/core/agents/manager/event-handler.ts
33976
- init_shared();
33977
-
33978
- // src/core/loop/continuation-lock.ts
33979
- var locks = /* @__PURE__ */ new Map();
33980
- var LOCK_TIMEOUT_MS = 3e4;
33981
- function tryAcquireContinuationLock(sessionID, source) {
33982
- const now = Date.now();
33983
- const existing = locks.get(sessionID);
33984
- if (existing?.acquired) {
33985
- const elapsed = now - existing.timestamp;
33986
- if (elapsed < LOCK_TIMEOUT_MS) {
33987
- log("[continuation-lock] Lock denied - already held", {
33988
- sessionID: sessionID.slice(0, 8),
33989
- heldBy: existing.source,
33990
- requestedBy: source,
33991
- elapsedMs: elapsed
33992
- });
33993
- return false;
33864
+ for (const key of Object.keys(schema)) {
33865
+ if (!RECOGNIZED_KEYS.has(key)) {
33866
+ extraMeta[key] = schema[key];
33994
33867
  }
33995
- log("[continuation-lock] Forcing stale lock release", {
33996
- sessionID: sessionID.slice(0, 8),
33997
- staleSource: existing.source,
33998
- elapsedMs: elapsed
33999
- });
34000
33868
  }
34001
- locks.set(sessionID, { acquired: true, timestamp: now, source });
34002
- log("[continuation-lock] Lock acquired", {
34003
- sessionID: sessionID.slice(0, 8),
34004
- source
34005
- });
34006
- return true;
34007
- }
34008
- function releaseContinuationLock(sessionID) {
34009
- const existing = locks.get(sessionID);
34010
- if (existing?.acquired) {
34011
- const duration5 = Date.now() - existing.timestamp;
34012
- log("[continuation-lock] Lock released", {
34013
- sessionID: sessionID.slice(0, 8),
34014
- source: existing.source,
34015
- heldMs: duration5
34016
- });
33869
+ if (Object.keys(extraMeta).length > 0) {
33870
+ ctx.registry.add(baseSchema, extraMeta);
34017
33871
  }
34018
- locks.delete(sessionID);
33872
+ return baseSchema;
34019
33873
  }
34020
- function hasContinuationLock(sessionID) {
34021
- const lock = locks.get(sessionID);
34022
- if (!lock?.acquired) return false;
34023
- if (Date.now() - lock.timestamp >= LOCK_TIMEOUT_MS) {
34024
- log("[continuation-lock] Stale lock detected during check", {
34025
- sessionID: sessionID.slice(0, 8)
34026
- });
34027
- locks.delete(sessionID);
34028
- return false;
33874
+ function fromJSONSchema(schema, params) {
33875
+ if (typeof schema === "boolean") {
33876
+ return schema ? z.any() : z.never();
34029
33877
  }
34030
- 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);
34031
33889
  }
34032
- function cleanupContinuationLock(sessionID) {
34033
- 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);
34034
33914
  }
34035
33915
 
34036
- // src/core/agents/manager/event-handler.ts
34037
- var EventHandler = class {
34038
- constructor(client2, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
34039
- this.client = client2;
34040
- this.store = store;
34041
- this.concurrency = concurrency;
34042
- this.findBySession = findBySession;
34043
- this.notifyParentIfAllComplete = notifyParentIfAllComplete;
34044
- this.scheduleCleanup = scheduleCleanup;
34045
- this.validateSessionHasOutput = validateSessionHasOutput2;
34046
- this.onTaskComplete = onTaskComplete;
34047
- }
34048
- /**
34049
- * Handle OpenCode session events for proper resource cleanup.
34050
- * Call this from your plugin's event hook.
34051
- */
34052
- handle(event) {
34053
- const props = event.properties;
34054
- if (event.type === SESSION_EVENTS.IDLE) {
34055
- const sessionID = props?.sessionID;
34056
- if (!sessionID) return;
34057
- recordSessionResponse(sessionID);
34058
- const task = this.findBySession(sessionID);
34059
- if (!task || task.status !== TASK_STATUS.RUNNING) return;
34060
- this.handleSessionIdle(task).catch((err) => {
34061
- log("Error handling session.idle:", err);
34062
- });
34063
- }
34064
- if (event.type === SESSION_EVENTS.DELETED) {
34065
- const sessionID = props?.info?.id ?? props?.sessionID;
34066
- if (!sessionID) return;
34067
- const task = this.findBySession(sessionID);
34068
- if (!task) return;
34069
- this.handleSessionDeleted(task);
34070
- }
34071
- }
34072
- async handleSessionIdle(task) {
34073
- const elapsed = Date.now() - task.startedAt.getTime();
34074
- if (elapsed < CONFIG.MIN_STABILITY_MS) {
34075
- log(`Session idle but too early for ${task.id}, waiting...`);
34076
- return;
34077
- }
34078
- const hasOutput = await this.validateSessionHasOutput(task.sessionID);
34079
- if (!hasOutput) {
34080
- log(`Session idle but no output for ${task.id}, waiting...`);
34081
- return;
34082
- }
34083
- task.status = TASK_STATUS.COMPLETED;
34084
- task.completedAt = /* @__PURE__ */ new Date();
34085
- if (task.concurrencyKey) {
34086
- this.concurrency.release(task.concurrencyKey);
34087
- this.concurrency.reportResult(task.concurrencyKey, true);
34088
- task.concurrencyKey = void 0;
34089
- }
34090
- this.store.untrackPending(task.parentSessionID, task.id);
34091
- this.store.queueNotification(task);
34092
- await this.notifyParentIfAllComplete(task.parentSessionID);
34093
- this.scheduleCleanup(task.id);
34094
- taskWAL.log(WAL_ACTIONS.COMPLETE, task).catch(() => {
34095
- });
34096
- if (this.onTaskComplete) {
34097
- Promise.resolve(this.onTaskComplete(task)).catch((err) => log("Error in onTaskComplete callback:", err));
34098
- }
34099
- progressNotifier.update();
34100
- log(`Task ${task.id} completed via session.idle event (${formatDuration(task.startedAt, task.completedAt)})`);
34101
- }
34102
- handleSessionDeleted(task) {
34103
- log(`Session deleted event for task ${task.id}`);
34104
- if (task.status === TASK_STATUS.RUNNING) {
34105
- task.status = TASK_STATUS.ERROR;
34106
- task.error = "Session deleted";
34107
- task.completedAt = /* @__PURE__ */ new Date();
34108
- }
34109
- if (task.concurrencyKey) {
34110
- this.concurrency.release(task.concurrencyKey);
34111
- this.concurrency.reportResult(task.concurrencyKey, false);
34112
- task.concurrencyKey = void 0;
34113
- }
34114
- this.store.untrackPending(task.parentSessionID, task.id);
34115
- this.store.clearNotificationsForTask(task.id);
34116
- this.store.delete(task.id);
34117
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
34118
- });
34119
- cleanupSessionHealth(task.sessionID);
34120
- cleanupContinuationLock(task.sessionID);
34121
- progressNotifier.update();
34122
- log(`Cleaned up deleted session task: ${task.id}`);
34123
- }
34124
- };
33916
+ // node_modules/zod/v4/classic/external.js
33917
+ config2(en_default2());
34125
33918
 
34126
- // src/core/agents/session-pool.ts
34127
- init_shared();
34128
- var DEFAULT_CONFIG = {
34129
- maxPoolSizePerAgent: 5,
34130
- idleTimeoutMs: 3e5,
34131
- // 5 minutes
34132
- maxReuseCount: 10,
34133
- healthCheckIntervalMs: 6e4
34134
- // 1 minute
34135
- };
34136
- var SessionPool = class _SessionPool {
34137
- static _instance;
34138
- pool = /* @__PURE__ */ new Map();
34139
- // key: agentName
34140
- sessionsById = /* @__PURE__ */ new Map();
34141
- config;
34142
- client;
34143
- directory;
34144
- healthCheckInterval = null;
34145
- // Statistics
34146
- stats = {
34147
- reuseHits: 0,
34148
- creationMisses: 0
34149
- };
34150
- constructor(client2, directory, config3 = {}) {
34151
- this.client = client2;
34152
- this.directory = directory;
34153
- this.config = { ...DEFAULT_CONFIG, ...config3 };
34154
- this.startHealthCheck();
34155
- }
34156
- static getInstance(client2, directory, config3) {
34157
- if (!_SessionPool._instance) {
34158
- if (!client2 || !directory) {
34159
- throw new Error("SessionPool requires client and directory on first call");
34160
- }
34161
- _SessionPool._instance = new _SessionPool(client2, directory, config3);
34162
- }
34163
- return _SessionPool._instance;
34164
- }
34165
- /**
34166
- * Acquire a session from the pool or create a new one.
34167
- */
34168
- async acquire(agentName, parentSessionID, description) {
34169
- const poolKey = this.getPoolKey(agentName);
34170
- const agentPool = this.pool.get(poolKey) || [];
34171
- const available = agentPool.find((s) => !s.inUse && s.reuseCount < this.config.maxReuseCount);
34172
- if (available) {
34173
- available.inUse = true;
34174
- available.lastUsedAt = /* @__PURE__ */ new Date();
34175
- available.reuseCount++;
34176
- this.stats.reuseHits++;
34177
- log(`[SessionPool] Reusing session ${available.id.slice(0, 8)}... for ${agentName} (reuse #${available.reuseCount})`);
34178
- 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);
34179
33943
  }
34180
- this.stats.creationMisses++;
34181
- return this.createSession(agentName, parentSessionID, description);
34182
33944
  }
34183
- /**
34184
- * Release a session back to the pool for reuse.
34185
- */
34186
- async release(sessionId) {
34187
- const session = this.sessionsById.get(sessionId);
34188
- if (!session) {
34189
- log(`[SessionPool] Session ${sessionId.slice(0, 8)}... not found in pool`);
34190
- return;
34191
- }
34192
- const now = Date.now();
34193
- const age = now - session.createdAt.getTime();
34194
- const idle = now - session.lastUsedAt.getTime();
34195
- if (session.reuseCount >= this.config.maxReuseCount || age > this.config.idleTimeoutMs * 2) {
34196
- await this.invalidate(sessionId);
34197
- return;
34198
- }
34199
- const poolKey = this.getPoolKey(session.agentName);
34200
- const agentPool = this.pool.get(poolKey) || [];
34201
- const availableCount = agentPool.filter((s) => !s.inUse).length;
34202
- if (availableCount >= this.config.maxPoolSizePerAgent) {
34203
- const oldest = agentPool.filter((s) => !s.inUse).sort((a, b) => a.lastUsedAt.getTime() - b.lastUsedAt.getTime())[0];
34204
- if (oldest) {
34205
- await this.deleteSession(oldest.id);
34206
- }
33945
+ static getInstance() {
33946
+ if (!_AgentRegistry.instance) {
33947
+ _AgentRegistry.instance = new _AgentRegistry();
34207
33948
  }
34208
- await this.resetSession(sessionId);
34209
- session.inUse = false;
34210
- log(`[SessionPool] Released session ${sessionId.slice(0, 8)}... to pool`);
33949
+ return _AgentRegistry.instance;
34211
33950
  }
34212
- /**
34213
- * Invalidate a session (remove from pool and delete).
34214
- */
34215
- async invalidate(sessionId) {
34216
- const session = this.sessionsById.get(sessionId);
34217
- if (!session) return;
34218
- await this.deleteSession(sessionId);
34219
- log(`[SessionPool] Invalidated session ${sessionId.slice(0, 8)}...`);
33951
+ setDirectory(dir) {
33952
+ this.directory = dir;
33953
+ this.loadCustomAgents();
34220
33954
  }
34221
33955
  /**
34222
- * Get current pool statistics.
33956
+ * Get agent definition by name
34223
33957
  */
34224
- getStats() {
34225
- const byAgent = {};
34226
- for (const [agentName, sessions] of this.pool.entries()) {
34227
- const inUse = sessions.filter((s) => s.inUse).length;
34228
- byAgent[agentName] = {
34229
- total: sessions.length,
34230
- inUse,
34231
- available: sessions.length - inUse
34232
- };
34233
- }
34234
- const allSessions = Array.from(this.sessionsById.values());
34235
- const inUseCount = allSessions.filter((s) => s.inUse).length;
34236
- return {
34237
- totalSessions: allSessions.length,
34238
- sessionsInUse: inUseCount,
34239
- availableSessions: allSessions.length - inUseCount,
34240
- reuseHits: this.stats.reuseHits,
34241
- creationMisses: this.stats.creationMisses,
34242
- byAgent
34243
- };
33958
+ getAgent(name) {
33959
+ return this.agents.get(name);
34244
33960
  }
34245
33961
  /**
34246
- * Cleanup stale sessions.
33962
+ * List all available agent names
34247
33963
  */
34248
- async cleanup() {
34249
- const now = Date.now();
34250
- let cleanedCount = 0;
34251
- for (const [sessionId, session] of this.sessionsById.entries()) {
34252
- if (session.inUse) continue;
34253
- const idle = now - session.lastUsedAt.getTime();
34254
- if (idle > this.config.idleTimeoutMs) {
34255
- await this.deleteSession(sessionId);
34256
- cleanedCount++;
34257
- }
34258
- }
34259
- if (cleanedCount > 0) {
34260
- log(`[SessionPool] Cleaned up ${cleanedCount} stale sessions`);
34261
- }
34262
- return cleanedCount;
33964
+ listAgents() {
33965
+ return Array.from(this.agents.keys());
34263
33966
  }
34264
33967
  /**
34265
- * Shutdown the pool.
33968
+ * Add or update an agent definition
34266
33969
  */
34267
- async shutdown() {
34268
- log("[SessionPool] Shutting down...");
34269
- if (this.healthCheckInterval) {
34270
- clearInterval(this.healthCheckInterval);
34271
- this.healthCheckInterval = null;
34272
- }
34273
- const deletePromises = Array.from(this.sessionsById.keys()).map(
34274
- (id) => this.deleteSession(id).catch(() => {
34275
- })
34276
- );
34277
- await Promise.all(deletePromises);
34278
- this.pool.clear();
34279
- this.sessionsById.clear();
34280
- log("[SessionPool] Shutdown complete");
33970
+ registerAgent(name, def) {
33971
+ this.agents.set(name, def);
33972
+ log(`[AgentRegistry] Registered agent: ${name}`);
34281
33973
  }
34282
- // =========================================================================
34283
- // Private Methods
34284
- // =========================================================================
34285
33974
  /**
34286
- * Reset/Compact a session to clear context for next reuse.
33975
+ * Load custom agents from .opencode/agents.json
34287
33976
  */
34288
- async resetSession(sessionId) {
34289
- const session = this.sessionsById.get(sessionId);
34290
- if (!session) return;
34291
- 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);
34292
33980
  try {
34293
- await this.client.session.compact?.({ path: { id: sessionId } });
34294
- session.lastResetAt = /* @__PURE__ */ new Date();
34295
- session.health = "healthy";
34296
- } catch (error92) {
34297
- log(`[SessionPool] Failed to reset session ${sessionId.slice(0, 8)}: ${error92}`);
34298
- session.health = "degraded";
34299
- }
34300
- }
34301
- getPoolKey(agentName) {
34302
- return agentName;
34303
- }
34304
- async createSession(agentName, parentSessionID, description) {
34305
- log(`[SessionPool] Creating new session for ${agentName}`);
34306
- const result = await Promise.race([
34307
- this.client.session.create({
34308
- body: {
34309
- parentID: parentSessionID,
34310
- title: `${PARALLEL_TASK.SESSION_TITLE_PREFIX}: ${description}`
34311
- },
34312
- query: { directory: this.directory }
34313
- }),
34314
- new Promise(
34315
- (_, reject) => setTimeout(() => reject(new Error("Session creation timed out after 60s")), 6e4)
34316
- )
34317
- ]);
34318
- if (result.error || !result.data?.id) {
34319
- throw new Error(`Session creation failed: ${result.error || "No ID"}`);
34320
- }
34321
- const session = {
34322
- id: result.data.id,
34323
- agentName,
34324
- projectDirectory: this.directory,
34325
- createdAt: /* @__PURE__ */ new Date(),
34326
- lastUsedAt: /* @__PURE__ */ new Date(),
34327
- reuseCount: 0,
34328
- inUse: true,
34329
- health: "healthy",
34330
- lastResetAt: /* @__PURE__ */ new Date()
34331
- };
34332
- const poolKey = this.getPoolKey(agentName);
34333
- const agentPool = this.pool.get(poolKey) || [];
34334
- agentPool.push(session);
34335
- this.pool.set(poolKey, agentPool);
34336
- this.sessionsById.set(session.id, session);
34337
- return session;
34338
- }
34339
- async deleteSession(sessionId) {
34340
- const session = this.sessionsById.get(sessionId);
34341
- if (!session) return;
34342
- this.sessionsById.delete(sessionId);
34343
- const poolKey = this.getPoolKey(session.agentName);
34344
- const agentPool = this.pool.get(poolKey);
34345
- if (agentPool) {
34346
- const idx = agentPool.findIndex((s) => s.id === sessionId);
34347
- if (idx !== -1) {
34348
- 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
+ }
34349
33992
  }
34350
- if (agentPool.length === 0) {
34351
- this.pool.delete(poolKey);
33993
+ } catch (error92) {
33994
+ if (error92.code !== "ENOENT") {
33995
+ log(`[AgentRegistry] Error loading custom agents: ${error92}`);
34352
33996
  }
34353
33997
  }
34354
- try {
34355
- await this.client.session.delete({ path: { id: sessionId } });
34356
- } catch {
34357
- }
34358
- }
34359
- startHealthCheck() {
34360
- this.healthCheckInterval = setInterval(() => {
34361
- this.cleanup().catch(() => {
34362
- });
34363
- }, this.config.healthCheckIntervalMs);
34364
- this.healthCheckInterval.unref?.();
34365
33998
  }
34366
33999
  };
34367
- var sessionPool = {
34368
- getInstance: SessionPool.getInstance.bind(SessionPool)
34369
- };
34370
-
34371
- // src/core/agents/manager.ts
34372
- init_core2();
34373
34000
 
34374
34001
  // src/core/todo/todo-manager.ts
34375
34002
  init_shared();
34376
34003
  import * as fs5 from "node:fs";
34377
34004
  import * as path5 from "node:path";
34378
- import * as crypto2 from "node:crypto";
34005
+ import * as crypto from "node:crypto";
34379
34006
  var TodoManager = class _TodoManager {
34380
34007
  static _instance;
34381
34008
  directory = "";
@@ -34445,13 +34072,22 @@ var TodoManager = class _TodoManager {
34445
34072
  });
34446
34073
  }
34447
34074
  async _internalUpdate(expectedVersion, updater, author) {
34448
- const MAX_RETRIES2 = 3;
34449
- const RETRY_DELAY = 50;
34075
+ const MAX_RETRIES2 = 5;
34076
+ const BASE_DELAY_MS = 50;
34450
34077
  for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
34451
34078
  try {
34452
34079
  const current = await this.readWithVersion();
34453
34080
  if (current.version.version !== expectedVersion) {
34454
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
+ }
34455
34091
  return {
34456
34092
  success: false,
34457
34093
  currentVersion: current.version.version,
@@ -34474,18 +34110,21 @@ var TodoManager = class _TodoManager {
34474
34110
  await fs5.promises.rename(tmpPath, this.todoPath);
34475
34111
  this.logChange(newVersion, newContent, author).catch(() => {
34476
34112
  });
34477
- log(`[TodoManager] Updated TODO to v${newVersion} by ${author}`);
34113
+ log(`[TodoManager] Updated TODO to v${newVersion} by ${author}${attempt > 0 ? ` (after ${attempt} retries)` : ""}`);
34478
34114
  return { success: true, currentVersion: newVersion };
34479
34115
  } catch (error92) {
34480
34116
  if (attempt === MAX_RETRIES2 - 1) throw error92;
34481
- 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));
34482
34120
  }
34483
34121
  }
34484
- throw new Error("Failed to update TODO");
34122
+ throw new Error("Failed to update TODO after max retries");
34485
34123
  }
34486
34124
  async updateItem(searchText, newStatus, author = "system") {
34487
- let retries = 5;
34488
- while (retries-- > 0) {
34125
+ const MAX_RETRIES2 = 5;
34126
+ const BASE_DELAY_MS = 50;
34127
+ for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
34489
34128
  const data = await this.readWithVersion();
34490
34129
  const statusMap = {
34491
34130
  [TODO_CONSTANTS.STATUS.PENDING]: TODO_CONSTANTS.MARKERS.PENDING,
@@ -34508,13 +34147,18 @@ var TodoManager = class _TodoManager {
34508
34147
  }, author);
34509
34148
  if (result.success) return true;
34510
34149
  if (!result.conflict) return false;
34511
- 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
+ }
34512
34155
  }
34513
34156
  return false;
34514
34157
  }
34515
34158
  async addSubTask(parentText, subTaskText, author = "system") {
34516
- let retries = 5;
34517
- while (retries-- > 0) {
34159
+ const MAX_RETRIES2 = 5;
34160
+ const BASE_DELAY_MS = 50;
34161
+ for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
34518
34162
  const data = await this.readWithVersion();
34519
34163
  const result = await this.update(data.version.version, (content) => {
34520
34164
  const lines = content.split("\n");
@@ -34538,7 +34182,11 @@ var TodoManager = class _TodoManager {
34538
34182
  }, author);
34539
34183
  if (result.success) return true;
34540
34184
  if (!result.conflict) return false;
34541
- 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
+ }
34542
34190
  }
34543
34191
  return false;
34544
34192
  }
@@ -34547,7 +34195,7 @@ var TodoManager = class _TodoManager {
34547
34195
  version: version3,
34548
34196
  timestamp: Date.now(),
34549
34197
  author,
34550
- contentHash: crypto2.createHash("sha256").update(content).digest("hex"),
34198
+ contentHash: crypto.createHash("sha256").update(content).digest("hex"),
34551
34199
  size: content.length
34552
34200
  };
34553
34201
  await fs5.promises.appendFile(this.historyPath, JSON.stringify(entry) + "\n", "utf-8");
@@ -34562,12 +34210,8 @@ var ParallelAgentManager = class _ParallelAgentManager {
34562
34210
  directory;
34563
34211
  concurrency = new ConcurrencyController();
34564
34212
  sessionPool;
34565
- // Composed components
34566
- launcher;
34567
- resumer;
34568
- poller;
34569
- cleaner;
34570
- eventHandler;
34213
+ // Unified executor (replaces 5 separate components)
34214
+ executor;
34571
34215
  constructor(client2, directory) {
34572
34216
  this.client = client2;
34573
34217
  this.directory = directory;
@@ -34578,42 +34222,12 @@ var ParallelAgentManager = class _ParallelAgentManager {
34578
34222
  AgentRegistry.getInstance().setDirectory(directory);
34579
34223
  TodoManager.getInstance().setDirectory(directory);
34580
34224
  this.sessionPool = SessionPool.getInstance(client2, directory);
34581
- this.cleaner = new TaskCleaner(client2, this.store, this.concurrency, this.sessionPool);
34582
- this.poller = new TaskPoller(
34583
- client2,
34584
- this.store,
34585
- this.concurrency,
34586
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
34587
- (taskId) => this.cleaner.scheduleCleanup(taskId),
34588
- () => this.cleaner.pruneExpiredTasks(),
34589
- (task) => this.handleTaskComplete(task),
34590
- (taskId, error92) => this.handleTaskError(taskId, error92)
34591
- );
34592
- this.launcher = new TaskLauncher(
34225
+ this.executor = new UnifiedTaskExecutor(
34593
34226
  client2,
34594
34227
  directory,
34595
34228
  this.store,
34596
34229
  this.concurrency,
34597
- this.sessionPool,
34598
- (taskId, error92) => this.handleTaskError(taskId, error92),
34599
- () => this.poller.start()
34600
- );
34601
- this.resumer = new TaskResumer(
34602
- client2,
34603
- this.store,
34604
- (sessionID) => this.findBySession(sessionID),
34605
- () => this.poller.start(),
34606
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
34607
- );
34608
- this.eventHandler = new EventHandler(
34609
- client2,
34610
- this.store,
34611
- this.concurrency,
34612
- (sessionID) => this.findBySession(sessionID),
34613
- (parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
34614
- (taskId) => this.cleaner.scheduleCleanup(taskId),
34615
- (sessionID) => this.poller.validateSessionHasOutput(sessionID),
34616
- (task) => this.handleTaskComplete(task)
34230
+ this.sessionPool
34617
34231
  );
34618
34232
  progressNotifier.setManager(this);
34619
34233
  this.recoverActiveTasks().catch((err) => {
@@ -34633,13 +34247,22 @@ var ParallelAgentManager = class _ParallelAgentManager {
34633
34247
  // Public API
34634
34248
  // ========================================================================
34635
34249
  async launch(inputs) {
34636
- this.cleaner.pruneExpiredTasks();
34637
- const result = await this.launcher.launch(inputs);
34638
- progressNotifier.update();
34639
- 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
+ }
34640
34259
  }
34641
34260
  async resume(input) {
34642
- 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;
34643
34266
  }
34644
34267
  getTask(id) {
34645
34268
  return this.store.get(id);
@@ -34654,25 +34277,11 @@ var ParallelAgentManager = class _ParallelAgentManager {
34654
34277
  return this.store.getByParent(parentSessionID);
34655
34278
  }
34656
34279
  async cancelTask(taskId) {
34657
- const task = this.store.get(taskId);
34658
- if (!task || task.status !== TASK_STATUS.RUNNING) return false;
34659
- task.status = TASK_STATUS.ERROR;
34660
- task.error = "Cancelled by user";
34661
- task.completedAt = /* @__PURE__ */ new Date();
34662
- if (task.concurrencyKey) this.concurrency.release(task.concurrencyKey);
34663
- this.store.untrackPending(task.parentSessionID, taskId);
34664
- try {
34665
- await this.client.session.delete({ path: { id: task.sessionID } });
34666
- log(`Session ${task.sessionID.slice(0, 8)}... deleted`);
34667
- } catch {
34668
- log(`Session ${task.sessionID.slice(0, 8)}... already gone`);
34280
+ const result = await this.executor.cancel(taskId);
34281
+ if (result) {
34282
+ progressNotifier.update();
34669
34283
  }
34670
- this.cleaner.scheduleCleanup(taskId);
34671
- taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
34672
- });
34673
- progressNotifier.update();
34674
- log(`Cancelled ${taskId}`);
34675
- return true;
34284
+ return result;
34676
34285
  }
34677
34286
  async getResult(taskId) {
34678
34287
  const task = this.store.get(taskId);
@@ -34703,7 +34312,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
34703
34312
  return this.concurrency;
34704
34313
  }
34705
34314
  cleanup() {
34706
- this.poller.stop();
34315
+ this.executor.cleanup();
34707
34316
  stopHealthCheck();
34708
34317
  this.store.clear();
34709
34318
  MemoryManager.getInstance().clearTaskMemory();
@@ -34715,7 +34324,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
34715
34324
  // Event Handling
34716
34325
  // ========================================================================
34717
34326
  handleEvent(event) {
34718
- this.eventHandler.handle(event);
34327
+ log("[ParallelAgentManager] Event received:", event.type);
34719
34328
  }
34720
34329
  // ========================================================================
34721
34330
  // Private Helpers
@@ -34724,87 +34333,17 @@ var ParallelAgentManager = class _ParallelAgentManager {
34724
34333
  return this.store.getAll().find((t) => t.sessionID === sessionID);
34725
34334
  }
34726
34335
  handleTaskError(taskId, error92) {
34727
- const task = this.store.get(taskId);
34728
- if (!task) return;
34729
- task.status = TASK_STATUS.ERROR;
34730
- task.error = error92 instanceof Error ? error92.message : String(error92);
34731
- task.completedAt = /* @__PURE__ */ new Date();
34732
- if (task.concurrencyKey) {
34733
- this.concurrency.release(task.concurrencyKey);
34734
- this.concurrency.reportResult(task.concurrencyKey, false);
34735
- }
34736
- this.store.untrackPending(task.parentSessionID, taskId);
34737
- this.cleaner.notifyParentIfAllComplete(task.parentSessionID);
34738
- this.cleaner.scheduleCleanup(taskId);
34739
- progressNotifier.update();
34740
- taskWAL.log(WAL_ACTIONS.UPDATE, task).catch(() => {
34741
- });
34336
+ log(`[ParallelAgentManager] Delegating error handling to executor for task ${taskId}`);
34742
34337
  }
34743
34338
  async handleTaskComplete(task) {
34744
- if (task.agent === AGENT_NAMES.WORKER && task.mode !== "race") {
34745
- log(`[MSVP] Triggering Unit Review for task ${task.id}`);
34746
- try {
34747
- await this.launch({
34748
- agent: AGENT_NAMES.REVIEWER,
34749
- description: `Unit Review: ${task.description}`,
34750
- prompt: `Perform a Unit Review (verification) for the completed task (\`${task.description}\`).
34751
- Key Checklist:
34752
- 1. Verify if unit test code for the module is written and passes.
34753
- 2. Check for code quality and modularity compliance.
34754
- 3. Instruct immediate correction of found defects or report them.
34755
-
34756
- This task ensures the completeness of the unit before global integration.`,
34757
- parentSessionID: task.parentSessionID,
34758
- depth: task.depth,
34759
- groupID: task.groupID || task.id
34760
- // Group reviews with their origins
34761
- });
34762
- } catch (error92) {
34763
- log(`[MSVP] Failed to trigger review for ${task.id}:`, error92);
34764
- }
34765
- }
34339
+ log(`[ParallelAgentManager] Task ${task.id} completed`);
34766
34340
  progressNotifier.update();
34767
34341
  }
34768
34342
  async recoverActiveTasks() {
34769
- const tasksMap = await taskWAL.readAll();
34770
- if (tasksMap.size === 0) return;
34771
- const tasks = Array.from(tasksMap.values());
34772
- log(`Attempting to recover ${tasks.length} tasks from WAL in parallel...`);
34773
- let recoveredCount = 0;
34774
- const chunks = [];
34775
- const chunkSize = 10;
34776
- for (let i = 0; i < tasks.length; i += chunkSize) {
34777
- chunks.push(tasks.slice(i, i + chunkSize));
34778
- }
34779
- for (const chunk of chunks) {
34780
- await Promise.all(chunk.map(async (task) => {
34781
- if (task.status === TASK_STATUS.RUNNING) {
34782
- try {
34783
- const status = await this.client.session.get({ path: { id: task.sessionID } });
34784
- if (!status.error) {
34785
- this.store.set(task.id, task);
34786
- this.store.trackPending(task.parentSessionID, task.id);
34787
- const toastManager = getTaskToastManager();
34788
- if (toastManager) {
34789
- toastManager.addTask({
34790
- id: task.id,
34791
- description: task.description,
34792
- agent: task.agent,
34793
- isBackground: true,
34794
- parentSessionID: task.parentSessionID,
34795
- sessionID: task.sessionID
34796
- });
34797
- }
34798
- recoveredCount++;
34799
- }
34800
- } catch {
34801
- }
34802
- }
34803
- }));
34804
- }
34343
+ const recoveredCount = await this.executor.recoverAll();
34805
34344
  if (recoveredCount > 0) {
34806
- log(`Recovered ${recoveredCount} active tasks.`);
34807
- this.poller.start();
34345
+ log(`Recovered ${recoveredCount} active tasks via UnifiedTaskExecutor.`);
34346
+ progressNotifier.update();
34808
34347
  }
34809
34348
  }
34810
34349
  };
@@ -35498,7 +35037,7 @@ async function list() {
35498
35037
  expired: new Date(entry.expiresAt) < now
35499
35038
  }));
35500
35039
  }
35501
- async function clear3() {
35040
+ async function clear2() {
35502
35041
  const metadata = await readMetadata();
35503
35042
  const count = Object.keys(metadata.documents).length;
35504
35043
  for (const filename of Object.keys(metadata.documents)) {
@@ -35984,7 +35523,7 @@ Cached: ${doc.fetchedAt}
35984
35523
  ${doc.content}`;
35985
35524
  }
35986
35525
  case CACHE_ACTIONS.CLEAR: {
35987
- const count = await clear3();
35526
+ const count = await clear2();
35988
35527
  return `Cleared ${count} cached documents`;
35989
35528
  }
35990
35529
  case CACHE_ACTIONS.STATS: {
@@ -36252,6 +35791,10 @@ Runs TypeScript compiler and/or ESLint to find issues.
36252
35791
  // src/index.ts
36253
35792
  init_shared();
36254
35793
 
35794
+ // src/core/notification/toast.ts
35795
+ init_toast_core();
35796
+ init_shared();
35797
+
36255
35798
  // src/hooks/constants.ts
36256
35799
  init_shared();
36257
35800
  var HOOK_ACTIONS = {
@@ -36907,7 +36450,7 @@ function getLatest(sessionId) {
36907
36450
  const history = progressHistory.get(sessionId);
36908
36451
  return history?.[history.length - 1];
36909
36452
  }
36910
- function clearSession2(sessionId) {
36453
+ function clearSession(sessionId) {
36911
36454
  progressHistory.delete(sessionId);
36912
36455
  sessionStartTimes.delete(sessionId);
36913
36456
  }
@@ -37238,15 +36781,29 @@ function verifyMissionCompletionSync(directory) {
37238
36781
  }
37239
36782
  if (hasChecklist) {
37240
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
+ }
37241
36787
  } else {
37242
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
+ }
37243
36797
  }
37244
36798
  log("[verification] Mission verification result", {
37245
36799
  passed: result.passed,
37246
36800
  hasChecklist,
37247
36801
  checklistProgress: result.checklistProgress,
37248
36802
  todoProgress: result.todoProgress,
36803
+ todoComplete: result.todoComplete,
37249
36804
  syncIssuesEmpty: result.syncIssuesEmpty,
36805
+ syncIssuesCount: result.syncIssuesCount,
36806
+ errorCount: result.errors.length,
37250
36807
  errors: result.errors.length > 0 ? result.errors : void 0
37251
36808
  });
37252
36809
  return result;
@@ -37319,8 +36876,19 @@ async function verifyMissionCompletionAsync(directory) {
37319
36876
  }
37320
36877
  if (hasChecklist) {
37321
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
+ }
37322
36882
  } else {
37323
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
+ }
37324
36892
  }
37325
36893
  lastVerificationResult.set(directory, { result, timestamp: Date.now() });
37326
36894
  return result;
@@ -37985,7 +37553,7 @@ function getNextPending(todos) {
37985
37553
  pending2.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
37986
37554
  return pending2[0];
37987
37555
  }
37988
- function getStats3(todos) {
37556
+ function getStats2(todos) {
37989
37557
  const stats2 = {
37990
37558
  total: todos.length,
37991
37559
  pending: todos.filter((t) => t.status === TODO_STATUS2.PENDING).length,
@@ -38005,7 +37573,7 @@ function getStats3(todos) {
38005
37573
  // src/core/loop/formatters.ts
38006
37574
  init_shared();
38007
37575
  function formatProgress(todos) {
38008
- const stats2 = getStats3(todos);
37576
+ const stats2 = getStats2(todos);
38009
37577
  const done = stats2.completed + stats2.cancelled;
38010
37578
  return `${done}/${stats2.total} (${stats2.percentComplete}%)`;
38011
37579
  }
@@ -38072,6 +37640,129 @@ ${LOOP_LABELS.ACTION_DONT_STOP}
38072
37640
  // src/core/recovery/session-recovery.ts
38073
37641
  init_shared();
38074
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
38075
37766
  var recoveryState = /* @__PURE__ */ new Map();
38076
37767
  function getState2(sessionID) {
38077
37768
  let state2 = recoveryState.get(sessionID);
@@ -38206,6 +37897,61 @@ function isSessionRecovering(sessionID) {
38206
37897
  return recoveryState.get(sessionID)?.isRecovering ?? false;
38207
37898
  }
38208
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
+
38209
37955
  // src/core/loop/todo-continuation.ts
38210
37956
  var sessionStates2 = /* @__PURE__ */ new Map();
38211
37957
  var COUNTDOWN_SECONDS = 2;
@@ -38749,6 +38495,11 @@ var TodoSyncService = class {
38749
38495
  taskTodos = /* @__PURE__ */ new Map();
38750
38496
  updateTimeout = null;
38751
38497
  watcher = null;
38498
+ // Batching support
38499
+ pendingUpdates = /* @__PURE__ */ new Set();
38500
+ batchTimer = null;
38501
+ BATCH_WINDOW_MS = 100;
38502
+ // 100ms batch window
38752
38503
  activeSessions = /* @__PURE__ */ new Set();
38753
38504
  constructor(client2, directory) {
38754
38505
  this.client = client2;
@@ -38757,6 +38508,7 @@ var TodoSyncService = class {
38757
38508
  }
38758
38509
  async start() {
38759
38510
  await this.reloadFileTodos();
38511
+ this.broadcastUpdate();
38760
38512
  if (fs10.existsSync(this.todoPath)) {
38761
38513
  let timer;
38762
38514
  this.watcher = fs10.watch(this.todoPath, (eventType) => {
@@ -38815,8 +38567,29 @@ var TodoSyncService = class {
38815
38567
  }
38816
38568
  }
38817
38569
  scheduleUpdate(sessionID) {
38818
- this.sendTodosToSession(sessionID).catch((err) => {
38819
- });
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
+ }
38820
38593
  }
38821
38594
  async sendTodosToSession(sessionID) {
38822
38595
  const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
@@ -38858,6 +38631,11 @@ var TodoSyncService = class {
38858
38631
  if (this.watcher) {
38859
38632
  this.watcher.close();
38860
38633
  }
38634
+ if (this.batchTimer) {
38635
+ clearTimeout(this.batchTimer);
38636
+ this.flushBatchedUpdates().catch(() => {
38637
+ });
38638
+ }
38861
38639
  }
38862
38640
  };
38863
38641
 
@@ -39325,7 +39103,7 @@ function createEventHandler(ctx) {
39325
39103
  const duration5 = totalTime < 6e4 ? `${Math.round(totalTime / 1e3)}s` : `${Math.round(totalTime / 6e4)}m`;
39326
39104
  sessions.delete(sessionID);
39327
39105
  state2.sessions.delete(sessionID);
39328
- clearSession2(sessionID);
39106
+ clearSession(sessionID);
39329
39107
  cleanupSessionRecovery(sessionID);
39330
39108
  cleanupSession2(sessionID);
39331
39109
  cleanupSession3(sessionID);
@@ -39564,6 +39342,8 @@ Wait for these tasks to complete before concluding the mission.
39564
39342
 
39565
39343
  // src/plugin-handlers/system-transform-handler.ts
39566
39344
  init_shared();
39345
+ import { existsSync as existsSync9, readFileSync as readFileSync3 } from "node:fs";
39346
+ import { join as join12 } from "node:path";
39567
39347
  function createSystemTransformHandler(ctx) {
39568
39348
  const { directory, sessions, state: state2 } = ctx;
39569
39349
  return async (input, output) => {
@@ -39593,6 +39373,17 @@ function createSystemTransformHandler(ctx) {
39593
39373
  }
39594
39374
  } catch {
39595
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
+ }
39596
39387
  if (systemAdditions.length > 0) {
39597
39388
  output.system.unshift(...systemAdditions);
39598
39389
  }
@@ -39630,6 +39421,27 @@ Use \`get_task_result\` to check completed tasks.
39630
39421
  Use \`delegate_task\` with background=true for parallel work.
39631
39422
  </orchestrator_background_tasks>`;
39632
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
+ }
39633
39445
 
39634
39446
  // src/index.ts
39635
39447
  var require2 = createRequire(import.meta.url);