opencode-immune 1.0.18 → 1.0.20

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 +65 -54
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -467,12 +467,12 @@ function createTodoEnforcerChatMessage(state) {
467
467
  await addManagedUltraworkSession(state, sessionID);
468
468
  await writeUltraworkMarker(state);
469
469
  }
470
- else if (sessionID && agent && record?.kind === "root") {
471
- await removeManagedUltraworkSession(state, sessionID, `session taken over by agent \"${agent}\"`);
472
- }
473
470
  else if (sessionID && agent && record?.kind === "child") {
474
471
  await updateManagedSessionAgent(state, sessionID, agent);
475
472
  }
473
+ // NOTE: Do NOT remove managed root sessions when agent changes.
474
+ // 0-ultrawork delegates to subagents (1-van, 7-backlog, etc.) which
475
+ // may appear as agent in chat.message but the session is still managed.
476
476
  // On user message, check previous assistant turn's counters
477
477
  // then reset for next turn
478
478
  if (state.toolCallCount > 3 && !state.todoWriteUsed) {
@@ -791,12 +791,11 @@ function createFallbackModels(state) {
791
791
  }
792
792
  }
793
793
  }
794
- else if (getManagedSession(state, input.sessionID)?.kind === "root") {
795
- await removeManagedUltraworkSession(state, input.sessionID, `session switched to agent \"${input.agent}\"`);
796
- }
797
794
  else if (getManagedSession(state, input.sessionID)?.kind === "child") {
798
795
  await updateManagedSessionAgent(state, input.sessionID, input.agent);
799
796
  }
797
+ // NOTE: Do NOT remove managed root sessions when agent changes in chat.params.
798
+ // Subagent calls from 0-ultrawork (1-van, 7-backlog, etc.) use the same session.
800
799
  // Log model and agent for observability
801
800
  const modelId = input.model && "id" in input.model
802
801
  ? input.model.id
@@ -903,64 +902,37 @@ function createEventHandler(state) {
903
902
  }
904
903
  // ═══════════════════════════════════════════════════════════════════════════════
905
904
  // HOOK 9: MULTI-CYCLE AUTOMATION (PRE_COMMIT + CYCLE_COMPLETE)
905
+ // Uses experimental.text.complete to detect signal markers in assistant output.
906
+ // chat.message only fires for user messages (output.message is UserMessage),
907
+ // so signal detection MUST use text.complete which fires for assistant text.
906
908
  // ═══════════════════════════════════════════════════════════════════════════════
907
909
  const MAX_CYCLES = 10;
908
910
  const PRE_COMMIT_MARKER = "0-ULTRAWORK: PRE_COMMIT";
909
911
  const CYCLE_COMPLETE_MARKER = "0-ULTRAWORK: CYCLE_COMPLETE";
910
912
  const NEXT_TASK_PATTERN = /Next task:\s*(.+)/;
911
- const RATE_LIMIT_MESSAGE_PATTERN = /too many requests|rate_limit|rate limit/i;
912
913
  const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
913
914
  /**
914
- * chat.message part: scans assistant messages for PRE_COMMIT and CYCLE_COMPLETE markers.
915
+ * experimental.text.complete: scans completed assistant text for signal markers.
915
916
  *
916
917
  * PRE_COMMIT → executes /commit via client.session.command()
917
918
  * CYCLE_COMPLETE → creates a new session and sends bootstrap prompt
919
+ * ALL_CYCLES_COMPLETE → clears ultrawork marker
918
920
  */
