opencode-immune 1.0.70 → 1.0.71

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.70";
3780
+ var PLUGIN_VERSION = "1.0.71";
3781
3781
  var PLUGIN_PACKAGE_NAME = "opencode-immune";
3782
3782
  var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
3783
3783
  function getServerAuthHeaders() {
@@ -3848,6 +3848,7 @@ function createState(input) {
3848
3848
  recoveryContext: null,
3849
3849
  managedUltraworkSessions: /* @__PURE__ */ new Map(),
3850
3850
  sessionRetryTimers: /* @__PURE__ */ new Map(),
3851
+ abortedMessageRetries: /* @__PURE__ */ new Set(),
3851
3852
  providerRetryWatchdogs: /* @__PURE__ */ new Map(),
3852
3853
  childFallbackRequests: /* @__PURE__ */ new Map(),
3853
3854
  sessionErrorRetryCount: /* @__PURE__ */ new Map(),
@@ -4346,6 +4347,46 @@ function getRetryableErrorType(error) {
4346
4347
  if (action === "retry") return `transient provider error${statusSuffix}`;
4347
4348
  return "non-retryable provider error";
4348
4349
  }
4350
+ function compactUnknown(value, maxLength = 8e3) {
4351
+ try {
4352
+ const serialized = typeof value === "string" ? value : JSON.stringify(value);
4353
+ return (serialized ?? "").slice(0, maxLength).toLowerCase();
4354
+ } catch {
4355
+ return String(value).slice(0, maxLength).toLowerCase();
4356
+ }
4357
+ }
4358
+ function isAbortedMessageEvent(eventType, properties) {
4359
+ if (eventType !== "message.updated" && eventType !== "message.part.updated") return false;
4360
+ const text = compactUnknown(properties ?? {});
4361
+ return text.includes("messageabortederror") || text.includes("tool execution aborted") || text.includes('"interrupted":true') || text.includes('"status":"error"') && text.includes('"aborted"');
4362
+ }
4363
+ function getMessageIDFromEvent(properties) {
4364
+ const info = isRecord(properties?.info) ? properties.info : void 0;
4365
+ const part = isRecord(properties?.part) ? properties.part : void 0;
4366
+ return stringifyErrorField(properties?.messageID) ?? stringifyErrorField(info?.id) ?? stringifyErrorField(part?.messageID) ?? stringifyErrorField(properties?.id);
4367
+ }
4368
+ async function recoverUntrackedRootSessionForActiveTask(state, sessionID, reason) {
4369
+ const markerActive = await isUltraworkMarkerActive(state);
4370
+ if (!markerActive) return void 0;
4371
+ const recovery = await parseTasksFile(state.input.directory);
4372
+ if (!recovery || recovery.phase === "ARCHIVE: DONE") return void 0;
4373
+ state.recoveryContext = recovery;
4374
+ await addManagedUltraworkSession(state, sessionID);
4375
+ const recovered = getManagedSession(state, sessionID);
4376
+ await writeDiagnosticLog(state, "session-retry:recovered-untracked-root", {
4377
+ sessionID,
4378
+ task: recovery.task,
4379
+ level: recovery.level,
4380
+ phase: recovery.phase,
4381
+ reason
4382
+ });
4383
+ writePluginLog(
4384
+ state,
4385
+ "warn",
4386
+ `[opencode-immune] Recovered untracked root session ${sessionID} for ${reason}.`
4387
+ );
4388
+ return recovered;
4389
+ }
4349
4390
  function recordChildFallbackRequest(state, managedSession, childSessionID, error) {
4350
4391
  const fallbackModel = selectFallbackModel(
4351
4392
  getFailedModelFromError(error) ?? managedSession.currentModel
@@ -4405,26 +4446,11 @@ async function setSessionFallbackModel(state, sessionID, model) {
4405
4446
  }
4406
4447
  async function recoverUntrackedRootSessionForRetry(state, sessionID, error) {
4407
4448
  if (!isRetryableApiError(error)) return void 0;
4408
- const markerActive = await isUltraworkMarkerActive(state);
4409
- if (!markerActive) return void 0;
4410
- const recovery = await parseTasksFile(state.input.directory);
4411
- if (!recovery || recovery.phase === "ARCHIVE: DONE") return void 0;
4412
- state.recoveryContext = recovery;
4413
- await addManagedUltraworkSession(state, sessionID);
4414
- const recovered = getManagedSession(state, sessionID);
4415
- await writeDiagnosticLog(state, "session-retry:recovered-untracked-root", {
4416
- sessionID,
4417
- task: recovery.task,
4418
- level: recovery.level,
4419
- phase: recovery.phase,
4420
- errorType: getRetryableErrorType(error)
4421
- });
4422
- writePluginLog(
4449
+ return recoverUntrackedRootSessionForActiveTask(
4423
4450
  state,
4424
- "warn",
4425
- `[opencode-immune] Recovered untracked root session ${sessionID} for retry after plugin restart or state loss.`
4451
+ sessionID,
4452
+ `retry after ${getRetryableErrorType(error)}`
4426
4453
  );
4427
- return recovered;
4428
4454
  }
4429
4455
  function getManagedSessionRetryContext(state, sessionID) {
4430
4456
  const managedSession = state.managedUltraworkSessions.get(sessionID);
@@ -5412,9 +5438,50 @@ function createEventHandler(state) {
5412
5438
  await sessionRecovery(input);
5413
5439
  const event = input.event;
5414
5440
  const eventType = event.type ?? "unknown";
5415
- const info = event.properties?.info;
5416
- const sessionID = event.properties?.sessionID ?? info?.id;
5417
- const error = event.properties?.error;
5441
+ const properties = event.properties;
5442
+ const info = properties?.info;
5443
+ const sessionID = properties?.sessionID ?? info?.sessionID ?? info?.id;
5444
+ const error = properties?.error ?? info?.error;
5445
+ if (sessionID && isAbortedMessageEvent(eventType, properties)) {
5446
+ const messageID = getMessageIDFromEvent(properties) ?? `${sessionID}:unknown`;
5447
+ const retryKey = `${sessionID}:${messageID}`;
5448
+ if (state.abortedMessageRetries.has(retryKey)) {
5449
+ return;
5450
+ }
5451
+ const managedSession = getManagedSession(state, sessionID) ?? await recoverUntrackedRootSessionForActiveTask(
5452
+ state,
5453
+ sessionID,
5454
+ "aborted message recovery"
5455
+ );
5456
+ if (managedSession?.kind === "root") {
5457
+ state.abortedMessageRetries.add(retryKey);
5458
+ await writeDiagnosticLog(state, "session-retry:aborted-message-observed", {
5459
+ sessionID,
5460
+ messageID,
5461
+ eventType
5462
+ });
5463
+ const count = state.sessionErrorRetryCount.get(sessionID) ?? 0;
5464
+ if (count < MAX_RETRIES) {
5465
+ state.sessionErrorRetryCount.set(sessionID, count + 1);
5466
+ const scheduled = scheduleManagedSessionRetry(state, sessionID, {
5467
+ delayMs: Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS),
5468
+ reason: "aborted message",
5469
+ attemptLabel: `aborted attempt ${count + 1}/${MAX_RETRIES}`,
5470
+ countAgainstBudget: true
5471
+ });
5472
+ if (!scheduled) {
5473
+ state.sessionErrorRetryCount.set(sessionID, count);
5474
+ }
5475
+ }
5476
+ } else if (managedSession?.kind === "child") {
5477
+ await writeDiagnosticLog(state, "session-retry:aborted-child-observed", {
5478
+ sessionID,
5479
+ messageID,
5480
+ rootSessionID: managedSession.rootSessionID,
5481
+ eventType
5482
+ });
5483
+ }
5484
+ }
5418
5485
  if (eventType === "session.error") {
5419
5486
  await writeDiagnosticLog(state, "session-error:observed", {
5420
5487
  sessionID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.70",
3
+ "version": "1.0.71",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {