opencode-orchestrator 1.2.62 → 1.2.67
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/README.md +197 -42
- package/dist/core/agents/consts/task-status.const.d.ts +2 -21
- package/dist/core/loop/circuit-breaker.d.ts +19 -0
- package/dist/core/loop/compaction-guard.d.ts +15 -0
- package/dist/core/loop/mission-loop-handler.d.ts +3 -12
- package/dist/core/loop/mission-loop.d.ts +0 -4
- package/dist/core/loop/progress-tracker.d.ts +39 -0
- package/dist/core/loop/session-state-store.d.ts +26 -0
- package/dist/core/notification/toast-core.d.ts +1 -1
- package/dist/core/plugins/plugin-manager.d.ts +2 -1
- package/dist/index.js +578 -113
- package/dist/scripts/postinstall.js +296 -38
- package/dist/scripts/preuninstall.js +268 -20
- package/dist/shared/index.d.ts +0 -1
- package/dist/shared/loop/constants/index.d.ts +1 -0
- package/dist/shared/loop/constants/task-status.d.ts +9 -0
- package/package.json +11 -6
- package/scripts/run-install-hook.mjs +66 -0
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
37009
|
-
inProgress: todos.filter((t) => t.status ===
|
|
37010
|
-
completed: todos.filter((t) => t.status ===
|
|
37011
|
-
cancelled: todos.filter((t) => t.status ===
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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 =
|
|
37787
|
+
let status = TODO_STATUS.PENDING;
|
|
37731
37788
|
switch (statusChar.toLowerCase()) {
|
|
37732
37789
|
case "x":
|
|
37733
|
-
status =
|
|
37790
|
+
status = TODO_STATUS.COMPLETED;
|
|
37734
37791
|
break;
|
|
37735
37792
|
case "/":
|
|
37736
37793
|
case ".":
|
|
37737
|
-
status =
|
|
37794
|
+
status = TODO_STATUS.IN_PROGRESS;
|
|
37738
37795
|
break;
|
|
37739
37796
|
case "-":
|
|
37740
|
-
status =
|
|
37797
|
+
status = TODO_STATUS.CANCELLED;
|
|
37741
37798
|
break;
|
|
37742
37799
|
case " ":
|
|
37743
37800
|
default:
|
|
37744
|
-
status =
|
|
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
|
|
40301
|
+
function resetProgress(sessionID) {
|
|
40101
40302
|
const state2 = sessionStates3.get(sessionID);
|
|
40102
|
-
if (state2
|
|
40103
|
-
|
|
40104
|
-
|
|
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
|
-
|
|
40118
|
-
|
|
40119
|
-
|
|
40120
|
-
|
|
40121
|
-
|
|
40122
|
-
|
|
40123
|
-
|
|
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
|
|
40134
|
-
if (
|
|
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
|
-
|
|
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
|
|
40644
|
+
const state2 = sessionStateStore.getState(sessionID);
|
|
40185
40645
|
const now = Date.now();
|
|
40186
|
-
if (
|
|
40646
|
+
if (state2.lastCheckTime && now - state2.lastCheckTime < LOOP.MIN_TIME_BETWEEN_CHECKS_MS) {
|
|
40187
40647
|
return;
|
|
40188
40648
|
}
|
|
40189
|
-
|
|
40190
|
-
|
|
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
|
-
|
|
40211
|
-
if (
|
|
40212
|
-
|
|
40213
|
-
|
|
40214
|
-
|
|
40215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40227
|
-
|
|
40228
|
-
|
|
40229
|
-
await injectContinuation2(client, directory, sessionID, newState,
|
|
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
|
-
|
|
40234
|
-
if (state2.countdownTimer) {
|
|
40235
|
-
cancelCountdown2(sessionID);
|
|
40236
|
-
}
|
|
40693
|
+
sessionStateStore.cancelCountdown(sessionID);
|
|
40237
40694
|
}
|
|
40238
40695
|
function handleAbort(sessionID) {
|
|
40239
|
-
const state2 =
|
|
40696
|
+
const state2 = sessionStateStore.getState(sessionID);
|
|
40240
40697
|
state2.isAborting = true;
|
|
40241
|
-
|
|
40698
|
+
sessionStateStore.cancelCountdown(sessionID);
|
|
40242
40699
|
}
|
|
40243
40700
|
function cleanupSession3(sessionID) {
|
|
40244
|
-
|
|
40245
|
-
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
41121
|
+
[PLUGIN_HOOKS.TOOL_EXECUTE_AFTER]: createToolExecuteAfterHandler(handlerContext),
|
|
40657
41122
|
// -----------------------------------------------------------------
|
|
40658
41123
|
// assistant.done hook - runs when the LLM finishes responding
|
|
40659
41124
|
// -----------------------------------------------------------------
|
|
40660
|
-
|
|
41125
|
+
[PLUGIN_HOOKS.ASSISTANT_DONE]: createAssistantDoneHandler(handlerContext),
|
|
40661
41126
|
// -----------------------------------------------------------------
|
|
40662
41127
|
// experimental.session.compacting hook - preserves mission context during compaction
|
|
40663
41128
|
// -----------------------------------------------------------------
|