opencode-immune 1.0.19 → 1.0.21

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 +68 -49
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -902,86 +902,84 @@ function createEventHandler(state) {
902
902
  }
903
903
  // ═══════════════════════════════════════════════════════════════════════════════
904
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.
905
908
  // ═══════════════════════════════════════════════════════════════════════════════
906
909
  const MAX_CYCLES = 10;
907
910
  const PRE_COMMIT_MARKER = "0-ULTRAWORK: PRE_COMMIT";
908
911
  const CYCLE_COMPLETE_MARKER = "0-ULTRAWORK: CYCLE_COMPLETE";
909
912
  const NEXT_TASK_PATTERN = /Next task:\s*(.+)/;
910
- const RATE_LIMIT_MESSAGE_PATTERN = /too many requests|rate_limit|rate limit/i;
911
913
  const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
912
914
  /**
913
- * chat.message part: scans assistant messages for PRE_COMMIT and CYCLE_COMPLETE markers.
915
+ * experimental.text.complete: scans completed assistant text for signal markers.
914
916
  *
915
- * PRE_COMMIT executes /commit via client.session.command()
916
- * CYCLE_COMPLETE creates a new session and sends bootstrap prompt
917
+ * Handles the full cycle-end sequence:
918
+ * 1. PRE_COMMIT detected execute /commit await result
919
+ * 2. CYCLE_COMPLETE detected → create new session → send AUTO-CYCLE prompt
920
+ * Steps are sequential: new session only starts AFTER commit completes (or fails).
921
+ *
922
+ * ALL_CYCLES_COMPLETE → clears ultrawork marker, no new session.
917
923
  */
