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.
- package/dist/plugin.js +65 -54
- 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
|
-
*
|
|
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
|
|
921
|
+
function createTextCompleteHandler(state) {
|
|
920
922
|
return async (input, output) => {
|
|
921
923
|
const sessionID = input.sessionID;
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
934
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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);
|
|
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 = {
|