opencode-immune 1.0.78 → 1.0.80

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.
Files changed (2) hide show
  1. package/dist/plugin/server.js +121 -48
  2. package/package.json +1 -1
@@ -3777,7 +3777,7 @@ import { fileURLToPath } from "url";
3777
3777
  import { createHash } from "crypto";
3778
3778
  import { tmpdir } from "os";
3779
3779
  import { execFile } from "child_process";
3780
- var PLUGIN_VERSION = "1.0.78";
3780
+ var PLUGIN_VERSION = "1.0.80";
3781
3781
  var PLUGIN_PACKAGE_NAME = "opencode-immune";
3782
3782
  var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
3783
3783
  function getServerAuthHeaders() {
@@ -3876,6 +3876,7 @@ function createState(input) {
3876
3876
  autoResumeInFlight: false,
3877
3877
  autoCycleInFlight: false,
3878
3878
  autoCycleSourceSessions: /* @__PURE__ */ new Set(),
3879
+ autoCycleFallbackTimers: /* @__PURE__ */ new Map(),
3879
3880
  autoCycleLockPath: join(
3880
3881
  input.directory,
3881
3882
  ".opencode",
@@ -3895,6 +3896,7 @@ var PROVIDER_RETRY_WATCHDOG_MS = 3e4;
3895
3896
  var RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
3896
3897
  var CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1e3;
3897
3898
  var AUTO_CYCLE_LOCK_TTL_MS = 30 * 60 * 1e3;
3899
+ var FALLBACK_AUTO_CYCLE_DELAY_MS = 6e4;
3898
3900
  var MODEL_NAME_CAPABILITY_SCORE = {
3899
3901
  "claude-opus-4-7": 100,
3900
3902
  "gpt-5.5": 100,
@@ -3914,6 +3916,13 @@ function isManagedRootUltraworkSession(state, sessionID) {
3914
3916
  const record = getManagedSession(state, sessionID);
3915
3917
  return !!record && record.kind === "root";
3916
3918
  }
3919
+ function hasRecentManagedChildSessionForRoot(state, rootSessionID, sinceMs) {
3920
+ const cutoff = Date.now() - sinceMs;
3921
+ for (const record of state.managedUltraworkSessions.values()) {
3922
+ if (record.kind === "child" && record.rootSessionID === rootSessionID && record.updatedAt >= cutoff) return true;
3923
+ }
3924
+ return false;
3925
+ }
3917
3926
  async function createManagedUltraworkSession(state, title) {
3918
3927
  const result = await state.client.session.create({
3919
3928
  directory: state.input.directory,
@@ -4110,6 +4119,11 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
4110
4119
  async function addManagedChildSession(state, sessionID, parentSessionID, timestamp = Date.now()) {
4111
4120
  const parent = state.managedUltraworkSessions.get(parentSessionID);
4112
4121
  if (!parent) return;
4122
+ cancelFallbackAutoCycle(
4123
+ state,
4124
+ parent.rootSessionID,
4125
+ `child session ${sessionID} created`
4126
+ );
4113
4127
  cancelProviderRetryWatchdog(
4114
4128
  state,
4115
4129
  parent.rootSessionID,
@@ -4155,8 +4169,13 @@ function cancelProviderRetryWatchdog(state, sessionID, reason) {
4155
4169
  );
4156
4170
  }
4157
4171
  async function removeManagedUltraworkSession(state, sessionID, reason) {
4172
+ const existing = state.managedUltraworkSessions.get(sessionID);
4158
4173
  cancelPendingSessionRetry(state, sessionID, reason);
4159
4174
  cancelProviderRetryWatchdog(state, sessionID, reason);
4175
+ cancelFallbackAutoCycle(state, sessionID, reason);
4176
+ if (existing?.kind === "child") {
4177
+ cancelFallbackAutoCycle(state, existing.rootSessionID, `child session ${sessionID} ${reason}`);
4178
+ }
4160
4179
  state.sessionErrorRetryCount.delete(sessionID);
4161
4180
  const existed = state.managedUltraworkSessions.delete(sessionID);
4162
4181
  if (!existed) return;
@@ -5760,7 +5779,15 @@ function createEventHandler(state) {
5760
5779
  state.sessionErrorRetryCount.delete(sessionID);
5761
5780
  if (markUltraworkSessionActive(state, sessionID)) {
5762
5781
  }
5782
+ if (managedSession?.kind === "root") {
5783
+ cancelFallbackAutoCycle(state, sessionID, "root session updated");
5784
+ }
5763
5785
  if (managedSession?.kind === "child") {
5786
+ cancelFallbackAutoCycle(
5787
+ state,
5788
+ managedSession.rootSessionID,
5789
+ `child session ${sessionID} updated`
5790
+ );
5764
5791
  cancelProviderRetryWatchdog(
5765
5792
  state,
5766
5793
  managedSession.rootSessionID,
@@ -5802,6 +5829,64 @@ async function commitCycleChanges(state, reason) {
5802
5829
  state.commitPending = false;
5803
5830
  }
5804
5831
  }
5832
+ function cancelFallbackAutoCycle(state, sessionID, reason) {
5833
+ const timer = state.autoCycleFallbackTimers.get(sessionID);
5834
+ if (!timer) return;
5835
+ clearTimeout(timer);
5836
+ state.autoCycleFallbackTimers.delete(sessionID);
5837
+ pluginLog.info(
5838
+ `[opencode-immune] Cancelled fallback AUTO-CYCLE for session ${sessionID}: ${reason}`
5839
+ );
5840
+ }
5841
+ function scheduleFallbackAutoCycle(state, sessionID) {
5842
+ if (state.autoCycleFallbackTimers.has(sessionID)) return;
5843
+ const timer = setTimeout(async () => {
5844
+ state.autoCycleFallbackTimers.delete(sessionID);
5845
+ if (!isManagedRootUltraworkSession(state, sessionID)) return;
5846
+ if (state.autoCycleInFlight || state.autoCycleSourceSessions.has(sessionID)) return;
5847
+ if (hasRecentManagedChildSessionForRoot(state, sessionID, FALLBACK_AUTO_CYCLE_DELAY_MS)) {
5848
+ scheduleFallbackAutoCycle(state, sessionID);
5849
+ return;
5850
+ }
5851
+ const recovery = await parseTasksFile(state.input.directory);
5852
+ if (recovery) return;
5853
+ const hasPendingTasks = await hasPendingBacklogTasks(state.input.directory);
5854
+ if (!hasPendingTasks) return;
5855
+ const lockAcquired = await acquireAutoCycleLock(
5856
+ state,
5857
+ "multi-cycle-fallback",
5858
+ sessionID
5859
+ );
5860
+ if (!lockAcquired) return;
5861
+ state.autoCycleSourceSessions.add(sessionID);
5862
+ state.autoCycleInFlight = true;
5863
+ pluginLog.warn(
5864
+ `[opencode-immune] Multi-Cycle fallback: no CYCLE_COMPLETE marker detected for ${sessionID} after ${FALLBACK_AUTO_CYCLE_DELAY_MS}ms grace period. Starting AUTO-CYCLE.`
5865
+ );
5866
+ try {
5867
+ await commitCycleChanges(state, "fallback AUTO-CYCLE before new session");
5868
+ await refreshAutoCycleLock(state, sessionID);
5869
+ await startAutoCycleInNewSession(
5870
+ state,
5871
+ {
5872
+ sourceSessionID: sessionID,
5873
+ logContext: "Multi-Cycle fallback",
5874
+ clearLockOnFailureReason: "fallback bootstrap failed",
5875
+ retireSourceSession: true
5876
+ }
5877
+ );
5878
+ } catch (err) {
5879
+ state.autoCycleSourceSessions.delete(sessionID);
5880
+ pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
5881
+ } finally {
5882
+ state.autoCycleInFlight = false;
5883
+ }
5884
+ }, FALLBACK_AUTO_CYCLE_DELAY_MS);
5885
+ state.autoCycleFallbackTimers.set(sessionID, timer);
5886
+ pluginLog.info(
5887
+ `[opencode-immune] Multi-Cycle fallback: scheduled delayed AUTO-CYCLE check for ${sessionID}.`
5888
+ );
5889
+ }
5805
5890
  async function archiveProgress(directory) {
5806
5891
  const progressPath = join(directory, "memory-bank", "progress.md");
5807
5892
  try {
@@ -5903,47 +5988,64 @@ function createTextCompleteHandler(state) {
5903
5988
  );
5904
5989
  }
5905
5990
  if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
5991
+ cancelFallbackAutoCycle(state, sessionID, "ALL_CYCLES_COMPLETE detected");
5906
5992
  await clearUltraworkMarker(state);
5907
5993
  await clearAutoCycleLock(state, "all cycles complete");
5908
5994
  pluginLog.info("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
5909
5995
  return;
5910
5996
  }
5911
5997
  if (text.includes(PRE_COMMIT_MARKER) && !text.includes(CYCLE_COMPLETE_MARKER)) {
5998
+ cancelFallbackAutoCycle(state, sessionID, "PRE_COMMIT detected");
5912
5999
  await commitCycleChanges(state, "PRE_COMMIT detected (standalone)");
5913
6000
  return;
5914
6001
  }
5915
6002
  if (text.includes(CYCLE_COMPLETE_MARKER)) {
6003
+ cancelFallbackAutoCycle(state, sessionID, "CYCLE_COMPLETE detected");
6004
+ if (state.autoCycleInFlight || state.autoCycleSourceSessions.has(sessionID)) return;
6005
+ const lockAcquired = await acquireAutoCycleLock(
6006
+ state,
6007
+ "cycle-complete",
6008
+ sessionID
6009
+ );
6010
+ if (!lockAcquired) return;
6011
+ state.autoCycleSourceSessions.add(sessionID);
6012
+ state.autoCycleInFlight = true;
5916
6013
  try {
5917
- await archiveProgress(state.input.directory);
5918
- } catch (err) {
5919
- pluginLog.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
5920
- }
5921
- await commitCycleChanges(state, "CYCLE_COMPLETE detected");
5922
- state.cycleCount++;
5923
- if (state.cycleCount >= MAX_CYCLES) {
6014
+ try {
6015
+ await archiveProgress(state.input.directory);
6016
+ } catch (err) {
6017
+ pluginLog.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
6018
+ }
6019
+ await commitCycleChanges(state, "CYCLE_COMPLETE detected");
6020
+ state.cycleCount++;
6021
+ if (state.cycleCount >= MAX_CYCLES) {
6022
+ pluginLog.info(
6023
+ `[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`
6024
+ );
6025
+ await clearUltraworkMarker(state);
6026
+ await clearAutoCycleLock(state, "max cycles reached");
6027
+ return;
6028
+ }
6029
+ const taskMatch = text.match(NEXT_TASK_PATTERN);
6030
+ const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
5924
6031
  pluginLog.info(
5925
- `[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`
6032
+ `[opencode-immune] Multi-Cycle: Starting next cycle in a new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
5926
6033
  );
5927
- await clearUltraworkMarker(state);
5928
- return;
5929
- }
5930
- const taskMatch = text.match(NEXT_TASK_PATTERN);
5931
- const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
5932
- pluginLog.info(
5933
- `[opencode-immune] Multi-Cycle: Starting next cycle in a new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
5934
- );
5935
- try {
5936
6034
  await startAutoCycleInNewSession(
5937
6035
  state,
5938
6036
  {
5939
6037
  sourceSessionID: sessionID,
5940
6038
  nextTask,
5941
6039
  logContext: "Multi-Cycle",
6040
+ clearLockOnFailureReason: "cycle complete bootstrap failed",
5942
6041
  retireSourceSession: true
5943
6042
  }
5944
6043
  );
5945
6044
  } catch (err) {
6045
+ state.autoCycleSourceSessions.delete(sessionID);
5946
6046
  pluginLog.error("[opencode-immune] Multi-Cycle: Failed to send prompt:", err);
6047
+ } finally {
6048
+ state.autoCycleInFlight = false;
5947
6049
  }
5948
6050
  return;
5949
6051
  }
@@ -5952,36 +6054,7 @@ function createTextCompleteHandler(state) {
5952
6054
  if (recovery) return;
5953
6055
  const hasPendingTasks = await hasPendingBacklogTasks(state.input.directory);
5954
6056
  if (!hasPendingTasks) return;
5955
- if (state.autoCycleSourceSessions.has(sessionID)) return;
5956
- const lockAcquired = await acquireAutoCycleLock(
5957
- state,
5958
- "multi-cycle-fallback",
5959
- sessionID
5960
- );
5961
- if (!lockAcquired) return;
5962
- state.autoCycleSourceSessions.add(sessionID);
5963
- state.autoCycleInFlight = true;
5964
- pluginLog.warn(
5965
- `[opencode-immune] Multi-Cycle fallback: no CYCLE_COMPLETE marker detected for ${sessionID}, but tasks.md has no active task and backlog has pending items. Starting AUTO-CYCLE.`
5966
- );
5967
- try {
5968
- await commitCycleChanges(state, "fallback AUTO-CYCLE before new session");
5969
- await refreshAutoCycleLock(state, sessionID);
5970
- await startAutoCycleInNewSession(
5971
- state,
5972
- {
5973
- sourceSessionID: sessionID,
5974
- logContext: "Multi-Cycle fallback",
5975
- clearLockOnFailureReason: "fallback bootstrap failed",
5976
- retireSourceSession: true
5977
- }
5978
- );
5979
- } catch (err) {
5980
- state.autoCycleSourceSessions.delete(sessionID);
5981
- pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
5982
- } finally {
5983
- state.autoCycleInFlight = false;
5984
- }
6057
+ scheduleFallbackAutoCycle(state, sessionID);
5985
6058
  };
5986
6059
  }
5987
6060
  function createMultiCycleHandler(state) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {