opencode-immune 1.0.65 → 1.0.67

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.
@@ -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.65";
3780
+ var PLUGIN_VERSION = "1.0.67";
3781
3781
  var PLUGIN_PACKAGE_NAME = "opencode-immune";
3782
3782
  var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
3783
3783
  function getServerAuthHeaders() {
@@ -3873,6 +3873,14 @@ function createState(input) {
3873
3873
  sessionActive: false,
3874
3874
  autoResumeAttempted: false,
3875
3875
  autoResumeInFlight: false,
3876
+ autoCycleInFlight: false,
3877
+ autoCycleSourceSessions: /* @__PURE__ */ new Set(),
3878
+ autoCycleLockPath: join(
3879
+ input.directory,
3880
+ ".opencode",
3881
+ "state",
3882
+ "ultrawork-auto-cycle-lock.json"
3883
+ ),
3876
3884
  cycleCount: 0,
3877
3885
  commitPending: false,
3878
3886
  pluginUpdateMessage: null
@@ -3910,6 +3918,7 @@ var CHILD_SESSION_FALLBACK_MODEL = {
3910
3918
  providerID: "claudehub",
3911
3919
  modelID: "claude-opus-4-7"
3912
3920
  };
3921
+ var AUTO_CYCLE_LOCK_TTL_MS = 30 * 60 * 1e3;
3913
3922
  var FALLBACK_MODEL_CANDIDATES = [
3914
3923
  CHILD_SESSION_FALLBACK_MODEL,
3915
3924
  RATE_LIMIT_FALLBACK_MODEL,
@@ -3966,6 +3975,7 @@ async function applyUltraworkSessionPermissions(state, sessionID) {
3966
3975
  }
3967
3976
  }
3968
3977
  async function promptManagedSession(state, sessionID, text, options = {}) {
3978
+ await applyUltraworkSessionPermissions(state, sessionID);
3969
3979
  const result = await state.client.session.promptAsync({
3970
3980
  directory: state.input.directory,
3971
3981
  sessionID,
@@ -4726,6 +4736,74 @@ async function parseTasksFile(directory) {
4726
4736
  return null;
4727
4737
  }
4728
4738
  }
4739
+ async function hasPendingBacklogTasks(directory) {
4740
+ try {
4741
+ const backlogPath = join(directory, "memory-bank", "backlog.md");
4742
+ const backlogContent = await readFile(backlogPath, "utf-8");
4743
+ return /- \[ \]/.test(backlogContent);
4744
+ } catch {
4745
+ return false;
4746
+ }
4747
+ }
4748
+ async function acquireAutoCycleLock(state, source, sourceSessionID) {
4749
+ try {
4750
+ const now = Date.now();
4751
+ try {
4752
+ const raw = await readFile(state.autoCycleLockPath, "utf-8");
4753
+ const existing = JSON.parse(raw);
4754
+ const lockTime = Date.parse(existing.updatedAt ?? existing.createdAt ?? "");
4755
+ if (Number.isFinite(lockTime) && now - lockTime < AUTO_CYCLE_LOCK_TTL_MS) {
4756
+ pluginLog.info(
4757
+ `[opencode-immune] Auto-cycle lock active for ${existing.sessionID ?? "unknown session"}; skipping ${source}.`
4758
+ );
4759
+ return false;
4760
+ }
4761
+ } catch {
4762
+ }
4763
+ await mkdir(dirname(state.autoCycleLockPath), { recursive: true });
4764
+ const payload = JSON.stringify(
4765
+ {
4766
+ active: true,
4767
+ source,
4768
+ sourceSessionID,
4769
+ createdAt: new Date(now).toISOString(),
4770
+ updatedAt: new Date(now).toISOString()
4771
+ },
4772
+ null,
4773
+ 2
4774
+ );
4775
+ await writeFile(state.autoCycleLockPath, payload, { flag: "wx" });
4776
+ return true;
4777
+ } catch (err) {
4778
+ const code = err.code;
4779
+ if (code === "EEXIST") {
4780
+ pluginLog.info(`[opencode-immune] Auto-cycle lock already exists; skipping ${source}.`);
4781
+ return false;
4782
+ }
4783
+ pluginLog.warn(
4784
+ `[opencode-immune] Auto-cycle lock failed for ${source}; refusing to start duplicate-prone AUTO-CYCLE:`,
4785
+ err
4786
+ );
4787
+ return false;
4788
+ }
4789
+ }
4790
+ async function refreshAutoCycleLock(state, sessionID) {
4791
+ try {
4792
+ const raw = await readFile(state.autoCycleLockPath, "utf-8");
4793
+ const existing = JSON.parse(raw);
4794
+ existing.sessionID = sessionID;
4795
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4796
+ await writeFile(state.autoCycleLockPath, JSON.stringify(existing, null, 2));
4797
+ } catch {
4798
+ }
4799
+ }
4800
+ async function clearAutoCycleLock(state, reason) {
4801
+ try {
4802
+ await unlink(state.autoCycleLockPath);
4803
+ pluginLog.info(`[opencode-immune] Auto-cycle lock cleared: ${reason}`);
4804
+ } catch {
4805
+ }
4806
+ }
4729
4807
  var HARNESS_VERSION_FILE = ".harness-version";
4730
4808
  var HARNESS_TOKEN_ENV = "OPENCODE_IMMUNE_TOKEN";
4731
4809
  var DEFAULT_HARNESS_REPO = "gendoor/opencode-immune-harness";
@@ -5040,6 +5118,7 @@ function createSessionRecoveryEvent(state) {
5040
5118
  const recovery = await parseTasksFile(state.input.directory);
5041
5119
  if (recovery) {
5042
5120
  state.recoveryContext = recovery;
5121
+ await clearAutoCycleLock(state, `active task observed in ${sessionID}`);
5043
5122
  pluginLog.info(
5044
5123
  `[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`
5045
5124
  );
@@ -5512,8 +5591,9 @@ function createTextCompleteHandler(state) {
5512
5591
  const sessionID = input.sessionID;
5513
5592
  const text = output.text ?? "";
5514
5593
  if (!text) return;
5594
+ const isManagedRootSession = isManagedRootUltraworkSession(state, sessionID);
5515
5595
  cancelProviderRetryWatchdog(state, sessionID, "assistant text completed");
5516
- if (isProviderRetryBanner(text) && isManagedRootUltraworkSession(state, sessionID)) {
5596
+ if (isProviderRetryBanner(text) && isManagedRootSession) {
5517
5597
  const fallbackModel = getSessionFallbackModel(state, sessionID);
5518
5598
  await setSessionFallbackModel(state, sessionID, fallbackModel);
5519
5599
  scheduleManagedSessionRetry(state, sessionID, {
@@ -5528,6 +5608,7 @@ function createTextCompleteHandler(state) {
5528
5608
  }
5529
5609
  if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
5530
5610
  await clearUltraworkMarker(state);
5611
+ await clearAutoCycleLock(state, "all cycles complete");
5531
5612
  pluginLog.info("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
5532
5613
  return;
5533
5614
  }
@@ -5595,6 +5676,49 @@ function createTextCompleteHandler(state) {
5595
5676
  } catch (err) {
5596
5677
  pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
5597
5678
  }
5679
+ return;
5680
+ }
5681
+ if (!isManagedRootSession || state.autoCycleInFlight) return;
5682
+ const recovery = await parseTasksFile(state.input.directory);
5683
+ if (recovery) return;
5684
+ const hasPendingTasks = await hasPendingBacklogTasks(state.input.directory);
5685
+ if (!hasPendingTasks) return;
5686
+ if (state.autoCycleSourceSessions.has(sessionID)) return;
5687
+ const lockAcquired = await acquireAutoCycleLock(
5688
+ state,
5689
+ "multi-cycle-fallback",
5690
+ sessionID
5691
+ );
5692
+ if (!lockAcquired) return;
5693
+ state.autoCycleSourceSessions.add(sessionID);
5694
+ state.autoCycleInFlight = true;
5695
+ pluginLog.warn(
5696
+ `[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.`
5697
+ );
5698
+ try {
5699
+ const newSessionID = await createManagedUltraworkSession(
5700
+ state,
5701
+ `AUTO-CYCLE: next backlog task`
5702
+ );
5703
+ if (!newSessionID) {
5704
+ await clearAutoCycleLock(state, "fallback create returned no session ID");
5705
+ pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to create new session \u2014 no ID returned.");
5706
+ return;
5707
+ }
5708
+ await refreshAutoCycleLock(state, newSessionID);
5709
+ pluginLog.info(`[opencode-immune] Multi-Cycle fallback: New session created: ${newSessionID}`);
5710
+ await promptManagedSession(
5711
+ state,
5712
+ newSessionID,
5713
+ `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`
5714
+ );
5715
+ pluginLog.info(`[opencode-immune] Multi-Cycle fallback: Bootstrap prompt sent to ${newSessionID}`);
5716
+ } catch (err) {
5717
+ state.autoCycleSourceSessions.delete(sessionID);
5718
+ await clearAutoCycleLock(state, "fallback bootstrap failed");
5719
+ pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to create session or send prompt:", err);
5720
+ } finally {
5721
+ state.autoCycleInFlight = false;
5598
5722
  }
5599
5723
  };
5600
5724
  }
@@ -5708,10 +5832,13 @@ async function server(input) {
5708
5832
  }, 5e3);
5709
5833
  } else {
5710
5834
  try {
5711
- const backlogPath = join(state.input.directory, "memory-bank", "backlog.md");
5712
- const backlogContent = await readFile(backlogPath, "utf-8");
5713
- const hasPendingTasks = /- \[ \]/.test(backlogContent);
5835
+ const hasPendingTasks = await hasPendingBacklogTasks(state.input.directory);
5714
5836
  if (hasPendingTasks) {
5837
+ const lockAcquired = await acquireAutoCycleLock(
5838
+ state,
5839
+ "plugin-init-auto-cycle"
5840
+ );
5841
+ if (!lockAcquired) return;
5715
5842
  pluginLog.info(
5716
5843
  `[opencode-immune] Plugin init: no active task but backlog has pending items. Will create new session to start next cycle.`
5717
5844
  );
@@ -5723,11 +5850,13 @@ async function server(input) {
5723
5850
  `AUTO-CYCLE: next backlog task`
5724
5851
  );
5725
5852
  if (!newSessionID) {
5853
+ await clearAutoCycleLock(state, "init auto-cycle create returned no session ID");
5726
5854
  pluginLog.error(
5727
5855
  "[opencode-immune] Auto-cycle: Failed to create session \u2014 no session ID returned."
5728
5856
  );
5729
5857
  return;
5730
5858
  }
5859
+ await refreshAutoCycleLock(state, newSessionID);
5731
5860
  pluginLog.info(
5732
5861
  `[opencode-immune] Auto-cycle: New session created: ${newSessionID}`
5733
5862
  );
@@ -5741,6 +5870,7 @@ async function server(input) {
5741
5870
  `[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`
5742
5871
  );
5743
5872
  } catch (err) {
5873
+ await clearAutoCycleLock(state, "init auto-cycle bootstrap failed");
5744
5874
  pluginLog.error(
5745
5875
  "[opencode-immune] Auto-cycle: Failed to create session or send prompt:",
5746
5876
  err
@@ -5751,12 +5881,14 @@ async function server(input) {
5751
5881
  }, 5e3);
5752
5882
  } else {
5753
5883
  await clearUltraworkMarker(state);
5884
+ await clearAutoCycleLock(state, "no pending backlog tasks");
5754
5885
  pluginLog.info(
5755
5886
  `[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`
5756
5887
  );
5757
5888
  }
5758
5889
  } catch {
5759
5890
  await clearUltraworkMarker(state);
5891
+ await clearAutoCycleLock(state, "backlog unreadable");
5760
5892
  pluginLog.info(
5761
5893
  `[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`
5762
5894
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.65",
3
+ "version": "1.0.67",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {