opencode-immune 1.0.7 → 1.0.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.
Files changed (2) hide show
  1. package/dist/plugin.js +58 -66
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -12,7 +12,7 @@ function createState(input) {
12
12
  managedUltraworkSessions: new Map(),
13
13
  sessionRetryTimers: new Map(),
14
14
  sessionErrorRetryCount: new Map(),
15
- managedSessionsCachePath: (0, path_1.join)(input.directory, ".opencode", "state", "opencode-immune-managed-sessions.json"),
15
+ ultraworkMarkerPath: (0, path_1.join)(input.directory, ".opencode", "state", "ultrawork-active.json"),
16
16
  diagnosticsLogPath: (0, path_1.join)(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
17
17
  lastEditAttempt: null,
18
18
  toolCallCount: 0,
@@ -52,21 +52,6 @@ function pruneExpiredManagedSessions(state, now = Date.now()) {
52
52
  }
53
53
  return removed;
54
54
  }
55
- async function writeManagedSessionsCache(state) {
56
- const removed = pruneExpiredManagedSessions(state);
57
- const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
58
- const tempPath = `${state.managedSessionsCachePath}.tmp`;
59
- const payload = {
60
- version: 1,
61
- sessions: Object.fromEntries(state.managedUltraworkSessions.entries()),
62
- };
63
- await (0, promises_1.mkdir)(cacheDir, { recursive: true });
64
- await (0, promises_1.writeFile)(tempPath, JSON.stringify(payload, null, 2), "utf-8");
65
- await (0, promises_1.rename)(tempPath, state.managedSessionsCachePath);
66
- if (removed > 0) {
67
- console.log(`[opencode-immune] Pruned ${removed} expired managed ultrawork session(s) while writing cache.`);
68
- }
69
- }
70
55
  async function writeDiagnosticLog(state, event, data = {}) {
71
56
  try {
72
57
  const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
@@ -78,46 +63,37 @@ async function writeDiagnosticLog(state, event, data = {}) {
78
63
  // diagnostics must never affect runtime behavior
79
64
  }
80
65
  }
81
- async function loadManagedSessionsCache(state) {
66
+ // ── Ultrawork Marker File ──
67
+ async function writeUltraworkMarker(state) {
68
+ try {
69
+ const dir = (0, path_1.join)(state.input.directory, ".opencode", "state");
70
+ await (0, promises_1.mkdir)(dir, { recursive: true });
71
+ const payload = JSON.stringify({
72
+ active: true,
73
+ updatedAt: new Date().toISOString(),
74
+ });
75
+ await (0, promises_1.writeFile)(state.ultraworkMarkerPath, payload, "utf-8");
76
+ }
77
+ catch {
78
+ // marker write must never affect runtime
79
+ }
80
+ }
81
+ async function clearUltraworkMarker(state) {
82
82
  try {
83
- const raw = await (0, promises_1.readFile)(state.managedSessionsCachePath, "utf-8");
83
+ await (0, promises_1.unlink)(state.ultraworkMarkerPath);
84
+ }
85
+ catch {
86
+ // file may not exist — that's fine
87
+ }
88
+ }
89
+ async function isUltraworkMarkerActive(state) {
90
+ try {
91
+ const raw = await (0, promises_1.readFile)(state.ultraworkMarkerPath, "utf-8");
84
92
  const parsed = JSON.parse(raw);
85
- if (parsed.version !== 1 || !parsed.sessions) {
86
- console.warn(`[opencode-immune] Managed sessions cache at ${state.managedSessionsCachePath} has unsupported format. Ignoring.`);
87
- return;
88
- }
89
- for (const [sessionID, record] of Object.entries(parsed.sessions)) {
90
- if (!record)
91
- continue;
92
- state.managedUltraworkSessions.set(sessionID, {
93
- kind: record.kind === "child" ? "child" : "root",
94
- agent: typeof record.agent === "string" && record.agent.length > 0
95
- ? record.agent
96
- : ULTRAWORK_AGENT,
97
- rootSessionID: typeof record.rootSessionID === "string" && record.rootSessionID.length > 0
98
- ? record.rootSessionID
99
- : sessionID,
100
- createdAt: typeof record.createdAt === "number" ? record.createdAt : Date.now(),
101
- updatedAt: typeof record.updatedAt === "number" ? record.updatedAt : Date.now(),
102
- fallbackModel: record.fallbackModel &&
103
- typeof record.fallbackModel.providerID === "string" &&
104
- typeof record.fallbackModel.modelID === "string"
105
- ? record.fallbackModel
106
- : undefined,
107
- });
108
- }
109
- const removed = pruneExpiredManagedSessions(state);
110
- console.log(`[opencode-immune] Loaded ${state.managedUltraworkSessions.size} managed ultrawork session(s) from cache.`);
111
- if (removed > 0) {
112
- console.log(`[opencode-immune] Pruned ${removed} expired managed ultrawork session(s) on startup.`);
113
- await writeManagedSessionsCache(state);
114
- }
93
+ return parsed?.active === true;
115
94
  }
116
- catch (err) {
117
- const message = err instanceof Error ? err.message : String(err);
118
- if (message.includes("ENOENT"))
119
- return;
120
- console.warn(`[opencode-immune] Failed to read managed sessions cache. Starting fresh.`);
95
+ catch {
96
+ return false;
121
97
  }
122
98
  }
123
99
  async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now()) {
@@ -137,7 +113,6 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
137
113
  return;
138
114
  }
139
115
  state.managedUltraworkSessions.set(sessionID, nextRecord);
140
- await writeManagedSessionsCache(state);
141
116
  }
142
117
  async function addManagedChildSession(state, sessionID, parentSessionID, timestamp = Date.now()) {
143
118
  const parent = state.managedUltraworkSessions.get(parentSessionID);
@@ -152,7 +127,6 @@ async function addManagedChildSession(state, sessionID, parentSessionID, timesta
152
127
  updatedAt: timestamp,
153
128
  fallbackModel: existing?.fallbackModel ?? parent.fallbackModel,
154
129
  });
155
- await writeManagedSessionsCache(state);
156
130
  }
157
131
  function cancelPendingSessionRetry(state, sessionID, reason) {
158
132
  const timer = state.sessionRetryTimers.get(sessionID);
@@ -168,7 +142,6 @@ async function removeManagedUltraworkSession(state, sessionID, reason) {
168
142
  const existed = state.managedUltraworkSessions.delete(sessionID);
169
143
  if (!existed)
170
144
  return;
171
- await writeManagedSessionsCache(state);
172
145
  console.log(`[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
173
146
  }
174
147
  async function updateManagedSessionAgent(state, sessionID, agent) {
@@ -180,7 +153,6 @@ async function updateManagedSessionAgent(state, sessionID, agent) {
180
153
  agent,
181
154
  updatedAt: Date.now(),
182
155
  });
183
- await writeManagedSessionsCache(state);
184
156
  }
185
157
  function markUltraworkSessionActive(state, sessionID) {
186
158
  const existing = state.managedUltraworkSessions.get(sessionID);
@@ -220,7 +192,6 @@ async function setSessionFallbackModel(state, sessionID, model) {
220
192
  updatedAt: Date.now(),
221
193
  fallbackModel: model,
222
194
  });
223
- await writeManagedSessionsCache(state);
224
195
  }
225
196
  function getManagedSessionRetryContext(state, sessionID) {
226
197
  const managedSession = state.managedUltraworkSessions.get(sessionID);
@@ -461,6 +432,7 @@ function createTodoEnforcerChatMessage(state) {
461
432
  const record = getManagedSession(state, sessionID);
462
433
  if (sessionID && agent === ULTRAWORK_AGENT) {
463
434
  await addManagedUltraworkSession(state, sessionID);
435
+ await writeUltraworkMarker(state);
464
436
  }
465
437
  else if (sessionID && agent && record?.kind === "root") {
466
438
  await removeManagedUltraworkSession(state, sessionID, `session taken over by agent \"${agent}\"`);
@@ -496,23 +468,36 @@ function createSessionRecoveryEvent(state) {
496
468
  const sessionInfo = event.properties?.info;
497
469
  const sessionID = sessionInfo?.id ?? event.properties?.sessionID;
498
470
  const parentID = sessionInfo?.parentID;
471
+ // Register child sessions under their parent
499
472
  if (sessionID && parentID && isManagedUltraworkSession(state, parentID)) {
500
473
  await addManagedChildSession(state, sessionID, parentID);
474
+ return;
501
475
  }
502
- if (!isManagedRootUltraworkSession(state, sessionID)) {
476
+ // Skip child sessions that don't belong to a managed parent
477
+ if (parentID) {
478
+ return;
479
+ }
480
+ // For root sessions (no parentID): check ultrawork marker + tasks.md.
481
+ // If marker is active and an incomplete task exists, auto-resume.
482
+ // This covers the restart case where the session ID is new but work is pending.
483
+ if (!sessionID) {
503
484
  return;
504
485
  }
505
- console.log(`[opencode-immune] Managed ultrawork session created, checking for active task...`);
486
+ const markerActive = await isUltraworkMarkerActive(state);
487
+ if (!markerActive) {
488
+ console.log(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
489
+ return;
490
+ }
491
+ console.log(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
506
492
  const recovery = await parseTasksFile(state.input.directory);
507
493
  if (recovery) {
508
494
  state.recoveryContext = recovery;
509
495
  console.log(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
510
- if (sessionID && recovery.phase !== "ARCHIVE: DONE") {
496
+ if (recovery.phase !== "ARCHIVE: DONE") {
497
+ // Register this root session as managed so retry/recovery works
498
+ await addManagedUltraworkSession(state, sessionID);
511
499
  setTimeout(async () => {
512
500
  try {
513
- if (!isManagedRootUltraworkSession(state, sessionID)) {
514
- return;
515
- }
516
501
  await state.input.client.session.promptAsync({
517
502
  body: {
518
503
  agent: ULTRAWORK_AGENT,
@@ -722,6 +707,7 @@ function createFallbackModels(state) {
722
707
  return async (input, _output) => {
723
708
  if (input.agent === ULTRAWORK_AGENT) {
724
709
  await addManagedUltraworkSession(state, input.sessionID);
710
+ await writeUltraworkMarker(state);
725
711
  }
726
712
  else if (getManagedSession(state, input.sessionID)?.kind === "root") {
727
713
  await removeManagedUltraworkSession(state, input.sessionID, `session switched to agent \"${input.agent}\"`);
@@ -803,7 +789,7 @@ function createEventHandler(state) {
803
789
  cancelPendingSessionRetry(state, sessionID, "session updated");
804
790
  state.sessionErrorRetryCount.delete(sessionID);
805
791
  if (markUltraworkSessionActive(state, sessionID)) {
806
- await writeManagedSessionsCache(state);
792
+ // session activity tracked in-memory only
807
793
  }
808
794
  }
809
795
  if (eventType === "session.deleted" && sessionID) {
@@ -831,6 +817,7 @@ const PRE_COMMIT_MARKER = "0-ULTRAWORK: PRE_COMMIT";
831
817
  const CYCLE_COMPLETE_MARKER = "0-ULTRAWORK: CYCLE_COMPLETE";
832
818
  const NEXT_TASK_PATTERN = /Next task:\s*(.+)/;
833
819
  const RATE_LIMIT_MESSAGE_PATTERN = /too many requests|rate_limit|rate limit/i;
820
+ const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
834
821
  /**
835
822
  * chat.message part: scans assistant messages for PRE_COMMIT and CYCLE_COMPLETE markers.
836
823
  *
@@ -872,6 +859,11 @@ function createMultiCycleHandler(state) {
872
859
  });
873
860
  }
874
861
  }
862
+ // ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
863
+ if (messageContent.includes(ALL_CYCLES_COMPLETE_MARKER)) {
864
+ await clearUltraworkMarker(state);
865
+ console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, ultrawork marker cleared.");
866
+ }
875
867
  // ── PRE_COMMIT: execute /commit ──
876
868
  if (messageContent.includes(PRE_COMMIT_MARKER) && !state.commitPending) {
877
869
  state.commitPending = true;
@@ -901,6 +893,7 @@ function createMultiCycleHandler(state) {
901
893
  state.cycleCount++;
902
894
  if (state.cycleCount >= MAX_CYCLES) {
903
895
  console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
896
+ await clearUltraworkMarker(state);
904
897
  return;
905
898
  }
906
899
  // Extract next task description
@@ -953,7 +946,6 @@ function createMultiCycleHandler(state) {
953
946
  // ═══════════════════════════════════════════════════════════════════════════════
954
947
  async function server(input) {
955
948
  const state = createState(input);
956
- await loadManagedSessionsCache(state);
957
949
  console.log(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
958
950
  // Compose tool.execute.after handlers:
959
951
  // Todo Enforcer (counter) + Ralph Loop (edit error) + Comment Checker
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"