opencode-immune 1.0.44 → 1.0.46

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 +172 -91
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -59,10 +59,10 @@ async function checkPluginUpdate(state) {
59
59
  state.pluginUpdateMessage =
60
60
  `[PLUGIN UPDATE] opencode-immune ${currentVersion} → ${latest} is available. ` +
61
61
  `Please inform the user: a plugin update is available. They should restart opencode to get the latest version.`;
62
- console.warn(`[opencode-immune] Plugin update available: ${currentVersion} → ${latest}.`);
62
+ writePluginLog(state, "warn", `[opencode-immune] Plugin update available: ${currentVersion} → ${latest}.`);
63
63
  }
64
64
  else if (latest) {
65
- console.log(`[opencode-immune] Plugin version ${currentVersion} is up to date.`);
65
+ writePluginLog(state, "info", `[opencode-immune] Plugin version ${currentVersion} is up to date.`);
66
66
  }
67
67
  }
68
68
  catch {
@@ -70,6 +70,7 @@ async function checkPluginUpdate(state) {
70
70
  }
71
71
  }
72
72
  function createState(input) {
73
+ activeLogDirectory = input.directory;
73
74
  return {
74
75
  input,
75
76
  recoveryContext: null,
@@ -92,6 +93,8 @@ function createState(input) {
92
93
  };
93
94
  }
94
95
  const ULTRAWORK_AGENT = "0-ultrawork";
96
+ const DIAGNOSTIC_LOG_MAX_BYTES = 5 * 1024 * 1024;
97
+ let activeLogDirectory = null;
95
98
  const MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
96
99
  const PROVIDER_RETRY_WATCHDOG_MS = 30_000;
97
100
  const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
@@ -130,6 +133,7 @@ async function writeDiagnosticLog(state, event, data = {}) {
130
133
  try {
131
134
  const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
132
135
  await (0, promises_1.mkdir)(cacheDir, { recursive: true });
136
+ await rotateDiagnosticLogIfNeeded(state.diagnosticsLogPath);
133
137
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data });
134
138
  await (0, promises_1.appendFile)(state.diagnosticsLogPath, `${line}\n`, "utf-8");
135
139
  }
@@ -137,6 +141,68 @@ async function writeDiagnosticLog(state, event, data = {}) {
137
141
  // diagnostics must never affect runtime behavior
138
142
  }
139
143
  }
144
+ async function rotateDiagnosticLogIfNeeded(logPath) {
145
+ try {
146
+ const current = await (0, promises_1.stat)(logPath);
147
+ if (current.size < DIAGNOSTIC_LOG_MAX_BYTES)
148
+ return;
149
+ const rotatedPath = `${logPath}.1`;
150
+ await (0, promises_1.rm)(rotatedPath, { force: true });
151
+ await (0, promises_1.rename)(logPath, rotatedPath);
152
+ }
153
+ catch {
154
+ // missing log or rotation failure must never affect runtime behavior
155
+ }
156
+ }
157
+ function normalizeLogValue(value) {
158
+ if (value instanceof Error) {
159
+ return {
160
+ name: value.name,
161
+ message: value.message,
162
+ stack: value.stack,
163
+ };
164
+ }
165
+ return value;
166
+ }
167
+ function writePluginLog(state, level, message, extra = {}) {
168
+ void writeDiagnosticLog(state, `log:${level}`, {
169
+ message,
170
+ ...extra,
171
+ });
172
+ }
173
+ function writePluginLogForDirectory(directory, level, message, extra = {}) {
174
+ const diagnosticsLogPath = (0, path_1.join)(directory, ".opencode", "state", "opencode-immune-debug.log");
175
+ void (async () => {
176
+ try {
177
+ await (0, promises_1.mkdir)((0, path_1.dirname)(diagnosticsLogPath), { recursive: true });
178
+ await rotateDiagnosticLogIfNeeded(diagnosticsLogPath);
179
+ const line = JSON.stringify({
180
+ ts: new Date().toISOString(),
181
+ event: `log:${level}`,
182
+ message,
183
+ ...extra,
184
+ });
185
+ await (0, promises_1.appendFile)(diagnosticsLogPath, `${line}\n`, "utf-8");
186
+ }
187
+ catch {
188
+ // file logging must never affect runtime behavior
189
+ }
190
+ })();
191
+ }
192
+ function writePluginLogFromArgs(level, values) {
193
+ if (!activeLogDirectory)
194
+ return;
195
+ const [first, ...rest] = values;
196
+ const message = typeof first === "string" ? first : JSON.stringify(normalizeLogValue(first));
197
+ writePluginLogForDirectory(activeLogDirectory, level, message, {
198
+ args: rest.map(normalizeLogValue),
199
+ });
200
+ }
201
+ const pluginLog = {
202
+ info: (...values) => writePluginLogFromArgs("info", values),
203
+ warn: (...values) => writePluginLogFromArgs("warn", values),
204
+ error: (...values) => writePluginLogFromArgs("error", values),
205
+ };
140
206
  // ── Ultrawork Marker File ──
