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.
@@ -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: 10,
64
- // 10 per agent type
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
- \u{1F4CB} **Mission Loop Active** - Iteration ${state2.iteration}/${state2.maxIterations}
16953
+ \u26A0\uFE0F **MISSION NOT COMPLETE** - Iteration ${state2.iteration}/${state2.maxIterations}
16908
16954
 
16909
- Your previous iteration did not seal the mission. Continue working.
16955
+ The mission is INCOMPLETE. You MUST continue working NOW.
16910
16956
 
16911
- **RULES**:
16912
- 1. Review your progress from the previous iteration
16913
- 2. Continue from where you left off
16914
- 3. Check TODO list for incomplete items
16915
- 4. When ALL work is TRULY complete, output:
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
- **IMPORTANT**:
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
- log2("[index.ts] Auto-applied mission mode", { originalLength: originalText.length });
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}/${session.maxSteps} | This step: ${stepDuration} | Total: ${totalElapsed}`;
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}/${session.maxSteps}]`
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}/${session.maxSteps} | ${progressInfo} | This step: ${stepDuration} | Total: ${totalElapsed}`
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
- session.active = false;
17688
- state.missionActive = false;
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: 10;
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.7",
5
+ "version": "0.8.9",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {