opencode-orchestrator 1.2.13 → 1.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/agents/manager/task-poller.d.ts +2 -1
- package/dist/core/loop/continuation-lock.d.ts +57 -0
- package/dist/core/loop/verification.d.ts +5 -44
- package/dist/core/session/session-health.d.ts +93 -0
- package/dist/index.js +455 -141
- package/dist/shared/constants/system-messages.d.ts +1 -1
- package/dist/shared/core/constants/memory-hooks.d.ts +4 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -417,6 +417,10 @@ var init_memory_hooks = __esm({
|
|
|
417
417
|
COMPLETED: "completed",
|
|
418
418
|
PROGRESS: "progress",
|
|
419
419
|
FAILED: "failed"
|
|
420
|
+
},
|
|
421
|
+
PREFIX: {
|
|
422
|
+
TASK: "task-",
|
|
423
|
+
FILE: "file-task-"
|
|
420
424
|
}
|
|
421
425
|
};
|
|
422
426
|
}
|
|
@@ -861,8 +865,8 @@ var init_types4 = __esm({
|
|
|
861
865
|
});
|
|
862
866
|
|
|
863
867
|
// src/core/notification/toast-core.ts
|
|
864
|
-
function initToastClient(
|
|
865
|
-
tuiClient =
|
|
868
|
+
function initToastClient(client2) {
|
|
869
|
+
tuiClient = client2;
|
|
866
870
|
}
|
|
867
871
|
function show(options) {
|
|
868
872
|
const toast = {
|
|
@@ -885,9 +889,9 @@ function show(options) {
|
|
|
885
889
|
}
|
|
886
890
|
}
|
|
887
891
|
if (tuiClient) {
|
|
888
|
-
const
|
|
889
|
-
if (
|
|
890
|
-
|
|
892
|
+
const client2 = tuiClient;
|
|
893
|
+
if (client2.tui?.showToast) {
|
|
894
|
+
client2.tui.showToast({
|
|
891
895
|
body: {
|
|
892
896
|
title: toast.title,
|
|
893
897
|
message: toast.message,
|
|
@@ -18819,6 +18823,75 @@ Use \`get_task_result({ taskId: "task_xxx" })\` to retrieve results.
|
|
|
18819
18823
|
</system-notification>`;
|
|
18820
18824
|
}
|
|
18821
18825
|
|
|
18826
|
+
// src/core/session/session-health.ts
|
|
18827
|
+
var sessionHealth = /* @__PURE__ */ new Map();
|
|
18828
|
+
var STALE_THRESHOLD_MS = 12e4;
|
|
18829
|
+
var HEALTH_CHECK_INTERVAL_MS = 3e4;
|
|
18830
|
+
var WARNING_THRESHOLD_MS = 6e4;
|
|
18831
|
+
var healthCheckTimer;
|
|
18832
|
+
var client;
|
|
18833
|
+
function recordSessionResponse(sessionID) {
|
|
18834
|
+
const health = sessionHealth.get(sessionID);
|
|
18835
|
+
if (health) {
|
|
18836
|
+
health.lastResponseTime = Date.now();
|
|
18837
|
+
health.isStale = false;
|
|
18838
|
+
}
|
|
18839
|
+
}
|
|
18840
|
+
function isSessionStale(sessionID) {
|
|
18841
|
+
const health = sessionHealth.get(sessionID);
|
|
18842
|
+
if (!health) return false;
|
|
18843
|
+
return health.isStale;
|
|
18844
|
+
}
|
|
18845
|
+
function startHealthCheck(opencodeClient) {
|
|
18846
|
+
if (healthCheckTimer) {
|
|
18847
|
+
log("[session-health] Health check already running");
|
|
18848
|
+
return;
|
|
18849
|
+
}
|
|
18850
|
+
client = opencodeClient;
|
|
18851
|
+
healthCheckTimer = setInterval(() => {
|
|
18852
|
+
performHealthCheck();
|
|
18853
|
+
}, HEALTH_CHECK_INTERVAL_MS);
|
|
18854
|
+
log("[session-health] Health check started", {
|
|
18855
|
+
intervalMs: HEALTH_CHECK_INTERVAL_MS,
|
|
18856
|
+
staleThresholdMs: STALE_THRESHOLD_MS
|
|
18857
|
+
});
|
|
18858
|
+
}
|
|
18859
|
+
function stopHealthCheck() {
|
|
18860
|
+
if (healthCheckTimer) {
|
|
18861
|
+
clearInterval(healthCheckTimer);
|
|
18862
|
+
healthCheckTimer = void 0;
|
|
18863
|
+
log("[session-health] Health check stopped");
|
|
18864
|
+
}
|
|
18865
|
+
}
|
|
18866
|
+
function performHealthCheck() {
|
|
18867
|
+
const now = Date.now();
|
|
18868
|
+
const warnings = [];
|
|
18869
|
+
const stales = [];
|
|
18870
|
+
for (const [sessionID, health] of sessionHealth) {
|
|
18871
|
+
const elapsed = now - health.lastResponseTime;
|
|
18872
|
+
if (elapsed > WARNING_THRESHOLD_MS && elapsed <= STALE_THRESHOLD_MS && !health.isStale) {
|
|
18873
|
+
warnings.push(sessionID.slice(0, 8));
|
|
18874
|
+
}
|
|
18875
|
+
if (elapsed > STALE_THRESHOLD_MS && !health.isStale) {
|
|
18876
|
+
health.isStale = true;
|
|
18877
|
+
stales.push(sessionID.slice(0, 8));
|
|
18878
|
+
}
|
|
18879
|
+
}
|
|
18880
|
+
if (warnings.length > 0) {
|
|
18881
|
+
log("[session-health] Sessions approaching stale threshold", {
|
|
18882
|
+
sessions: warnings
|
|
18883
|
+
});
|
|
18884
|
+
}
|
|
18885
|
+
if (stales.length > 0) {
|
|
18886
|
+
log("[session-health] Sessions marked as stale", {
|
|
18887
|
+
sessions: stales
|
|
18888
|
+
});
|
|
18889
|
+
}
|
|
18890
|
+
}
|
|
18891
|
+
function cleanupSessionHealth(sessionID) {
|
|
18892
|
+
sessionHealth.delete(sessionID);
|
|
18893
|
+
}
|
|
18894
|
+
|
|
18822
18895
|
// src/core/agents/manager/task-launcher.ts
|
|
18823
18896
|
init_shared();
|
|
18824
18897
|
init_shared();
|
|
@@ -18833,8 +18906,8 @@ var TaskToastManager = class {
|
|
|
18833
18906
|
/**
|
|
18834
18907
|
* Initialize the manager with OpenCode client
|
|
18835
18908
|
*/
|
|
18836
|
-
init(
|
|
18837
|
-
this.client =
|
|
18909
|
+
init(client2, concurrency) {
|
|
18910
|
+
this.client = client2;
|
|
18838
18911
|
this.concurrency = concurrency ?? null;
|
|
18839
18912
|
}
|
|
18840
18913
|
/**
|
|
@@ -19094,11 +19167,11 @@ var instance = null;
|
|
|
19094
19167
|
function getTaskToastManager() {
|
|
19095
19168
|
return instance;
|
|
19096
19169
|
}
|
|
19097
|
-
function initTaskToastManager(
|
|
19170
|
+
function initTaskToastManager(client2, concurrency) {
|
|
19098
19171
|
if (!instance) {
|
|
19099
19172
|
instance = new TaskToastManager();
|
|
19100
19173
|
}
|
|
19101
|
-
instance.init(
|
|
19174
|
+
instance.init(client2, concurrency);
|
|
19102
19175
|
return instance;
|
|
19103
19176
|
}
|
|
19104
19177
|
|
|
@@ -33310,8 +33383,8 @@ var AgentRegistry = class _AgentRegistry {
|
|
|
33310
33383
|
|
|
33311
33384
|
// src/core/agents/manager/task-launcher.ts
|
|
33312
33385
|
var TaskLauncher = class {
|
|
33313
|
-
constructor(
|
|
33314
|
-
this.client =
|
|
33386
|
+
constructor(client2, directory, store, concurrency, sessionPool2, onTaskError, startPolling) {
|
|
33387
|
+
this.client = client2;
|
|
33315
33388
|
this.directory = directory;
|
|
33316
33389
|
this.store = store;
|
|
33317
33390
|
this.concurrency = concurrency;
|
|
@@ -33471,8 +33544,8 @@ ${action.modifyPrompt}`;
|
|
|
33471
33544
|
// src/core/agents/manager/task-resumer.ts
|
|
33472
33545
|
init_shared();
|
|
33473
33546
|
var TaskResumer = class {
|
|
33474
|
-
constructor(
|
|
33475
|
-
this.client =
|
|
33547
|
+
constructor(client2, store, findBySession, startPolling, notifyParentIfAllComplete) {
|
|
33548
|
+
this.client = client2;
|
|
33476
33549
|
this.store = store;
|
|
33477
33550
|
this.findBySession = findBySession;
|
|
33478
33551
|
this.startPolling = startPolling;
|
|
@@ -33624,15 +33697,17 @@ var ProgressNotifier = class _ProgressNotifier {
|
|
|
33624
33697
|
var progressNotifier = ProgressNotifier.getInstance();
|
|
33625
33698
|
|
|
33626
33699
|
// src/core/agents/manager/task-poller.ts
|
|
33700
|
+
var MAX_TASK_DURATION_MS = 6e5;
|
|
33627
33701
|
var TaskPoller = class {
|
|
33628
|
-
constructor(
|
|
33629
|
-
this.client =
|
|
33702
|
+
constructor(client2, store, concurrency, notifyParentIfAllComplete, scheduleCleanup, pruneExpiredTasks, onTaskComplete, onTaskError) {
|
|
33703
|
+
this.client = client2;
|
|
33630
33704
|
this.store = store;
|
|
33631
33705
|
this.concurrency = concurrency;
|
|
33632
33706
|
this.notifyParentIfAllComplete = notifyParentIfAllComplete;
|
|
33633
33707
|
this.scheduleCleanup = scheduleCleanup;
|
|
33634
33708
|
this.pruneExpiredTasks = pruneExpiredTasks;
|
|
33635
33709
|
this.onTaskComplete = onTaskComplete;
|
|
33710
|
+
this.onTaskError = onTaskError;
|
|
33636
33711
|
}
|
|
33637
33712
|
pollingInterval;
|
|
33638
33713
|
messageCache = /* @__PURE__ */ new Map();
|
|
@@ -33664,6 +33739,17 @@ var TaskPoller = class {
|
|
|
33664
33739
|
const allStatuses = statusResult.data ?? {};
|
|
33665
33740
|
for (const task of running) {
|
|
33666
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
|
+
}
|
|
33667
33753
|
if (task.status === TASK_STATUS.PENDING) continue;
|
|
33668
33754
|
const sessionStatus = allStatuses[task.sessionID];
|
|
33669
33755
|
if (sessionStatus?.type === SESSION_STATUS.IDLE) {
|
|
@@ -33776,8 +33862,8 @@ var TaskPoller = class {
|
|
|
33776
33862
|
init_shared();
|
|
33777
33863
|
init_store();
|
|
33778
33864
|
var TaskCleaner = class {
|
|
33779
|
-
constructor(
|
|
33780
|
-
this.client =
|
|
33865
|
+
constructor(client2, store, concurrency, sessionPool2) {
|
|
33866
|
+
this.client = client2;
|
|
33781
33867
|
this.store = store;
|
|
33782
33868
|
this.concurrency = concurrency;
|
|
33783
33869
|
this.sessionPool = sessionPool2;
|
|
@@ -33888,9 +33974,69 @@ You will be notified when ALL tasks complete. Continue productive work.`;
|
|
|
33888
33974
|
|
|
33889
33975
|
// src/core/agents/manager/event-handler.ts
|
|
33890
33976
|
init_shared();
|
|
33977
|
+
|
|
33978
|
+
// src/core/loop/continuation-lock.ts
|
|
33979
|
+
var locks = /* @__PURE__ */ new Map();
|
|
33980
|
+
var LOCK_TIMEOUT_MS = 3e4;
|
|
33981
|
+
function tryAcquireContinuationLock(sessionID, source) {
|
|
33982
|
+
const now = Date.now();
|
|
33983
|
+
const existing = locks.get(sessionID);
|
|
33984
|
+
if (existing?.acquired) {
|
|
33985
|
+
const elapsed = now - existing.timestamp;
|
|
33986
|
+
if (elapsed < LOCK_TIMEOUT_MS) {
|
|
33987
|
+
log("[continuation-lock] Lock denied - already held", {
|
|
33988
|
+
sessionID: sessionID.slice(0, 8),
|
|
33989
|
+
heldBy: existing.source,
|
|
33990
|
+
requestedBy: source,
|
|
33991
|
+
elapsedMs: elapsed
|
|
33992
|
+
});
|
|
33993
|
+
return false;
|
|
33994
|
+
}
|
|
33995
|
+
log("[continuation-lock] Forcing stale lock release", {
|
|
33996
|
+
sessionID: sessionID.slice(0, 8),
|
|
33997
|
+
staleSource: existing.source,
|
|
33998
|
+
elapsedMs: elapsed
|
|
33999
|
+
});
|
|
34000
|
+
}
|
|
34001
|
+
locks.set(sessionID, { acquired: true, timestamp: now, source });
|
|
34002
|
+
log("[continuation-lock] Lock acquired", {
|
|
34003
|
+
sessionID: sessionID.slice(0, 8),
|
|
34004
|
+
source
|
|
34005
|
+
});
|
|
34006
|
+
return true;
|
|
34007
|
+
}
|
|
34008
|
+
function releaseContinuationLock(sessionID) {
|
|
34009
|
+
const existing = locks.get(sessionID);
|
|
34010
|
+
if (existing?.acquired) {
|
|
34011
|
+
const duration5 = Date.now() - existing.timestamp;
|
|
34012
|
+
log("[continuation-lock] Lock released", {
|
|
34013
|
+
sessionID: sessionID.slice(0, 8),
|
|
34014
|
+
source: existing.source,
|
|
34015
|
+
heldMs: duration5
|
|
34016
|
+
});
|
|
34017
|
+
}
|
|
34018
|
+
locks.delete(sessionID);
|
|
34019
|
+
}
|
|
34020
|
+
function hasContinuationLock(sessionID) {
|
|
34021
|
+
const lock = locks.get(sessionID);
|
|
34022
|
+
if (!lock?.acquired) return false;
|
|
34023
|
+
if (Date.now() - lock.timestamp >= LOCK_TIMEOUT_MS) {
|
|
34024
|
+
log("[continuation-lock] Stale lock detected during check", {
|
|
34025
|
+
sessionID: sessionID.slice(0, 8)
|
|
34026
|
+
});
|
|
34027
|
+
locks.delete(sessionID);
|
|
34028
|
+
return false;
|
|
34029
|
+
}
|
|
34030
|
+
return true;
|
|
34031
|
+
}
|
|
34032
|
+
function cleanupContinuationLock(sessionID) {
|
|
34033
|
+
locks.delete(sessionID);
|
|
34034
|
+
}
|
|
34035
|
+
|
|
34036
|
+
// src/core/agents/manager/event-handler.ts
|
|
33891
34037
|
var EventHandler = class {
|
|
33892
|
-
constructor(
|
|
33893
|
-
this.client =
|
|
34038
|
+
constructor(client2, store, concurrency, findBySession, notifyParentIfAllComplete, scheduleCleanup, validateSessionHasOutput2, onTaskComplete) {
|
|
34039
|
+
this.client = client2;
|
|
33894
34040
|
this.store = store;
|
|
33895
34041
|
this.concurrency = concurrency;
|
|
33896
34042
|
this.findBySession = findBySession;
|
|
@@ -33908,6 +34054,7 @@ var EventHandler = class {
|
|
|
33908
34054
|
if (event.type === SESSION_EVENTS.IDLE) {
|
|
33909
34055
|
const sessionID = props?.sessionID;
|
|
33910
34056
|
if (!sessionID) return;
|
|
34057
|
+
recordSessionResponse(sessionID);
|
|
33911
34058
|
const task = this.findBySession(sessionID);
|
|
33912
34059
|
if (!task || task.status !== TASK_STATUS.RUNNING) return;
|
|
33913
34060
|
this.handleSessionIdle(task).catch((err) => {
|
|
@@ -33969,6 +34116,8 @@ var EventHandler = class {
|
|
|
33969
34116
|
this.store.delete(task.id);
|
|
33970
34117
|
taskWAL.log(WAL_ACTIONS.DELETE, task).catch(() => {
|
|
33971
34118
|
});
|
|
34119
|
+
cleanupSessionHealth(task.sessionID);
|
|
34120
|
+
cleanupContinuationLock(task.sessionID);
|
|
33972
34121
|
progressNotifier.update();
|
|
33973
34122
|
log(`Cleaned up deleted session task: ${task.id}`);
|
|
33974
34123
|
}
|
|
@@ -33998,18 +34147,18 @@ var SessionPool = class _SessionPool {
|
|
|
33998
34147
|
reuseHits: 0,
|
|
33999
34148
|
creationMisses: 0
|
|
34000
34149
|
};
|
|
34001
|
-
constructor(
|
|
34002
|
-
this.client =
|
|
34150
|
+
constructor(client2, directory, config3 = {}) {
|
|
34151
|
+
this.client = client2;
|
|
34003
34152
|
this.directory = directory;
|
|
34004
34153
|
this.config = { ...DEFAULT_CONFIG, ...config3 };
|
|
34005
34154
|
this.startHealthCheck();
|
|
34006
34155
|
}
|
|
34007
|
-
static getInstance(
|
|
34156
|
+
static getInstance(client2, directory, config3) {
|
|
34008
34157
|
if (!_SessionPool._instance) {
|
|
34009
|
-
if (!
|
|
34158
|
+
if (!client2 || !directory) {
|
|
34010
34159
|
throw new Error("SessionPool requires client and directory on first call");
|
|
34011
34160
|
}
|
|
34012
|
-
_SessionPool._instance = new _SessionPool(
|
|
34161
|
+
_SessionPool._instance = new _SessionPool(client2, directory, config3);
|
|
34013
34162
|
}
|
|
34014
34163
|
return _SessionPool._instance;
|
|
34015
34164
|
}
|
|
@@ -34419,27 +34568,29 @@ var ParallelAgentManager = class _ParallelAgentManager {
|
|
|
34419
34568
|
poller;
|
|
34420
34569
|
cleaner;
|
|
34421
34570
|
eventHandler;
|
|
34422
|
-
constructor(
|
|
34423
|
-
this.client =
|
|
34571
|
+
constructor(client2, directory) {
|
|
34572
|
+
this.client = client2;
|
|
34424
34573
|
this.directory = directory;
|
|
34574
|
+
startHealthCheck(client2);
|
|
34425
34575
|
const memory = MemoryManager.getInstance();
|
|
34426
34576
|
memory.add("system" /* SYSTEM */, CORE_PHILOSOPHY, 1);
|
|
34427
34577
|
memory.add("project" /* PROJECT */, `Working directory: ${directory}`, 0.9);
|
|
34428
34578
|
AgentRegistry.getInstance().setDirectory(directory);
|
|
34429
34579
|
TodoManager.getInstance().setDirectory(directory);
|
|
34430
|
-
this.sessionPool = SessionPool.getInstance(
|
|
34431
|
-
this.cleaner = new TaskCleaner(
|
|
34580
|
+
this.sessionPool = SessionPool.getInstance(client2, directory);
|
|
34581
|
+
this.cleaner = new TaskCleaner(client2, this.store, this.concurrency, this.sessionPool);
|
|
34432
34582
|
this.poller = new TaskPoller(
|
|
34433
|
-
|
|
34583
|
+
client2,
|
|
34434
34584
|
this.store,
|
|
34435
34585
|
this.concurrency,
|
|
34436
34586
|
(parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID),
|
|
34437
34587
|
(taskId) => this.cleaner.scheduleCleanup(taskId),
|
|
34438
34588
|
() => this.cleaner.pruneExpiredTasks(),
|
|
34439
|
-
(task) => this.handleTaskComplete(task)
|
|
34589
|
+
(task) => this.handleTaskComplete(task),
|
|
34590
|
+
(taskId, error92) => this.handleTaskError(taskId, error92)
|
|
34440
34591
|
);
|
|
34441
34592
|
this.launcher = new TaskLauncher(
|
|
34442
|
-
|
|
34593
|
+
client2,
|
|
34443
34594
|
directory,
|
|
34444
34595
|
this.store,
|
|
34445
34596
|
this.concurrency,
|
|
@@ -34448,14 +34599,14 @@ var ParallelAgentManager = class _ParallelAgentManager {
|
|
|
34448
34599
|
() => this.poller.start()
|
|
34449
34600
|
);
|
|
34450
34601
|
this.resumer = new TaskResumer(
|
|
34451
|
-
|
|
34602
|
+
client2,
|
|
34452
34603
|
this.store,
|
|
34453
34604
|
(sessionID) => this.findBySession(sessionID),
|
|
34454
34605
|
() => this.poller.start(),
|
|
34455
34606
|
(parentSessionID) => this.cleaner.notifyParentIfAllComplete(parentSessionID)
|
|
34456
34607
|
);
|
|
34457
34608
|
this.eventHandler = new EventHandler(
|
|
34458
|
-
|
|
34609
|
+
client2,
|
|
34459
34610
|
this.store,
|
|
34460
34611
|
this.concurrency,
|
|
34461
34612
|
(sessionID) => this.findBySession(sessionID),
|
|
@@ -34469,12 +34620,12 @@ var ParallelAgentManager = class _ParallelAgentManager {
|
|
|
34469
34620
|
log("Recovery error:", err);
|
|
34470
34621
|
});
|
|
34471
34622
|
}
|
|
34472
|
-
static getInstance(
|
|
34623
|
+
static getInstance(client2, directory) {
|
|
34473
34624
|
if (!_ParallelAgentManager._instance) {
|
|
34474
|
-
if (!
|
|
34625
|
+
if (!client2 || !directory) {
|
|
34475
34626
|
throw new Error("ParallelAgentManager requires client and directory on first call");
|
|
34476
34627
|
}
|
|
34477
|
-
_ParallelAgentManager._instance = new _ParallelAgentManager(
|
|
34628
|
+
_ParallelAgentManager._instance = new _ParallelAgentManager(client2, directory);
|
|
34478
34629
|
}
|
|
34479
34630
|
return _ParallelAgentManager._instance;
|
|
34480
34631
|
}
|
|
@@ -34553,6 +34704,7 @@ var ParallelAgentManager = class _ParallelAgentManager {
|
|
|
34553
34704
|
}
|
|
34554
34705
|
cleanup() {
|
|
34555
34706
|
this.poller.stop();
|
|
34707
|
+
stopHealthCheck();
|
|
34556
34708
|
this.store.clear();
|
|
34557
34709
|
MemoryManager.getInstance().clearTaskMemory();
|
|
34558
34710
|
Promise.resolve().then(() => (init_store(), store_exports)).then((store) => store.clearAll()).catch(() => {
|
|
@@ -34783,7 +34935,7 @@ async function extractSessionResult(session, sessionID) {
|
|
|
34783
34935
|
return "(Error extracting result)";
|
|
34784
34936
|
}
|
|
34785
34937
|
}
|
|
34786
|
-
var createDelegateTaskTool = (manager,
|
|
34938
|
+
var createDelegateTaskTool = (manager, client2) => tool({
|
|
34787
34939
|
description: `Delegate a task to an agent.
|
|
34788
34940
|
|
|
34789
34941
|
${PROMPT_TAGS.MODE.open}
|
|
@@ -34836,7 +34988,7 @@ If your task is too complex, please:
|
|
|
34836
34988
|
2. Request task decomposition at the ${AGENT_NAMES.PLANNER} level
|
|
34837
34989
|
3. Complete your assigned file directly without delegation`;
|
|
34838
34990
|
}
|
|
34839
|
-
const sessionClient =
|
|
34991
|
+
const sessionClient = client2;
|
|
34840
34992
|
if (background === void 0) {
|
|
34841
34993
|
return `${OUTPUT_LABEL.ERROR} 'background' parameter is REQUIRED.`;
|
|
34842
34994
|
}
|
|
@@ -35170,9 +35322,9 @@ var createUpdateTodoTool = () => tool({
|
|
|
35170
35322
|
|
|
35171
35323
|
// src/tools/parallel/index.ts
|
|
35172
35324
|
init_shared();
|
|
35173
|
-
function createAsyncAgentTools(manager,
|
|
35325
|
+
function createAsyncAgentTools(manager, client2) {
|
|
35174
35326
|
return {
|
|
35175
|
-
[TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager,
|
|
35327
|
+
[TOOL_NAMES.DELEGATE_TASK]: createDelegateTaskTool(manager, client2),
|
|
35176
35328
|
[TOOL_NAMES.GET_TASK_RESULT]: createGetTaskResultTool(manager),
|
|
35177
35329
|
[TOOL_NAMES.LIST_TASKS]: createListTasksTool(manager),
|
|
35178
35330
|
[TOOL_NAMES.CANCEL_TASK]: createCancelTaskTool(manager),
|
|
@@ -36466,15 +36618,15 @@ var CONTINUE_INSTRUCTION = `<auto_continue>
|
|
|
36466
36618
|
</auto_continue>`;
|
|
36467
36619
|
var STAGNATION_INTERVENTION = `
|
|
36468
36620
|
<system_intervention type="stagnation_detected">
|
|
36469
|
-
\u26A0\uFE0F
|
|
36470
|
-
|
|
36621
|
+
\u26A0\uFE0F **WARNING: STAGNATION DETECTED**
|
|
36622
|
+
No substantial progress has been detected for several turns. Simply "monitoring" or repeating the same actions is prohibited.
|
|
36471
36623
|
|
|
36472
|
-
|
|
36473
|
-
1.
|
|
36474
|
-
2.
|
|
36475
|
-
3.
|
|
36624
|
+
**Self-Diagnosis and Resolution Guidelines:**
|
|
36625
|
+
1. **Check Live Logs**: Use \`check_background_task\` or \`read_file\` to directly check the output logs of running tasks.
|
|
36626
|
+
2. **Process Health Diagnosis**: If a task appears to be a zombie or stuck, kill it immediately and restart it with more granular steps.
|
|
36627
|
+
3. **Strategy Pivot**: If the same approach keeps failing, use different tools or methods to reach the goal.
|
|
36476
36628
|
|
|
36477
|
-
|
|
36629
|
+
**Intervene proactively NOW. Do NOT wait.**
|
|
36478
36630
|
</system_intervention>`;
|
|
36479
36631
|
var CLEANUP_INSTRUCTION = `
|
|
36480
36632
|
<system_maintenance type="continuous_hygiene">
|
|
@@ -36823,8 +36975,27 @@ function formatElapsedTime(startMs, endMs = Date.now()) {
|
|
|
36823
36975
|
// src/core/loop/verification.ts
|
|
36824
36976
|
init_shared();
|
|
36825
36977
|
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
36978
|
+
import { access, readFile as readFile5 } from "node:fs/promises";
|
|
36979
|
+
import { constants } from "node:fs";
|
|
36826
36980
|
import { join as join8 } from "node:path";
|
|
36827
36981
|
var CHECKLIST_FILE = CHECKLIST.FILE;
|
|
36982
|
+
var fileCache = /* @__PURE__ */ new Map();
|
|
36983
|
+
var CACHE_TTL_MS = 5e3;
|
|
36984
|
+
async function readFileWithCache(filePath) {
|
|
36985
|
+
const now = Date.now();
|
|
36986
|
+
const cached3 = fileCache.get(filePath);
|
|
36987
|
+
if (cached3 && now - cached3.timestamp < CACHE_TTL_MS) {
|
|
36988
|
+
return cached3.content;
|
|
36989
|
+
}
|
|
36990
|
+
try {
|
|
36991
|
+
await access(filePath, constants.R_OK);
|
|
36992
|
+
const content = await readFile5(filePath, "utf-8");
|
|
36993
|
+
fileCache.set(filePath, { content, timestamp: now });
|
|
36994
|
+
return content;
|
|
36995
|
+
} catch {
|
|
36996
|
+
return null;
|
|
36997
|
+
}
|
|
36998
|
+
}
|
|
36828
36999
|
function parseChecklistLine(line, currentCategory) {
|
|
36829
37000
|
const trimmedLine = line.trim();
|
|
36830
37001
|
const idMatch = trimmedLine.match(CHECKLIST_PATTERNS.ITEM_WITH_ID);
|
|
@@ -36900,6 +37071,7 @@ function readChecklist(directory) {
|
|
|
36900
37071
|
return [];
|
|
36901
37072
|
}
|
|
36902
37073
|
}
|
|
37074
|
+
var lastVerificationResult = /* @__PURE__ */ new Map();
|
|
36903
37075
|
function verifyChecklist(directory) {
|
|
36904
37076
|
const result = {
|
|
36905
37077
|
passed: false,
|
|
@@ -36939,6 +37111,40 @@ function verifyChecklist(directory) {
|
|
|
36939
37111
|
});
|
|
36940
37112
|
return result;
|
|
36941
37113
|
}
|
|
37114
|
+
async function verifyChecklistAsync(directory) {
|
|
37115
|
+
const result = {
|
|
37116
|
+
passed: false,
|
|
37117
|
+
totalItems: 0,
|
|
37118
|
+
completedItems: 0,
|
|
37119
|
+
incompleteItems: 0,
|
|
37120
|
+
progress: "0/0",
|
|
37121
|
+
incompleteList: [],
|
|
37122
|
+
errors: []
|
|
37123
|
+
};
|
|
37124
|
+
const filePath = join8(directory, CHECKLIST_FILE);
|
|
37125
|
+
const content = await readFileWithCache(filePath);
|
|
37126
|
+
if (!content) {
|
|
37127
|
+
result.errors.push(`Verification checklist not found at ${CHECKLIST_FILE}`);
|
|
37128
|
+
result.errors.push("Create checklist with at least: build, tests, and any environment-specific checks");
|
|
37129
|
+
return result;
|
|
37130
|
+
}
|
|
37131
|
+
const items = parseChecklist(content);
|
|
37132
|
+
if (items.length === 0) {
|
|
37133
|
+
result.errors.push("Verification checklist is empty");
|
|
37134
|
+
result.errors.push("Add verification items (build, tests, environment checks)");
|
|
37135
|
+
return result;
|
|
37136
|
+
}
|
|
37137
|
+
result.totalItems = items.length;
|
|
37138
|
+
result.completedItems = items.filter((i) => i.completed).length;
|
|
37139
|
+
result.incompleteItems = result.totalItems - result.completedItems;
|
|
37140
|
+
result.progress = `${result.completedItems}/${result.totalItems}`;
|
|
37141
|
+
result.incompleteList = items.filter((i) => !i.completed).map((i) => `[${CHECKLIST_CATEGORIES.LABELS[i.category]}] ${i.description}`);
|
|
37142
|
+
if (result.incompleteItems > 0) {
|
|
37143
|
+
result.errors.push(`Checklist incomplete: ${result.progress}`);
|
|
37144
|
+
}
|
|
37145
|
+
result.passed = result.incompleteItems === 0 && result.totalItems > 0;
|
|
37146
|
+
return result;
|
|
37147
|
+
}
|
|
36942
37148
|
var TODO_INCOMPLETE_PATTERN = /^[-*]\s*\[\s*\]/gm;
|
|
36943
37149
|
var TODO_COMPLETE_PATTERN = /^[-*]\s*\[[xX]\]/gm;
|
|
36944
37150
|
var SYNC_ISSUE_PATTERNS = [
|
|
@@ -36964,7 +37170,7 @@ function hasRealSyncIssues(content) {
|
|
|
36964
37170
|
});
|
|
36965
37171
|
return lines.length > 0;
|
|
36966
37172
|
}
|
|
36967
|
-
function
|
|
37173
|
+
function verifyMissionCompletionSync(directory) {
|
|
36968
37174
|
const result = {
|
|
36969
37175
|
passed: false,
|
|
36970
37176
|
todoComplete: false,
|
|
@@ -37045,6 +37251,89 @@ function verifyMissionCompletion(directory) {
|
|
|
37045
37251
|
});
|
|
37046
37252
|
return result;
|
|
37047
37253
|
}
|
|
37254
|
+
async function verifyMissionCompletionAsync(directory) {
|
|
37255
|
+
const result = {
|
|
37256
|
+
passed: false,
|
|
37257
|
+
todoComplete: false,
|
|
37258
|
+
todoProgress: "0/0",
|
|
37259
|
+
todoIncomplete: 0,
|
|
37260
|
+
syncIssuesEmpty: true,
|
|
37261
|
+
syncIssuesCount: 0,
|
|
37262
|
+
checklistComplete: false,
|
|
37263
|
+
checklistProgress: "0/0",
|
|
37264
|
+
errors: []
|
|
37265
|
+
};
|
|
37266
|
+
const checklistResult = await verifyChecklistAsync(directory);
|
|
37267
|
+
result.checklistComplete = checklistResult.passed;
|
|
37268
|
+
result.checklistProgress = checklistResult.progress;
|
|
37269
|
+
const hasChecklist = checklistResult.totalItems > 0;
|
|
37270
|
+
if (hasChecklist && !checklistResult.passed) {
|
|
37271
|
+
result.errors.push(`Verification checklist incomplete: ${checklistResult.progress}`);
|
|
37272
|
+
result.errors.push(...checklistResult.incompleteList.slice(0, 5).map((i) => ` - ${i}`));
|
|
37273
|
+
if (checklistResult.incompleteList.length > 5) {
|
|
37274
|
+
result.errors.push(` ... and ${checklistResult.incompleteList.length - 5} more`);
|
|
37275
|
+
}
|
|
37276
|
+
}
|
|
37277
|
+
const todoPath = join8(directory, PATHS.TODO);
|
|
37278
|
+
const todoContent = await readFileWithCache(todoPath);
|
|
37279
|
+
if (todoContent) {
|
|
37280
|
+
try {
|
|
37281
|
+
const incompleteCount = countMatches(todoContent, TODO_INCOMPLETE_PATTERN);
|
|
37282
|
+
const completeCount = countMatches(todoContent, TODO_COMPLETE_PATTERN);
|
|
37283
|
+
const total = incompleteCount + completeCount;
|
|
37284
|
+
result.todoIncomplete = incompleteCount;
|
|
37285
|
+
result.todoComplete = incompleteCount === 0 && total > 0;
|
|
37286
|
+
result.todoProgress = `${completeCount}/${total}`;
|
|
37287
|
+
if (!result.todoComplete && !hasChecklist) {
|
|
37288
|
+
if (total === 0) {
|
|
37289
|
+
result.errors.push("No TODO items found - create tasks first");
|
|
37290
|
+
} else {
|
|
37291
|
+
result.errors.push(
|
|
37292
|
+
`TODO incomplete: ${result.todoProgress} (${incompleteCount} remaining)`
|
|
37293
|
+
);
|
|
37294
|
+
}
|
|
37295
|
+
}
|
|
37296
|
+
} catch (error92) {
|
|
37297
|
+
result.errors.push(`Failed to read TODO: ${error92}`);
|
|
37298
|
+
}
|
|
37299
|
+
} else if (!hasChecklist) {
|
|
37300
|
+
result.errors.push(`TODO file not found at ${PATHS.TODO}`);
|
|
37301
|
+
}
|
|
37302
|
+
const syncPath = join8(directory, PATHS.SYNC_ISSUES);
|
|
37303
|
+
const syncContent = await readFileWithCache(syncPath);
|
|
37304
|
+
if (syncContent) {
|
|
37305
|
+
try {
|
|
37306
|
+
result.syncIssuesEmpty = !hasRealSyncIssues(syncContent);
|
|
37307
|
+
if (!result.syncIssuesEmpty) {
|
|
37308
|
+
const issueLines = syncContent.split("\n").filter(
|
|
37309
|
+
(l) => /^[-*]\s+\S/.test(l.trim()) || /ERROR|FAIL|CONFLICT/i.test(l)
|
|
37310
|
+
);
|
|
37311
|
+
result.syncIssuesCount = issueLines.length;
|
|
37312
|
+
result.errors.push(
|
|
37313
|
+
`Sync issues not resolved: ${result.syncIssuesCount} issue(s) remain`
|
|
37314
|
+
);
|
|
37315
|
+
}
|
|
37316
|
+
} catch (error92) {
|
|
37317
|
+
result.syncIssuesEmpty = true;
|
|
37318
|
+
}
|
|
37319
|
+
}
|
|
37320
|
+
if (hasChecklist) {
|
|
37321
|
+
result.passed = result.checklistComplete && result.syncIssuesEmpty;
|
|
37322
|
+
} else {
|
|
37323
|
+
result.passed = result.todoComplete && result.syncIssuesEmpty;
|
|
37324
|
+
}
|
|
37325
|
+
lastVerificationResult.set(directory, { result, timestamp: Date.now() });
|
|
37326
|
+
return result;
|
|
37327
|
+
}
|
|
37328
|
+
function verifyMissionCompletion(directory) {
|
|
37329
|
+
const cached3 = lastVerificationResult.get(directory);
|
|
37330
|
+
if (cached3 && Date.now() - cached3.timestamp < CACHE_TTL_MS) {
|
|
37331
|
+
return cached3.result;
|
|
37332
|
+
}
|
|
37333
|
+
const result = verifyMissionCompletionSync(directory);
|
|
37334
|
+
lastVerificationResult.set(directory, { result, timestamp: Date.now() });
|
|
37335
|
+
return result;
|
|
37336
|
+
}
|
|
37048
37337
|
function buildVerificationFailurePrompt(result) {
|
|
37049
37338
|
const errorList = result.errors.map((e) => `\u274C ${e}`).join("\n");
|
|
37050
37339
|
const hasChecklist = result.checklistProgress !== "0/0";
|
|
@@ -37813,11 +38102,18 @@ There was a temporary processing issue. Please continue from where you left off.
|
|
|
37813
38102
|
3. Continue execution
|
|
37814
38103
|
</action>
|
|
37815
38104
|
</recovery>`;
|
|
37816
|
-
async function handleSessionError(
|
|
38105
|
+
async function handleSessionError(client2, sessionID, error92, properties) {
|
|
37817
38106
|
const state2 = getState2(sessionID);
|
|
37818
38107
|
if (state2.isRecovering) {
|
|
37819
|
-
|
|
37820
|
-
|
|
38108
|
+
const RECOVERY_TIMEOUT_MS = 1e4;
|
|
38109
|
+
const elapsed = Date.now() - (state2.recoveryStartTime ?? 0);
|
|
38110
|
+
if (elapsed > RECOVERY_TIMEOUT_MS) {
|
|
38111
|
+
log("[session-recovery] Forcibly clearing stale recovery state", { sessionID, elapsed });
|
|
38112
|
+
state2.isRecovering = false;
|
|
38113
|
+
} else {
|
|
38114
|
+
log("[session-recovery] Already recovering, skipping", { sessionID });
|
|
38115
|
+
return false;
|
|
38116
|
+
}
|
|
37821
38117
|
}
|
|
37822
38118
|
const now = Date.now();
|
|
37823
38119
|
if (now - state2.lastErrorTime < BACKGROUND_TASK.RETRY_COOLDOWN_MS) {
|
|
@@ -37838,6 +38134,7 @@ async function handleSessionError(client, sessionID, error92, properties) {
|
|
|
37838
38134
|
return false;
|
|
37839
38135
|
}
|
|
37840
38136
|
state2.isRecovering = true;
|
|
38137
|
+
state2.recoveryStartTime = Date.now();
|
|
37841
38138
|
try {
|
|
37842
38139
|
let recoveryPrompt = null;
|
|
37843
38140
|
let toastMessage = null;
|
|
@@ -37863,23 +38160,19 @@ async function handleSessionError(client, sessionID, error92, properties) {
|
|
|
37863
38160
|
log("[session-recovery] Rate limit, waiting", { delay: action.delay });
|
|
37864
38161
|
await new Promise((r) => setTimeout(r, action.delay));
|
|
37865
38162
|
}
|
|
37866
|
-
state2.isRecovering = false;
|
|
37867
38163
|
return true;
|
|
37868
38164
|
case ERROR_TYPE.CONTEXT_OVERFLOW:
|
|
37869
38165
|
toastMessage = "Context Overflow - Consider compaction";
|
|
37870
|
-
state2.isRecovering = false;
|
|
37871
38166
|
return false;
|
|
37872
38167
|
case ERROR_TYPE.MESSAGE_ABORTED:
|
|
37873
38168
|
log("[session-recovery] Message aborted by user, not recovering", { sessionID });
|
|
37874
|
-
state2.isRecovering = false;
|
|
37875
38169
|
return false;
|
|
37876
38170
|
default:
|
|
37877
|
-
state2.isRecovering = false;
|
|
37878
38171
|
return false;
|
|
37879
38172
|
}
|
|
37880
38173
|
if (recoveryPrompt && toastMessage) {
|
|
37881
38174
|
presets_exports.errorRecovery(toastMessage);
|
|
37882
|
-
|
|
38175
|
+
client2.session.prompt({
|
|
37883
38176
|
path: { id: sessionID },
|
|
37884
38177
|
body: {
|
|
37885
38178
|
parts: [{ type: PART_TYPES.TEXT, text: recoveryPrompt }]
|
|
@@ -37888,15 +38181,15 @@ async function handleSessionError(client, sessionID, error92, properties) {
|
|
|
37888
38181
|
log("[session-recovery] Failed to inject recovery prompt", { sessionID, error: injectionError });
|
|
37889
38182
|
});
|
|
37890
38183
|
log("[session-recovery] Recovery prompt injected (async)", { sessionID, errorType });
|
|
37891
|
-
state2.isRecovering = false;
|
|
37892
38184
|
return true;
|
|
37893
38185
|
}
|
|
37894
|
-
state2.isRecovering = false;
|
|
37895
38186
|
return false;
|
|
37896
|
-
} catch (
|
|
37897
|
-
log("[session-recovery]
|
|
37898
|
-
state2.isRecovering = false;
|
|
38187
|
+
} catch (recoveryError) {
|
|
38188
|
+
log("[session-recovery] Recovery failed", { sessionID, error: recoveryError });
|
|
37899
38189
|
return false;
|
|
38190
|
+
} finally {
|
|
38191
|
+
state2.isRecovering = false;
|
|
38192
|
+
state2.recoveryStartTime = void 0;
|
|
37900
38193
|
}
|
|
37901
38194
|
}
|
|
37902
38195
|
function markRecoveryComplete(sessionID) {
|
|
@@ -37957,9 +38250,9 @@ function hasRunningBackgroundTasks(parentSessionID) {
|
|
|
37957
38250
|
return false;
|
|
37958
38251
|
}
|
|
37959
38252
|
}
|
|
37960
|
-
async function showCountdownToast(
|
|
38253
|
+
async function showCountdownToast(client2, secondsRemaining, incompleteCount) {
|
|
37961
38254
|
try {
|
|
37962
|
-
const tuiClient2 =
|
|
38255
|
+
const tuiClient2 = client2;
|
|
37963
38256
|
if (tuiClient2.tui?.showToast) {
|
|
37964
38257
|
await tuiClient2.tui.showToast({
|
|
37965
38258
|
body: {
|
|
@@ -37973,7 +38266,7 @@ async function showCountdownToast(client, secondsRemaining, incompleteCount) {
|
|
|
37973
38266
|
} catch {
|
|
37974
38267
|
}
|
|
37975
38268
|
}
|
|
37976
|
-
async function injectContinuation(
|
|
38269
|
+
async function injectContinuation(client2, directory, sessionID, todos) {
|
|
37977
38270
|
const state2 = getState3(sessionID);
|
|
37978
38271
|
if (state2.isAborting) {
|
|
37979
38272
|
log("[todo-continuation] Skipped: user is aborting", { sessionID });
|
|
@@ -38003,24 +38296,22 @@ async function injectContinuation(client, directory, sessionID, todos) {
|
|
|
38003
38296
|
return;
|
|
38004
38297
|
}
|
|
38005
38298
|
try {
|
|
38006
|
-
|
|
38299
|
+
await client2.session.prompt({
|
|
38007
38300
|
path: { id: sessionID },
|
|
38008
38301
|
body: {
|
|
38009
38302
|
parts: [{ type: PART_TYPES.TEXT, text: prompt }]
|
|
38010
38303
|
}
|
|
38011
|
-
}).catch((error92) => {
|
|
38012
|
-
log("[todo-continuation] Failed to inject continuation", { sessionID, error: error92 });
|
|
38013
38304
|
});
|
|
38014
|
-
log("[todo-continuation] Injected continuation prompt
|
|
38305
|
+
log("[todo-continuation] Injected continuation prompt", {
|
|
38015
38306
|
sessionID,
|
|
38016
38307
|
incompleteCount: getIncompleteCount(todos),
|
|
38017
38308
|
progress: formatProgress(todos)
|
|
38018
38309
|
});
|
|
38019
38310
|
} catch (error92) {
|
|
38020
|
-
log("[todo-continuation] Failed to
|
|
38311
|
+
log("[todo-continuation] Failed to inject continuation", { sessionID, error: error92 });
|
|
38021
38312
|
}
|
|
38022
38313
|
}
|
|
38023
|
-
async function handleSessionIdle(
|
|
38314
|
+
async function handleSessionIdle(client2, directory, sessionID, mainSessionID) {
|
|
38024
38315
|
const state2 = getState3(sessionID);
|
|
38025
38316
|
const now = Date.now();
|
|
38026
38317
|
if (state2.lastIdleTime && now - state2.lastIdleTime < MIN_TIME_BETWEEN_CONTINUATIONS_MS) {
|
|
@@ -38050,18 +38341,23 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
|
|
|
38050
38341
|
log("[todo-continuation] Skipped: background tasks running", { sessionID });
|
|
38051
38342
|
return;
|
|
38052
38343
|
}
|
|
38344
|
+
if (hasContinuationLock(sessionID)) {
|
|
38345
|
+
log("[todo-continuation] Mission loop has priority, skipping");
|
|
38346
|
+
return;
|
|
38347
|
+
}
|
|
38053
38348
|
let todos = [];
|
|
38054
38349
|
try {
|
|
38055
|
-
const response = await
|
|
38350
|
+
const response = await client2.session.todo({ path: { id: sessionID } });
|
|
38056
38351
|
todos = parseTodos(response.data ?? response);
|
|
38057
38352
|
} catch (error92) {
|
|
38058
38353
|
log("[todo-continuation] Failed to fetch todos", { sessionID, error: error92 });
|
|
38354
|
+
cancelCountdown(sessionID);
|
|
38059
38355
|
return;
|
|
38060
38356
|
}
|
|
38061
38357
|
const hasBuiltinWork = hasRemainingWork(todos);
|
|
38062
38358
|
let hasFileWork = false;
|
|
38063
38359
|
try {
|
|
38064
|
-
const verification =
|
|
38360
|
+
const verification = await verifyMissionCompletionAsync(directory);
|
|
38065
38361
|
hasFileWork = !verification.passed && (verification.todoIncomplete > 0 || verification.checklistProgress !== "0/0" && !verification.checklistComplete);
|
|
38066
38362
|
} catch (err) {
|
|
38067
38363
|
log("[todo-continuation] Failed to check file-based todos", err);
|
|
@@ -38078,26 +38374,34 @@ async function handleSessionIdle(client, directory, sessionID, mainSessionID) {
|
|
|
38078
38374
|
incompleteCount,
|
|
38079
38375
|
nextPending: nextPending?.id
|
|
38080
38376
|
});
|
|
38081
|
-
await showCountdownToast(
|
|
38377
|
+
await showCountdownToast(client2, COUNTDOWN_SECONDS, incompleteCount);
|
|
38082
38378
|
state2.countdownStartedAt = now;
|
|
38083
38379
|
state2.countdownTimer = setTimeout(async () => {
|
|
38084
38380
|
cancelCountdown(sessionID);
|
|
38381
|
+
if (!tryAcquireContinuationLock(sessionID, "todo-continuation")) {
|
|
38382
|
+
log("[todo-continuation] Failed to acquire lock, skipping");
|
|
38383
|
+
return;
|
|
38384
|
+
}
|
|
38085
38385
|
try {
|
|
38086
|
-
const freshResponse = await client.session.todo({ path: { id: sessionID } });
|
|
38087
|
-
const freshTodos = parseTodos(freshResponse.data ?? freshResponse);
|
|
38088
|
-
let freshFileWork = false;
|
|
38089
38386
|
try {
|
|
38090
|
-
const
|
|
38091
|
-
|
|
38387
|
+
const freshResponse = await client2.session.todo({ path: { id: sessionID } });
|
|
38388
|
+
const freshTodos = parseTodos(freshResponse.data ?? freshResponse);
|
|
38389
|
+
let freshFileWork = false;
|
|
38390
|
+
try {
|
|
38391
|
+
const v = await verifyMissionCompletionAsync(directory);
|
|
38392
|
+
freshFileWork = !v.passed && (v.todoIncomplete > 0 || v.checklistProgress !== "0/0" && !v.checklistComplete);
|
|
38393
|
+
} catch {
|
|
38394
|
+
}
|
|
38395
|
+
if (hasRemainingWork(freshTodos) || freshFileWork) {
|
|
38396
|
+
await injectContinuation(client2, directory, sessionID, freshTodos);
|
|
38397
|
+
} else {
|
|
38398
|
+
log("[todo-continuation] All work completed during countdown", { sessionID });
|
|
38399
|
+
}
|
|
38092
38400
|
} catch {
|
|
38401
|
+
log("[todo-continuation] Failed to re-fetch todos for continuation", { sessionID });
|
|
38093
38402
|
}
|
|
38094
|
-
|
|
38095
|
-
|
|
38096
|
-
} else {
|
|
38097
|
-
log("[todo-continuation] All work completed during countdown", { sessionID });
|
|
38098
|
-
}
|
|
38099
|
-
} catch {
|
|
38100
|
-
log("[todo-continuation] Failed to re-fetch todos for continuation", { sessionID });
|
|
38403
|
+
} finally {
|
|
38404
|
+
releaseContinuationLock(sessionID);
|
|
38101
38405
|
}
|
|
38102
38406
|
}, COUNTDOWN_SECONDS * TIME.SECOND);
|
|
38103
38407
|
}
|
|
@@ -38130,6 +38434,9 @@ function cleanupSession2(sessionID) {
|
|
|
38130
38434
|
cancelCountdown(sessionID);
|
|
38131
38435
|
sessionStates2.delete(sessionID);
|
|
38132
38436
|
}
|
|
38437
|
+
function hasPendingContinuation(sessionID) {
|
|
38438
|
+
return !!sessionStates2.get(sessionID)?.countdownTimer;
|
|
38439
|
+
}
|
|
38133
38440
|
|
|
38134
38441
|
// src/hooks/custom/user-activity.ts
|
|
38135
38442
|
var UserActivityHook = class {
|
|
@@ -38389,11 +38696,12 @@ import * as fs10 from "node:fs";
|
|
|
38389
38696
|
import * as path9 from "node:path";
|
|
38390
38697
|
|
|
38391
38698
|
// src/core/sync/todo-parser.ts
|
|
38699
|
+
init_shared();
|
|
38392
38700
|
function parseTodoMd(content) {
|
|
38393
38701
|
const lines = content.split("\n");
|
|
38394
38702
|
const todos = [];
|
|
38395
38703
|
const generateId = (text, index2) => {
|
|
38396
|
-
return
|
|
38704
|
+
return `${TODO_CONSTANTS.PREFIX.FILE}${index2}-${text.substring(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
38397
38705
|
};
|
|
38398
38706
|
let index = 0;
|
|
38399
38707
|
for (const line of lines) {
|
|
@@ -38401,28 +38709,28 @@ function parseTodoMd(content) {
|
|
|
38401
38709
|
if (match) {
|
|
38402
38710
|
const [, statusChar, text] = match;
|
|
38403
38711
|
const content2 = text.trim();
|
|
38404
|
-
let status =
|
|
38712
|
+
let status = TODO_STATUS2.PENDING;
|
|
38405
38713
|
switch (statusChar.toLowerCase()) {
|
|
38406
38714
|
case "x":
|
|
38407
|
-
status =
|
|
38715
|
+
status = TODO_STATUS2.COMPLETED;
|
|
38408
38716
|
break;
|
|
38409
38717
|
case "/":
|
|
38410
38718
|
case ".":
|
|
38411
|
-
status =
|
|
38719
|
+
status = TODO_STATUS2.IN_PROGRESS;
|
|
38412
38720
|
break;
|
|
38413
38721
|
case "-":
|
|
38414
|
-
status =
|
|
38722
|
+
status = TODO_STATUS2.CANCELLED;
|
|
38415
38723
|
break;
|
|
38416
38724
|
case " ":
|
|
38417
38725
|
default:
|
|
38418
|
-
status =
|
|
38726
|
+
status = TODO_STATUS2.PENDING;
|
|
38419
38727
|
break;
|
|
38420
38728
|
}
|
|
38421
38729
|
todos.push({
|
|
38422
38730
|
id: generateId(content2, index),
|
|
38423
38731
|
content: content2,
|
|
38424
38732
|
status,
|
|
38425
|
-
priority:
|
|
38733
|
+
priority: STATUS_LABEL.MEDIUM,
|
|
38426
38734
|
// Default priority for file items
|
|
38427
38735
|
createdAt: /* @__PURE__ */ new Date()
|
|
38428
38736
|
});
|
|
@@ -38441,19 +38749,9 @@ var TodoSyncService = class {
|
|
|
38441
38749
|
taskTodos = /* @__PURE__ */ new Map();
|
|
38442
38750
|
updateTimeout = null;
|
|
38443
38751
|
watcher = null;
|
|
38444
|
-
// We only want to sync to the "primary" session or all sessions?
|
|
38445
|
-
// The design says `syncTaskStore(sessionID)`.
|
|
38446
|
-
// Usually TUI TODO is per session.
|
|
38447
|
-
// However, `todo.md` is global (project level).
|
|
38448
|
-
// So we should probably broadcast to active sessions or just the one associated with the tasks?
|
|
38449
|
-
// Current TUI limitation: we might need to know which session to update.
|
|
38450
|
-
// For TUI sidebar, we usually update the session the user is looking at.
|
|
38451
|
-
// But we don't know that.
|
|
38452
|
-
// We will maintain a set of "active sessions" provided by index.ts or just update relevant ones.
|
|
38453
|
-
// For Phase 1, we might just update the sessions we know about (parents of tasks) or register sessions.
|
|
38454
38752
|
activeSessions = /* @__PURE__ */ new Set();
|
|
38455
|
-
constructor(
|
|
38456
|
-
this.client =
|
|
38753
|
+
constructor(client2, directory) {
|
|
38754
|
+
this.client = client2;
|
|
38457
38755
|
this.directory = directory;
|
|
38458
38756
|
this.todoPath = path9.join(this.directory, PATHS.TODO);
|
|
38459
38757
|
}
|
|
@@ -38522,18 +38820,18 @@ var TodoSyncService = class {
|
|
|
38522
38820
|
}
|
|
38523
38821
|
async sendTodosToSession(sessionID) {
|
|
38524
38822
|
const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
|
|
38525
|
-
let status =
|
|
38823
|
+
let status = TODO_STATUS2.PENDING;
|
|
38526
38824
|
const s = t.status.toLowerCase();
|
|
38527
|
-
if (s.includes(
|
|
38528
|
-
else if (s.includes(
|
|
38529
|
-
else if (s.includes(
|
|
38530
|
-
else if (s.includes(
|
|
38825
|
+
if (s.includes(STATUS_LABEL.RUNNING) || s.includes("wait") || s.includes("que")) status = TODO_STATUS2.IN_PROGRESS;
|
|
38826
|
+
else if (s.includes(STATUS_LABEL.COMPLETED) || s.includes(STATUS_LABEL.DONE)) status = TODO_STATUS2.COMPLETED;
|
|
38827
|
+
else if (s.includes(STATUS_LABEL.FAILED) || s.includes(STATUS_LABEL.ERROR)) status = TODO_STATUS2.CANCELLED;
|
|
38828
|
+
else if (s.includes(STATUS_LABEL.CANCELLED)) status = TODO_STATUS2.CANCELLED;
|
|
38531
38829
|
return {
|
|
38532
|
-
id:
|
|
38830
|
+
id: `${TODO_CONSTANTS.PREFIX.TASK}${t.id}`,
|
|
38533
38831
|
// Prefix to avoid collision
|
|
38534
38832
|
content: `[${t.agent.toUpperCase()}] ${t.description}`,
|
|
38535
38833
|
status,
|
|
38536
|
-
priority: t.isBackground ?
|
|
38834
|
+
priority: t.isBackground ? STATUS_LABEL.LOW : STATUS_LABEL.HIGH,
|
|
38537
38835
|
createdAt: /* @__PURE__ */ new Date()
|
|
38538
38836
|
};
|
|
38539
38837
|
});
|
|
@@ -38541,11 +38839,17 @@ var TodoSyncService = class {
|
|
|
38541
38839
|
...this.fileTodos,
|
|
38542
38840
|
...taskTodosList
|
|
38543
38841
|
];
|
|
38842
|
+
const payloadTodos = merged.map((todo) => ({
|
|
38843
|
+
id: todo.id,
|
|
38844
|
+
content: todo.content,
|
|
38845
|
+
status: todo.status,
|
|
38846
|
+
priority: todo.priority
|
|
38847
|
+
}));
|
|
38544
38848
|
try {
|
|
38545
38849
|
await this.client.session.todo({
|
|
38546
38850
|
path: { id: sessionID },
|
|
38547
38851
|
// Standardize to id
|
|
38548
|
-
body: { todos:
|
|
38852
|
+
body: { todos: payloadTodos }
|
|
38549
38853
|
});
|
|
38550
38854
|
} catch (error92) {
|
|
38551
38855
|
}
|
|
@@ -38856,9 +39160,9 @@ function hasRunningBackgroundTasks2(parentSessionID) {
|
|
|
38856
39160
|
return false;
|
|
38857
39161
|
}
|
|
38858
39162
|
}
|
|
38859
|
-
async function showCompletedToast(
|
|
39163
|
+
async function showCompletedToast(client2, state2) {
|
|
38860
39164
|
try {
|
|
38861
|
-
const tuiClient2 =
|
|
39165
|
+
const tuiClient2 = client2;
|
|
38862
39166
|
if (tuiClient2.tui?.showToast) {
|
|
38863
39167
|
await tuiClient2.tui.showToast({
|
|
38864
39168
|
body: {
|
|
@@ -38872,14 +39176,14 @@ async function showCompletedToast(client, state2) {
|
|
|
38872
39176
|
} catch {
|
|
38873
39177
|
}
|
|
38874
39178
|
}
|
|
38875
|
-
async function injectContinuation2(
|
|
39179
|
+
async function injectContinuation2(client2, directory, sessionID, loopState, customPrompt) {
|
|
38876
39180
|
const handlerState = getState4(sessionID);
|
|
38877
39181
|
if (handlerState.isAborting) return;
|
|
38878
39182
|
if (hasRunningBackgroundTasks2(sessionID)) return;
|
|
38879
39183
|
if (isSessionRecovering(sessionID)) return;
|
|
38880
|
-
const verification =
|
|
39184
|
+
const verification = await verifyMissionCompletionAsync(directory);
|
|
38881
39185
|
if (verification.passed) {
|
|
38882
|
-
await handleMissionComplete(
|
|
39186
|
+
await handleMissionComplete(client2, directory, loopState);
|
|
38883
39187
|
return;
|
|
38884
39188
|
}
|
|
38885
39189
|
const summary = buildVerificationSummary(verification);
|
|
@@ -38890,21 +39194,20 @@ async function injectContinuation2(client, directory, sessionID, loopState, cust
|
|
|
38890
39194
|
${prompt}`;
|
|
38891
39195
|
}
|
|
38892
39196
|
try {
|
|
38893
|
-
|
|
39197
|
+
await client2.session.prompt({
|
|
38894
39198
|
path: { id: sessionID },
|
|
38895
39199
|
body: {
|
|
38896
39200
|
parts: [{ type: PART_TYPES.TEXT, text: prompt }]
|
|
38897
39201
|
}
|
|
38898
|
-
}).catch((error92) => {
|
|
38899
|
-
log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
|
|
38900
39202
|
});
|
|
38901
|
-
} catch {
|
|
39203
|
+
} catch (error92) {
|
|
39204
|
+
log("[mission-loop-handler] Failed to inject continuation prompt", { sessionID, error: error92 });
|
|
38902
39205
|
}
|
|
38903
39206
|
}
|
|
38904
|
-
async function handleMissionComplete(
|
|
39207
|
+
async function handleMissionComplete(client2, directory, loopState) {
|
|
38905
39208
|
const cleared = clearLoopState(directory);
|
|
38906
39209
|
if (cleared) {
|
|
38907
|
-
await showCompletedToast(
|
|
39210
|
+
await showCompletedToast(client2, loopState);
|
|
38908
39211
|
await sendMissionCompleteNotification(loopState);
|
|
38909
39212
|
}
|
|
38910
39213
|
}
|
|
@@ -38923,7 +39226,7 @@ async function sendMissionCompleteNotification(loopState) {
|
|
|
38923
39226
|
} catch {
|
|
38924
39227
|
}
|
|
38925
39228
|
}
|
|
38926
|
-
async function handleMissionIdle(
|
|
39229
|
+
async function handleMissionIdle(client2, directory, sessionID, mainSessionID) {
|
|
38927
39230
|
const handlerState = getState4(sessionID);
|
|
38928
39231
|
const now = Date.now();
|
|
38929
39232
|
if (handlerState.lastCheckTime && now - handlerState.lastCheckTime < LOOP.MIN_TIME_BETWEEN_CHECKS_MS) {
|
|
@@ -38943,10 +39246,10 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
|
|
|
38943
39246
|
if (loopState.sessionID !== sessionID) {
|
|
38944
39247
|
return;
|
|
38945
39248
|
}
|
|
38946
|
-
const verification =
|
|
39249
|
+
const verification = await verifyMissionCompletionAsync(directory);
|
|
38947
39250
|
if (verification.passed) {
|
|
38948
39251
|
log(`[${MISSION_CONTROL.LOG_SOURCE}-handler] Verification passed for ${sessionID}. Completion confirmed.`);
|
|
38949
|
-
await handleMissionComplete(
|
|
39252
|
+
await handleMissionComplete(client2, directory, loopState);
|
|
38950
39253
|
return;
|
|
38951
39254
|
}
|
|
38952
39255
|
const currentProgress = verification.todoProgress;
|
|
@@ -38969,7 +39272,18 @@ async function handleMissionIdle(client, directory, sessionID, mainSessionID) {
|
|
|
38969
39272
|
const countdownMsg = isStagnant ? "Stagnation Detected! Intervening..." : `Continuing in ${MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS}s... (iteration ${newState.iteration}/${newState.maxIterations})`;
|
|
38970
39273
|
handlerState.countdownTimer = setTimeout(async () => {
|
|
38971
39274
|
cancelCountdown2(sessionID);
|
|
38972
|
-
|
|
39275
|
+
if (hasPendingContinuation(sessionID)) {
|
|
39276
|
+
log("[mission-loop-handler] Todo continuation pending, deferring");
|
|
39277
|
+
return;
|
|
39278
|
+
}
|
|
39279
|
+
if (!tryAcquireContinuationLock(sessionID, "mission-loop")) {
|
|
39280
|
+
return;
|
|
39281
|
+
}
|
|
39282
|
+
try {
|
|
39283
|
+
await injectContinuation2(client2, directory, sessionID, newState, isStagnant ? STAGNATION_INTERVENTION : void 0);
|
|
39284
|
+
} finally {
|
|
39285
|
+
releaseContinuationLock(sessionID);
|
|
39286
|
+
}
|
|
38973
39287
|
}, MISSION_CONTROL.DEFAULT_COUNTDOWN_SECONDS * 1e3);
|
|
38974
39288
|
}
|
|
38975
39289
|
function handleUserMessage2(sessionID) {
|
|
@@ -38991,7 +39305,7 @@ function cleanupSession3(sessionID) {
|
|
|
38991
39305
|
// src/plugin-handlers/event-handler.ts
|
|
38992
39306
|
init_shared();
|
|
38993
39307
|
function createEventHandler(ctx) {
|
|
38994
|
-
const { client, directory, sessions, state: state2 } = ctx;
|
|
39308
|
+
const { client: client2, directory, sessions, state: state2 } = ctx;
|
|
38995
39309
|
return async (input) => {
|
|
38996
39310
|
const { event } = input;
|
|
38997
39311
|
try {
|
|
@@ -39028,7 +39342,7 @@ function createEventHandler(ctx) {
|
|
|
39028
39342
|
}
|
|
39029
39343
|
if (sessionID && error92) {
|
|
39030
39344
|
const recovered = await handleSessionError(
|
|
39031
|
-
|
|
39345
|
+
client2,
|
|
39032
39346
|
sessionID,
|
|
39033
39347
|
error92,
|
|
39034
39348
|
event.properties
|
|
@@ -39066,7 +39380,7 @@ function createEventHandler(ctx) {
|
|
|
39066
39380
|
if (session?.active) {
|
|
39067
39381
|
if (isLoopActive(directory, sessionID)) {
|
|
39068
39382
|
await handleMissionIdle(
|
|
39069
|
-
|
|
39383
|
+
client2,
|
|
39070
39384
|
directory,
|
|
39071
39385
|
sessionID,
|
|
39072
39386
|
sessionID
|
|
@@ -39074,7 +39388,7 @@ function createEventHandler(ctx) {
|
|
|
39074
39388
|
});
|
|
39075
39389
|
} else {
|
|
39076
39390
|
await handleSessionIdle(
|
|
39077
|
-
|
|
39391
|
+
client2,
|
|
39078
39392
|
directory,
|
|
39079
39393
|
sessionID,
|
|
39080
39394
|
sessionID
|
|
@@ -39132,7 +39446,7 @@ function createToolExecuteAfterHandler(ctx) {
|
|
|
39132
39446
|
// src/plugin-handlers/assistant-done-handler.ts
|
|
39133
39447
|
init_shared();
|
|
39134
39448
|
function createAssistantDoneHandler(ctx) {
|
|
39135
|
-
const { client, directory, sessions } = ctx;
|
|
39449
|
+
const { client: client2, directory, sessions } = ctx;
|
|
39136
39450
|
const hooks = HookRegistry.getInstance();
|
|
39137
39451
|
return async (assistantInput, assistantOutput) => {
|
|
39138
39452
|
const sessionID = assistantInput.sessionID;
|
|
@@ -39156,12 +39470,12 @@ function createAssistantDoneHandler(ctx) {
|
|
|
39156
39470
|
session.timestamp = now;
|
|
39157
39471
|
session.lastStepTime = now;
|
|
39158
39472
|
try {
|
|
39159
|
-
if (
|
|
39473
|
+
if (client2?.session?.prompt) {
|
|
39160
39474
|
const parts2 = result.prompts.map((p) => ({
|
|
39161
39475
|
type: PART_TYPES.TEXT,
|
|
39162
39476
|
text: p
|
|
39163
39477
|
}));
|
|
39164
|
-
|
|
39478
|
+
client2.session.prompt({
|
|
39165
39479
|
path: { id: sessionID },
|
|
39166
39480
|
body: { parts: parts2 }
|
|
39167
39481
|
}).catch((error92) => {
|
|
@@ -39321,24 +39635,24 @@ Use \`delegate_task\` with background=true for parallel work.
|
|
|
39321
39635
|
var require2 = createRequire(import.meta.url);
|
|
39322
39636
|
var { version: PLUGIN_VERSION } = require2("../package.json");
|
|
39323
39637
|
var OrchestratorPlugin = async (input) => {
|
|
39324
|
-
const { directory, client } = input;
|
|
39638
|
+
const { directory, client: client2 } = input;
|
|
39325
39639
|
initializeHooks();
|
|
39326
|
-
initToastClient(
|
|
39327
|
-
const taskToastManager = initTaskToastManager(
|
|
39640
|
+
initToastClient(client2);
|
|
39641
|
+
const taskToastManager = initTaskToastManager(client2);
|
|
39328
39642
|
const sessions = /* @__PURE__ */ new Map();
|
|
39329
|
-
const parallelAgentManager2 = ParallelAgentManager.getInstance(
|
|
39330
|
-
const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2,
|
|
39643
|
+
const parallelAgentManager2 = ParallelAgentManager.getInstance(client2, directory);
|
|
39644
|
+
const asyncAgentTools = createAsyncAgentTools(parallelAgentManager2, client2);
|
|
39331
39645
|
const pluginManager = PluginManager.getInstance();
|
|
39332
39646
|
await pluginManager.initialize(directory);
|
|
39333
39647
|
const dynamicTools = pluginManager.getDynamicTools();
|
|
39334
39648
|
taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
|
|
39335
|
-
const todoSync = new TodoSyncService(
|
|
39649
|
+
const todoSync = new TodoSyncService(client2, directory);
|
|
39336
39650
|
await todoSync.start();
|
|
39337
39651
|
taskToastManager.setTodoSync(todoSync);
|
|
39338
39652
|
const cleanupScheduler = new CleanupScheduler(directory);
|
|
39339
39653
|
cleanupScheduler.start();
|
|
39340
39654
|
const handlerContext = {
|
|
39341
|
-
client,
|
|
39655
|
+
client: client2,
|
|
39342
39656
|
directory,
|
|
39343
39657
|
sessions,
|
|
39344
39658
|
state
|