919
- function createMultiCycleHandler(state) {
921
+ function createTextCompleteHandler(state) {
920
922
  return async (input, output) => {
921
923
  const sessionID = input.sessionID;
922
- // Extract text content from parts
923
- const parts = output.parts ?? [];
924
- let messageContent = "";
925
- for (const p of parts) {
926
- if ("type" in p && p.type === "text" && "text" in p) {
927
- messageContent += " " + p.text;
928
- }
929
- }
930
- messageContent = messageContent.trim();
931
- if (!messageContent)
924
+ const text = output.text ?? "";
925
+ if (!text)
932
926
  return;
933
- const messageRole = output.message?.role;
934
- // ── ALL_CYCLES_COMPLETE: clear ultrawork marker (checked before managed session guard) ──
935
- if (messageContent.includes(ALL_CYCLES_COMPLETE_MARKER)) {
927
+ // ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
928
+ if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
936
929
  await clearUltraworkMarker(state);
937
- console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, ultrawork marker cleared.");
938
- }
939
- if (!isManagedRootUltraworkSession(state, sessionID))
940
- return;
941
- const managedSession = getManagedSession(state, sessionID);
942
- if (sessionID &&
943
- messageRole === "assistant" &&
944
- RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
945
- if (managedSession && !managedSession.fallbackModel) {
946
- await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
947
- console.log(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
948
- `Fallback model pinned to ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
949
- }
950
- if (managedSession) {
951
- scheduleManagedSessionRetry(state, sessionID, {
952
- delayMs: 1_000,
953
- reason: "rate-limit message fallback",
954
- countAgainstBudget: false,
955
- abortBeforePrompt: true,
956
- });
957
- }
930
+ console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected in text.complete, ultrawork marker cleared.");
958
931
  }
959
932
  // ── PRE_COMMIT: execute /commit ──
960
- if (messageContent.includes(PRE_COMMIT_MARKER) && !state.commitPending) {
933
+ if (text.includes(PRE_COMMIT_MARKER) && !state.commitPending && sessionID) {
961
934
  state.commitPending = true;
962
- console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected, executing /commit...");
963
- // Small delay to let the message finish rendering
935
+ console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected in text.complete, executing /commit...");
964
936
  setTimeout(async () => {
965
937
  try {
966
938
  await state.input.client.session.command({
@@ -981,28 +953,25 @@ function createMultiCycleHandler(state) {
981
953
  }, 2_000);
982
954
  }
983
955
  // ── CYCLE_COMPLETE: create new session ──
984
- if (messageContent.includes(CYCLE_COMPLETE_MARKER)) {
956
+ if (text.includes(CYCLE_COMPLETE_MARKER)) {
985
957
  state.cycleCount++;
986
958
  if (state.cycleCount >= MAX_CYCLES) {
987
959
  console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
988
960
  await clearUltraworkMarker(state);
989
961
  return;
990
962
  }
991
- // Extract next task description
992
- const taskMatch = messageContent.match(NEXT_TASK_PATTERN);
963
+ const taskMatch = text.match(NEXT_TASK_PATTERN);
993
964
  const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
994
- console.log(`[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected (cycle ${state.cycleCount}/${MAX_CYCLES}). ` +
965
+ console.log(`[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected in text.complete (cycle ${state.cycleCount}/${MAX_CYCLES}). ` +
995
966
  `Creating new session for: "${nextTask}"`);
996
967
  // Delay to let commit finish
997
968
  setTimeout(async () => {
998
969
  try {
999
- // Create a new session
1000
970
  const createResult = await state.input.client.session.create({
1001
971
  body: {
1002
972
  title: `Ultrawork Cycle ${state.cycleCount + 1}`,
1003
973
  },
1004
974
  });
1005
- // Extract new session ID from the response
1006
975
  const newSessionData = createResult?.data;
1007
976
  const newSessionID = newSessionData?.id;
1008
977
  if (!newSessionID) {
@@ -1011,7 +980,6 @@ function createMultiCycleHandler(state) {
1011
980
  }
1012
981
  console.log(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1013
982
  await addManagedUltraworkSession(state, newSessionID);
1014
- // Send bootstrap prompt to the new session
1015
983
  await state.input.client.session.promptAsync({
1016
984
  body: {
1017
985
  agent: ULTRAWORK_AGENT,
@@ -1029,7 +997,49 @@ function createMultiCycleHandler(state) {
1029
997
  catch (err) {
1030
998
  console.error("[opencode-immune] Multi-Cycle: Failed to create new session or send prompt:", err);
1031
999
  }
1032
- }, 8_000); // 8s delay: let /commit finish first
1000
+ }, 8_000);
1001
+ }
1002
+ };
1003
+ }
1004
+ /**
1005
+ * chat.message part: handles rate-limit text detection in managed root sessions.
1006
+ * NOTE: chat.message fires for user messages only (output.message = UserMessage).
1007
+ * Signal detection (PRE_COMMIT/CYCLE_COMPLETE) moved to experimental.text.complete.
1008
+ */
1009
+ function createMultiCycleHandler(state) {
1010
+ return async (input, output) => {
1011
+ const sessionID = input.sessionID;
1012
+ // Extract text content from parts
1013
+ const parts = output.parts ?? [];
1014
+ let messageContent = "";
1015
+ for (const p of parts) {
1016
+ if ("type" in p && p.type === "text" && "text" in p) {
1017
+ messageContent += " " + p.text;
1018
+ }
1019
+ }
1020
+ messageContent = messageContent.trim();
1021
+ if (!messageContent)
1022
+ return;
1023
+ if (!isManagedRootUltraworkSession(state, sessionID))
1024
+ return;
1025
+ const managedSession = getManagedSession(state, sessionID);
1026
+ // Rate-limit message detection (only for managed root sessions)
1027
+ const RATE_LIMIT_MESSAGE_PATTERN = /too many requests|rate_limit|rate limit/i;
1028
+ if (sessionID &&
1029
+ RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
1030
+ if (managedSession && !managedSession.fallbackModel) {
1031
+ await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1032
+ console.log(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1033
+ `Fallback model pinned to ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1034
+ }
1035
+ if (managedSession) {
1036
+ scheduleManagedSessionRetry(state, sessionID, {
1037
+ delayMs: 1_000,
1038
+ reason: "rate-limit message fallback",
1039
+ countAgainstBudget: false,
1040
+ abortBeforePrompt: true,
1041
+ });
1042
+ }
1033
1043
  }
1034
1044
  };
1035
1045
  }
@@ -1166,6 +1176,7 @@ async function server(input) {
1166
1176
  "tool.execute.after": withErrorBoundary("tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1167
1177
  "experimental.chat.system.transform": withErrorBoundary("experimental.chat.system.transform", createSystemTransform(state)),
1168
1178
  "experimental.session.compacting": withErrorBoundary("experimental.session.compacting", createCompactionHandler(state)),
1179
+ "experimental.text.complete": withErrorBoundary("experimental.text.complete", createTextCompleteHandler(state)),
1169
1180
  };
1170
1181
  }
1171
1182
  exports.default = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"