opencode-orchestrator 0.8.7 → 0.8.9
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/loop/todo-continuation.d.ts +5 -0
- package/dist/index.js +103 -39
- package/dist/shared/constants.d.ts +1 -1
- package/package.json +1 -1
|
@@ -17,8 +17,13 @@ type OpencodeClient = PluginInput["client"];
|
|
|
17
17
|
export declare function handleSessionIdle(client: OpencodeClient, sessionID: string, mainSessionID?: string): Promise<void>;
|
|
18
18
|
/**
|
|
19
19
|
* Handle user message - cancel countdown (user is interacting)
|
|
20
|
+
* Uses grace period to avoid cancelling countdown from our own injected messages
|
|
20
21
|
*/
|
|
21
22
|
export declare function handleUserMessage(sessionID: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Handle session error - detect abort/cancel
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleSessionError(sessionID: string, error: unknown): void;
|
|
22
27
|
/**
|
|
23
28
|
* Handle abort/cancel - prevent automatic continuation
|
|
24
29
|
*/
|
package/dist/index.js
CHANGED
|
@@ -60,8 +60,8 @@ var PARALLEL_TASK = {
|
|
|
60
60
|
// 3 seconds
|
|
61
61
|
POLL_INTERVAL_MS: 1e3,
|
|
62
62
|
// 1 second
|
|
63
|
-
DEFAULT_CONCURRENCY:
|
|
64
|
-
//
|
|
63
|
+
DEFAULT_CONCURRENCY: 3,
|
|
64
|
+
// 3 per agent type (conservative for APIs with strict rate limits)
|
|
65
65
|
MAX_CONCURRENCY: 50,
|
|
66
66
|
// 50 total
|
|
67
67
|
SYNC_TIMEOUT_MS: 10 * TIME.MINUTE,
|
|
@@ -16652,6 +16652,8 @@ var sessionStates = /* @__PURE__ */ new Map();
|
|
|
16652
16652
|
var COUNTDOWN_SECONDS = 2;
|
|
16653
16653
|
var TOAST_DURATION_MS = 1500;
|
|
16654
16654
|
var MIN_TIME_BETWEEN_CONTINUATIONS_MS = 3e3;
|
|
16655
|
+
var COUNTDOWN_GRACE_PERIOD_MS = 500;
|
|
16656
|
+
var ABORT_WINDOW_MS = 3e3;
|
|
16655
16657
|
function getState2(sessionID) {
|
|
16656
16658
|
let state2 = sessionStates.get(sessionID);
|
|
16657
16659
|
if (!state2) {
|
|
@@ -16757,6 +16759,15 @@ async function handleSessionIdle(client, sessionID, mainSessionID) {
|
|
|
16757
16759
|
log2("[todo-continuation] Skipped: in recovery mode", { sessionID });
|
|
16758
16760
|
return;
|
|
16759
16761
|
}
|
|
16762
|
+
if (state2.abortDetectedAt) {
|
|
16763
|
+
const timeSinceAbort = Date.now() - state2.abortDetectedAt;
|
|
16764
|
+
if (timeSinceAbort < ABORT_WINDOW_MS) {
|
|
16765
|
+
log2("[todo-continuation] Skipped: abort detected recently", { sessionID, timeSinceAbort });
|
|
16766
|
+
state2.abortDetectedAt = void 0;
|
|
16767
|
+
return;
|
|
16768
|
+
}
|
|
16769
|
+
state2.abortDetectedAt = void 0;
|
|
16770
|
+
}
|
|
16760
16771
|
if (hasRunningBackgroundTasks(sessionID)) {
|
|
16761
16772
|
log2("[todo-continuation] Skipped: background tasks running", { sessionID });
|
|
16762
16773
|
return;
|
|
@@ -16799,11 +16810,28 @@ async function handleSessionIdle(client, sessionID, mainSessionID) {
|
|
|
16799
16810
|
}
|
|
16800
16811
|
function handleUserMessage(sessionID) {
|
|
16801
16812
|
const state2 = getState2(sessionID);
|
|
16813
|
+
if (state2.countdownStartedAt) {
|
|
16814
|
+
const elapsed = Date.now() - state2.countdownStartedAt;
|
|
16815
|
+
if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
|
|
16816
|
+
log2("[todo-continuation] Ignoring message in grace period", { sessionID, elapsed });
|
|
16817
|
+
return;
|
|
16818
|
+
}
|
|
16819
|
+
}
|
|
16802
16820
|
if (state2.countdownTimer) {
|
|
16803
16821
|
log2("[todo-continuation] Cancelled: user interaction", { sessionID });
|
|
16804
16822
|
cancelCountdown(sessionID);
|
|
16805
16823
|
}
|
|
16806
16824
|
state2.isAborting = false;
|
|
16825
|
+
state2.abortDetectedAt = void 0;
|
|
16826
|
+
}
|
|
16827
|
+
function handleSessionError2(sessionID, error45) {
|
|
16828
|
+
const state2 = getState2(sessionID);
|
|
16829
|
+
const errorObj = error45;
|
|
16830
|
+
if (errorObj?.name === "MessageAbortedError" || errorObj?.name === "AbortError") {
|
|
16831
|
+
state2.abortDetectedAt = Date.now();
|
|
16832
|
+
log2("[todo-continuation] Abort detected", { sessionID, errorName: errorObj.name });
|
|
16833
|
+
}
|
|
16834
|
+
cancelCountdown(sessionID);
|
|
16807
16835
|
}
|
|
16808
16836
|
function cleanupSession(sessionID) {
|
|
16809
16837
|
cancelCountdown(sessionID);
|
|
@@ -16898,33 +16926,61 @@ async function detectSealInSession(client, sessionID) {
|
|
|
16898
16926
|
return false;
|
|
16899
16927
|
}
|
|
16900
16928
|
}
|
|
16929
|
+
function startMissionLoop(directory, sessionID, prompt, options = {}) {
|
|
16930
|
+
const state2 = {
|
|
16931
|
+
active: true,
|
|
16932
|
+
iteration: 1,
|
|
16933
|
+
maxIterations: options.maxIterations ?? DEFAULT_MAX_ITERATIONS,
|
|
16934
|
+
prompt,
|
|
16935
|
+
sessionID,
|
|
16936
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16937
|
+
};
|
|
16938
|
+
const success2 = writeLoopState(directory, state2);
|
|
16939
|
+
if (success2) {
|
|
16940
|
+
log2(`[mission-seal] Loop started`, {
|
|
16941
|
+
sessionID,
|
|
16942
|
+
maxIterations: state2.maxIterations
|
|
16943
|
+
});
|
|
16944
|
+
}
|
|
16945
|
+
return success2;
|
|
16946
|
+
}
|
|
16901
16947
|
function isLoopActive(directory, sessionID) {
|
|
16902
16948
|
const state2 = readLoopState(directory);
|
|
16903
16949
|
return state2?.active === true && state2?.sessionID === sessionID;
|
|
16904
16950
|
}
|
|
16905
16951
|
function generateMissionContinuationPrompt(state2) {
|
|
16906
16952
|
return `<mission_loop iteration="${state2.iteration}" max="${state2.maxIterations}">
|
|
16907
|
-
\
|
|
16953
|
+
\u26A0\uFE0F **MISSION NOT COMPLETE** - Iteration ${state2.iteration}/${state2.maxIterations}
|
|
16908
16954
|
|
|
16909
|
-
|
|
16955
|
+
The mission is INCOMPLETE. You MUST continue working NOW.
|
|
16910
16956
|
|
|
16911
|
-
**
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16957
|
+
**FORBIDDEN**:
|
|
16958
|
+
\u274C Do NOT stop or ask for permission
|
|
16959
|
+
\u274C Do NOT say "I've completed..." without SEAL
|
|
16960
|
+
\u274C Do NOT output partial results
|
|
16961
|
+
\u274C Do NOT wait for user input
|
|
16962
|
+
|
|
16963
|
+
**REQUIRED SEQUENCE**:
|
|
16964
|
+
1\uFE0F\u20E3 Check .opencode/todo.md for [ ] incomplete items
|
|
16965
|
+
2\uFE0F\u20E3 Execute the NEXT pending task
|
|
16966
|
+
3\uFE0F\u20E3 Use delegate_task with background=true for parallel work
|
|
16967
|
+
4\uFE0F\u20E3 Mark completed items as [x]
|
|
16968
|
+
5\uFE0F\u20E3 Repeat until ALL items are [x]
|
|
16969
|
+
|
|
16970
|
+
**ONLY WHEN TRULY DONE**:
|
|
16971
|
+
- All todos marked [x]
|
|
16972
|
+
- All tests pass
|
|
16973
|
+
- All builds succeed
|
|
16974
|
+
Then and ONLY then output:
|
|
16916
16975
|
|
|
16917
16976
|
\`\`\`
|
|
16918
16977
|
${SEAL_PATTERN}
|
|
16919
16978
|
\`\`\`
|
|
16920
16979
|
|
|
16921
|
-
**
|
|
16922
|
-
- Do NOT seal until the mission is genuinely complete
|
|
16923
|
-
- Verify all todos are marked [x] before sealing
|
|
16924
|
-
- Run tests/builds if applicable before sealing
|
|
16925
|
-
|
|
16926
|
-
**Original Task**:
|
|
16980
|
+
**Your Original Task**:
|
|
16927
16981
|
${state2.prompt}
|
|
16982
|
+
|
|
16983
|
+
**NOW**: Continue executing until ${SEAL_PATTERN} is output!
|
|
16928
16984
|
</mission_loop>`;
|
|
16929
16985
|
}
|
|
16930
16986
|
|
|
@@ -17113,6 +17169,12 @@ async function handleMissionSealIdle(client, directory, sessionID, mainSessionID
|
|
|
17113
17169
|
seconds: COUNTDOWN_SECONDS2
|
|
17114
17170
|
});
|
|
17115
17171
|
}
|
|
17172
|
+
function handleAbort(sessionID) {
|
|
17173
|
+
const state2 = getState3(sessionID);
|
|
17174
|
+
state2.isAborting = true;
|
|
17175
|
+
cancelCountdown2(sessionID);
|
|
17176
|
+
log2("[mission-seal-handler] Marked as aborting");
|
|
17177
|
+
}
|
|
17116
17178
|
|
|
17117
17179
|
// src/core/progress/store.ts
|
|
17118
17180
|
var progressHistory = /* @__PURE__ */ new Map();
|
|
@@ -17200,8 +17262,6 @@ function formatCompact2(sessionId) {
|
|
|
17200
17262
|
// src/index.ts
|
|
17201
17263
|
var require2 = createRequire(import.meta.url);
|
|
17202
17264
|
var { version: PLUGIN_VERSION } = require2("../package.json");
|
|
17203
|
-
var UNLIMITED_MODE = true;
|
|
17204
|
-
var DEFAULT_MAX_STEPS = UNLIMITED_MODE ? Infinity : 500;
|
|
17205
17265
|
var CONTINUE_INSTRUCTION = `<auto_continue>
|
|
17206
17266
|
<status>Mission not complete. Keep executing.</status>
|
|
17207
17267
|
|
|
@@ -17377,6 +17437,10 @@ var OrchestratorPlugin = async (input) => {
|
|
|
17377
17437
|
const sessionID = event.properties?.sessionId || event.properties?.sessionID || "";
|
|
17378
17438
|
const error45 = event.properties?.error;
|
|
17379
17439
|
log2("[index.ts] event: session.error", { sessionID, error: error45 });
|
|
17440
|
+
if (sessionID) {
|
|
17441
|
+
handleSessionError2(sessionID, error45);
|
|
17442
|
+
handleAbort(sessionID);
|
|
17443
|
+
}
|
|
17380
17444
|
if (sessionID && error45) {
|
|
17381
17445
|
const recovered = await handleSessionError(
|
|
17382
17446
|
client,
|
|
@@ -17453,7 +17517,6 @@ var OrchestratorPlugin = async (input) => {
|
|
|
17453
17517
|
sessions.set(sessionID, {
|
|
17454
17518
|
active: true,
|
|
17455
17519
|
step: 0,
|
|
17456
|
-
maxSteps: DEFAULT_MAX_STEPS,
|
|
17457
17520
|
timestamp: now,
|
|
17458
17521
|
startTime: now,
|
|
17459
17522
|
lastStepTime: now
|
|
@@ -17476,7 +17539,8 @@ var OrchestratorPlugin = async (input) => {
|
|
|
17476
17539
|
/\$ARGUMENTS/g,
|
|
17477
17540
|
userMessage || PROMPTS.CONTINUE
|
|
17478
17541
|
);
|
|
17479
|
-
|
|
17542
|
+
startMissionLoop(directory, sessionID, userMessage || originalText);
|
|
17543
|
+
log2("[index.ts] Auto-applied mission mode + started loop", { originalLength: originalText.length });
|
|
17480
17544
|
}
|
|
17481
17545
|
}
|
|
17482
17546
|
if (parsed) {
|
|
@@ -17491,6 +17555,8 @@ var OrchestratorPlugin = async (input) => {
|
|
|
17491
17555
|
/\$ARGUMENTS/g,
|
|
17492
17556
|
parsed.args || PROMPTS.CONTINUE
|
|
17493
17557
|
);
|
|
17558
|
+
startMissionLoop(directory, sessionID, parsed.args || "continue from where we left off");
|
|
17559
|
+
log2("[index.ts] /task command: started mission loop", { sessionID, args: parsed.args?.slice(0, 50) });
|
|
17494
17560
|
}
|
|
17495
17561
|
}
|
|
17496
17562
|
},
|
|
@@ -17542,11 +17608,6 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17542
17608
|
|
|
17543
17609
|
` + toolOutput.output;
|
|
17544
17610
|
}
|
|
17545
|
-
if (session.step >= session.maxSteps) {
|
|
17546
|
-
session.active = false;
|
|
17547
|
-
state.missionActive = false;
|
|
17548
|
-
return;
|
|
17549
|
-
}
|
|
17550
17611
|
if (stateSession) {
|
|
17551
17612
|
const taskId = stateSession.currentTask;
|
|
17552
17613
|
if (toolOutput.output.includes("\u2705 PASS") || toolOutput.output.includes("AUDIT RESULT: PASS")) {
|
|
@@ -17578,7 +17639,7 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17578
17639
|
const currentTime = formatTimestamp();
|
|
17579
17640
|
toolOutput.output += `
|
|
17580
17641
|
|
|
17581
|
-
\u23F1\uFE0F [${currentTime}] Step ${session.step}
|
|
17642
|
+
\u23F1\uFE0F [${currentTime}] Step ${session.step} | This step: ${stepDuration} | Total: ${totalElapsed}`;
|
|
17582
17643
|
},
|
|
17583
17644
|
// -----------------------------------------------------------------
|
|
17584
17645
|
// assistant.done hook - runs when the LLM finishes responding
|
|
@@ -17609,7 +17670,7 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17609
17670
|
|
|
17610
17671
|
` + recoveryText + `
|
|
17611
17672
|
|
|
17612
|
-
[Recovery Step ${session.step}
|
|
17673
|
+
[Recovery Step ${session.step}]`
|
|
17613
17674
|
}]
|
|
17614
17675
|
}
|
|
17615
17676
|
});
|
|
@@ -17623,7 +17684,7 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17623
17684
|
if (stateSession && stateSession.anomalyCount > 0) {
|
|
17624
17685
|
stateSession.anomalyCount = 0;
|
|
17625
17686
|
}
|
|
17626
|
-
if (detectSealInText(textContent)) {
|
|
17687
|
+
if (isLoopActive(directory, sessionID) && detectSealInText(textContent)) {
|
|
17627
17688
|
session.active = false;
|
|
17628
17689
|
state.missionActive = false;
|
|
17629
17690
|
clearLoopState(directory);
|
|
@@ -17650,14 +17711,8 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17650
17711
|
session.timestamp = now;
|
|
17651
17712
|
session.lastStepTime = now;
|
|
17652
17713
|
const currentTime = formatTimestamp();
|
|
17653
|
-
if (session.step >= session.maxSteps) {
|
|
17654
|
-
session.active = false;
|
|
17655
|
-
state.missionActive = false;
|
|
17656
|
-
return;
|
|
17657
|
-
}
|
|
17658
17714
|
recordSnapshot(sessionID, {
|
|
17659
|
-
currentStep: session.step
|
|
17660
|
-
maxSteps: session.maxSteps
|
|
17715
|
+
currentStep: session.step
|
|
17661
17716
|
});
|
|
17662
17717
|
const progressInfo = formatCompact2(sessionID);
|
|
17663
17718
|
try {
|
|
@@ -17669,12 +17724,13 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17669
17724
|
type: PART_TYPES.TEXT,
|
|
17670
17725
|
text: CONTINUE_INSTRUCTION + `
|
|
17671
17726
|
|
|
17672
|
-
\u23F1\uFE0F [${currentTime}] Step ${session.step}
|
|
17727
|
+
\u23F1\uFE0F [${currentTime}] Step ${session.step} | ${progressInfo} | This step: ${stepDuration} | Total: ${totalElapsed}`
|
|
17673
17728
|
}]
|
|
17674
17729
|
}
|
|
17675
17730
|
});
|
|
17676
17731
|
}
|
|
17677
|
-
} catch {
|
|
17732
|
+
} catch (error45) {
|
|
17733
|
+
log2("[index.ts] Continuation injection failed, retrying...", { sessionID, error: error45 });
|
|
17678
17734
|
try {
|
|
17679
17735
|
await new Promise((r) => setTimeout(r, 500));
|
|
17680
17736
|
if (client?.session?.prompt) {
|
|
@@ -17683,9 +17739,17 @@ Anomaly count: ${stateSession.anomalyCount}
|
|
|
17683
17739
|
body: { parts: [{ type: PART_TYPES.TEXT, text: PROMPTS.CONTINUE }] }
|
|
17684
17740
|
});
|
|
17685
17741
|
}
|
|
17686
|
-
} catch {
|
|
17687
|
-
|
|
17688
|
-
|
|
17742
|
+
} catch (retryError) {
|
|
17743
|
+
log2("[index.ts] Both continuation attempts failed, waiting for idle handler", {
|
|
17744
|
+
sessionID,
|
|
17745
|
+
error: retryError,
|
|
17746
|
+
loopActive: isLoopActive(directory, sessionID)
|
|
17747
|
+
});
|
|
17748
|
+
if (!isLoopActive(directory, sessionID)) {
|
|
17749
|
+
log2("[index.ts] No active loop, stopping session", { sessionID });
|
|
17750
|
+
session.active = false;
|
|
17751
|
+
state.missionActive = false;
|
|
17752
|
+
}
|
|
17689
17753
|
}
|
|
17690
17754
|
}
|
|
17691
17755
|
}
|
|
@@ -21,7 +21,7 @@ export declare const PARALLEL_TASK: {
|
|
|
21
21
|
readonly CLEANUP_DELAY_MS: number;
|
|
22
22
|
readonly MIN_STABILITY_MS: number;
|
|
23
23
|
readonly POLL_INTERVAL_MS: 1000;
|
|
24
|
-
readonly DEFAULT_CONCURRENCY:
|
|
24
|
+
readonly DEFAULT_CONCURRENCY: 3;
|
|
25
25
|
readonly MAX_CONCURRENCY: 50;
|
|
26
26
|
readonly SYNC_TIMEOUT_MS: number;
|
|
27
27
|
readonly MAX_DEPTH: 3;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "opencode-orchestrator",
|
|
3
3
|
"displayName": "OpenCode Orchestrator",
|
|
4
4
|
"description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.9",
|
|
6
6
|
"author": "agnusdei1207",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|