opencode-immune 1.0.1 → 1.0.3

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 +74 -14
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -31,6 +31,13 @@ const RATE_LIMIT_FALLBACK_MODEL = {
31
31
  function isManagedUltraworkSession(state, sessionID) {
32
32
  return !!sessionID && state.managedUltraworkSessions.has(sessionID);
33
33
  }
34
+ function getManagedSession(state, sessionID) {
35
+ return sessionID ? state.managedUltraworkSessions.get(sessionID) : undefined;
36
+ }
37
+ function isManagedRootUltraworkSession(state, sessionID) {
38
+ const record = getManagedSession(state, sessionID);
39
+ return !!record && record.kind === "root";
40
+ }
34
41
  function pruneExpiredManagedSessions(state, now = Date.now()) {
35
42
  let removed = 0;
36
43
  for (const [sessionID, record] of state.managedUltraworkSessions.entries()) {
@@ -68,12 +75,23 @@ async function loadManagedSessionsCache(state) {
68
75
  return;
69
76
  }
70
77
  for (const [sessionID, record] of Object.entries(parsed.sessions)) {
71
- if (!record || record.agent !== ULTRAWORK_AGENT)
78
+ if (!record)
72
79
  continue;
73
80
  state.managedUltraworkSessions.set(sessionID, {
74
- agent: ULTRAWORK_AGENT,
81
+ kind: record.kind === "child" ? "child" : "root",
82
+ agent: typeof record.agent === "string" && record.agent.length > 0
83
+ ? record.agent
84
+ : ULTRAWORK_AGENT,
85
+ rootSessionID: typeof record.rootSessionID === "string" && record.rootSessionID.length > 0
86
+ ? record.rootSessionID
87
+ : sessionID,
75
88
  createdAt: typeof record.createdAt === "number" ? record.createdAt : Date.now(),
76
89
  updatedAt: typeof record.updatedAt === "number" ? record.updatedAt : Date.now(),
90
+ fallbackModel: record.fallbackModel &&
91
+ typeof record.fallbackModel.providerID === "string" &&
92
+ typeof record.fallbackModel.modelID === "string"
93
+ ? record.fallbackModel
94
+ : undefined,
77
95
  });
78
96
  }
79
97
  const removed = pruneExpiredManagedSessions(state);
@@ -93,7 +111,9 @@ async function loadManagedSessionsCache(state) {
93
111
  async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now()) {
94
112
  const existing = state.managedUltraworkSessions.get(sessionID);
95
113
  const nextRecord = {
114
+ kind: existing?.kind ?? "root",
96
115
  agent: ULTRAWORK_AGENT,
116
+ rootSessionID: existing?.rootSessionID ?? sessionID,
97
117
  createdAt: existing?.createdAt ?? timestamp,
98
118
  updatedAt: timestamp,
99
119
  fallbackModel: existing?.fallbackModel,
@@ -107,6 +127,21 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
107
127
  state.managedUltraworkSessions.set(sessionID, nextRecord);
108
128
  await writeManagedSessionsCache(state);
109
129
  }
130
+ async function addManagedChildSession(state, sessionID, parentSessionID, timestamp = Date.now()) {
131
+ const parent = state.managedUltraworkSessions.get(parentSessionID);
132
+ if (!parent)
133
+ return;
134
+ const existing = state.managedUltraworkSessions.get(sessionID);
135
+ state.managedUltraworkSessions.set(sessionID, {
136
+ kind: "child",
137
+ agent: existing?.agent ?? "unknown",
138
+ rootSessionID: parent.rootSessionID,
139
+ createdAt: existing?.createdAt ?? timestamp,
140
+ updatedAt: timestamp,
141
+ fallbackModel: existing?.fallbackModel ?? parent.fallbackModel,
142
+ });
143
+ await writeManagedSessionsCache(state);
144
+ }
110
145
  function cancelRetry(state, sessionID, reason) {
111
146
  const timer = state.retryTimers.get(sessionID);
112
147
  if (!timer)
@@ -124,6 +159,17 @@ async function removeManagedUltraworkSession(state, sessionID, reason) {
124
159
  await writeManagedSessionsCache(state);
125
160
  console.log(`[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
126
161
  }
162
+ async function updateManagedSessionAgent(state, sessionID, agent) {
163
+ const existing = state.managedUltraworkSessions.get(sessionID);
164
+ if (!existing || existing.agent === agent)
165
+ return;
166
+ state.managedUltraworkSessions.set(sessionID, {
167
+ ...existing,
168
+ agent,
169
+ updatedAt: Date.now(),
170
+ });
171
+ await writeManagedSessionsCache(state);
172
+ }
127
173
  function markUltraworkSessionActive(state, sessionID) {
128
174
  const existing = state.managedUltraworkSessions.get(sessionID);
129
175
  if (!existing)
@@ -151,9 +197,7 @@ function isRateLimitApiError(error) {
151
197
  const maybeError = error;
152
198
  const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
153
199
  const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
154
- return (maybeError.name === "APIError" &&
155
- maybeError.data?.isRetryable === true &&
156
- (type.includes("rate_limit") || message.includes("too many requests") || message.includes("rate limit")));
200
+ return ((type.includes("rate_limit") || message.includes("too many requests") || message.includes("rate limit")));
157
201
  }
158
202
  async function setSessionFallbackModel(state, sessionID, model) {
159
203
  const existing = state.managedUltraworkSessions.get(sessionID);
@@ -302,12 +346,16 @@ function createTodoEnforcerChatMessage(state) {
302
346
  return async (input, _output) => {
303
347
  const sessionID = input.sessionID;
304
348
  const agent = input.agent;
349
+ const record = getManagedSession(state, sessionID);
305
350
  if (sessionID && agent === ULTRAWORK_AGENT) {
306
351
  await addManagedUltraworkSession(state, sessionID);
307
352
  }
308
- else if (sessionID && agent && isManagedUltraworkSession(state, sessionID)) {
353
+ else if (sessionID && agent && record?.kind === "root") {
309
354
  await removeManagedUltraworkSession(state, sessionID, `session taken over by agent \"${agent}\"`);
310
355
  }
356
+ else if (sessionID && agent && record?.kind === "child") {
357
+ await updateManagedSessionAgent(state, sessionID, agent);
358
+ }
311
359
  // On user message, check previous assistant turn's counters
312
360
  // then reset for next turn
313
361
  if (state.toolCallCount > 3 && !state.todoWriteUsed) {
@@ -335,7 +383,11 @@ function createSessionRecoveryEvent(state) {
335
383
  state.sessionActive = true;
336
384
  const sessionInfo = event.properties?.info;
337
385
  const sessionID = sessionInfo?.id ?? event.properties?.sessionID;
338
- if (!isManagedUltraworkSession(state, sessionID)) {
386
+ const parentID = sessionInfo?.parentID;
387
+ if (sessionID && parentID && isManagedUltraworkSession(state, parentID)) {
388
+ await addManagedChildSession(state, sessionID, parentID);
389
+ }
390
+ if (!isManagedRootUltraworkSession(state, sessionID)) {
339
391
  return;
340
392
  }
341
393
  console.log(`[opencode-immune] Managed ultrawork session created, checking for active task...`);
@@ -346,7 +398,7 @@ function createSessionRecoveryEvent(state) {
346
398
  if (sessionID && recovery.phase !== "ARCHIVE: DONE") {
347
399
  setTimeout(async () => {
348
400
  try {
349
- if (!isManagedUltraworkSession(state, sessionID)) {
401
+ if (!isManagedRootUltraworkSession(state, sessionID)) {
350
402
  return;
351
403
  }
352
404
  await state.input.client.session.promptAsync({
@@ -383,7 +435,7 @@ function createSessionRecoveryEvent(state) {
383
435
  function createSystemTransform(state) {
384
436
  return async (input, output) => {
385
437
  // Session Recovery injection
386
- if (state.recoveryContext && isManagedUltraworkSession(state, input.sessionID)) {
438
+ if (state.recoveryContext && isManagedRootUltraworkSession(state, input.sessionID)) {
387
439
  const ctx = state.recoveryContext;
388
440
  const intentInfo = ctx.intent ? `, Intent: ${ctx.intent}` : "";
389
441
  const categoryInfo = ctx.category ? `, Category: ${ctx.category}` : "";
@@ -559,9 +611,12 @@ function createFallbackModels(state) {
559
611
  if (input.agent === ULTRAWORK_AGENT) {
560
612
  await addManagedUltraworkSession(state, input.sessionID);
561
613
  }
562
- else if (isManagedUltraworkSession(state, input.sessionID)) {
614
+ else if (getManagedSession(state, input.sessionID)?.kind === "root") {
563
615
  await removeManagedUltraworkSession(state, input.sessionID, `session switched to agent \"${input.agent}\"`);
564
616
  }
617
+ else if (getManagedSession(state, input.sessionID)?.kind === "child") {
618
+ await updateManagedSessionAgent(state, input.sessionID, input.agent);
619
+ }
565
620
  // Log model and agent for observability
566
621
  const modelId = input.model && "id" in input.model
567
622
  ? input.model.id
@@ -617,7 +672,12 @@ function createEventHandler(state) {
617
672
  console.log(`[opencode-immune] Rate limit detected for session ${sessionID}. ` +
618
673
  `Retry will use fallback model ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
619
674
  }
620
- const fallbackModel = state.managedUltraworkSessions.get(sessionID)?.fallbackModel;
675
+ const managedSession = state.managedUltraworkSessions.get(sessionID);
676
+ const fallbackModel = managedSession?.fallbackModel;
677
+ const retryAgent = managedSession?.agent || ULTRAWORK_AGENT;
678
+ const retryText = retryAgent === ULTRAWORK_AGENT
679
+ ? "[SYSTEM: Previous API call failed with a transient error. Re-read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use the exact neutral prompt from your Step 5 table for the next router call. Do NOT analyze or evaluate file contents.]"
680
+ : `[SYSTEM: Previous API call failed with a transient error. Resume the current task in your current role as ${retryAgent}. Continue from the existing session state. Do not restart from scratch unless the current session state is missing.]`;
621
681
  console.log(`[opencode-immune] Session error detected (attempt ${count + 1}/${MAX_RETRIES}). ` +
622
682
  `Waiting ${delay / 1000}s before retry...`);
623
683
  const timer = setTimeout(async () => {
@@ -629,11 +689,11 @@ function createEventHandler(state) {
629
689
  await state.input.client.session.promptAsync({
630
690
  body: {
631
691
  ...(fallbackModel ? { model: fallbackModel } : {}),
632
- agent: ULTRAWORK_AGENT,
692
+ agent: retryAgent,
633
693
  parts: [
634
694
  {
635
695
  type: "text",
636
- text: "[SYSTEM: Previous API call failed with a transient error. Re-read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use the exact neutral prompt from your Step 5 table for the next router call. Do NOT analyze or evaluate file contents.]",
696
+ text: retryText,
637
697
  },
638
698
  ],
639
699
  },
@@ -696,7 +756,7 @@ const NEXT_TASK_PATTERN = /Next task:\s*(.+)/;
696
756
  function createMultiCycleHandler(state) {
697
757
  return async (input, output) => {
698
758
  const sessionID = input.sessionID;
699
- if (!isManagedUltraworkSession(state, sessionID))
759
+ if (!isManagedRootUltraworkSession(state, sessionID))
700
760
  return;
701
761
  // Extract text content from parts
702
762
  const parts = output.parts ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"