918
- function createMultiCycleHandler(state) {
924
+ function createTextCompleteHandler(state) {
919
925
  return async (input, output) => {
920
926
  const sessionID = input.sessionID;
921
- // Extract text content from parts
922
- const parts = output.parts ?? [];
923
- let messageContent = "";
924
- for (const p of parts) {
925
- if ("type" in p && p.type === "text" && "text" in p) {
926
- messageContent += " " + p.text;
927
- }
928
- }
929
- messageContent = messageContent.trim();
930
- if (!messageContent)
927
+ const text = output.text ?? "";
928
+ if (!text)
931
929
  return;
932
- const messageRole = output.message?.role;
933
- // ── ALL_CYCLES_COMPLETE: clear ultrawork marker (checked before managed session guard) ──
934
- if (messageContent.includes(ALL_CYCLES_COMPLETE_MARKER)) {
930
+ // ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
931
+ if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
935
932
  await clearUltraworkMarker(state);
936
- console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, ultrawork marker cleared.");
933
+ console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected in text.complete, ultrawork marker cleared.");
934
+ return;
937
935
  }
938
- // ── PRE_COMMIT: execute /commit (checked before managed session guard) ──
939
- if (messageContent.includes(PRE_COMMIT_MARKER) && !state.commitPending && sessionID) {
940
- state.commitPending = true;
941
- console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected, executing /commit...");
942
- // Small delay to let the message finish rendering
943
- setTimeout(async () => {
936
+ const hasPRECOMMIT = text.includes(PRE_COMMIT_MARKER);
937
+ const hasCYCLECOMPLETE = text.includes(CYCLE_COMPLETE_MARKER);
938
+ if (!hasPRECOMMIT && !hasCYCLECOMPLETE)
939
+ return;
940
+ // Run the full sequence in one async flow (no timers):
941
+ // commit wait → new session
942
+ // Use setTimeout(0) only to not block the hook return.
943
+ setTimeout(async () => {
944
+ // ── Step 1: PRE_COMMIT → execute /commit and await result ──
945
+ if (hasPRECOMMIT && !state.commitPending && sessionID) {
946
+ state.commitPending = true;
947
+ console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected, executing /commit...");
944
948
  try {
945
- await state.input.client.session.command({
949
+ const commitResult = await state.input.client.session.command({
946
950
  body: {
947
951
  command: "/commit",
948
952
  arguments: "",
949
953
  },
950
954
  path: { id: sessionID },
951
955
  });
952
- console.log("[opencode-immune] Multi-Cycle: /commit executed successfully.");
956
+ console.log("[opencode-immune] Multi-Cycle: /commit completed.", commitResult?.response?.status ?? "");
953
957
  }
954
958
  catch (err) {
955
- console.error("[opencode-immune] Multi-Cycle: /commit failed:", err);
959
+ console.error("[opencode-immune] Multi-Cycle: /commit failed (continuing to next cycle anyway):", err);
956
960
  }
957
961
  finally {
958
962
  state.commitPending = false;
959
963
  }
960
- }, 2_000);
961
- }
962
- // ── CYCLE_COMPLETE: create new session (checked before managed session guard) ──
963
- if (messageContent.includes(CYCLE_COMPLETE_MARKER)) {
964
- state.cycleCount++;
965
- if (state.cycleCount >= MAX_CYCLES) {
966
- console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
967
- await clearUltraworkMarker(state);
968
- return;
969
964
  }
970
- // Extract next task description
971
- const taskMatch = messageContent.match(NEXT_TASK_PATTERN);
972
- const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
973
- console.log(`[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected (cycle ${state.cycleCount}/${MAX_CYCLES}). ` +
974
- `Creating new session for: "${nextTask}"`);
975
- // Delay to let commit finish
976
- setTimeout(async () => {
965
+ // ── Step 2: CYCLE_COMPLETE → create new session ──
966
+ if (hasCYCLECOMPLETE) {
967
+ state.cycleCount++;
968
+ if (state.cycleCount >= MAX_CYCLES) {
969
+ console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
970
+ await clearUltraworkMarker(state);
971
+ return;
972
+ }
973
+ const taskMatch = text.match(NEXT_TASK_PATTERN);
974
+ const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
975
+ console.log(`[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected (cycle ${state.cycleCount}/${MAX_CYCLES}). ` +
976
+ `Creating new session for: "${nextTask}"`);
977
977
  try {
978
- // Create a new session
979
978
  const createResult = await state.input.client.session.create({
980
979
  body: {
981
980
  title: `Ultrawork Cycle ${state.cycleCount + 1}`,
982
981
  },
983
982
  });
984
- // Extract new session ID from the response
985
983
  const newSessionData = createResult?.data;
986
984
  const newSessionID = newSessionData?.id;
987
985
  if (!newSessionID) {
@@ -990,7 +988,6 @@ function createMultiCycleHandler(state) {
990
988
  }
991
989
  console.log(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
992
990
  await addManagedUltraworkSession(state, newSessionID);
993
- // Send bootstrap prompt to the new session
994
991
  await state.input.client.session.promptAsync({
995
992
  body: {
996
993
  agent: ULTRAWORK_AGENT,
@@ -1008,14 +1005,35 @@ function createMultiCycleHandler(state) {
1008
1005
  catch (err) {
1009
1006
  console.error("[opencode-immune] Multi-Cycle: Failed to create new session or send prompt:", err);
1010
1007
  }
1011
- }, 8_000); // 8s delay: let /commit finish first
1008
+ }
1009
+ }, 0);
1010
+ };
1011
+ }
1012
+ /**
1013
+ * chat.message part: handles rate-limit text detection in managed root sessions.
1014
+ * NOTE: chat.message fires for user messages only (output.message = UserMessage).
1015
+ * Signal detection (PRE_COMMIT/CYCLE_COMPLETE) moved to experimental.text.complete.
1016
+ */
1017
+ function createMultiCycleHandler(state) {
1018
+ return async (input, output) => {
1019
+ const sessionID = input.sessionID;
1020
+ // Extract text content from parts
1021
+ const parts = output.parts ?? [];
1022
+ let messageContent = "";
1023
+ for (const p of parts) {
1024
+ if ("type" in p && p.type === "text" && "text" in p) {
1025
+ messageContent += " " + p.text;
1026
+ }
1012
1027
  }
1028
+ messageContent = messageContent.trim();
1029
+ if (!messageContent)
1030
+ return;
1013
1031
  if (!isManagedRootUltraworkSession(state, sessionID))
1014
1032
  return;
1015
1033
  const managedSession = getManagedSession(state, sessionID);
1016
1034
  // Rate-limit message detection (only for managed root sessions)
1035
+ const RATE_LIMIT_MESSAGE_PATTERN = /too many requests|rate_limit|rate limit/i;
1017
1036
  if (sessionID &&
1018
- messageRole === "assistant" &&
1019
1037
  RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
1020
1038
  if (managedSession && !managedSession.fallbackModel) {
1021
1039
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
@@ -1166,6 +1184,7 @@ async function server(input) {
1166
1184
  "tool.execute.after": withErrorBoundary("tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1167
1185
  "experimental.chat.system.transform": withErrorBoundary("experimental.chat.system.transform", createSystemTransform(state)),
1168
1186
  "experimental.session.compacting": withErrorBoundary("experimental.session.compacting", createCompactionHandler(state)),
1187
+ "experimental.text.complete": withErrorBoundary("experimental.text.complete", createTextCompleteHandler(state)),
1169
1188
  };
1170
1189
  }
1171
1190
  exports.default = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"