opencode-immune 1.0.79 → 1.0.81

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 +133 -34
  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.79";
3780
+ var PLUGIN_VERSION = "1.0.81";
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,8 @@ 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;
3900
+ var ROOT_RETRY_CHILD_ACTIVITY_SUPPRESSION_MS = 2 * 6e4;
3898
3901
  var MODEL_NAME_CAPABILITY_SCORE = {
3899
3902
  "claude-opus-4-7": 100,
3900
3903
  "gpt-5.5": 100,
@@ -3914,6 +3917,26 @@ function isManagedRootUltraworkSession(state, sessionID) {
3914
3917
  const record = getManagedSession(state, sessionID);
3915
3918
  return !!record && record.kind === "root";
3916
3919
  }
3920
+ function hasRecentManagedChildSessionForRoot(state, rootSessionID, sinceMs) {
3921
+ const cutoff = Date.now() - sinceMs;
3922
+ for (const record of state.managedUltraworkSessions.values()) {
3923
+ if (record.kind === "child" && record.rootSessionID === rootSessionID && record.updatedAt >= cutoff) return true;
3924
+ }
3925
+ return false;
3926
+ }
3927
+ function hasPendingChildFallbackRequestForRoot(state, rootSessionID, now = Date.now()) {
3928
+ for (const request of state.childFallbackRequests.values()) {
3929
+ if (request.rootSessionID === rootSessionID && now - request.createdAt <= CHILD_FALLBACK_REQUEST_TTL_MS) return true;
3930
+ }
3931
+ return false;
3932
+ }
3933
+ function shouldSuppressRootRetryForChildActivity(state, rootSessionID) {
3934
+ return hasPendingChildFallbackRequestForRoot(state, rootSessionID) || hasRecentManagedChildSessionForRoot(
3935
+ state,
3936
+ rootSessionID,
3937
+ ROOT_RETRY_CHILD_ACTIVITY_SUPPRESSION_MS
3938
+ );
3939
+ }
3917
3940
  async function createManagedUltraworkSession(state, title) {
3918
3941
  const result = await state.client.session.create({
3919
3942
  directory: state.input.directory,
@@ -4092,8 +4115,15 @@ async function isUltraworkMarkerActive(state) {
4092
4115
  }
4093
4116
  async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now()) {
4094
4117
  const existing = state.managedUltraworkSessions.get(sessionID);
4118
+ if (existing?.kind === "child") {
4119
+ state.managedUltraworkSessions.set(sessionID, {
4120
+ ...existing,
4121
+ updatedAt: timestamp
4122
+ });
4123
+ return;
4124
+ }
4095
4125
  const nextRecord = {
4096
- kind: existing?.kind ?? "root",
4126
+ kind: "root",
4097
4127
  agent: ULTRAWORK_AGENT,
4098
4128
  rootSessionID: existing?.rootSessionID ?? sessionID,
4099
4129
  parentSessionID: existing?.parentSessionID,
@@ -4110,6 +4140,11 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
4110
4140
  async function addManagedChildSession(state, sessionID, parentSessionID, timestamp = Date.now()) {
4111
4141
  const parent = state.managedUltraworkSessions.get(parentSessionID);
4112
4142
  if (!parent) return;
4143
+ cancelFallbackAutoCycle(
4144
+ state,
4145
+ parent.rootSessionID,
4146
+ `child session ${sessionID} created`
4147
+ );
4113
4148
  cancelProviderRetryWatchdog(
4114
4149
  state,
4115
4150
  parent.rootSessionID,
@@ -4155,8 +4190,13 @@ function cancelProviderRetryWatchdog(state, sessionID, reason) {
4155
4190
  );
4156
4191
  }
4157
4192
  async function removeManagedUltraworkSession(state, sessionID, reason) {
4193
+ const existing = state.managedUltraworkSessions.get(sessionID);
4158
4194
  cancelPendingSessionRetry(state, sessionID, reason);
4159
4195
  cancelProviderRetryWatchdog(state, sessionID, reason);
4196
+ cancelFallbackAutoCycle(state, sessionID, reason);
4197
+ if (existing?.kind === "child") {
4198
+ cancelFallbackAutoCycle(state, existing.rootSessionID, `child session ${sessionID} ${reason}`);
4199
+ }
4160
4200
  state.sessionErrorRetryCount.delete(sessionID);
4161
4201
  const existed = state.managedUltraworkSessions.delete(sessionID);
4162
4202
  if (!existed) return;
@@ -5622,6 +5662,14 @@ function createEventHandler(state) {
5622
5662
  "aborted message recovery"
5623
5663
  );
5624
5664
  if (managedSession?.kind === "root") {
5665
+ if (shouldSuppressRootRetryForChildActivity(state, sessionID)) {
5666
+ await writeDiagnosticLog(state, "session-retry:root-suppressed-child-active", {
5667
+ sessionID,
5668
+ messageID,
5669
+ eventType
5670
+ });
5671
+ return;
5672
+ }
5625
5673
  state.abortedMessageRetries.add(retryKey);
5626
5674
  await writeDiagnosticLog(state, "session-retry:aborted-message-observed", {
5627
5675
  sessionID,
@@ -5715,6 +5763,17 @@ function createEventHandler(state) {
5715
5763
  );
5716
5764
  return;
5717
5765
  }
5766
+ if (isRoot && shouldSuppressRootRetryForChildActivity(state, sessionID)) {
5767
+ await writeDiagnosticLog(state, "session-retry:root-suppressed-child-active", {
5768
+ sessionID,
5769
+ eventType,
5770
+ errorType: getRetryableErrorType(error)
5771
+ });
5772
+ pluginLog.info(
5773
+ `[opencode-immune] Suppressed root retry for ${sessionID}: child session activity owns this failure.`
5774
+ );
5775
+ return;
5776
+ }
5718
5777
  const count = state.sessionErrorRetryCount.get(sessionID) ?? 0;
5719
5778
  if (count < MAX_RETRIES) {
5720
5779
  const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
@@ -5760,7 +5819,15 @@ function createEventHandler(state) {
5760
5819
  state.sessionErrorRetryCount.delete(sessionID);
5761
5820
  if (markUltraworkSessionActive(state, sessionID)) {
5762
5821
  }
5822
+ if (managedSession?.kind === "root") {
5823
+ cancelFallbackAutoCycle(state, sessionID, "root session updated");
5824
+ }
5763
5825
  if (managedSession?.kind === "child") {
5826
+ cancelFallbackAutoCycle(
5827
+ state,
5828
+ managedSession.rootSessionID,
5829
+ `child session ${sessionID} updated`
5830
+ );
5764
5831
  cancelProviderRetryWatchdog(
5765
5832
  state,
5766
5833
  managedSession.rootSessionID,
@@ -5802,6 +5869,64 @@ async function commitCycleChanges(state, reason) {
5802
5869
  state.commitPending = false;
5803
5870
  }
5804
5871
  }
5872
+ function cancelFallbackAutoCycle(state, sessionID, reason) {
5873
+ const timer = state.autoCycleFallbackTimers.get(sessionID);
5874
+ if (!timer) return;
5875
+ clearTimeout(timer);
5876
+ state.autoCycleFallbackTimers.delete(sessionID);
5877
+ pluginLog.info(
5878
+ `[opencode-immune] Cancelled fallback AUTO-CYCLE for session ${sessionID}: ${reason}`
5879
+ );
5880
+ }
5881
+ function scheduleFallbackAutoCycle(state, sessionID) {
5882
+ if (state.autoCycleFallbackTimers.has(sessionID)) return;
5883
+ const timer = setTimeout(async () => {
5884
+ state.autoCycleFallbackTimers.delete(sessionID);
5885
+ if (!isManagedRootUltraworkSession(state, sessionID)) return;
5886
+ if (state.autoCycleInFlight || state.autoCycleSourceSessions.has(sessionID)) return;
5887
+ if (hasRecentManagedChildSessionForRoot(state, sessionID, FALLBACK_AUTO_CYCLE_DELAY_MS)) {
5888
+ scheduleFallbackAutoCycle(state, sessionID);
5889
+ return;
5890
+ }
5891
+ const recovery = await parseTasksFile(state.input.directory);
5892
+ if (recovery) return;
5893
+ const hasPendingTasks = await hasPendingBacklogTasks(state.input.directory);
5894
+ if (!hasPendingTasks) return;
5895
+ const lockAcquired = await acquireAutoCycleLock(
5896
+ state,
5897
+ "multi-cycle-fallback",
5898
+ sessionID
5899
+ );
5900
+ if (!lockAcquired) return;
5901
+ state.autoCycleSourceSessions.add(sessionID);
5902
+ state.autoCycleInFlight = true;
5903
+ pluginLog.warn(
5904
+ `[opencode-immune] Multi-Cycle fallback: no CYCLE_COMPLETE marker detected for ${sessionID} after ${FALLBACK_AUTO_CYCLE_DELAY_MS}ms grace period. Starting AUTO-CYCLE.`
5905
+ );
5906
+ try {
5907
+ await commitCycleChanges(state, "fallback AUTO-CYCLE before new session");
5908
+ await refreshAutoCycleLock(state, sessionID);
5909
+ await startAutoCycleInNewSession(
5910
+ state,
5911
+ {
5912
+ sourceSessionID: sessionID,
5913
+ logContext: "Multi-Cycle fallback",
5914
+ clearLockOnFailureReason: "fallback bootstrap failed",
5915
+ retireSourceSession: true
5916
+ }
5917
+ );
5918
+ } catch (err) {
5919
+ state.autoCycleSourceSessions.delete(sessionID);
5920
+ pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
5921
+ } finally {
5922
+ state.autoCycleInFlight = false;
5923
+ }
5924
+ }, FALLBACK_AUTO_CYCLE_DELAY_MS);
5925
+ state.autoCycleFallbackTimers.set(sessionID, timer);
5926
+ pluginLog.info(
5927
+ `[opencode-immune] Multi-Cycle fallback: scheduled delayed AUTO-CYCLE check for ${sessionID}.`
5928
+ );
5929
+ }
5805
5930
  async function archiveProgress(directory) {
5806
5931
  const progressPath = join(directory, "memory-bank", "progress.md");
5807
5932
  try {
@@ -5903,23 +6028,26 @@ function createTextCompleteHandler(state) {
5903
6028
  );
5904
6029
  }
5905
6030
  if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
6031
+ cancelFallbackAutoCycle(state, sessionID, "ALL_CYCLES_COMPLETE detected");
5906
6032
  await clearUltraworkMarker(state);
5907
6033
  await clearAutoCycleLock(state, "all cycles complete");
5908
6034
  pluginLog.info("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
5909
6035
  return;
5910
6036
  }
5911
6037
  if (text.includes(PRE_COMMIT_MARKER) && !text.includes(CYCLE_COMPLETE_MARKER)) {
6038
+ cancelFallbackAutoCycle(state, sessionID, "PRE_COMMIT detected");
5912
6039
  await commitCycleChanges(state, "PRE_COMMIT detected (standalone)");
5913
6040
  return;
5914
6041
  }
5915
6042
  if (text.includes(CYCLE_COMPLETE_MARKER)) {
6043
+ cancelFallbackAutoCycle(state, sessionID, "CYCLE_COMPLETE detected");
5916
6044
  if (state.autoCycleInFlight || state.autoCycleSourceSessions.has(sessionID)) return;
5917
- const lockAcquired2 = await acquireAutoCycleLock(
6045
+ const lockAcquired = await acquireAutoCycleLock(
5918
6046
  state,
5919
6047
  "cycle-complete",
5920
6048
  sessionID
5921
6049
  );
5922
- if (!lockAcquired2) return;
6050
+ if (!lockAcquired) return;
5923
6051
  state.autoCycleSourceSessions.add(sessionID);
5924
6052
  state.autoCycleInFlight = true;
5925
6053
  try {
@@ -5966,36 +6094,7 @@ function createTextCompleteHandler(state) {
5966
6094
  if (recovery) return;
5967
6095
  const hasPendingTasks = await hasPendingBacklogTasks(state.input.directory);
5968
6096
  if (!hasPendingTasks) return;
5969
- if (state.autoCycleSourceSessions.has(sessionID)) return;
5970
- const lockAcquired = await acquireAutoCycleLock(
5971
- state,
5972
- "multi-cycle-fallback",
5973
- sessionID
5974
- );
5975
- if (!lockAcquired) return;
5976
- state.autoCycleSourceSessions.add(sessionID);
5977
- state.autoCycleInFlight = true;
5978
- pluginLog.warn(
5979
- `[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.`
5980
- );
5981
- try {
5982
- await commitCycleChanges(state, "fallback AUTO-CYCLE before new session");
5983
- await refreshAutoCycleLock(state, sessionID);
5984
- await startAutoCycleInNewSession(
5985
- state,
5986
- {
5987
- sourceSessionID: sessionID,
5988
- logContext: "Multi-Cycle fallback",
5989
- clearLockOnFailureReason: "fallback bootstrap failed",
5990
- retireSourceSession: true
5991
- }
5992
- );
5993
- } catch (err) {
5994
- state.autoCycleSourceSessions.delete(sessionID);
5995
- pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
5996
- } finally {
5997
- state.autoCycleInFlight = false;
5998
- }
6097
+ scheduleFallbackAutoCycle(state, sessionID);
5999
6098
  };
6000
6099
  }
6001
6100
  function createMultiCycleHandler(state) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {