opencode-immune 1.0.7 → 1.0.9
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 +58 -66
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -12,7 +12,7 @@ function createState(input) {
|
|
|
12
12
|
managedUltraworkSessions: new Map(),
|
|
13
13
|
sessionRetryTimers: new Map(),
|
|
14
14
|
sessionErrorRetryCount: new Map(),
|
|
15
|
-
|
|
15
|
+
ultraworkMarkerPath: (0, path_1.join)(input.directory, ".opencode", "state", "ultrawork-active.json"),
|
|
16
16
|
diagnosticsLogPath: (0, path_1.join)(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
|
|
17
17
|
lastEditAttempt: null,
|
|
18
18
|
toolCallCount: 0,
|
|
@@ -52,21 +52,6 @@ function pruneExpiredManagedSessions(state, now = Date.now()) {
|
|
|
52
52
|
}
|
|
53
53
|
return removed;
|
|
54
54
|
}
|
|
55
|
-
async function writeManagedSessionsCache(state) {
|
|
56
|
-
const removed = pruneExpiredManagedSessions(state);
|
|
57
|
-
const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
|
|
58
|
-
const tempPath = `${state.managedSessionsCachePath}.tmp`;
|
|
59
|
-
const payload = {
|
|
60
|
-
version: 1,
|
|
61
|
-
sessions: Object.fromEntries(state.managedUltraworkSessions.entries()),
|
|
62
|
-
};
|
|
63
|
-
await (0, promises_1.mkdir)(cacheDir, { recursive: true });
|
|
64
|
-
await (0, promises_1.writeFile)(tempPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
65
|
-
await (0, promises_1.rename)(tempPath, state.managedSessionsCachePath);
|
|
66
|
-
if (removed > 0) {
|
|
67
|
-
console.log(`[opencode-immune] Pruned ${removed} expired managed ultrawork session(s) while writing cache.`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
55
|
async function writeDiagnosticLog(state, event, data = {}) {
|
|
71
56
|
try {
|
|
72
57
|
const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
|
|
@@ -78,46 +63,37 @@ async function writeDiagnosticLog(state, event, data = {}) {
|
|
|
78
63
|
// diagnostics must never affect runtime behavior
|
|
79
64
|
}
|
|
80
65
|
}
|
|
81
|
-
|
|
66
|
+
// ── Ultrawork Marker File ──
|
|
67
|
+
async function writeUltraworkMarker(state) {
|
|
68
|
+
try {
|
|
69
|
+
const dir = (0, path_1.join)(state.input.directory, ".opencode", "state");
|
|
70
|
+
await (0, promises_1.mkdir)(dir, { recursive: true });
|
|
71
|
+
const payload = JSON.stringify({
|
|
72
|
+
active: true,
|
|
73
|
+
updatedAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
await (0, promises_1.writeFile)(state.ultraworkMarkerPath, payload, "utf-8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// marker write must never affect runtime
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function clearUltraworkMarker(state) {
|
|
82
82
|
try {
|
|
83
|
-
|
|
83
|
+
await (0, promises_1.unlink)(state.ultraworkMarkerPath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// file may not exist — that's fine
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function isUltraworkMarkerActive(state) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await (0, promises_1.readFile)(state.ultraworkMarkerPath, "utf-8");
|
|
84
92
|
const parsed = JSON.parse(raw);
|
|
85
|
-
|
|
86
|
-
console.warn(`[opencode-immune] Managed sessions cache at ${state.managedSessionsCachePath} has unsupported format. Ignoring.`);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
for (const [sessionID, record] of Object.entries(parsed.sessions)) {
|
|
90
|
-
if (!record)
|
|
91
|
-
continue;
|
|
92
|
-
state.managedUltraworkSessions.set(sessionID, {
|
|
93
|
-
kind: record.kind === "child" ? "child" : "root",
|
|
94
|
-
agent: typeof record.agent === "string" && record.agent.length > 0
|
|
95
|
-
? record.agent
|
|
96
|
-
: ULTRAWORK_AGENT,
|
|
97
|
-
rootSessionID: typeof record.rootSessionID === "string" && record.rootSessionID.length > 0
|
|
98
|
-
? record.rootSessionID
|
|
99
|
-
: sessionID,
|
|
100
|
-
createdAt: typeof record.createdAt === "number" ? record.createdAt : Date.now(),
|
|
101
|
-
updatedAt: typeof record.updatedAt === "number" ? record.updatedAt : Date.now(),
|
|
102
|
-
fallbackModel: record.fallbackModel &&
|
|
103
|
-
typeof record.fallbackModel.providerID === "string" &&
|
|
104
|
-
typeof record.fallbackModel.modelID === "string"
|
|
105
|
-
? record.fallbackModel
|
|
106
|
-
: undefined,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
const removed = pruneExpiredManagedSessions(state);
|
|
110
|
-
console.log(`[opencode-immune] Loaded ${state.managedUltraworkSessions.size} managed ultrawork session(s) from cache.`);
|
|
111
|
-
if (removed > 0) {
|
|
112
|
-
console.log(`[opencode-immune] Pruned ${removed} expired managed ultrawork session(s) on startup.`);
|
|
113
|
-
await writeManagedSessionsCache(state);
|
|
114
|
-
}
|
|
93
|
+
return parsed?.active === true;
|
|
115
94
|
}
|
|
116
|
-
catch
|
|
117
|
-
|
|
118
|
-
if (message.includes("ENOENT"))
|
|
119
|
-
return;
|
|
120
|
-
console.warn(`[opencode-immune] Failed to read managed sessions cache. Starting fresh.`);
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
121
97
|
}
|
|
122
98
|
}
|
|
123
99
|
async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now()) {
|
|
@@ -137,7 +113,6 @@ async function addManagedUltraworkSession(state, sessionID, timestamp = Date.now
|
|
|
137
113
|
return;
|
|
138
114
|
}
|
|
139
115
|
state.managedUltraworkSessions.set(sessionID, nextRecord);
|
|
140
|
-
await writeManagedSessionsCache(state);
|
|
141
116
|
}
|
|
142
117
|
async function addManagedChildSession(state, sessionID, parentSessionID, timestamp = Date.now()) {
|
|
143
118
|
const parent = state.managedUltraworkSessions.get(parentSessionID);
|
|
@@ -152,7 +127,6 @@ async function addManagedChildSession(state, sessionID, parentSessionID, timesta
|
|
|
152
127
|
updatedAt: timestamp,
|
|
153
128
|
fallbackModel: existing?.fallbackModel ?? parent.fallbackModel,
|
|
154
129
|
});
|
|
155
|
-
await writeManagedSessionsCache(state);
|
|
156
130
|
}
|
|
157
131
|
function cancelPendingSessionRetry(state, sessionID, reason) {
|
|
158
132
|
const timer = state.sessionRetryTimers.get(sessionID);
|
|
@@ -168,7 +142,6 @@ async function removeManagedUltraworkSession(state, sessionID, reason) {
|
|
|
168
142
|
const existed = state.managedUltraworkSessions.delete(sessionID);
|
|
169
143
|
if (!existed)
|
|
170
144
|
return;
|
|
171
|
-
await writeManagedSessionsCache(state);
|
|
172
145
|
console.log(`[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
|
|
173
146
|
}
|
|
174
147
|
async function updateManagedSessionAgent(state, sessionID, agent) {
|
|
@@ -180,7 +153,6 @@ async function updateManagedSessionAgent(state, sessionID, agent) {
|
|
|
180
153
|
agent,
|
|
181
154
|
updatedAt: Date.now(),
|
|
182
155
|
});
|
|
183
|
-
await writeManagedSessionsCache(state);
|
|
184
156
|
}
|
|
185
157
|
function markUltraworkSessionActive(state, sessionID) {
|
|
186
158
|
const existing = state.managedUltraworkSessions.get(sessionID);
|
|
@@ -220,7 +192,6 @@ async function setSessionFallbackModel(state, sessionID, model) {
|
|
|
220
192
|
updatedAt: Date.now(),
|
|
221
193
|
fallbackModel: model,
|
|
222
194
|
});
|
|
223
|
-
await writeManagedSessionsCache(state);
|
|
224
195
|
}
|
|
225
196
|
function getManagedSessionRetryContext(state, sessionID) {
|
|
226
197
|
const managedSession = state.managedUltraworkSessions.get(sessionID);
|
|
@@ -461,6 +432,7 @@ function createTodoEnforcerChatMessage(state) {
|
|
|
461
432
|
const record = getManagedSession(state, sessionID);
|
|
462
433
|
if (sessionID && agent === ULTRAWORK_AGENT) {
|
|
463
434
|
await addManagedUltraworkSession(state, sessionID);
|
|
435
|
+
await writeUltraworkMarker(state);
|
|
464
436
|
}
|
|
465
437
|
else if (sessionID && agent && record?.kind === "root") {
|
|
466
438
|
await removeManagedUltraworkSession(state, sessionID, `session taken over by agent \"${agent}\"`);
|
|
@@ -496,23 +468,36 @@ function createSessionRecoveryEvent(state) {
|
|
|
496
468
|
const sessionInfo = event.properties?.info;
|
|
497
469
|
const sessionID = sessionInfo?.id ?? event.properties?.sessionID;
|
|
498
470
|
const parentID = sessionInfo?.parentID;
|
|
471
|
+
// Register child sessions under their parent
|
|
499
472
|
if (sessionID && parentID && isManagedUltraworkSession(state, parentID)) {
|
|
500
473
|
await addManagedChildSession(state, sessionID, parentID);
|
|
474
|
+
return;
|
|
501
475
|
}
|
|
502
|
-
|
|
476
|
+
// Skip child sessions that don't belong to a managed parent
|
|
477
|
+
if (parentID) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// For root sessions (no parentID): check ultrawork marker + tasks.md.
|
|
481
|
+
// If marker is active and an incomplete task exists, auto-resume.
|
|
482
|
+
// This covers the restart case where the session ID is new but work is pending.
|
|
483
|
+
if (!sessionID) {
|
|
503
484
|
return;
|
|
504
485
|
}
|
|
505
|
-
|
|
486
|
+
const markerActive = await isUltraworkMarkerActive(state);
|
|
487
|
+
if (!markerActive) {
|
|
488
|
+
console.log(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
console.log(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
|
|
506
492
|
const recovery = await parseTasksFile(state.input.directory);
|
|
507
493
|
if (recovery) {
|
|
508
494
|
state.recoveryContext = recovery;
|
|
509
495
|
console.log(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
|
|
510
|
-
if (
|
|
496
|
+
if (recovery.phase !== "ARCHIVE: DONE") {
|
|
497
|
+
// Register this root session as managed so retry/recovery works
|
|
498
|
+
await addManagedUltraworkSession(state, sessionID);
|
|
511
499
|
setTimeout(async () => {
|
|
512
500
|
try {
|
|
513
|
-
if (!isManagedRootUltraworkSession(state, sessionID)) {
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
501
|
await state.input.client.session.promptAsync({
|
|
517
502
|
body: {
|
|
518
503
|
agent: ULTRAWORK_AGENT,
|
|
@@ -722,6 +707,7 @@ function createFallbackModels(state) {
|
|
|
722
707
|
return async (input, _output) => {
|
|
723
708
|
if (input.agent === ULTRAWORK_AGENT) {
|
|
724
709
|
await addManagedUltraworkSession(state, input.sessionID);
|
|
710
|
+
await writeUltraworkMarker(state);
|
|
725
711
|
}
|
|
726
712
|
else if (getManagedSession(state, input.sessionID)?.kind === "root") {
|
|
727
713
|
await removeManagedUltraworkSession(state, input.sessionID, `session switched to agent \"${input.agent}\"`);
|
|
@@ -803,7 +789,7 @@ function createEventHandler(state) {
|
|
|
803
789
|
cancelPendingSessionRetry(state, sessionID, "session updated");
|
|
804
790
|
state.sessionErrorRetryCount.delete(sessionID);
|
|
805
791
|
if (markUltraworkSessionActive(state, sessionID)) {
|
|
806
|
-
|
|
792
|
+
// session activity tracked in-memory only
|
|
807
793
|
}
|
|
808
794
|
}
|
|
809
795
|
if (eventType === "session.deleted" && sessionID) {
|
|
@@ -831,6 +817,7 @@ const PRE_COMMIT_MARKER = "0-ULTRAWORK: PRE_COMMIT";
|
|
|
831
817
|
const CYCLE_COMPLETE_MARKER = "0-ULTRAWORK: CYCLE_COMPLETE";
|
|
832
818
|
const NEXT_TASK_PATTERN = /Next task:\s*(.+)/;
|
|
833
819
|
const RATE_LIMIT_MESSAGE_PATTERN = /too many requests|rate_limit|rate limit/i;
|
|
820
|
+
const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
|
|
834
821
|
/**
|
|
835
822
|
* chat.message part: scans assistant messages for PRE_COMMIT and CYCLE_COMPLETE markers.
|
|
836
823
|
*
|
|
@@ -872,6 +859,11 @@ function createMultiCycleHandler(state) {
|
|
|
872
859
|
});
|
|
873
860
|
}
|
|
874
861
|
}
|
|
862
|
+
// ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
|
|
863
|
+
if (messageContent.includes(ALL_CYCLES_COMPLETE_MARKER)) {
|
|
864
|
+
await clearUltraworkMarker(state);
|
|
865
|
+
console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, ultrawork marker cleared.");
|
|
866
|
+
}
|
|
875
867
|
// ── PRE_COMMIT: execute /commit ──
|
|
876
868
|
if (messageContent.includes(PRE_COMMIT_MARKER) && !state.commitPending) {
|
|
877
869
|
state.commitPending = true;
|
|
@@ -901,6 +893,7 @@ function createMultiCycleHandler(state) {
|
|
|
901
893
|
state.cycleCount++;
|
|
902
894
|
if (state.cycleCount >= MAX_CYCLES) {
|
|
903
895
|
console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
|
|
896
|
+
await clearUltraworkMarker(state);
|
|
904
897
|
return;
|
|
905
898
|
}
|
|
906
899
|
// Extract next task description
|
|
@@ -953,7 +946,6 @@ function createMultiCycleHandler(state) {
|
|
|
953
946
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
954
947
|
async function server(input) {
|
|
955
948
|
const state = createState(input);
|
|
956
|
-
await loadManagedSessionsCache(state);
|
|
957
949
|
console.log(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
|
|
958
950
|
// Compose tool.execute.after handlers:
|
|
959
951
|
// Todo Enforcer (counter) + Ralph Loop (edit error) + Comment Checker
|