opencode-orchestrator 1.2.62 → 1.2.66

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
@@ -702,6 +702,24 @@ var init_mission_control = __esm({
702
702
  }
703
703
  });
704
704
 
705
+ // src/shared/loop/constants/task-status.ts
706
+ var TASK_STATUS;
707
+ var init_task_status = __esm({
708
+ "src/shared/loop/constants/task-status.ts"() {
709
+ "use strict";
710
+ init_status_labels();
711
+ TASK_STATUS = {
712
+ PENDING: STATUS_LABEL.PENDING,
713
+ RUNNING: STATUS_LABEL.RUNNING,
714
+ COMPLETED: STATUS_LABEL.COMPLETED,
715
+ FAILED: STATUS_LABEL.FAILED,
716
+ ERROR: STATUS_LABEL.ERROR,
717
+ TIMEOUT: STATUS_LABEL.TIMEOUT,
718
+ CANCELLED: STATUS_LABEL.CANCELLED
719
+ };
720
+ }
721
+ });
722
+
705
723
  // src/shared/loop/constants/todo-status.ts
706
724
  var TODO_STATUS;
707
725
  var init_todo_status = __esm({
@@ -767,6 +785,7 @@ var init_constants4 = __esm({
767
785
  "use strict";
768
786
  init_loop();
769
787
  init_mission_control();
788
+ init_task_status();
770
789
  init_todo_status();
771
790
  init_labels();
772
791
  }
@@ -923,6 +942,12 @@ var init_types4 = __esm({
923
942
  // src/core/notification/toast-core.ts
924
943
  function initToastClient(client) {
925
944
  tuiClient = client;
945
+ const cleanup = () => {
946
+ tuiClient = null;
947
+ toasts.length = 0;
948
+ handlers.length = 0;
949
+ };
950
+ return cleanup;
926
951
  }
927
952
  function show(options) {
928
953
  const toast = {
@@ -947,15 +972,34 @@ function show(options) {
947
972
  if (tuiClient) {
948
973
  const client = tuiClient;
949
974
  if (client.tui?.showToast) {
950
- client.tui.showToast({
951
- body: {
952
- title: toast.title,
953
- message: toast.message,
954
- variant: toast.variant,
955
- duration: toast.duration
975
+ try {
976
+ const ac = new AbortController();
977
+ const timeoutMs = Math.max(2e3, Math.min(toast.duration, 1e4));
978
+ const timer = setTimeout(() => {
979
+ try {
980
+ ac.abort();
981
+ } catch {
982
+ }
983
+ }, timeoutMs);
984
+ const promise3 = client.tui.showToast?.({
985
+ body: {
986
+ title: toast.title,
987
+ message: toast.message,
988
+ variant: toast.variant,
989
+ duration: toast.duration
990
+ },
991
+ signal: ac.signal
992
+ });
993
+ if (promise3 && typeof promise3.then === "function") {
994
+ Promise.resolve(promise3).finally(() => {
995
+ if (timer) {
996
+ clearTimeout(timer);
997
+ }
998
+ }).catch(() => {
999
+ });
956
1000
  }
957
- }).catch(() => {
958
- });
1001
+ } catch {
1002
+ }
959
1003
  }
960
1004
  }
961
1005
  return toast;
@@ -1778,9 +1822,18 @@ var init_slash_commands = __esm({
1778
1822
  });
1779
1823
 
1780
1824
  // src/shared/message/constants/plugin-hooks.ts
1825
+ var PLUGIN_HOOKS;
1781
1826
  var init_plugin_hooks = __esm({
1782
1827
  "src/shared/message/constants/plugin-hooks.ts"() {
1783
1828
  "use strict";
1829
+ PLUGIN_HOOKS = {
1830
+ /** Intercepts user messages before sending to LLM */
1831
+ CHAT_MESSAGE: "chat.message",
1832
+ /** Runs after LLM finishes responding */
1833
+ ASSISTANT_DONE: "assistant.done",
1834
+ /** Runs after any tool call completes */
1835
+ TOOL_EXECUTE_AFTER: "tool.execute.after"
1836
+ };
1784
1837
  }
1785
1838
  });
1786
1839
 
@@ -2518,30 +2571,6 @@ var init_prompt = __esm({
2518
2571
  }
2519
2572
  });
2520
2573
 
2521
- // src/core/agents/consts/task-status.const.ts
2522
- var TASK_STATUS, TODO_STATUS2;
2523
- var init_task_status_const = __esm({
2524
- "src/core/agents/consts/task-status.const.ts"() {
2525
- "use strict";
2526
- init_shared();
2527
- TASK_STATUS = {
2528
- PENDING: STATUS_LABEL.PENDING,
2529
- RUNNING: STATUS_LABEL.RUNNING,
2530
- COMPLETED: STATUS_LABEL.COMPLETED,
2531
- FAILED: STATUS_LABEL.FAILED,
2532
- ERROR: STATUS_LABEL.ERROR,
2533
- TIMEOUT: STATUS_LABEL.TIMEOUT,
2534
- CANCELLED: STATUS_LABEL.CANCELLED
2535
- };
2536
- TODO_STATUS2 = {
2537
- PENDING: STATUS_LABEL.PENDING,
2538
- IN_PROGRESS: STATUS_LABEL.IN_PROGRESS,
2539
- COMPLETED: STATUS_LABEL.COMPLETED,
2540
- CANCELLED: STATUS_LABEL.CANCELLED
2541
- };
2542
- }
2543
- });
2544
-
2545
2574
  // src/shared/index.ts
2546
2575
  var init_shared = __esm({
2547
2576
  "src/shared/index.ts"() {
@@ -2562,7 +2591,6 @@ var init_shared = __esm({
2562
2591
  init_lifecycle2();
2563
2592
  init_errors();
2564
2593
  init_prompt();
2565
- init_task_status_const();
2566
2594
  }
2567
2595
  });
2568
2596
 
@@ -21535,7 +21563,8 @@ var TaskCleaner = class {
21535
21563
  try {
21536
21564
  await this.sessionPool.release(sessionID);
21537
21565
  clear2(sessionID);
21538
- } catch {
21566
+ } catch (error92) {
21567
+ log(`Session cleanup error for ${sessionID}:`, error92);
21539
21568
  }
21540
21569
  }
21541
21570
  this.store.delete(taskId);
@@ -36315,6 +36344,7 @@ function verifyMissionCompletion(directory) {
36315
36344
  );
36316
36345
  }
36317
36346
  } catch (error92) {
36347
+ log(`[verification] Failed to read sync issues file: ${error92}`);
36318
36348
  result.syncIssuesEmpty = true;
36319
36349
  }
36320
36350
  }
@@ -36652,13 +36682,13 @@ var MissionControlHook = class {
36652
36682
  return this.handleMissionComplete(directory, verification);
36653
36683
  }
36654
36684
  const loopState = readLoopState(directory);
36655
- let isStagnant = false;
36685
+ let isStagnant2 = false;
36656
36686
  if (loopState && loopState.active && loopState.sessionID === sessionID) {
36657
36687
  const currentProgress = verification.todoProgress;
36658
36688
  if (loopState.lastProgress === currentProgress) {
36659
36689
  loopState.stagnationCount = (loopState.stagnationCount || 0) + 1;
36660
36690
  if (loopState.stagnationCount >= 2) {
36661
- isStagnant = true;
36691
+ isStagnant2 = true;
36662
36692
  }
36663
36693
  } else {
36664
36694
  loopState.stagnationCount = 0;
@@ -36669,7 +36699,7 @@ var MissionControlHook = class {
36669
36699
  const failurePrompt = verification.checklistProgress !== "0/0" ? buildVerificationFailurePrompt(verification) : buildTodoIncompletePrompt(verification);
36670
36700
  const continuation = this.buildContinuationResponse(session, sessionID);
36671
36701
  const prompts = [failurePrompt];
36672
- if (isStagnant) {
36702
+ if (isStagnant2) {
36673
36703
  prompts.push(STAGNATION_INTERVENTION);
36674
36704
  }
36675
36705
  if (continuation.action === HOOK_ACTIONS.INJECT) {
@@ -36984,7 +37014,7 @@ init_logger();
36984
37014
  init_shared();
36985
37015
  function getIncompleteCount(todos) {
36986
37016
  return todos.filter(
36987
- (t) => t.status !== TODO_STATUS2.COMPLETED && t.status !== TODO_STATUS2.CANCELLED
37017
+ (t) => t.status !== TODO_STATUS.COMPLETED && t.status !== TODO_STATUS.CANCELLED
36988
37018
  ).length;
36989
37019
  }
36990
37020
  function hasRemainingWork(todos) {
@@ -36992,7 +37022,7 @@ function hasRemainingWork(todos) {
36992
37022
  }
36993
37023
  function getNextPending(todos) {
36994
37024
  const pending2 = todos.filter(
36995
- (t) => t.status === TODO_STATUS2.PENDING || t.status === TODO_STATUS2.IN_PROGRESS
37025
+ (t) => t.status === TODO_STATUS.PENDING || t.status === TODO_STATUS.IN_PROGRESS
36996
37026
  );
36997
37027
  const priorityOrder = {
36998
37028
  [STATUS_LABEL.HIGH]: 0,
@@ -37005,10 +37035,10 @@ function getNextPending(todos) {
37005
37035
  function getStats3(todos) {
37006
37036
  const stats2 = {
37007
37037
  total: todos.length,
37008
- pending: todos.filter((t) => t.status === TODO_STATUS2.PENDING).length,
37009
- inProgress: todos.filter((t) => t.status === TODO_STATUS2.IN_PROGRESS).length,
37010
- completed: todos.filter((t) => t.status === TODO_STATUS2.COMPLETED).length,
37011
- cancelled: todos.filter((t) => t.status === TODO_STATUS2.CANCELLED).length,
37038
+ pending: todos.filter((t) => t.status === TODO_STATUS.PENDING).length,
37039
+ inProgress: todos.filter((t) => t.status === TODO_STATUS.IN_PROGRESS).length,
37040
+ completed: todos.filter((t) => t.status === TODO_STATUS.COMPLETED).length,
37041
+ cancelled: todos.filter((t) => t.status === TODO_STATUS.CANCELLED).length,
37012
37042
  percentComplete: 0
37013
37043
  };
37014
37044
  if (stats2.total > 0) {
@@ -37028,13 +37058,13 @@ function formatProgress(todos) {
37028
37058
  }
37029
37059
  function generateContinuationPrompt(todos) {
37030
37060
  const incomplete = todos.filter(
37031
- (t) => t.status !== TODO_STATUS2.COMPLETED && t.status !== TODO_STATUS2.CANCELLED
37061
+ (t) => t.status !== TODO_STATUS.COMPLETED && t.status !== TODO_STATUS.CANCELLED
37032
37062
  );
37033
37063
  if (incomplete.length === 0) {
37034
37064
  return "";
37035
37065
  }
37036
37066
  const next = getNextPending(todos);
37037
- const pendingTasks = incomplete.filter((t) => t.status === TODO_STATUS2.PENDING);
37067
+ const pendingTasks = incomplete.filter((t) => t.status === TODO_STATUS.PENDING);
37038
37068
  const pendingCount = pendingTasks.length;
37039
37069
  let prompt = `${PROMPT_TAGS.TODO_CONTINUATION.open}
37040
37070
  ${LOOP_LABELS.PROGRESS_PREFIX} ${formatProgress(todos)}
@@ -37042,7 +37072,7 @@ ${LOOP_LABELS.PROGRESS_PREFIX} ${formatProgress(todos)}
37042
37072
  **${LOOP_LABELS.INCOMPLETE_TASKS}** (${incomplete.length} remaining):
37043
37073
  `;
37044
37074
  for (const todo of incomplete.slice(0, 5)) {
37045
- const statusLabel = todo.status === TODO_STATUS2.IN_PROGRESS ? LOOP_LABELS.STATUS.RUNNING : LOOP_LABELS.STATUS.WAITING;
37075
+ const statusLabel = todo.status === TODO_STATUS.IN_PROGRESS ? LOOP_LABELS.STATUS.RUNNING : LOOP_LABELS.STATUS.WAITING;
37046
37076
  const priorityLabel = todo.priority === STATUS_LABEL.HIGH ? LOOP_LABELS.PRIORITY.HIGH : todo.priority === STATUS_LABEL.MEDIUM ? LOOP_LABELS.PRIORITY.MEDIUM : LOOP_LABELS.PRIORITY.LOW;
37047
37077
  prompt += `${statusLabel} ${priorityLabel} [${todo.id}] ${todo.content}
37048
37078
  `;
@@ -37221,17 +37251,40 @@ function isSessionRecovering(sessionID) {
37221
37251
  }
37222
37252
 
37223
37253
  // src/core/loop/todo-continuation.ts
37254
+ var CONTINUATION_TTL_MS = 10 * 60 * 1e3;
37255
+ var PRUNE_INTERVAL_MS = 2 * 60 * 1e3;
37224
37256
  var sessionStates2 = /* @__PURE__ */ new Map();
37257
+ var pruneInterval;
37225
37258
  var COUNTDOWN_SECONDS = 2;
37226
37259
  var TOAST_DURATION_MS = TOAST_DURATION.EXTRA_SHORT;
37227
37260
  var MIN_TIME_BETWEEN_CONTINUATIONS_MS = LOOP.MIN_TIME_BETWEEN_CHECKS_MS;
37228
37261
  var COUNTDOWN_GRACE_PERIOD_MS = LOOP.COUNTDOWN_GRACE_PERIOD_MS;
37229
37262
  var ABORT_WINDOW_MS = LOOP.ABORT_WINDOW_MS;
37263
+ function startPruneTimer() {
37264
+ if (pruneInterval) return;
37265
+ pruneInterval = setInterval(() => {
37266
+ const now = Date.now();
37267
+ for (const [sessionID, state2] of sessionStates2.entries()) {
37268
+ if (now - state2.lastAccessedAt > CONTINUATION_TTL_MS) {
37269
+ if (state2.countdownTimer) {
37270
+ clearTimeout(state2.countdownTimer);
37271
+ }
37272
+ sessionStates2.delete(sessionID);
37273
+ log(`[todo-continuation] Pruned stale state`, { sessionID });
37274
+ }
37275
+ }
37276
+ }, PRUNE_INTERVAL_MS);
37277
+ if (pruneInterval && typeof pruneInterval.unref === "function") {
37278
+ pruneInterval.unref();
37279
+ }
37280
+ }
37230
37281
  function getState3(sessionID) {
37231
37282
  let state2 = sessionStates2.get(sessionID);
37232
37283
  if (!state2) {
37233
- state2 = {};
37284
+ state2 = { lastAccessedAt: Date.now() };
37234
37285
  sessionStates2.set(sessionID, state2);
37286
+ } else {
37287
+ state2.lastAccessedAt = Date.now();
37235
37288
  }
37236
37289
  return state2;
37237
37290
  }
@@ -37260,7 +37313,8 @@ function hasRunningBackgroundTasks(parentSessionID) {
37260
37313
  const manager = ParallelAgentManager.getInstance();
37261
37314
  const tasks = manager.getTasksByParent(parentSessionID);
37262
37315
  return tasks.some((t) => t.status === STATUS_LABEL.RUNNING);
37263
- } catch {
37316
+ } catch (err) {
37317
+ log("[todo-continuation] Failed to check background tasks", { sessionID: parentSessionID, error: err });
37264
37318
  return false;
37265
37319
  }
37266
37320
  }
@@ -37277,7 +37331,8 @@ async function showCountdownToast(client, secondsRemaining, incompleteCount) {
37277
37331
  }
37278
37332
  });
37279
37333
  }
37280
- } catch {
37334
+ } catch (error92) {
37335
+ log(`[todo-continuation] Toast failed:`, error92);
37281
37336
  }
37282
37337
  }
37283
37338
  async function injectContinuation(client, directory, sessionID, todos) {
@@ -37396,7 +37451,8 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
37396
37451
  try {
37397
37452
  const v = verifyMissionCompletion(directory);
37398
37453
  freshFileWork = !v.passed && (v.todoIncomplete > 0 || v.checklistProgress !== "0/0" && !v.checklistComplete);
37399
- } catch {
37454
+ } catch (err) {
37455
+ log("[todo-continuation] Failed to verify file work", { sessionID, error: err });
37400
37456
  }
37401
37457
  if (hasRemainingWork(freshTodos) || freshFileWork) {
37402
37458
  await injectContinuation(client, directory, sessionID, freshTodos);
@@ -37437,6 +37493,7 @@ function cleanupSession2(sessionID) {
37437
37493
  cancelCountdown(sessionID);
37438
37494
  sessionStates2.delete(sessionID);
37439
37495
  }
37496
+ startPruneTimer();
37440
37497
 
37441
37498
  // src/hooks/custom/user-activity.ts
37442
37499
  var UserActivityHook = class {
@@ -37727,21 +37784,21 @@ function parseTodoMd(content) {
37727
37784
  if (match) {
37728
37785
  const [, statusChar, text] = match;
37729
37786
  const content2 = text.trim();
37730
- let status = TODO_STATUS2.PENDING;
37787
+ let status = TODO_STATUS.PENDING;
37731
37788
  switch (statusChar.toLowerCase()) {
37732
37789
  case "x":
37733
- status = TODO_STATUS2.COMPLETED;
37790
+ status = TODO_STATUS.COMPLETED;
37734
37791
  break;
37735
37792
  case "/":
37736
37793
  case ".":
37737
- status = TODO_STATUS2.IN_PROGRESS;
37794
+ status = TODO_STATUS.IN_PROGRESS;
37738
37795
  break;
37739
37796
  case "-":
37740
- status = TODO_STATUS2.CANCELLED;
37797
+ status = TODO_STATUS.CANCELLED;
37741
37798
  break;
37742
37799
  case " ":
37743
37800
  default:
37744
- status = TODO_STATUS2.PENDING;
37801
+ status = TODO_STATUS.PENDING;
37745
37802
  break;
37746
37803
  }
37747
37804
  todos.push({
@@ -40088,20 +40145,407 @@ ${claudeRules}`;
40088
40145
  // src/core/loop/mission-loop-handler.ts
40089
40146
  init_logger();
40090
40147
  init_shared();
40148
+
40149
+ // src/core/loop/session-state-store.ts
40150
+ init_logger();
40151
+ var SESSION_STATE_TTL_MS = 10 * 60 * 1e3;
40152
+ var PRUNE_INTERVAL_MS2 = 2 * 60 * 1e3;
40153
+ function createSessionStateStore() {
40154
+ const sessions = /* @__PURE__ */ new Map();
40155
+ let pruneInterval5;
40156
+ pruneInterval5 = setInterval(() => {
40157
+ const now = Date.now();
40158
+ for (const [sessionID, tracked] of sessions.entries()) {
40159
+ if (now - tracked.lastAccessedAt > SESSION_STATE_TTL_MS) {
40160
+ cancelCountdownInternal(tracked.state);
40161
+ sessions.delete(sessionID);
40162
+ log(`[session-state-store] Pruned stale session`, { sessionID });
40163
+ }
40164
+ }
40165
+ }, PRUNE_INTERVAL_MS2);
40166
+ if (typeof pruneInterval5 === "object" && typeof pruneInterval5.unref === "function") {
40167
+ pruneInterval5.unref();
40168
+ }
40169
+ function getTrackedSession(sessionID) {
40170
+ const existing = sessions.get(sessionID);
40171
+ if (existing) {
40172
+ existing.lastAccessedAt = Date.now();
40173
+ return existing;
40174
+ }
40175
+ const rawState = {
40176
+ stagnationCount: 0,
40177
+ isRecovering: false,
40178
+ isAborting: false,
40179
+ inFlight: false
40180
+ };
40181
+ const trackedSession = {
40182
+ state: rawState,
40183
+ lastAccessedAt: Date.now()
40184
+ };
40185
+ sessions.set(sessionID, trackedSession);
40186
+ return trackedSession;
40187
+ }
40188
+ function getState6(sessionID) {
40189
+ return getTrackedSession(sessionID).state;
40190
+ }
40191
+ function getExistingState(sessionID) {
40192
+ const existing = sessions.get(sessionID);
40193
+ if (existing) {
40194
+ existing.lastAccessedAt = Date.now();
40195
+ return existing.state;
40196
+ }
40197
+ return void 0;
40198
+ }
40199
+ function cancelCountdownInternal(state2) {
40200
+ if (state2.countdownTimer) {
40201
+ clearTimeout(state2.countdownTimer);
40202
+ state2.countdownTimer = void 0;
40203
+ }
40204
+ state2.countdownStartedAt = void 0;
40205
+ state2.inFlight = false;
40206
+ }
40207
+ function cancelCountdown2(sessionID) {
40208
+ const tracked = sessions.get(sessionID);
40209
+ if (!tracked) return;
40210
+ cancelCountdownInternal(tracked.state);
40211
+ }
40212
+ function cleanup(sessionID) {
40213
+ const tracked = sessions.get(sessionID);
40214
+ if (!tracked) return;
40215
+ cancelCountdownInternal(tracked.state);
40216
+ sessions.delete(sessionID);
40217
+ }
40218
+ function cancelAllCountdowns() {
40219
+ for (const tracked of sessions.values()) {
40220
+ cancelCountdownInternal(tracked.state);
40221
+ }
40222
+ }
40223
+ function shutdown() {
40224
+ if (pruneInterval5 !== void 0) {
40225
+ clearInterval(pruneInterval5);
40226
+ }
40227
+ cancelAllCountdowns();
40228
+ sessions.clear();
40229
+ }
40230
+ return {
40231
+ getState: getState6,
40232
+ getExistingState,
40233
+ cancelCountdown: cancelCountdown2,
40234
+ cleanup,
40235
+ cancelAllCountdowns,
40236
+ shutdown
40237
+ };
40238
+ }
40239
+
40240
+ // src/core/loop/progress-tracker.ts
40241
+ init_logger();
40242
+ var DEFAULT_STAGNATION_THRESHOLD = 3;
40243
+ var TRACKER_TTL_MS = 10 * 60 * 1e3;
40244
+ var PRUNE_INTERVAL_MS3 = 2 * 60 * 1e3;
40091
40245
  var sessionStates3 = /* @__PURE__ */ new Map();
40246
+ var pruneInterval2;
40247
+ function startPruneTimer2() {
40248
+ if (pruneInterval2) return;
40249
+ pruneInterval2 = setInterval(() => {
40250
+ const now = Date.now();
40251
+ for (const [sessionID, state2] of sessionStates3.entries()) {
40252
+ if (now - (state2.lastAccessedAt ?? 0) > TRACKER_TTL_MS) {
40253
+ sessionStates3.delete(sessionID);
40254
+ log(`[progress-tracker] Pruned stale state`, { sessionID });
40255
+ }
40256
+ }
40257
+ }, PRUNE_INTERVAL_MS3);
40258
+ if (pruneInterval2 && typeof pruneInterval2.unref === "function") {
40259
+ pruneInterval2.unref();
40260
+ }
40261
+ }
40262
+ function hashTodos(todos) {
40263
+ const normalized = todos.map((todo) => ({
40264
+ id: todo.id ?? null,
40265
+ content: todo.content,
40266
+ priority: todo.priority,
40267
+ status: todo.status
40268
+ })).sort((left, right) => {
40269
+ const leftId = left.id ?? "\uFFFF";
40270
+ const rightId = right.id ?? "\uFFFF";
40271
+ if (leftId !== rightId) {
40272
+ return leftId.localeCompare(rightId);
40273
+ }
40274
+ if (left.content !== right.content) {
40275
+ return left.content.localeCompare(right.content);
40276
+ }
40277
+ if (left.priority !== right.priority) {
40278
+ return left.priority.localeCompare(right.priority);
40279
+ }
40280
+ return left.status.localeCompare(right.status);
40281
+ });
40282
+ return JSON.stringify(normalized);
40283
+ }
40284
+ function countCompleted(todos) {
40285
+ return todos.filter((todo) => todo.status === "completed").length;
40286
+ }
40092
40287
  function getState4(sessionID) {
40093
40288
  let state2 = sessionStates3.get(sessionID);
40094
40289
  if (!state2) {
40095
- state2 = {};
40290
+ state2 = {
40291
+ stagnationCount: 0,
40292
+ awaitingPostInjectionProgressCheck: false,
40293
+ lastAccessedAt: Date.now()
40294
+ };
40096
40295
  sessionStates3.set(sessionID, state2);
40296
+ } else {
40297
+ state2.lastAccessedAt = Date.now();
40097
40298
  }
40098
40299
  return state2;
40099
40300
  }
40100
- function cancelCountdown2(sessionID) {
40301
+ function resetProgress(sessionID) {
40101
40302
  const state2 = sessionStates3.get(sessionID);
40102
- if (state2?.countdownTimer) {
40103
- clearTimeout(state2.countdownTimer);
40104
- state2.countdownTimer = void 0;
40303
+ if (!state2) return;
40304
+ state2.stagnationCount = 0;
40305
+ state2.lastIncompleteCount = void 0;
40306
+ state2.lastSnapshot = void 0;
40307
+ state2.countCompleted = void 0;
40308
+ state2.awaitingPostInjectionProgressCheck = false;
40309
+ }
40310
+ function trackProgress(sessionID, incompleteCount, todos) {
40311
+ const state2 = getState4(sessionID);
40312
+ const previousIncompleteCount = state2.lastIncompleteCount;
40313
+ const currentSnapshot = todos ? hashTodos(todos) : void 0;
40314
+ const currentCompletedCount = todos ? countCompleted(todos) : void 0;
40315
+ const hasCompletedMoreTodos = currentCompletedCount !== void 0 && state2.lastIncompleteCount !== void 0 && currentCompletedCount > (state2.countCompleted ?? 0);
40316
+ const hasSnapshotChanged = currentSnapshot !== void 0 && state2.lastSnapshot !== void 0 && currentSnapshot !== state2.lastSnapshot;
40317
+ let hasProgressed = false;
40318
+ let progressSource = "none";
40319
+ if (incompleteCount < (previousIncompleteCount ?? Infinity)) {
40320
+ hasProgressed = true;
40321
+ progressSource = "count";
40322
+ } else if (hasSnapshotChanged) {
40323
+ hasProgressed = true;
40324
+ progressSource = "snapshot";
40325
+ }
40326
+ state2.lastIncompleteCount = incompleteCount;
40327
+ if (currentSnapshot) {
40328
+ state2.lastSnapshot = currentSnapshot;
40329
+ }
40330
+ if (currentCompletedCount !== void 0) {
40331
+ state2.countCompleted = currentCompletedCount;
40332
+ }
40333
+ if (previousIncompleteCount === void 0) {
40334
+ state2.stagnationCount = 0;
40335
+ return {
40336
+ hasProgressed: false,
40337
+ stagnationCount: 0,
40338
+ previousIncompleteCount,
40339
+ progressSource: "none"
40340
+ };
40341
+ }
40342
+ if (hasProgressed) {
40343
+ const wasStagnant = state2.stagnationCount >= DEFAULT_STAGNATION_THRESHOLD;
40344
+ state2.stagnationCount = 0;
40345
+ state2.awaitingPostInjectionProgressCheck = false;
40346
+ log(`[progress-tracker] Progress detected: ${progressSource}`, {
40347
+ sessionID,
40348
+ previousIncompleteCount,
40349
+ incompleteCount,
40350
+ stagnationCount: state2.stagnationCount,
40351
+ recoveredFromStagnation: wasStagnant
40352
+ });
40353
+ return {
40354
+ hasProgressed: true,
40355
+ stagnationCount: state2.stagnationCount,
40356
+ previousIncompleteCount,
40357
+ progressSource,
40358
+ recoveredFromStagnation: wasStagnant
40359
+ };
40360
+ }
40361
+ if (!state2.awaitingPostInjectionProgressCheck) {
40362
+ return {
40363
+ hasProgressed: false,
40364
+ stagnationCount: state2.stagnationCount,
40365
+ previousIncompleteCount,
40366
+ progressSource: "none"
40367
+ };
40368
+ }
40369
+ state2.stagnationCount += 1;
40370
+ log(`[progress-tracker] Stagnation detected`, {
40371
+ sessionID,
40372
+ incompleteCount,
40373
+ previousIncompleteCount,
40374
+ stagnationCount: state2.stagnationCount,
40375
+ threshold: DEFAULT_STAGNATION_THRESHOLD
40376
+ });
40377
+ return {
40378
+ hasProgressed: false,
40379
+ stagnationCount: state2.stagnationCount,
40380
+ previousIncompleteCount,
40381
+ progressSource: "none"
40382
+ };
40383
+ }
40384
+ function markInjectionPerformed(sessionID) {
40385
+ const state2 = getState4(sessionID);
40386
+ state2.awaitingPostInjectionProgressCheck = true;
40387
+ }
40388
+ function isStagnant(sessionID, threshold = DEFAULT_STAGNATION_THRESHOLD) {
40389
+ const state2 = sessionStates3.get(sessionID);
40390
+ if (!state2) return false;
40391
+ return state2.stagnationCount >= threshold;
40392
+ }
40393
+ startPruneTimer2();
40394
+
40395
+ // src/core/loop/compaction-guard.ts
40396
+ init_logger();
40397
+ var COMPACTION_TTL_MS = 10 * 60 * 1e3;
40398
+ var PRUNE_INTERVAL_MS4 = 2 * 60 * 1e3;
40399
+ var compactionStates = /* @__PURE__ */ new Map();
40400
+ var pruneInterval3;
40401
+ function startPruneTimer3() {
40402
+ if (pruneInterval3) return;
40403
+ pruneInterval3 = setInterval(() => {
40404
+ const now = Date.now();
40405
+ for (const [sessionID, state2] of compactionStates.entries()) {
40406
+ if (now - state2.lastAccessedAt > COMPACTION_TTL_MS) {
40407
+ compactionStates.delete(sessionID);
40408
+ log(`[compaction-guard] Pruned stale state`, { sessionID });
40409
+ }
40410
+ }
40411
+ }, PRUNE_INTERVAL_MS4);
40412
+ if (pruneInterval3 && typeof pruneInterval3.unref === "function") {
40413
+ pruneInterval3.unref();
40414
+ }
40415
+ }
40416
+ function armCompactionGuard(sessionID, timestamp) {
40417
+ let state2 = compactionStates.get(sessionID);
40418
+ if (!state2) {
40419
+ state2 = { compactionEpoch: 0, lastAccessedAt: Date.now() };
40420
+ compactionStates.set(sessionID, state2);
40421
+ }
40422
+ state2.compactionEpoch = timestamp;
40423
+ state2.lastAccessedAt = Date.now();
40424
+ log(`[compaction-guard] Armed`, { sessionID, epoch: timestamp });
40425
+ return timestamp;
40426
+ }
40427
+ function isCompactionSafe(sessionID, currentEpoch) {
40428
+ const state2 = compactionStates.get(sessionID);
40429
+ if (!state2) return true;
40430
+ state2.lastAccessedAt = Date.now();
40431
+ if (currentEpoch > state2.compactionEpoch) {
40432
+ log(`[compaction-guard] Unsafe: newer compaction exists`, {
40433
+ sessionID,
40434
+ currentEpoch,
40435
+ compactionEpoch: state2.compactionEpoch
40436
+ });
40437
+ return false;
40438
+ }
40439
+ return true;
40440
+ }
40441
+ function clearCompactionState(sessionID) {
40442
+ compactionStates.delete(sessionID);
40443
+ }
40444
+ startPruneTimer3();
40445
+
40446
+ // src/core/loop/circuit-breaker.ts
40447
+ init_logger();
40448
+ var REPETITION_THRESHOLD = 3;
40449
+ var HISTORY_SIZE = 10;
40450
+ var CIRCUIT_TTL_MS = 10 * 60 * 1e3;
40451
+ var CIRCUIT_RESET_TIMEOUT_MS = 30 * 1e3;
40452
+ var PRUNE_INTERVAL_MS5 = 2 * 60 * 1e3;
40453
+ var circuitStates = /* @__PURE__ */ new Map();
40454
+ var pruneInterval4;
40455
+ function startPruneTimer4() {
40456
+ if (pruneInterval4) return;
40457
+ pruneInterval4 = setInterval(() => {
40458
+ const now = Date.now();
40459
+ for (const [sessionID, state2] of circuitStates.entries()) {
40460
+ if (now - state2.lastAccessedAt > CIRCUIT_TTL_MS) {
40461
+ circuitStates.delete(sessionID);
40462
+ log(`[circuit-breaker] Pruned stale state`, { sessionID });
40463
+ }
40464
+ }
40465
+ }, PRUNE_INTERVAL_MS5);
40466
+ if (pruneInterval4 && typeof pruneInterval4.unref === "function") {
40467
+ pruneInterval4.unref();
40468
+ }
40469
+ }
40470
+ function getState5(sessionID) {
40471
+ let state2 = circuitStates.get(sessionID);
40472
+ if (!state2) {
40473
+ state2 = {
40474
+ lastAccessedAt: Date.now(),
40475
+ lastTrippedAt: 0,
40476
+ isOpen: false,
40477
+ toolCallHistory: []
40478
+ };
40479
+ circuitStates.set(sessionID, state2);
40480
+ } else {
40481
+ state2.lastAccessedAt = Date.now();
40482
+ }
40483
+ return state2;
40484
+ }
40485
+ function isCircuitOpen(sessionID) {
40486
+ const state2 = circuitStates.get(sessionID);
40487
+ if (!state2) return false;
40488
+ state2.lastAccessedAt = Date.now();
40489
+ if (state2.isOpen) {
40490
+ const now = Date.now();
40491
+ if (now - state2.lastTrippedAt > CIRCUIT_RESET_TIMEOUT_MS) {
40492
+ state2.isOpen = false;
40493
+ state2.toolCallHistory = [];
40494
+ log(`[circuit-breaker] Circuit HALF-OPEN (auto-reset)`, { sessionID });
40495
+ return false;
40496
+ }
40497
+ return true;
40498
+ }
40499
+ return false;
40500
+ }
40501
+ function detectRepetitiveToolUse(sessionID) {
40502
+ const state2 = circuitStates.get(sessionID);
40503
+ if (!state2 || state2.toolCallHistory.length < REPETITION_THRESHOLD) {
40504
+ return null;
40505
+ }
40506
+ const recent = state2.toolCallHistory.slice(-REPETITION_THRESHOLD);
40507
+ if (recent.every((tool2) => tool2 === recent[0])) {
40508
+ return recent[0];
40509
+ }
40510
+ return null;
40511
+ }
40512
+ function shouldTripCircuit(sessionID) {
40513
+ const state2 = circuitStates.get(sessionID);
40514
+ if (!state2) return false;
40515
+ if (state2.isOpen) return false;
40516
+ const repetitiveTool = detectRepetitiveToolUse(sessionID);
40517
+ if (repetitiveTool) {
40518
+ state2.isOpen = true;
40519
+ state2.lastTrippedAt = Date.now();
40520
+ log(`[circuit-breaker] Circuit OPENED: repetitive tool detected: ${repetitiveTool}`, { sessionID });
40521
+ return true;
40522
+ }
40523
+ return false;
40524
+ }
40525
+ function recordToolCall(sessionID, toolName) {
40526
+ const state2 = getState5(sessionID);
40527
+ state2.toolCallHistory.push(toolName);
40528
+ if (state2.toolCallHistory.length > HISTORY_SIZE) {
40529
+ state2.toolCallHistory.shift();
40530
+ }
40531
+ }
40532
+ function clearCircuitState(sessionID) {
40533
+ circuitStates.delete(sessionID);
40534
+ }
40535
+ startPruneTimer4();
40536
+
40537
+ // src/core/loop/mission-loop-handler.ts
40538
+ var sessionStateStore = createSessionStateStore();
40539
+ async function showToastSafely(client, title, message, variant, duration5) {
40540
+ try {
40541
+ const tui = client?.tui;
40542
+ if (tui?.showToast) {
40543
+ await tui.showToast({
40544
+ body: { title, message, variant, duration: duration5 }
40545
+ });
40546
+ }
40547
+ } catch (err) {
40548
+ log("[mission-loop-handler] Toast failed", { error: err });
40105
40549
  }
40106
40550
  }
40107
40551
  function hasRunningBackgroundTasks2(parentSessionID) {
@@ -40109,31 +40553,43 @@ function hasRunningBackgroundTasks2(parentSessionID) {
40109
40553
  const manager = ParallelAgentManager.getInstance();
40110
40554
  const tasks = manager.getTasksByParent(parentSessionID);
40111
40555
  return tasks.some((t) => t.status === STATUS_LABEL.RUNNING);
40112
- } catch {
40556
+ } catch (err) {
40557
+ log("[mission-loop-handler] Failed to check background tasks", { sessionID: parentSessionID, error: err });
40113
40558
  return false;
40114
40559
  }
40115
40560
  }
40561
+ async function showCountdownToast2(client, seconds, iteration, maxIterations) {
40562
+ await showToastSafely(
40563
+ client,
40564
+ "\u{1F504} Mission Loop",
40565
+ `Continuing in ${seconds}s... (iteration ${iteration}/${maxIterations})`,
40566
+ TOAST_VARIANTS.WARNING,
40567
+ TOAST_DURATION.EXTRA_SHORT
40568
+ );
40569
+ }
40116
40570
  async function showCompletedToast(client, state2) {
40117
- try {
40118
- const tuiClient2 = client;
40119
- if (tuiClient2.tui?.showToast) {
40120
- await tuiClient2.tui.showToast({
40121
- body: {
40122
- title: "\u{1F396}\uFE0F Mission Complete!",
40123
- message: `Verified and finished after ${state2.iteration} iteration(s)`,
40124
- variant: TOAST_VARIANTS.SUCCESS,
40125
- duration: TOAST_DURATION.LONG
40126
- }
40127
- });
40128
- }
40129
- } catch {
40130
- }
40571
+ await showToastSafely(
40572
+ client,
40573
+ "\u{1F396}\uFE0F Mission Complete!",
40574
+ `Verified and finished after ${state2.iteration} iteration(s)`,
40575
+ TOAST_VARIANTS.SUCCESS,
40576
+ TOAST_DURATION.LONG
40577
+ );
40131
40578
  }
40132
40579
  async function injectContinuation2(client, directory, sessionID, loopState, customPrompt) {
40133
- const handlerState = getState4(sessionID);
40134
- if (handlerState.isAborting) return;
40580
+ const state2 = sessionStateStore.getState(sessionID);
40581
+ if (state2.isAborting) return;
40135
40582
  if (hasRunningBackgroundTasks2(sessionID)) return;
40136
40583
  if (isSessionRecovering(sessionID)) return;
40584
+ if (isCircuitOpen(sessionID)) {
40585
+ log(`[mission-loop-handler] Skipped: circuit breaker open`, { sessionID });
40586
+ return;
40587
+ }
40588
+ const currentEpoch = Date.now();
40589
+ if (!isCompactionSafe(sessionID, currentEpoch)) {
40590
+ log(`[mission-loop-handler] Skipped: post-compaction unsafe`, { sessionID, currentEpoch });
40591
+ return;
40592
+ }
40137
40593
  const verification = verifyMissionCompletion(directory);
40138
40594
  if (verification.passed) {
40139
40595
  await handleMissionComplete(client, directory, loopState);
@@ -40147,15 +40603,15 @@ async function injectContinuation2(client, directory, sessionID, loopState, cust
40147
40603
  ${prompt}`;
40148
40604
  }
40149
40605
  try {
40150
- client.session.prompt({
40606
+ await client.session.prompt({
40151
40607
  path: { id: sessionID },
40152
40608
  body: {
40153
40609
  parts: [{ type: PART_TYPES.TEXT, text: prompt }]
40154
40610
  }
40155
- }).catch((error92) => {
40156
- log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
40157
40611
  });
40158
- } catch {
40612
+ markInjectionPerformed(sessionID);
40613
+ } catch (err) {
40614
+ log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: err });
40159
40615
  }
40160
40616
  }
40161
40617
  async function handleMissionComplete(client, directory, loopState) {
@@ -40163,6 +40619,9 @@ async function handleMissionComplete(client, directory, loopState) {
40163
40619
  if (cleared) {
40164
40620
  await showCompletedToast(client, loopState);
40165
40621
  await sendMissionCompleteNotification(loopState);
40622
+ sessionStateStore.cleanup(loopState.sessionID);
40623
+ clearCompactionState(loopState.sessionID);
40624
+ clearCircuitState(loopState.sessionID);
40166
40625
  }
40167
40626
  }
40168
40627
  async function sendMissionCompleteNotification(loopState) {
@@ -40177,17 +40636,18 @@ async function sendMissionCompleteNotification(loopState) {
40177
40636
  if (soundPath) {
40178
40637
  await playSound(platform2, soundPath);
40179
40638
  }
40180
- } catch {
40639
+ } catch (err) {
40640
+ log("[mission-loop-handler] Notification failed", { sessionID: loopState.sessionID, error: err });
40181
40641
  }
40182
40642
  }
40183
40643
  async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
40184
- const handlerState = getState4(sessionID);
40644
+ const state2 = sessionStateStore.getState(sessionID);
40185
40645
  const now = Date.now();
40186
- if (handlerState.lastCheckTime && now - handlerState.lastCheckTime < LOOP.MIN_TIME_BETWEEN_CHECKS_MS) {
40646
+ if (state2.lastCheckTime && now - state2.lastCheckTime < LOOP.MIN_TIME_BETWEEN_CHECKS_MS) {
40187
40647
  return;
40188
40648
  }
40189
- handlerState.lastCheckTime = now;
40190
- cancelCountdown2(sessionID);
40649
+ state2.lastCheckTime = now;
40650
+ sessionStateStore.cancelCountdown(sessionID);
40191
40651
  if (mainSessionID && sessionID !== mainSessionID) {
40192
40652
  return;
40193
40653
  }
@@ -40206,43 +40666,46 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
40206
40666
  await handleMissionComplete(client, directory, loopState);
40207
40667
  return;
40208
40668
  }
40669
+ if (shouldTripCircuit(sessionID)) {
40670
+ log(`[${MISSION_CONTROL.LOG_SOURCE}-handler] Circuit breaker tripped for ${sessionID}`);
40671
+ return;
40672
+ }
40209
40673
  const currentProgress = verification.todoProgress;
40210
- let isStagnant = false;
40211
- if (loopState.lastProgress === currentProgress) {
40212
- loopState.stagnationCount = (loopState.stagnationCount || 0) + 1;
40213
- if (loopState.stagnationCount >= 2) {
40214
- isStagnant = true;
40215
- log(`[${MISSION_CONTROL.LOG_SOURCE}-handler] Stagnation detected for ${sessionID} (${currentProgress}). Intervention ready.`);
40216
- }
40217
- } else {
40218
- loopState.stagnationCount = 0;
40674
+ const progressResult = trackProgress(sessionID, verification.todoIncomplete);
40675
+ if (progressResult.hasProgressed) {
40676
+ log(`[${MISSION_CONTROL.LOG_SOURCE}-handler] Progress made`, {
40677
+ sessionID,
40678
+ source: progressResult.progressSource
40679
+ });
40219
40680
  }
40220
- loopState.lastProgress = currentProgress;
40681
+ const stagnant = isStagnant(sessionID, DEFAULT_STAGNATION_THRESHOLD);
40221
40682
  const newState = incrementIteration(directory);
40222
40683
  if (!newState) return;
40223
40684
  newState.lastProgress = currentProgress;
40224
- newState.stagnationCount = loopState.stagnationCount;
40225
40685
  writeLoopState(directory, newState);
40226
- const countdownMsg = isStagnant ? "Stagnation Detected! Intervening..." : `Continuing in ${MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS}s... (iteration ${newState.iteration}/${newState.maxIterations})`;
40227
- handlerState.countdownTimer = setTimeout(async () => {
40228
- cancelCountdown2(sessionID);
40229
- await injectContinuation2(client, directory, sessionID, newState, isStagnant ? STAGNATION_INTERVENTION : void 0);
40686
+ await showCountdownToast2(client, MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS, newState.iteration, newState.maxIterations);
40687
+ state2.countdownTimer = setTimeout(async () => {
40688
+ sessionStateStore.cancelCountdown(sessionID);
40689
+ await injectContinuation2(client, directory, sessionID, newState, stagnant ? STAGNATION_INTERVENTION : void 0);
40230
40690
  }, MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS * 1e3);
40231
40691
  }
40232
40692
  function handleUserMessage2(sessionID) {
40233
- const state2 = getState4(sessionID);
40234
- if (state2.countdownTimer) {
40235
- cancelCountdown2(sessionID);
40236
- }
40693
+ sessionStateStore.cancelCountdown(sessionID);
40237
40694
  }
40238
40695
  function handleAbort(sessionID) {
40239
- const state2 = getState4(sessionID);
40696
+ const state2 = sessionStateStore.getState(sessionID);
40240
40697
  state2.isAborting = true;
40241
- cancelCountdown2(sessionID);
40698
+ sessionStateStore.cancelCountdown(sessionID);
40242
40699
  }
40243
40700
  function cleanupSession3(sessionID) {
40244
- cancelCountdown2(sessionID);
40245
- sessionStates3.delete(sessionID);
40701
+ sessionStateStore.cleanup(sessionID);
40702
+ clearCompactionState(sessionID);
40703
+ clearCircuitState(sessionID);
40704
+ resetProgress(sessionID);
40705
+ }
40706
+ function handleSessionCompacted(sessionID) {
40707
+ armCompactionGuard(sessionID, Date.now());
40708
+ sessionStateStore.cancelCountdown(sessionID);
40246
40709
  }
40247
40710
 
40248
40711
  // src/plugin-handlers/event-handler.ts
@@ -40374,6 +40837,7 @@ function createToolExecuteAfterHandler(ctx) {
40374
40837
  toolInput.arguments || {},
40375
40838
  toolOutput
40376
40839
  );
40840
+ recordToolCall(toolInput.sessionID, toolInput.tool);
40377
40841
  log(`[tool.execute.after] Completed ${toolInput.tool}`, {
40378
40842
  sessionID: toolInput.sessionID,
40379
40843
  step: session.step,
@@ -40465,6 +40929,7 @@ function createSessionCompactingHandler(ctx) {
40465
40929
  if (contextItems.length > 0) {
40466
40930
  output.context.push(...contextItems);
40467
40931
  }
40932
+ handleSessionCompacted(sessionID);
40468
40933
  };
40469
40934
  }
40470
40935
  function buildMissionContext(loopState) {
@@ -40634,7 +41099,7 @@ var OrchestratorPlugin = async (input) => {
40634
41099
  event: async (payload) => {
40635
41100
  const result = await createEventHandler(handlerContext)(payload);
40636
41101
  const { event } = payload;
40637
- if (event.type === "session.created" && event.properties) {
41102
+ if (event.type === SESSION_EVENTS.CREATED && event.properties) {
40638
41103
  const sessionID = event.properties.sessionID || event.properties.id || event.properties.info?.sessionID;
40639
41104
  if (sessionID) {
40640
41105
  todoSync.registerSession(sessionID);
@@ -40645,7 +41110,7 @@ var OrchestratorPlugin = async (input) => {
40645
41110
  // -----------------------------------------------------------------
40646
41111
  // chat.message hook - intercepts commands and sets up sessions
40647
41112
  // -----------------------------------------------------------------
40648
- "chat.message": createChatMessageHandler(handlerContext),
41113
+ [PLUGIN_HOOKS.CHAT_MESSAGE]: createChatMessageHandler(handlerContext),
40649
41114
  // -----------------------------------------------------------------
40650
41115
  // tool.execute.before hook - runs before any tool call
40651
41116
  // -----------------------------------------------------------------
@@ -40653,11 +41118,11 @@ var OrchestratorPlugin = async (input) => {
40653
41118
  // -----------------------------------------------------------------
40654
41119
  // tool.execute.after hook - runs after any tool call completes
40655
41120
  // -----------------------------------------------------------------
40656
- "tool.execute.after": createToolExecuteAfterHandler(handlerContext),
41121
+ [PLUGIN_HOOKS.TOOL_EXECUTE_AFTER]: createToolExecuteAfterHandler(handlerContext),
40657
41122
  // -----------------------------------------------------------------
40658
41123
  // assistant.done hook - runs when the LLM finishes responding
40659
41124
  // -----------------------------------------------------------------
40660
- "assistant.done": createAssistantDoneHandler(handlerContext),
41125
+ [PLUGIN_HOOKS.ASSISTANT_DONE]: createAssistantDoneHandler(handlerContext),
40661
41126
  // -----------------------------------------------------------------
40662
41127
  // experimental.session.compacting hook - preserves mission context during compaction
40663
41128
  // -----------------------------------------------------------------