141
207
  async function writeUltraworkMarker(state) {
142
208
  try {
@@ -192,6 +258,12 @@ async function addManagedChildSession(state, sessionID, parentSessionID, timesta
192
258
  const parent = state.managedUltraworkSessions.get(parentSessionID);
193
259
  if (!parent)
194
260
  return;
261
+ cancelProviderRetryWatchdog(state, parent.rootSessionID, `child session ${sessionID} created`);
262
+ await writeDiagnosticLog(state, "provider-retry-watchdog:cancel-child-created", {
263
+ sessionID,
264
+ parentSessionID,
265
+ rootSessionID: parent.rootSessionID,
266
+ });
195
267
  const existing = state.managedUltraworkSessions.get(sessionID);
196
268
  state.managedUltraworkSessions.set(sessionID, {
197
269
  kind: "child",
@@ -208,7 +280,7 @@ function cancelPendingSessionRetry(state, sessionID, reason) {
208
280
  return;
209
281
  clearTimeout(timer);
210
282
  state.sessionRetryTimers.delete(sessionID);
211
- console.log(`[opencode-immune] Cancelled pending retry for session ${sessionID}: ${reason}`);
283
+ writePluginLog(state, "info", `[opencode-immune] Cancelled pending retry for session ${sessionID}: ${reason}`);
212
284
  }
213
285
  function cancelProviderRetryWatchdog(state, sessionID, reason) {
214
286
  const timer = state.providerRetryWatchdogs.get(sessionID);
@@ -216,7 +288,7 @@ function cancelProviderRetryWatchdog(state, sessionID, reason) {
216
288
  return;
217
289
  clearTimeout(timer);
218
290
  state.providerRetryWatchdogs.delete(sessionID);
219
- console.log(`[opencode-immune] Cancelled provider retry watchdog for session ${sessionID}: ${reason}`);
291
+ writePluginLog(state, "info", `[opencode-immune] Cancelled provider retry watchdog for session ${sessionID}: ${reason}`);
220
292
  }
221
293
  async function removeManagedUltraworkSession(state, sessionID, reason) {
222
294
  cancelPendingSessionRetry(state, sessionID, reason);
@@ -225,7 +297,7 @@ async function removeManagedUltraworkSession(state, sessionID, reason) {
225
297
  const existed = state.managedUltraworkSessions.delete(sessionID);
226
298
  if (!existed)
227
299
  return;
228
- console.log(`[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
300
+ writePluginLog(state, "info", `[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
229
301
  }
230
302
  async function updateManagedSessionAgent(state, sessionID, agent) {
231
303
  const existing = state.managedUltraworkSessions.get(sessionID);
@@ -487,7 +559,7 @@ async function sendManagedSessionRetryPrompt(state, sessionID, reason, options =
487
559
  },
488
560
  path: { id: sessionID },
489
561
  });
490
- console.log(`[opencode-immune] Retry prompt sent to session ${sessionID} (${reason})` +
562
+ writePluginLog(state, "info", `[opencode-immune] Retry prompt sent to session ${sessionID} (${reason})` +
491
563
  (fallbackModel
492
564
  ? ` using fallback model ${fallbackModel.providerID}/${fallbackModel.modelID}`
493
565
  : ""));
@@ -504,11 +576,11 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
504
576
  return false;
505
577
  }
506
578
  if (state.sessionRetryTimers.has(sessionID)) {
507
- console.log(`[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
579
+ writePluginLog(state, "info", `[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
508
580
  return false;
509
581
  }
510
582
  const attemptInfo = options.attemptLabel ? ` (${options.attemptLabel})` : "";
511
- console.log(`[opencode-immune] Scheduling retry for session ${sessionID}${attemptInfo}. ` +
583
+ writePluginLog(state, "info", `[opencode-immune] Scheduling retry for session ${sessionID}${attemptInfo}. ` +
512
584
  `Waiting ${options.delayMs / 1000}s before retry...`);
513
585
  const timer = setTimeout(async () => {
514
586
  state.sessionRetryTimers.delete(sessionID);
@@ -524,7 +596,7 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
524
596
  if (options.countAgainstBudget) {
525
597
  state.sessionErrorRetryCount.set(sessionID, Math.max((state.sessionErrorRetryCount.get(sessionID) ?? 1) - 1, 0));
526
598
  }
527
- console.log(`[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
599
+ writePluginLog(state, "warn", `[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
528
600
  `Will wait for the next retry signal.`);
529
601
  }
530
602
  }, options.delayMs);
@@ -538,13 +610,17 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
538
610
  * Wraps a hook handler in a try/catch to prevent any single hook failure
539
611
  * from crashing the entire agent session.
540
612
  */
541
- function withErrorBoundary(hookName, handler) {
613
+ function withErrorBoundary(state, hookName, handler) {
542
614
  return (async (...args) => {
543
615
  try {
544
616
  return await handler(...args);
545
617
  }
546
618
  catch (err) {
547
- console.error(`[opencode-immune] Hook "${hookName}" error:`, err);
619
+ const hookInput = args.find((arg) => !!arg && typeof arg === "object" && "sessionID" in arg);
620
+ writePluginLog(state, "error", `[opencode-immune] Hook "${hookName}" error.`, {
621
+ error: normalizeLogValue(err),
622
+ sessionID: hookInput?.sessionID,
623
+ });
548
624
  // Error is swallowed — hook failure must not crash agent session
549
625
  }
550
626
  });
@@ -708,7 +784,7 @@ async function resolveEnvValue(directory, key) {
708
784
  * Fetch latest release info from the harness GitHub repo.
709
785
  * Returns null if token is missing, network fails, or no release found.
710
786
  */
711
- async function fetchLatestHarnessRelease(repo, token) {
787
+ async function fetchLatestHarnessRelease(directory, repo, token) {
712
788
  try {
713
789
  const url = `https://api.github.com/repos/${repo}/releases/latest`;
714
790
  const resp = await fetch(url, {
@@ -720,10 +796,10 @@ async function fetchLatestHarnessRelease(repo, token) {
720
796
  });
721
797
  if (!resp.ok) {
722
798
  if (resp.status === 404) {
723
- console.log(`[opencode-immune] Harness sync: no releases found in ${repo}`);
799
+ writePluginLogForDirectory(directory, "info", `[opencode-immune] Harness sync: no releases found in ${repo}`);
724
800
  }
725
801
  else {
726
- console.warn(`[opencode-immune] Harness sync: GitHub API returned ${resp.status} ${resp.statusText}`);
802
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: GitHub API returned ${resp.status} ${resp.statusText}`);
727
803
  }
728
804
  return null;
729
805
  }
@@ -736,7 +812,7 @@ async function fetchLatestHarnessRelease(repo, token) {
736
812
  const assetName = isWindows ? "harness-windows.tar.gz" : "harness.tar.gz";
737
813
  const asset = data.assets?.find((a) => a.name === assetName);
738
814
  if (!asset) {
739
- console.warn(`[opencode-immune] Harness sync: release ${tagName} has no ${assetName} asset`);
815
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: release ${tagName} has no ${assetName} asset`);
740
816
  return null;
741
817
  }
742
818
  return {
@@ -746,7 +822,7 @@ async function fetchLatestHarnessRelease(repo, token) {
746
822
  };
747
823
  }
748
824
  catch (err) {
749
- console.warn(`[opencode-immune] Harness sync: failed to fetch release info:`, err instanceof Error ? err.message : String(err));
825
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: failed to fetch release info.`, { error: normalizeLogValue(err) });
750
826
  return null;
751
827
  }
752
828
  }
@@ -811,7 +887,7 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
811
887
  for (const entry of entries) {
812
888
  // Skip files only at the root destination level
813
889
  if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
814
- console.log(`[opencode-immune] Harness sync: skipping root .gitignore`);
890
+ pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
815
891
  continue;
816
892
  }
817
893
  const srcPath = (0, path_1.join)(src, entry.name);
@@ -862,16 +938,16 @@ async function syncHarness(state) {
862
938
  || DEFAULT_HARNESS_REPO;
863
939
  try {
864
940
  // 1. Fetch latest release
865
- const release = await fetchLatestHarnessRelease(repo, token);
941
+ const release = await fetchLatestHarnessRelease(state.input.directory, repo, token);
866
942
  if (!release)
867
943
  return;
868
944
  // 2. Compare versions
869
945
  const localVersion = await readLocalHarnessVersion(state.input.directory);
870
946
  if (localVersion === release.tagName) {
871
- console.log(`[opencode-immune] Harness sync: already up to date (${release.tagName})`);
947
+ pluginLog.info(`[opencode-immune] Harness sync: already up to date (${release.tagName})`);
872
948
  return;
873
949
  }
874
- console.log(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
950
+ pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
875
951
  // 3. Hash opencode.json before update
876
952
  const configPath = (0, path_1.join)(state.input.directory, "opencode.json");
877
953
  const hashBefore = await fileHash(configPath);
@@ -893,10 +969,10 @@ async function syncHarness(state) {
893
969
  // 8. Check if opencode.json changed
894
970
  const hashAfter = await fileHash(configPath);
895
971
  if (hashBefore && hashAfter && hashBefore !== hashAfter) {
896
- console.warn(`[opencode-immune] ⚠ Harness sync: opencode.json was updated. ` +
972
+ pluginLog.warn(`[opencode-immune] ⚠ Harness sync: opencode.json was updated. ` +
897
973
  `Please restart opencode for the new agent configuration to take effect.`);
898
974
  }
899
- console.log(`[opencode-immune] Harness sync: successfully updated to ${release.tagName}`);
975
+ pluginLog.info(`[opencode-immune] Harness sync: successfully updated to ${release.tagName}`);
900
976
  await writeDiagnosticLog(state, "harness-sync:success", {
901
977
  from: localVersion,
902
978
  to: release.tagName,
@@ -917,7 +993,7 @@ async function syncHarness(state) {
917
993
  }
918
994
  catch (err) {
919
995
  // Sync failure must never prevent plugin from working
920
- console.warn(`[opencode-immune] Harness sync failed:`, err instanceof Error ? err.message : String(err));
996
+ pluginLog.warn(`[opencode-immune] Harness sync failed:`, err instanceof Error ? err.message : String(err));
921
997
  await writeDiagnosticLog(state, "harness-sync:error", {
922
998
  error: err instanceof Error ? err.message : String(err),
923
999
  });
@@ -960,7 +1036,7 @@ function createTodoEnforcerChatMessage(state) {
960
1036
  // On user message, check previous assistant turn's counters
961
1037
  // then reset for next turn
962
1038
  if (state.toolCallCount > 3 && !state.todoWriteUsed) {
963
- console.warn(`[opencode-immune] Todo Enforcer: ${state.toolCallCount} tool calls without TodoWrite. ` +
1039
+ pluginLog.warn(`[opencode-immune] Todo Enforcer: ${state.toolCallCount} tool calls without TodoWrite. ` +
964
1040
  `Consider using todo list for multi-step tasks.`);
965
1041
  }
966
1042
  // Reset per-message counters for the next assistant turn
@@ -1002,20 +1078,20 @@ function createSessionRecoveryEvent(state) {
1002
1078
  }
1003
1079
  const markerActive = await isUltraworkMarkerActive(state);
1004
1080
  if (!markerActive) {
1005
- console.log(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
1081
+ pluginLog.info(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
1006
1082
  return;
1007
1083
  }
1008
- console.log(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
1084
+ pluginLog.info(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
1009
1085
  const recovery = await parseTasksFile(state.input.directory);
1010
1086
  if (recovery) {
1011
1087
  state.recoveryContext = recovery;
1012
- console.log(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
1088
+ pluginLog.info(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
1013
1089
  if (recovery.phase !== "ARCHIVE: DONE") {
1014
1090
  // Register this root session as managed so retry/recovery works
1015
1091
  await addManagedUltraworkSession(state, sessionID);
1016
1092
  // Skip sending AUTO-RESUME if already sent from plugin init
1017
1093
  if (state.autoResumeAttempted) {
1018
- console.log(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
1094
+ pluginLog.info(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
1019
1095
  return;
1020
1096
  }
1021
1097
  setTimeout(async () => {
@@ -1032,17 +1108,17 @@ function createSessionRecoveryEvent(state) {
1032
1108
  },
1033
1109
  path: { id: sessionID },
1034
1110
  });
1035
- console.log(`[opencode-immune] Auto-resume prompt sent to managed ultrawork session ${sessionID}`);
1111
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to managed ultrawork session ${sessionID}`);
1036
1112
  }
1037
1113
  catch (err) {
1038
- console.log(`[opencode-immune] Auto-resume failed (session may have been taken over):`, err);
1114
+ pluginLog.info(`[opencode-immune] Auto-resume failed (session may have been taken over):`, err);
1039
1115
  }
1040
1116
  }, 3_000);
1041
1117
  }
1042
1118
  }
1043
1119
  else {
1044
1120
  state.recoveryContext = null;
1045
- console.log("[opencode-immune] No active task found.");
1121
+ pluginLog.info("[opencode-immune] No active task found.");
1046
1122
  }
1047
1123
  }
1048
1124
  };
@@ -1124,7 +1200,7 @@ function createRalphLoopToolAfter(state) {
1124
1200
  newString: input.args?.newString ?? "",
1125
1201
  timestamp: Date.now(),
1126
1202
  };
1127
- console.warn(`[opencode-immune] Ralph Loop: Edit failed for "${state.lastEditAttempt.filePath}". ` +
1203
+ pluginLog.warn(`[opencode-immune] Ralph Loop: Edit failed for "${state.lastEditAttempt.filePath}". ` +
1128
1204
  `Recovery hint will be injected in next system transform.`);
1129
1205
  }
1130
1206
  else {
@@ -1151,7 +1227,7 @@ function createContextMonitorChatMessage(state) {
1151
1227
  if (state.approximateTokens >
1152
1228
  ESTIMATED_CONTEXT_LIMIT * CONTEXT_WARNING_THRESHOLD) {
1153
1229
  const pct = Math.round((state.approximateTokens / ESTIMATED_CONTEXT_LIMIT) * 100);
1154
- console.warn(`[opencode-immune] Context Monitor: ~${state.approximateTokens} tokens estimated (${pct}% of ~${ESTIMATED_CONTEXT_LIMIT}). ` +
1230
+ pluginLog.warn(`[opencode-immune] Context Monitor: ~${state.approximateTokens} tokens estimated (${pct}% of ~${ESTIMATED_CONTEXT_LIMIT}). ` +
1155
1231
  `Consider compacting the session.`);
1156
1232
  }
1157
1233
  }
@@ -1174,7 +1250,7 @@ function createCompactionHandler(state) {
1174
1250
  "After compaction, the agent should still be able to resume the current phase without re-reading all Memory Bank files.");
1175
1251
  // Reset token counter after compaction
1176
1252
  state.approximateTokens = 0;
1177
- console.log("[opencode-immune] Context Monitor: Compaction triggered, token counter reset.");
1253
+ pluginLog.info("[opencode-immune] Context Monitor: Compaction triggered, token counter reset.");
1178
1254
  };
1179
1255
  }
1180
1256
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1198,13 +1274,13 @@ function createCommentCheckerToolAfter(state) {
1198
1274
  return;
1199
1275
  // Check for emoji
1200
1276
  if (EMOJI_PATTERN.test(content)) {
1201
- console.warn(`[opencode-immune] Comment Checker: Emoji detected in ${input.tool} operation. ` +
1277
+ pluginLog.warn(`[opencode-immune] Comment Checker: Emoji detected in ${input.tool} operation. ` +
1202
1278
  `Avoid emojis in code unless the user explicitly requested them.`);
1203
1279
  }
1204
1280
  // Check for TODO/FIXME/HACK
1205
1281
  const todoMatch = content.match(TODO_PATTERN);
1206
1282
  if (todoMatch) {
1207
- console.warn(`[opencode-immune] Comment Checker: "${todoMatch[0]}" comment found in ${input.tool} operation. ` +
1283
+ pluginLog.warn(`[opencode-immune] Comment Checker: "${todoMatch[0]}" comment found in ${input.tool} operation. ` +
1208
1284
  `Consider resolving it or tracking it in the todo list.`);
1209
1285
  }
1210
1286
  };
@@ -1235,11 +1311,11 @@ function createKeywordDetectorChatMessage(_state) {
1235
1311
  if (!messageContent)
1236
1312
  return;
1237
1313
  if (ERROR_KEYWORDS.test(messageContent)) {
1238
- console.log(`[opencode-immune] Keyword Detector: Error-related keywords found. ` +
1314
+ pluginLog.info(`[opencode-immune] Keyword Detector: Error-related keywords found. ` +
1239
1315
  `Consider using 1-van to analyze the issue systematically.`);
1240
1316
  }
1241
1317
  if (DEPLOY_KEYWORDS.test(messageContent)) {
1242
- console.log(`[opencode-immune] Keyword Detector: Deploy/release keywords found. ` +
1318
+ pluginLog.info(`[opencode-immune] Keyword Detector: Deploy/release keywords found. ` +
1243
1319
  `Consider running 5-reflect first to verify implementation quality.`);
1244
1320
  }
1245
1321
  };
@@ -1268,7 +1344,7 @@ function createFallbackModels(state) {
1268
1344
  const recovery = await parseTasksFile(state.input.directory);
1269
1345
  if (recovery && recovery.phase !== "ARCHIVE: DONE") {
1270
1346
  state.recoveryContext = recovery;
1271
- console.log(`[opencode-immune] Auto-recovery on existing session: ` +
1347
+ pluginLog.info(`[opencode-immune] Auto-recovery on existing session: ` +
1272
1348
  `task="${recovery.task}", level=${recovery.level}, phase=${recovery.phase}. ` +
1273
1349
  `Sending AUTO-RESUME prompt in 3s...`);
1274
1350
  const sid = input.sessionID;
@@ -1286,10 +1362,10 @@ function createFallbackModels(state) {
1286
1362
  },
1287
1363
  path: { id: sid },
1288
1364
  });
1289
- console.log(`[opencode-immune] Auto-resume prompt sent to session ${sid}`);
1365
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to session ${sid}`);
1290
1366
  }
1291
1367
  catch (err) {
1292
- console.log(`[opencode-immune] Auto-resume prompt failed:`, err);
1368
+ pluginLog.info(`[opencode-immune] Auto-resume prompt failed:`, err);
1293
1369
  }
1294
1370
  }, 3_000);
1295
1371
  }
@@ -1308,7 +1384,7 @@ function createFallbackModels(state) {
1308
1384
  const providerId = input.provider?.info && "id" in input.provider.info
1309
1385
  ? input.provider.info.id
1310
1386
  : "unknown";
1311
- console.log(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1387
+ pluginLog.info(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1312
1388
  `model="${modelId}", provider="${providerId}"`);
1313
1389
  };
1314
1390
  }
@@ -1346,7 +1422,7 @@ function createEventHandler(state) {
1346
1422
  if (count < MAX_RETRIES && !state.sessionRetryTimers.has(fallbackSessionID)) {
1347
1423
  const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
1348
1424
  state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
1349
- console.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
1425
+ pluginLog.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
1350
1426
  `Retrying sole managed root session ${fallbackSessionID}.`);
1351
1427
  scheduleManagedSessionRetry(state, fallbackSessionID, {
1352
1428
  delayMs: delay,
@@ -1371,7 +1447,7 @@ function createEventHandler(state) {
1371
1447
  return;
1372
1448
  }
1373
1449
  if (state.sessionRetryTimers.has(sessionID)) {
1374
- console.log(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
1450
+ pluginLog.info(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
1375
1451
  return;
1376
1452
  }
1377
1453
  const count = state.sessionErrorRetryCount.get(sessionID) ?? 0;
@@ -1382,7 +1458,7 @@ function createEventHandler(state) {
1382
1458
  // child after the router advances can create two writers in one pipeline.
1383
1459
  if (isChild) {
1384
1460
  recordChildFallbackRequest(state, managedSession, sessionID, error);
1385
- console.log(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1461
+ pluginLog.info(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1386
1462
  `Recorded router-owned fallback request and skipped plugin auto-retry.`);
1387
1463
  state.sessionErrorRetryCount.set(sessionID, count);
1388
1464
  return;
@@ -1390,7 +1466,7 @@ function createEventHandler(state) {
1390
1466
  else if (isRoot && (isRateLimitApiError(error) || isCertificateApiError(error))) {
1391
1467
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1392
1468
  const errorType = isCertificateApiError(error) ? "certificate error" : "rate limit";
1393
- console.log(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1469
+ pluginLog.info(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1394
1470
  `Retry will use fallback model ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1395
1471
  }
1396
1472
  const scheduled = scheduleManagedSessionRetry(state, sessionID, {
@@ -1404,17 +1480,22 @@ function createEventHandler(state) {
1404
1480
  }
1405
1481
  }
1406
1482
  else {
1407
- console.log(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
1483
+ pluginLog.info(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
1408
1484
  }
1409
1485
  }
1410
1486
  // Reset retry counter on successful activity
1411
1487
  if (eventType === "session.updated" && sessionID) {
1488
+ const managedSession = getManagedSession(state, sessionID);
1412
1489
  cancelPendingSessionRetry(state, sessionID, "session updated");
1413
1490
  cancelProviderRetryWatchdog(state, sessionID, "session updated");
1414
1491
  state.sessionErrorRetryCount.delete(sessionID);
1415
1492
  if (markUltraworkSessionActive(state, sessionID)) {
1416
1493
  // session activity tracked in-memory only
1417
1494
  }
1495
+ if (managedSession?.kind === "child") {
1496
+ cancelProviderRetryWatchdog(state, managedSession.rootSessionID, `child session ${sessionID} updated`);
1497
+ markUltraworkSessionActive(state, managedSession.rootSessionID);
1498
+ }
1418
1499
  }
1419
1500
  if (eventType === "session.deleted" && sessionID) {
1420
1501
  await removeManagedUltraworkSession(state, sessionID, "session deleted");
@@ -1429,7 +1510,7 @@ function createEventHandler(state) {
1429
1510
  "file.edited",
1430
1511
  ];
1431
1512
  if (significantEvents.includes(eventType)) {
1432
- console.log(`[opencode-immune] Event: ${eventType}`);
1513
+ pluginLog.info(`[opencode-immune] Event: ${eventType}`);
1433
1514
  }
1434
1515
  };
1435
1516
  }
@@ -1455,7 +1536,7 @@ async function archiveProgress(directory) {
1455
1536
  const content = await (0, promises_1.readFile)(progressPath, "utf-8");
1456
1537
  // Skip if empty or trivially empty
1457
1538
  if (!content.trim() || content.trim() === "# Progress") {
1458
- console.log("[opencode-immune] Archive progress: nothing to archive (empty).");
1539
+ pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
1459
1540
  return;
1460
1541
  }
1461
1542
  const archiveDir = (0, path_1.join)(directory, "memory-bank", "archive");
@@ -1466,13 +1547,13 @@ async function archiveProgress(directory) {
1466
1547
  const archiveName = `progress-${dateStr}-${ts}.md`;
1467
1548
  const archivePath = (0, path_1.join)(archiveDir, archiveName);
1468
1549
  await (0, promises_1.rename)(progressPath, archivePath);
1469
- console.log(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1550
+ pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1470
1551
  }
1471
1552
  catch (err) {
1472
1553
  // File doesn't exist or move failed — not critical
1473
1554
  const msg = err instanceof Error ? err.message : String(err);
1474
1555
  if (!msg.includes("ENOENT")) {
1475
- console.warn("[opencode-immune] Archive progress failed:", msg);
1556
+ pluginLog.warn("[opencode-immune] Archive progress failed:", msg);
1476
1557
  }
1477
1558
  }
1478
1559
  }
@@ -1518,7 +1599,7 @@ function runGitCommit(directory) {
1518
1599
  // Stage all changes
1519
1600
  (0, child_process_1.execFile)("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1520
1601
  if (addErr) {
1521
- console.error("[opencode-immune] git add failed:", addErr.message);
1602
+ pluginLog.error("[opencode-immune] git add failed:", addErr.message);
1522
1603
  resolve(false);
1523
1604
  return;
1524
1605
  }
@@ -1529,15 +1610,15 @@ function runGitCommit(directory) {
1529
1610
  (0, child_process_1.execFile)("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1530
1611
  if (commitErr) {
1531
1612
  if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
1532
- console.log("[opencode-immune] git commit: nothing to commit (clean tree).");
1613
+ pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
1533
1614
  resolve(true);
1534
1615
  return;
1535
1616
  }
1536
- console.error("[opencode-immune] git commit failed:", commitErr.message, stderr);
1617
+ pluginLog.error("[opencode-immune] git commit failed:", commitErr.message, stderr);
1537
1618
  resolve(false);
1538
1619
  return;
1539
1620
  }
1540
- console.log("[opencode-immune] git commit succeeded:", stdout?.trim());
1621
+ pluginLog.info("[opencode-immune] git commit succeeded:", stdout?.trim());
1541
1622
  resolve(true);
1542
1623
  });
1543
1624
  });
@@ -1572,25 +1653,25 @@ function createTextCompleteHandler(state) {
1572
1653
  countAgainstBudget: false,
1573
1654
  abortBeforePrompt: true,
1574
1655
  });
1575
- console.log(`[opencode-immune] Provider retry banner detected for session ${sessionID}. ` +
1656
+ pluginLog.info(`[opencode-immune] Provider retry banner detected for session ${sessionID}. ` +
1576
1657
  `Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`);
1577
1658
  }
1578
1659
  // ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
1579
1660
  if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
1580
1661
  await clearUltraworkMarker(state);
1581
- console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
1662
+ pluginLog.info("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
1582
1663
  return;
1583
1664
  }
1584
1665
  // ── PRE_COMMIT only (without CYCLE_COMPLETE in same part): run commit ──
1585
1666
  if (text.includes(PRE_COMMIT_MARKER) && !text.includes(CYCLE_COMPLETE_MARKER)) {
1586
1667
  if (!state.commitPending) {
1587
1668
  state.commitPending = true;
1588
- console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected (standalone), running git commit...");
1669
+ pluginLog.info("[opencode-immune] Multi-Cycle: PRE_COMMIT detected (standalone), running git commit...");
1589
1670
  try {
1590
1671
  await runGitCommit(state.input.directory);
1591
1672
  }
1592
1673
  catch (err) {
1593
- console.error("[opencode-immune] Multi-Cycle: git commit failed (standalone):", err);
1674
+ pluginLog.error("[opencode-immune] Multi-Cycle: git commit failed (standalone):", err);
1594
1675
  }
1595
1676
  finally {
1596
1677
  state.commitPending = false;
@@ -1605,18 +1686,18 @@ function createTextCompleteHandler(state) {
1605
1686
  await archiveProgress(state.input.directory);
1606
1687
  }
1607
1688
  catch (err) {
1608
- console.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
1689
+ pluginLog.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
1609
1690
  }
1610
1691
  // Step 1: Always commit first
1611
1692
  if (!state.commitPending) {
1612
1693
  state.commitPending = true;
1613
- console.log("[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected, running git commit first...");
1694
+ pluginLog.info("[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected, running git commit first...");
1614
1695
  try {
1615
1696
  await runGitCommit(state.input.directory);
1616
- console.log("[opencode-immune] Multi-Cycle: git commit completed before new cycle.");
1697
+ pluginLog.info("[opencode-immune] Multi-Cycle: git commit completed before new cycle.");
1617
1698
  }
1618
1699
  catch (err) {
1619
- console.error("[opencode-immune] Multi-Cycle: git commit failed (continuing anyway):", err);
1700
+ pluginLog.error("[opencode-immune] Multi-Cycle: git commit failed (continuing anyway):", err);
1620
1701
  }
1621
1702
  finally {
1622
1703
  state.commitPending = false;
@@ -1625,13 +1706,13 @@ function createTextCompleteHandler(state) {
1625
1706
  // Step 2: Create new session
1626
1707
  state.cycleCount++;
1627
1708
  if (state.cycleCount >= MAX_CYCLES) {
1628
- console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
1709
+ pluginLog.info(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
1629
1710
  await clearUltraworkMarker(state);
1630
1711
  return;
1631
1712
  }
1632
1713
  const taskMatch = text.match(NEXT_TASK_PATTERN);
1633
1714
  const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
1634
- console.log(`[opencode-immune] Multi-Cycle: Creating new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`);
1715
+ pluginLog.info(`[opencode-immune] Multi-Cycle: Creating new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`);
1635
1716
  try {
1636
1717
  const createResult = await state.input.client.session.create({
1637
1718
  body: {
@@ -1641,10 +1722,10 @@ function createTextCompleteHandler(state) {
1641
1722
  const newSessionData = createResult?.data;
1642
1723
  const newSessionID = newSessionData?.id;
1643
1724
  if (!newSessionID) {
1644
- console.error("[opencode-immune] Multi-Cycle: Failed to create new session — no ID returned.");
1725
+ pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create new session — no ID returned.");
1645
1726
  return;
1646
1727
  }
1647
- console.log(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1728
+ pluginLog.info(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1648
1729
  await addManagedUltraworkSession(state, newSessionID);
1649
1730
  await state.input.client.session.promptAsync({
1650
1731
  body: {
@@ -1658,10 +1739,10 @@ function createTextCompleteHandler(state) {
1658
1739
  },
1659
1740
  path: { id: newSessionID },
1660
1741
  });
1661
- console.log(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${newSessionID}`);
1742
+ pluginLog.info(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${newSessionID}`);
1662
1743
  }
1663
1744
  catch (err) {
1664
- console.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
1745
+ pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
1665
1746
  }
1666
1747
  }
1667
1748
  };
@@ -1694,7 +1775,7 @@ function createMultiCycleHandler(state) {
1694
1775
  RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
1695
1776
  if (managedSession && !managedSession.fallbackModel) {
1696
1777
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1697
- console.log(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1778
+ pluginLog.info(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1698
1779
  `Fallback model pinned to ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1699
1780
  }
1700
1781
  if (managedSession) {
@@ -1719,7 +1800,7 @@ async function server(input) {
1719
1800
  // Runs in background so it doesn't delay plugin initialization.
1720
1801
  // If sync fails, plugin continues normally with existing config.
1721
1802
  syncHarness(state).catch((err) => {
1722
- console.warn(`[opencode-immune] Harness sync background error:`, err instanceof Error ? err.message : String(err));
1803
+ pluginLog.warn(`[opencode-immune] Harness sync background error:`, err instanceof Error ? err.message : String(err));
1723
1804
  });
1724
1805
  // Eagerly load recovery context at plugin init so it's available
1725
1806
  // for the very first system.transform call (before chat.params fires).
@@ -1730,7 +1811,7 @@ async function server(input) {
1730
1811
  // Active task exists with incomplete phases — resume it
1731
1812
  state.recoveryContext = recovery;
1732
1813
  state.autoResumeAttempted = true;
1733
- console.log(`[opencode-immune] Plugin init: ultrawork marker active, recovery context loaded: ` +
1814
+ pluginLog.info(`[opencode-immune] Plugin init: ultrawork marker active, recovery context loaded: ` +
1734
1815
  `task="${recovery.task}", level=${recovery.level}, phase=${recovery.phase}. ` +
1735
1816
  `Will create new session and send AUTO-RESUME.`);
1736
1817
  // Create a new session and send AUTO-RESUME prompt (same pattern as CYCLE_COMPLETE).
@@ -1745,10 +1826,10 @@ async function server(input) {
1745
1826
  const newSessionData = createResult?.data;
1746
1827
  const newSessionID = newSessionData?.id;
1747
1828
  if (!newSessionID) {
1748
- console.error("[opencode-immune] Auto-resume: Failed to create session — no session ID returned.");
1829
+ pluginLog.error("[opencode-immune] Auto-resume: Failed to create session — no session ID returned.");
1749
1830
  return;
1750
1831
  }
1751
- console.log(`[opencode-immune] Auto-resume: New session created: ${newSessionID}`);
1832
+ pluginLog.info(`[opencode-immune] Auto-resume: New session created: ${newSessionID}`);
1752
1833
  await addManagedUltraworkSession(state, newSessionID);
1753
1834
  await state.input.client.session.promptAsync({
1754
1835
  body: {
@@ -1762,10 +1843,10 @@ async function server(input) {
1762
1843
  },
1763
1844
  path: { id: newSessionID },
1764
1845
  });
1765
- console.log(`[opencode-immune] Auto-resume prompt sent to new session ${newSessionID}`);
1846
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to new session ${newSessionID}`);
1766
1847
  }
1767
1848
  catch (err) {
1768
- console.error("[opencode-immune] Auto-resume: Failed to create session or send prompt:", err);
1849
+ pluginLog.error("[opencode-immune] Auto-resume: Failed to create session or send prompt:", err);
1769
1850
  }
1770
1851
  }, 5_000);
1771
1852
  }
@@ -1777,7 +1858,7 @@ async function server(input) {
1777
1858
  const hasPendingTasks = /- \[ \]/.test(backlogContent);
1778
1859
  if (hasPendingTasks) {
1779
1860
  state.autoResumeAttempted = true;
1780
- console.log(`[opencode-immune] Plugin init: no active task but backlog has pending items. ` +
1861
+ pluginLog.info(`[opencode-immune] Plugin init: no active task but backlog has pending items. ` +
1781
1862
  `Will create new session to start next cycle.`);
1782
1863
  setTimeout(async () => {
1783
1864
  try {
@@ -1789,10 +1870,10 @@ async function server(input) {
1789
1870
  const newSessionData = createResult?.data;
1790
1871
  const newSessionID = newSessionData?.id;
1791
1872
  if (!newSessionID) {
1792
- console.error("[opencode-immune] Auto-cycle: Failed to create session — no session ID returned.");
1873
+ pluginLog.error("[opencode-immune] Auto-cycle: Failed to create session — no session ID returned.");
1793
1874
  return;
1794
1875
  }
1795
- console.log(`[opencode-immune] Auto-cycle: New session created: ${newSessionID}`);
1876
+ pluginLog.info(`[opencode-immune] Auto-cycle: New session created: ${newSessionID}`);
1796
1877
  await addManagedUltraworkSession(state, newSessionID);
1797
1878
  await state.input.client.session.promptAsync({
1798
1879
  body: {
@@ -1806,27 +1887,27 @@ async function server(input) {
1806
1887
  },
1807
1888
  path: { id: newSessionID },
1808
1889
  });
1809
- console.log(`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`);
1890
+ pluginLog.info(`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`);
1810
1891
  }
1811
1892
  catch (err) {
1812
- console.error("[opencode-immune] Auto-cycle: Failed to create session or send prompt:", err);
1893
+ pluginLog.error("[opencode-immune] Auto-cycle: Failed to create session or send prompt:", err);
1813
1894
  }
1814
1895
  }, 5_000);
1815
1896
  }
1816
1897
  else {
1817
1898
  // No active task and no pending backlog — clear marker
1818
1899
  await clearUltraworkMarker(state);
1819
- console.log(`[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`);
1900
+ pluginLog.info(`[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`);
1820
1901
  }
1821
1902
  }
1822
1903
  catch {
1823
1904
  // backlog.md doesn't exist or can't be read — clear marker
1824
1905
  await clearUltraworkMarker(state);
1825
- console.log(`[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`);
1906
+ pluginLog.info(`[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`);
1826
1907
  }
1827
1908
  }
1828
1909
  }
1829
- console.log(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
1910
+ pluginLog.info(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
1830
1911
  // Compose tool.execute.after handlers:
1831
1912
  // Todo Enforcer (counter) + Ralph Loop (edit error) + Comment Checker
1832
1913
  const toolAfterHandlers = [
@@ -1843,13 +1924,13 @@ async function server(input) {
1843
1924
  createMultiCycleHandler(state),
1844
1925
  ];
1845
1926
  return {
1846
- event: withErrorBoundary("event", createEventHandler(state)),
1847
- "chat.message": withErrorBoundary("chat.message", compositeChatMessage(chatMessageHandlers)),
1848
- "chat.params": withErrorBoundary("chat.params", createFallbackModels(state)),
1849
- "tool.execute.after": withErrorBoundary("tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1850
- "experimental.chat.system.transform": withErrorBoundary("experimental.chat.system.transform", createSystemTransform(state)),
1851
- "experimental.session.compacting": withErrorBoundary("experimental.session.compacting", createCompactionHandler(state)),
1852
- "experimental.text.complete": withErrorBoundary("experimental.text.complete", createTextCompleteHandler(state)),
1927
+ event: withErrorBoundary(state, "event", createEventHandler(state)),
1928
+ "chat.message": withErrorBoundary(state, "chat.message", compositeChatMessage(chatMessageHandlers)),
1929
+ "chat.params": withErrorBoundary(state, "chat.params", createFallbackModels(state)),
1930
+ "tool.execute.after": withErrorBoundary(state, "tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1931
+ "experimental.chat.system.transform": withErrorBoundary(state, "experimental.chat.system.transform", createSystemTransform(state)),
1932
+ "experimental.session.compacting": withErrorBoundary(state, "experimental.session.compacting", createCompactionHandler(state)),
1933
+ "experimental.text.complete": withErrorBoundary(state, "experimental.text.complete", createTextCompleteHandler(state)),
1853
1934
  };
1854
1935
  }
1855
1936
  exports.default = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"