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.
- package/dist/plugin.js +68 -49
- 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
|
-
*
|
|
915
|
+
* experimental.text.complete: scans completed assistant text for signal markers.
|
|
914
916
|
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
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
|
|
924
|
+
function createTextCompleteHandler(state) {
|
|
919
925
|
return async (input, output) => {
|
|
920
926
|
const sessionID = input.sessionID;
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
933
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
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
|
-
//
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
}
|
|
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 = {
|