@bybrawe/opencode-loop 0.5.37 → 0.6.0
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/CHANGELOG.md +26 -0
- package/README.md +31 -1
- package/examples/OPENCODE2-0.6.0.md +54 -0
- package/package.json +12 -8
- package/scripts/branch-hygiene.mjs +326 -0
- package/scripts/comprehensive-test.mjs +19 -10
- package/scripts/control-plane-until-test.mjs +39 -0
- package/scripts/fixtures/opencode2-real-adapter-probe.js +6 -1
- package/scripts/goal-compaction-safety-test.mjs +68 -0
- package/scripts/goal-companion-test.mjs +1 -1
- package/scripts/host-adapter-contract-test.mjs +1 -0
- package/scripts/install-node.mjs +105 -58
- package/scripts/install-test.mjs +6 -1
- package/scripts/install-with-goals.mjs +17 -4
- package/scripts/job-workspace-test.mjs +11 -1
- package/scripts/loopd-test.mjs +74 -4
- package/scripts/loopd.mjs +153 -25
- package/scripts/native-cli-host-canary.mjs +118 -0
- package/scripts/native-cli-io-test.mjs +66 -0
- package/scripts/native-cli-test.mjs +130 -0
- package/scripts/native-coexistence-test.mjs +38 -0
- package/scripts/native-entry-test.mjs +77 -0
- package/scripts/native-guide-test.mjs +45 -0
- package/scripts/native-install-pair-test.mjs +73 -0
- package/scripts/native-install-test.mjs +107 -0
- package/scripts/native-lifecycle-test.mjs +100 -0
- package/scripts/native-plugin-lifetime-test.mjs +92 -0
- package/scripts/native-presets-test.mjs +227 -0
- package/scripts/native-process-tree-fixture.mjs +72 -0
- package/scripts/native-process-tree-test.mjs +40 -0
- package/scripts/native-release.mjs +121 -0
- package/scripts/native-session-location-test.mjs +86 -0
- package/scripts/native-shell-test.mjs +38 -0
- package/scripts/native-v2-test.mjs +262 -0
- package/scripts/network-recovery-test.mjs +38 -1
- package/scripts/opencode-host.mjs +47 -0
- package/scripts/opencode2-loop-canary.mjs +119 -10
- package/scripts/package-native-smoke.mjs +31 -0
- package/scripts/publish-workflow-test.mjs +60 -14
- package/scripts/pulse-check.mjs +246 -0
- package/scripts/run-finalization-test.mjs +33 -0
- package/scripts/server-entry-test.mjs +15 -0
- package/scripts/session-error-recovery-test.mjs +186 -0
- package/scripts/session-status-test.mjs +42 -0
- package/scripts/v2-command-adapter-test.mjs +14 -2
- package/scripts/v2-plugin-sentinel-test.mjs +35 -4
- package/scripts/v2-runtime-adapter-test.mjs +75 -0
- package/src/index.js +227 -53
- package/src/native.js +3135 -0
- package/src/server.js +4055 -0
- package/src/source/core/continuation.js +23 -0
- package/src/source/core/jobs.js +17 -0
- package/src/source/core/process.js +6 -4
- package/src/source/core/progress.js +58 -0
- package/src/source/legacy-v1.js +14 -3
- package/src/source/native.js +1 -0
- package/src/source/opencode/host.js +1 -0
- package/src/source/opencode/loop-commands.js +1 -27
- package/src/source/opencode/loop-registration.js +2 -12
- package/src/source/opencode/messages.js +1 -1
- package/src/source/opencode2/capabilities.js +1 -0
- package/src/source/opencode2/commands.js +52 -11
- package/src/source/opencode2/event-bridge.js +15 -3
- package/src/source/opencode2/events.js +31 -0
- package/src/source/opencode2/experimental.js +53 -7
- package/src/source/opencode2/native-companion.js +24 -0
- package/src/source/opencode2/native-plugin.js +153 -0
- package/src/source/opencode2/native-policy.js +106 -0
- package/src/source/opencode2/native-runtime.js +464 -0
- package/src/source/opencode2/native-shell.js +81 -0
- package/src/source/opencode2/prompt-runtime.js +38 -6
- package/src/source/opencode2/runtime-adapter.js +15 -2
- package/src/source/opencode2/session-location.js +16 -0
- package/src/source/runtime/executor.js +83 -0
- package/src/source/runtime/goal-steering.js +17 -3
- package/src/source/runtime/job-workspace.js +11 -2
- package/src/source/runtime/run-finalization.js +10 -0
- package/src/source/runtime/session-status.js +16 -2
- package/src/source/runtime/terminal-guard.js +32 -7
- package/src/source/server.js +26 -0
- package/src/v1.js +1 -0
|
@@ -3,7 +3,7 @@ import plugin, {
|
|
|
3
3
|
OPENCODE_LOOP_V2_COMMANDS,
|
|
4
4
|
OPENCODE_LOOP_V2_PLUGIN_ID,
|
|
5
5
|
} from "../src/source/opencode2/experimental.js"
|
|
6
|
-
import { parseOpenCode2LoopCommandText } from "../src/source/opencode2/commands.js"
|
|
6
|
+
import { parseOpenCode2LoopCommandText, registerOpenCode2LoopCommands } from "../src/source/opencode2/commands.js"
|
|
7
7
|
import {
|
|
8
8
|
OPENCODE_LOOP_V2_COMMAND_SOURCE,
|
|
9
9
|
OPENCODE_LOOP_V2_HOST_REQUIREMENTS,
|
|
@@ -77,13 +77,43 @@ for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
|
|
|
77
77
|
assert.equal(command?.description, definition.description)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
let
|
|
80
|
+
let missingDraftMutationFailed = false
|
|
81
81
|
try {
|
|
82
82
|
await registered({})
|
|
83
83
|
} catch (error) {
|
|
84
|
-
|
|
84
|
+
missingDraftMutationFailed = /neither add\(\) nor update\(\)/.test(String(error))
|
|
85
85
|
}
|
|
86
|
-
assert.equal(
|
|
86
|
+
assert.equal(missingDraftMutationFailed, true)
|
|
87
|
+
|
|
88
|
+
const addedCommands = new Map()
|
|
89
|
+
const executedCommands = []
|
|
90
|
+
registerOpenCode2LoopCommands({
|
|
91
|
+
add(definition) {
|
|
92
|
+
addedCommands.set(definition.name, definition)
|
|
93
|
+
},
|
|
94
|
+
}, {
|
|
95
|
+
execute: async (event) => {
|
|
96
|
+
executedCommands.push(structuredClone(event))
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
assert.deepEqual([...addedCommands.keys()], Object.keys(OPENCODE_LOOP_V2_COMMANDS))
|
|
100
|
+
for (const [name, definition] of Object.entries(OPENCODE_LOOP_V2_COMMANDS)) {
|
|
101
|
+
const command = addedCommands.get(name)
|
|
102
|
+
assert.equal(command?.name, name)
|
|
103
|
+
assert.equal(command?.description, definition.description)
|
|
104
|
+
assert.equal(typeof command?.execute, "function")
|
|
105
|
+
}
|
|
106
|
+
await addedCommands.get("loop").execute({
|
|
107
|
+
sessionID: "ses_2_0_11",
|
|
108
|
+
prompt: { text: "devam et" },
|
|
109
|
+
delivery: "steer",
|
|
110
|
+
})
|
|
111
|
+
assert.deepEqual(executedCommands, [{
|
|
112
|
+
name: "loop",
|
|
113
|
+
sessionID: "ses_2_0_11",
|
|
114
|
+
arguments: "devam et",
|
|
115
|
+
delivery: "steer",
|
|
116
|
+
}])
|
|
87
117
|
|
|
88
118
|
assert.deepEqual(inspectOpenCode2Context(currentContext), {
|
|
89
119
|
commandTransform: true,
|
|
@@ -98,6 +128,7 @@ assert.deepEqual(inspectOpenCode2Context(currentContext), {
|
|
|
98
128
|
assert.deepEqual(inspectOpenCode2CommandDraft(currentDraft), {
|
|
99
129
|
list: true,
|
|
100
130
|
get: true,
|
|
131
|
+
add: false,
|
|
101
132
|
update: true,
|
|
102
133
|
remove: true,
|
|
103
134
|
})
|
|
@@ -164,6 +164,8 @@ async function verifyTwoTurnLoop({ directory, sessionID, events, prompts, native
|
|
|
164
164
|
sessionID: "ses_v2",
|
|
165
165
|
noReply: true,
|
|
166
166
|
parts: [{ type: "text", text: "OpenCode loop status:\nNo active loop jobs." }],
|
|
167
|
+
text: "OpenCode loop status:\nNo active loop jobs.",
|
|
168
|
+
resume: false,
|
|
167
169
|
agent: "build",
|
|
168
170
|
model: { providerID: "canary", modelID: "canary" },
|
|
169
171
|
})
|
|
@@ -301,6 +303,79 @@ async function verifyTwoTurnLoop({ directory, sessionID, events, prompts, native
|
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
{
|
|
307
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "opencode-loop-v2-2-0-11-command-add-"))
|
|
308
|
+
const sessionID = "ses_v2_2_0_11_add"
|
|
309
|
+
const events = controllableStream()
|
|
310
|
+
const prompts = []
|
|
311
|
+
const commands = new Map()
|
|
312
|
+
const ctx = {
|
|
313
|
+
app: { name: "opencode", version: "2.0.11", channel: "latest" },
|
|
314
|
+
location: { directory },
|
|
315
|
+
command: {
|
|
316
|
+
transform: async (callback) => {
|
|
317
|
+
await callback({
|
|
318
|
+
add(definition) {
|
|
319
|
+
commands.set(definition.name, definition)
|
|
320
|
+
},
|
|
321
|
+
})
|
|
322
|
+
return { dispose: async () => {} }
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
event: {
|
|
326
|
+
// OpenCode 2.0.11 exposes subscribe(options), so Function.length === 1
|
|
327
|
+
// even though it returns an AsyncIterable rather than accepting a callback.
|
|
328
|
+
subscribe(options) {
|
|
329
|
+
assert.equal(options, undefined)
|
|
330
|
+
return events.stream
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
session: {
|
|
334
|
+
prompt: async (input) => {
|
|
335
|
+
prompts.push(structuredClone(input))
|
|
336
|
+
return { accepted: true }
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const cleanup = await OpenCodeLoopV2ExperimentalPlugin.setup(ctx)
|
|
342
|
+
try {
|
|
343
|
+
assert.ok(commands.has("loop"), "OpenCode 2.0.11 add() command editor must receive /loop")
|
|
344
|
+
await commands.get("loop").execute({
|
|
345
|
+
sessionID,
|
|
346
|
+
prompt: { text: "0s --max-runs 2 continue exact 2.0.11 command path" },
|
|
347
|
+
delivery: "steer",
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const stateFile = path.join(directory, ".opencode", "opencode-loop", `${sessionID}.json`)
|
|
351
|
+
await waitFor(async () => {
|
|
352
|
+
try { return JSON.parse(await readFile(stateFile, "utf8")).jobs?.length === 1 } catch { return false }
|
|
353
|
+
}, "OpenCode 2.0.11 direct command state creation")
|
|
354
|
+
|
|
355
|
+
await waitFor(() => prompts.length === 1, "OpenCode 2.0.11 direct command first local-kick dispatch")
|
|
356
|
+
assert.equal(prompts[0].sessionID, sessionID)
|
|
357
|
+
assert.match(prompts[0].text || "", /continue exact 2\.0\.11 command path/)
|
|
358
|
+
|
|
359
|
+
events.push({
|
|
360
|
+
type: "session.status",
|
|
361
|
+
location: { directory },
|
|
362
|
+
data: { sessionID, status: { type: "busy" } },
|
|
363
|
+
})
|
|
364
|
+
events.push({
|
|
365
|
+
type: "session.status",
|
|
366
|
+
location: { directory },
|
|
367
|
+
data: { sessionID, status: { type: "idle" } },
|
|
368
|
+
})
|
|
369
|
+
await waitFor(() => prompts.length === 2, "OpenCode 2.0.11 second dispatch from native idle status")
|
|
370
|
+
const state = JSON.parse(await readFile(stateFile, "utf8"))
|
|
371
|
+
assert.equal(state.jobs[0].runCount, 2)
|
|
372
|
+
assert.equal(state.jobs[0].enabled, false)
|
|
373
|
+
} finally {
|
|
374
|
+
await cleanup?.()
|
|
375
|
+
await rm(directory, { recursive: true, force: true })
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
304
379
|
{
|
|
305
380
|
let commandDisposals = 0
|
|
306
381
|
const result = await OpenCodeLoopV2ExperimentalPlugin.setup({
|
package/src/index.js
CHANGED
|
@@ -619,7 +619,9 @@ async function runProcess(command, args, cwd, timeoutMs = 60000) {
|
|
|
619
619
|
const child = spawn(command, args, { cwd, shell: false, windowsHide: true });
|
|
620
620
|
const stdout = [];
|
|
621
621
|
const stderr = [];
|
|
622
|
+
let timedOut = false;
|
|
622
623
|
const timer = setTimeout(() => {
|
|
624
|
+
timedOut = true;
|
|
623
625
|
try {
|
|
624
626
|
child.kill("SIGTERM");
|
|
625
627
|
} catch {}
|
|
@@ -632,7 +634,7 @@ async function runProcess(command, args, cwd, timeoutMs = 60000) {
|
|
|
632
634
|
});
|
|
633
635
|
child.on("close", (code) => {
|
|
634
636
|
clearTimeout(timer);
|
|
635
|
-
resolve({ code: code ??
|
|
637
|
+
resolve({ code: timedOut ? 124 : code ?? -1, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
|
|
636
638
|
});
|
|
637
639
|
});
|
|
638
640
|
}
|
|
@@ -641,7 +643,9 @@ async function runShellCommand(command, cwd, timeoutMs = 120000) {
|
|
|
641
643
|
const child = spawn(command, [], { cwd, shell: true, windowsHide: true });
|
|
642
644
|
const stdout = [];
|
|
643
645
|
const stderr = [];
|
|
646
|
+
let timedOut = false;
|
|
644
647
|
const timer = setTimeout(() => {
|
|
648
|
+
timedOut = true;
|
|
645
649
|
try {
|
|
646
650
|
child.kill("SIGTERM");
|
|
647
651
|
} catch {}
|
|
@@ -654,7 +658,7 @@ async function runShellCommand(command, cwd, timeoutMs = 120000) {
|
|
|
654
658
|
});
|
|
655
659
|
child.on("close", (code) => {
|
|
656
660
|
clearTimeout(timer);
|
|
657
|
-
resolve({ code: code ??
|
|
661
|
+
resolve({ code: timedOut ? 124 : code ?? -1, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
|
|
658
662
|
});
|
|
659
663
|
});
|
|
660
664
|
}
|
|
@@ -819,6 +823,8 @@ async function activeRunCompletionFromMessages(directory, client, sessionID, act
|
|
|
819
823
|
if (!messages)
|
|
820
824
|
return "unknown";
|
|
821
825
|
const ordered = orderedSessionMessages(messages);
|
|
826
|
+
if (ordered.length === 0)
|
|
827
|
+
return "unknown";
|
|
822
828
|
const tail = ordered.at(-1);
|
|
823
829
|
const info = tail?.info || tail;
|
|
824
830
|
if (!info || info.role !== "assistant")
|
|
@@ -1086,6 +1092,19 @@ var NEXT_WORK_PATTERNS = [
|
|
|
1086
1092
|
/\bremaining (?:work|task|todo|step)/i,
|
|
1087
1093
|
/devam (?:edece\u011Fim|ediyorum|etmek gerek)(?=\s|[.,;:!\u2014-]|$)/i
|
|
1088
1094
|
];
|
|
1095
|
+
var WAITING_USER_PATTERNS = [
|
|
1096
|
+
/(?:^|\n)\s*onay bekleyenler\s*:/i,
|
|
1097
|
+
/(?:^|\n)\s*(?:kullan\u0131c\u0131|user) (?:onay\u0131|onayi|aksiyonu|i\u015Flemi|islemi|eri\u015Fimi|erisimi) (?:bekleniyor|gerekiyor|gerekli)\b/i,
|
|
1098
|
+
/\b(?:senin|sizin) (?:onay\u0131n|onay\u0131n\u0131z|onayiniz|aksiyonun|aksiyonunuz|eri\u015Fimin|erisimin) (?:bekleniyor|gerekiyor|gerekli)\b/i,
|
|
1099
|
+
/\b(?:onay|eri\u015Fim|erisim|yetki) olmadan (?:devam edemem|ilerleyemem|i\u015Flem yapamam|islem yapamam)\b/i,
|
|
1100
|
+
/(?:^|\n)\s*(?:waiting (?:for|on) (?:your|user) (?:approval|input|action|access|credentials?)|(?:approval|user action|access) (?:required|pending))\s*:?/i,
|
|
1101
|
+
/\b(?:requires?|needs?) (?:your|user) (?:approval|input|action|access|credentials?)\b/i,
|
|
1102
|
+
/\bblocked (?:pending|until|on) (?:your|user|approval|access|credentials?)\b/i
|
|
1103
|
+
];
|
|
1104
|
+
var WAITING_USER_NEGATIONS = [
|
|
1105
|
+
/\b(?:onay|eri\u015Fim|erisim|yetki) (?:gerekmiyor|gerekli de\u011Fil|gerekli degil|beklenmiyor)\b/i,
|
|
1106
|
+
/\bno (?:user )?(?:approval|input|action|access|credentials?) (?:is )?(?:needed|required|pending)\b/i
|
|
1107
|
+
];
|
|
1089
1108
|
function isContinuationShorthand(value) {
|
|
1090
1109
|
return CONTINUATION_SHORTHANDS.has(String(value || "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
1091
1110
|
}
|
|
@@ -1093,6 +1112,16 @@ function isCompletionBoundedContinuation(value) {
|
|
|
1093
1112
|
const text = String(value || "").trim();
|
|
1094
1113
|
return COMPLETION_BOUNDED_PATTERNS.some((pattern) => pattern.test(text));
|
|
1095
1114
|
}
|
|
1115
|
+
function isWaitingUserReply(value) {
|
|
1116
|
+
const text = String(value || "").trim();
|
|
1117
|
+
if (!text)
|
|
1118
|
+
return false;
|
|
1119
|
+
if (WAITING_USER_NEGATIONS.some((pattern) => pattern.test(text)))
|
|
1120
|
+
return false;
|
|
1121
|
+
if (NEXT_WORK_PATTERNS.some((pattern) => pattern.test(text)))
|
|
1122
|
+
return false;
|
|
1123
|
+
return WAITING_USER_PATTERNS.some((pattern) => pattern.test(text));
|
|
1124
|
+
}
|
|
1096
1125
|
function isTerminalNoWorkReply(value) {
|
|
1097
1126
|
const text = String(value || "").trim();
|
|
1098
1127
|
if (!text || NEXT_WORK_PATTERNS.some((pattern) => pattern.test(text)))
|
|
@@ -1215,6 +1244,20 @@ function goalStatusText(job) {
|
|
|
1215
1244
|
return "paused";
|
|
1216
1245
|
return status;
|
|
1217
1246
|
}
|
|
1247
|
+
function applyTestfixPreset(job, defaults = {}) {
|
|
1248
|
+
if (defaults.testfixPreset) {
|
|
1249
|
+
const defaultCommand = String(defaults.verifyCommand || "npm test");
|
|
1250
|
+
const parsedAction = String(job.action || "").trim();
|
|
1251
|
+
const usedDefaultAction = parsedAction === String(defaults.action || "").trim();
|
|
1252
|
+
if (!usedDefaultAction && job.verifyCommand === defaults.verifyCommand) {
|
|
1253
|
+
job.verifyCommand = parsedAction;
|
|
1254
|
+
job.action = `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${parsedAction}`;
|
|
1255
|
+
} else if (usedDefaultAction && job.verifyCommand !== defaults.verifyCommand) {
|
|
1256
|
+
job.action = `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${job.verifyCommand || defaultCommand}`;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return job;
|
|
1260
|
+
}
|
|
1218
1261
|
|
|
1219
1262
|
// src/source/opencode/command-router.js
|
|
1220
1263
|
var HANDLER_NAMES = [
|
|
@@ -1716,6 +1759,34 @@ function createGoalCommandHandlers(options = {}) {
|
|
|
1716
1759
|
import { promises as fs6 } from "fs";
|
|
1717
1760
|
import path7 from "path";
|
|
1718
1761
|
|
|
1762
|
+
// src/source/core/progress.js
|
|
1763
|
+
var DEFAULT_PROGRESS_MD = `# Progress
|
|
1764
|
+
|
|
1765
|
+
## Current Goal
|
|
1766
|
+
Describe the current project goal here.
|
|
1767
|
+
|
|
1768
|
+
## Agent Rules
|
|
1769
|
+
- Do not ask questions unless truly blocked.
|
|
1770
|
+
- Make reasonable assumptions and continue.
|
|
1771
|
+
- Work on unfinished TODOs in order.
|
|
1772
|
+
- Mark completed TODOs with [x].
|
|
1773
|
+
- Add new bugs, ideas, and follow-up work as TODOs.
|
|
1774
|
+
- Run tests, lint, or build when available.
|
|
1775
|
+
- Do not run destructive commands, force pushes, production deploys, or database resets.
|
|
1776
|
+
|
|
1777
|
+
## Active TODO
|
|
1778
|
+
- [ ] Review the project structure and pick the next safe improvement.
|
|
1779
|
+
|
|
1780
|
+
## Completed
|
|
1781
|
+
- [x] Created progress.md.
|
|
1782
|
+
|
|
1783
|
+
## Backlog Ideas
|
|
1784
|
+
- [ ] Add more project-specific tasks here.
|
|
1785
|
+
|
|
1786
|
+
## Blocked
|
|
1787
|
+
- None.
|
|
1788
|
+
`;
|
|
1789
|
+
|
|
1719
1790
|
// src/source/runtime/companion-goal.js
|
|
1720
1791
|
import { promises as fs4 } from "fs";
|
|
1721
1792
|
import path5 from "path";
|
|
@@ -1893,32 +1964,6 @@ async function listPersistedLoopSessions(directory, currentSessionID) {
|
|
|
1893
1964
|
|
|
1894
1965
|
// src/source/opencode/loop-commands.js
|
|
1895
1966
|
var SERVICE2 = "opencode-loop";
|
|
1896
|
-
var DEFAULT_PROGRESS_MD = `# Progress
|
|
1897
|
-
|
|
1898
|
-
## Current Goal
|
|
1899
|
-
Describe the current project goal here.
|
|
1900
|
-
|
|
1901
|
-
## Agent Rules
|
|
1902
|
-
- Do not ask questions unless truly blocked.
|
|
1903
|
-
- Make reasonable assumptions and continue.
|
|
1904
|
-
- Work on unfinished TODOs in order.
|
|
1905
|
-
- Mark completed TODOs with [x].
|
|
1906
|
-
- Add new bugs, ideas, and follow-up work as TODOs.
|
|
1907
|
-
- Run tests, lint, or build when available.
|
|
1908
|
-
- Do not run destructive commands, force pushes, production deploys, or database resets.
|
|
1909
|
-
|
|
1910
|
-
## Active TODO
|
|
1911
|
-
- [ ] Review the project structure and pick the next safe improvement.
|
|
1912
|
-
|
|
1913
|
-
## Completed
|
|
1914
|
-
- [x] Created progress.md.
|
|
1915
|
-
|
|
1916
|
-
## Backlog Ideas
|
|
1917
|
-
- [ ] Add more project-specific tasks here.
|
|
1918
|
-
|
|
1919
|
-
## Blocked
|
|
1920
|
-
- None.
|
|
1921
|
-
`;
|
|
1922
1967
|
function requireFunction3(value, name) {
|
|
1923
1968
|
if (typeof value !== "function")
|
|
1924
1969
|
throw new TypeError(`createLoopCommandHandlers requires ${name}`);
|
|
@@ -2218,17 +2263,7 @@ function createLoopRegistration(options = {}) {
|
|
|
2218
2263
|
const executionContext = getSessionExecutionContext2(sessionID) || { agent: "build" };
|
|
2219
2264
|
parsed.job.agent = defaults.agent || executionContext.agent || "build";
|
|
2220
2265
|
parsed.job.model = normalizedModelRef2(defaults.model) || executionContext.model;
|
|
2221
|
-
|
|
2222
|
-
const defaultCommand = String(defaults.verifyCommand || "npm test");
|
|
2223
|
-
const parsedAction = String(parsed.job.action || "").trim();
|
|
2224
|
-
const usedDefaultAction = parsedAction === String(defaults.action || "").trim();
|
|
2225
|
-
if (!usedDefaultAction && parsed.job.verifyCommand === defaults.verifyCommand) {
|
|
2226
|
-
parsed.job.verifyCommand = parsedAction;
|
|
2227
|
-
parsed.job.action = `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${parsedAction}`;
|
|
2228
|
-
} else if (usedDefaultAction && parsed.job.verifyCommand !== defaults.verifyCommand) {
|
|
2229
|
-
parsed.job.action = `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${parsed.job.verifyCommand || defaultCommand}`;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2266
|
+
applyTestfixPreset(parsed.job, defaults);
|
|
2232
2267
|
if (parsed.job.watchPaths.length)
|
|
2233
2268
|
parsed.job.watchSnapshot = await snapshotPaths(directory, parsed.job.watchPaths);
|
|
2234
2269
|
if (!parsed.job.activeRecoveryMs) {
|
|
@@ -2753,13 +2788,14 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2753
2788
|
const branch = safeID(job.branch);
|
|
2754
2789
|
const inRepo = await runProcess2("git", ["rev-parse", "--is-inside-work-tree"], directory, 1e4);
|
|
2755
2790
|
if (inRepo.code !== 0) {
|
|
2756
|
-
job.branchDone =
|
|
2791
|
+
job.branchDone = false;
|
|
2792
|
+
job.branchUnavailable = true;
|
|
2757
2793
|
return job;
|
|
2758
2794
|
}
|
|
2759
2795
|
let result = await runProcess2("git", ["switch", branch], directory, 30000);
|
|
2760
2796
|
if (result.code !== 0)
|
|
2761
2797
|
result = await runProcess2("git", ["switch", "-c", branch], directory, 30000);
|
|
2762
|
-
job.branchDone =
|
|
2798
|
+
job.branchDone = result.code === 0;
|
|
2763
2799
|
await toast(client, result.code === 0 ? `Loop branch active: ${branch}` : `Could not switch/create branch: ${branch}`, result.code === 0 ? "success" : "warning");
|
|
2764
2800
|
await appendLoopLog2(directory, "branch", { sessionID, branch, code: result.code });
|
|
2765
2801
|
return job;
|
|
@@ -2799,6 +2835,10 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2799
2835
|
async function untilReached(directory, job) {
|
|
2800
2836
|
if (!job.until)
|
|
2801
2837
|
return false;
|
|
2838
|
+
const controlRoots = new Set([
|
|
2839
|
+
stateDir(directory),
|
|
2840
|
+
...["goals", "goal-locks", "goal-handoff-locks", "goal-sequences"].map((name) => path8.join(directory, ".opencode", name))
|
|
2841
|
+
].map((root) => path8.resolve(root).toLowerCase()));
|
|
2802
2842
|
const files = ["progress.md", "PROGRESS.md", "todo.md", "TODO.md", "todolist.md", "TODOLIST.md", path8.join(".opencode", "opencode-loop", "until.txt")];
|
|
2803
2843
|
for (const file of files)
|
|
2804
2844
|
if (await fileContains(path8.resolve(directory, file), job.until))
|
|
@@ -2819,6 +2859,8 @@ ${text.trim().slice(0, 20000)}`);
|
|
|
2819
2859
|
if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name))
|
|
2820
2860
|
continue;
|
|
2821
2861
|
const full = path8.join(current, entry.name);
|
|
2862
|
+
if (controlRoots.has(path8.resolve(full).toLowerCase()))
|
|
2863
|
+
continue;
|
|
2822
2864
|
if (entry.isDirectory()) {
|
|
2823
2865
|
if (await walk(full))
|
|
2824
2866
|
return true;
|
|
@@ -2971,8 +3013,10 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
2971
3013
|
async function canFinalizeActiveRun(directory, client, sessionID, active, options = {}) {
|
|
2972
3014
|
if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID))
|
|
2973
3015
|
return false;
|
|
2974
|
-
if (!options.requireIdle && !options.forceStale)
|
|
2975
|
-
|
|
3016
|
+
if (!options.requireIdle && !options.forceStale) {
|
|
3017
|
+
const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, active);
|
|
3018
|
+
return completion !== "incomplete";
|
|
3019
|
+
}
|
|
2976
3020
|
const completion = options.forceStale ? await activeRunCompletionFromMessages2(directory, client, sessionID, active) : undefined;
|
|
2977
3021
|
if (settledAssistantCompletion(completion))
|
|
2978
3022
|
return true;
|
|
@@ -3048,7 +3092,11 @@ function createSessionStatusRuntime(options = {}) {
|
|
|
3048
3092
|
...completion === "completed" ? {} : { staleStatus: live.type }
|
|
3049
3093
|
};
|
|
3050
3094
|
const recoveryEvent = completion === "empty" ? "status-message-empty-recovery" : completion === "completed" ? "status-message-complete-recovery" : "status-stale-recovery";
|
|
3051
|
-
|
|
3095
|
+
active.statusRecoveryLogEvents ??= new Set;
|
|
3096
|
+
if (!active.statusRecoveryLogEvents.has(recoveryEvent)) {
|
|
3097
|
+
active.statusRecoveryLogEvents.add(recoveryEvent);
|
|
3098
|
+
await appendLoopLog2(directory, recoveryEvent, logDetails);
|
|
3099
|
+
}
|
|
3052
3100
|
return "idle";
|
|
3053
3101
|
}
|
|
3054
3102
|
}
|
|
@@ -3280,8 +3328,10 @@ function messageText(message) {
|
|
|
3280
3328
|
return "";
|
|
3281
3329
|
}
|
|
3282
3330
|
async function applyTerminalContinuationGuard(directory, client, sessionID, job, options = {}) {
|
|
3283
|
-
|
|
3284
|
-
|
|
3331
|
+
const completionBounded = isCompletionBoundedContinuation(job?.action);
|
|
3332
|
+
const continuation = completionBounded || isContinuationShorthand(job?.action);
|
|
3333
|
+
if (job?.scheduleMode !== "idle" || !continuation) {
|
|
3334
|
+
return { job, terminal: false, pausedNow: false, waitingUser: false, waitingUserPausedNow: false };
|
|
3285
3335
|
}
|
|
3286
3336
|
const messages = await readRecentSessionMessages(client, sessionID, directory, options.messageLimit || 8);
|
|
3287
3337
|
if (!messages)
|
|
@@ -3299,11 +3349,31 @@ async function applyTerminalContinuationGuard(directory, client, sessionID, job,
|
|
|
3299
3349
|
return { job, terminal: false, pausedNow: false };
|
|
3300
3350
|
}
|
|
3301
3351
|
const text = messageText(tail);
|
|
3352
|
+
const waitingUser = isWaitingUserReply(text);
|
|
3353
|
+
if (waitingUser) {
|
|
3354
|
+
if (job.terminalNoWorkCount)
|
|
3355
|
+
job.terminalNoWorkCount = 0;
|
|
3356
|
+
job.waitingUserCount = (job.waitingUserCount || 0) + 1;
|
|
3357
|
+
job.lastWaitingUserAt = Date.now();
|
|
3358
|
+
job.lastWaitingUserSummary = text.slice(0, 1000);
|
|
3359
|
+
const threshold = Math.max(2, Number(options.waitingUserThreshold) || 2);
|
|
3360
|
+
const waitingUserPausedNow = job.waitingUserCount >= threshold && !job.paused;
|
|
3361
|
+
if (waitingUserPausedNow) {
|
|
3362
|
+
job.paused = true;
|
|
3363
|
+
job.lastFailureReason = "waiting_user";
|
|
3364
|
+
}
|
|
3365
|
+
return { job, terminal: false, pausedNow: false, waitingUser: true, waitingUserPausedNow, text };
|
|
3366
|
+
}
|
|
3367
|
+
if (job.waitingUserCount)
|
|
3368
|
+
job.waitingUserCount = 0;
|
|
3369
|
+
if (!completionBounded) {
|
|
3370
|
+
return { job, terminal: false, pausedNow: false, waitingUser: false, waitingUserPausedNow: false, text };
|
|
3371
|
+
}
|
|
3302
3372
|
const terminal = isTerminalNoWorkReply(text);
|
|
3303
3373
|
if (!terminal) {
|
|
3304
3374
|
if (job.terminalNoWorkCount)
|
|
3305
3375
|
job.terminalNoWorkCount = 0;
|
|
3306
|
-
return { job, terminal: false, pausedNow: false, text };
|
|
3376
|
+
return { job, terminal: false, pausedNow: false, waitingUser: false, waitingUserPausedNow: false, text };
|
|
3307
3377
|
}
|
|
3308
3378
|
job.terminalNoWorkCount = (job.terminalNoWorkCount || 0) + 1;
|
|
3309
3379
|
job.lastTerminalNoWorkAt = Date.now();
|
|
@@ -3314,7 +3384,7 @@ async function applyTerminalContinuationGuard(directory, client, sessionID, job,
|
|
|
3314
3384
|
job.paused = true;
|
|
3315
3385
|
job.lastFailureReason = "terminal_no_work";
|
|
3316
3386
|
}
|
|
3317
|
-
return { job, terminal: true, pausedNow, text };
|
|
3387
|
+
return { job, terminal: true, pausedNow, waitingUser: false, waitingUserPausedNow: false, text };
|
|
3318
3388
|
}
|
|
3319
3389
|
|
|
3320
3390
|
// src/source/runtime/run-finalization.js
|
|
@@ -3408,6 +3478,16 @@ exit=` + postrun.code + `
|
|
|
3408
3478
|
}
|
|
3409
3479
|
const terminal = await applyTerminalContinuationGuard2(directory, client, sessionID, job);
|
|
3410
3480
|
job = terminal.job;
|
|
3481
|
+
if (terminal.waitingUserPausedNow) {
|
|
3482
|
+
await appendLoopLog2(directory, "waiting-user", {
|
|
3483
|
+
sessionID,
|
|
3484
|
+
job: job.name || job.id,
|
|
3485
|
+
count: job.waitingUserCount,
|
|
3486
|
+
summary: String(terminal.text || "").slice(0, 1000)
|
|
3487
|
+
});
|
|
3488
|
+
await notifyJob2(directory, job, "waiting_user");
|
|
3489
|
+
await toast2(client, "Loop paused: autonomous continuation is waiting for explicit user approval, access, or action.", "warning");
|
|
3490
|
+
}
|
|
3411
3491
|
if (terminal.pausedNow) {
|
|
3412
3492
|
await appendLoopLog2(directory, "terminal-no-work", {
|
|
3413
3493
|
sessionID,
|
|
@@ -3615,6 +3695,25 @@ function requireFunction10(value, label) {
|
|
|
3615
3695
|
throw new TypeError(`createLoopExecutor requires ${label}`);
|
|
3616
3696
|
return value;
|
|
3617
3697
|
}
|
|
3698
|
+
function sessionErrorDetails(event, fallbackMessage) {
|
|
3699
|
+
if (event?.type !== "session.error")
|
|
3700
|
+
return;
|
|
3701
|
+
const sessionID = event?.properties?.sessionID;
|
|
3702
|
+
if (typeof sessionID !== "string" || !sessionID)
|
|
3703
|
+
return;
|
|
3704
|
+
const error = event?.properties?.error;
|
|
3705
|
+
const name = error && typeof error === "object" && typeof error.name === "string" ? error.name : "SessionError";
|
|
3706
|
+
const data = error && typeof error === "object" && error.data && typeof error.data === "object" ? error.data : undefined;
|
|
3707
|
+
const message = typeof data?.message === "string" && data.message.trim() ? data.message.trim() : fallbackMessage(error);
|
|
3708
|
+
return {
|
|
3709
|
+
sessionID,
|
|
3710
|
+
error,
|
|
3711
|
+
name,
|
|
3712
|
+
message,
|
|
3713
|
+
aborted: name === "MessageAbortedError",
|
|
3714
|
+
retryable: name === "APIError" && data?.isRetryable === true
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3618
3717
|
function createLoopExecutor(options = {}) {
|
|
3619
3718
|
const workspace = options.workspace || {};
|
|
3620
3719
|
const goalPolicy = options.goalPolicy || {};
|
|
@@ -3793,6 +3892,56 @@ function createLoopExecutor(options = {}) {
|
|
|
3793
3892
|
await scheduleDueWork(directory, client, sessionID, retryDelay);
|
|
3794
3893
|
return true;
|
|
3795
3894
|
}
|
|
3895
|
+
async function handleSessionError(directory, client, event) {
|
|
3896
|
+
const details = sessionErrorDetails(event, errorMessage);
|
|
3897
|
+
if (!details || details.aborted)
|
|
3898
|
+
return false;
|
|
3899
|
+
const active = activeRuns.get(details.sessionID);
|
|
3900
|
+
if (!active || active.compactionOnly)
|
|
3901
|
+
return false;
|
|
3902
|
+
const snapshot = active;
|
|
3903
|
+
clearActiveRun(details.sessionID);
|
|
3904
|
+
clearSessionStatus(details.sessionID);
|
|
3905
|
+
if (details.retryable) {
|
|
3906
|
+
const refunded = await persistInfrastructureRefund(directory, details.sessionID, snapshot, {
|
|
3907
|
+
reason: "session_error_retryable",
|
|
3908
|
+
error: details.message
|
|
3909
|
+
});
|
|
3910
|
+
await appendLoopLog2(directory, "provider-session-error-retry", {
|
|
3911
|
+
sessionID: details.sessionID,
|
|
3912
|
+
job: refunded.job?.name || snapshot.jobId,
|
|
3913
|
+
errorName: details.name,
|
|
3914
|
+
error: details.message,
|
|
3915
|
+
retryInMs: refunded.delayMs
|
|
3916
|
+
});
|
|
3917
|
+
await toast2(client, `Loop provider turn failed after OpenCode retries; the logical run was refunded and will retry with backoff: ${details.message}`, "warning");
|
|
3918
|
+
await scheduleDueWork(directory, client, details.sessionID, refunded.delayMs);
|
|
3919
|
+
return true;
|
|
3920
|
+
}
|
|
3921
|
+
const state = await readState2(directory, details.sessionID);
|
|
3922
|
+
const job = (state.jobs || []).find((candidate) => candidate.id === snapshot.jobId);
|
|
3923
|
+
if (job) {
|
|
3924
|
+
job.failureCount = (job.failureCount || 0) + 1;
|
|
3925
|
+
job.paused = true;
|
|
3926
|
+
job.lastFailureReason = "session_error";
|
|
3927
|
+
job.lastSessionErrorName = details.name;
|
|
3928
|
+
job.lastSessionError = details.message.slice(0, 4000);
|
|
3929
|
+
job.lastSessionErrorAt = now2();
|
|
3930
|
+
state.jobs = (state.jobs || []).map((candidate) => candidate.id === job.id ? job : candidate);
|
|
3931
|
+
await writeState2(directory, details.sessionID, state);
|
|
3932
|
+
await notifyJob2(directory, job, "session_error");
|
|
3933
|
+
}
|
|
3934
|
+
await appendLoopLog2(directory, "session-error", {
|
|
3935
|
+
sessionID: details.sessionID,
|
|
3936
|
+
job: job?.name || snapshot.jobId,
|
|
3937
|
+
errorName: details.name,
|
|
3938
|
+
error: details.message,
|
|
3939
|
+
paused: Boolean(job?.paused)
|
|
3940
|
+
});
|
|
3941
|
+
await toast2(client, `Loop paused after a terminal OpenCode session error: ${details.message}`, "error");
|
|
3942
|
+
await scheduleDueWork(directory, client, details.sessionID);
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3796
3945
|
async function recoverStuckProviderRetry(directory, client, sessionID) {
|
|
3797
3946
|
const active = activeRuns.get(sessionID);
|
|
3798
3947
|
if (!active || active.compactionOnly) {
|
|
@@ -4067,6 +4216,7 @@ function createLoopExecutor(options = {}) {
|
|
|
4067
4216
|
clearActiveRun,
|
|
4068
4217
|
disposeSession,
|
|
4069
4218
|
recoverActiveDispatchFailure,
|
|
4219
|
+
handleSessionError,
|
|
4070
4220
|
recoverStuckProviderRetry,
|
|
4071
4221
|
finalizeActiveRun,
|
|
4072
4222
|
fireAction,
|
|
@@ -4139,6 +4289,7 @@ function createGoalSteeringRuntime(options = {}) {
|
|
|
4139
4289
|
const now2 = typeof options.now === "function" ? options.now : now;
|
|
4140
4290
|
const suppressionMs = Number.isFinite(Number(options.suppressionMs)) && Number(options.suppressionMs) > 0 ? Number(options.suppressionMs) : DEFAULT_STEERING_SUPPRESSION_MS;
|
|
4141
4291
|
const seenUserMessageMs = Number.isFinite(Number(options.seenUserMessageMs)) && Number(options.seenUserMessageMs) > 0 ? Number(options.seenUserMessageMs) : DEFAULT_SEEN_USER_MESSAGE_MS;
|
|
4292
|
+
const compactingSessions = new Set;
|
|
4142
4293
|
const pendingSteering = new Map;
|
|
4143
4294
|
const seenUserMessages = new Map;
|
|
4144
4295
|
function pendingForSession(sessionID) {
|
|
@@ -4212,7 +4363,7 @@ function createGoalSteeringRuntime(options = {}) {
|
|
|
4212
4363
|
return { handled: false, sessionID, messageID };
|
|
4213
4364
|
const active = getActiveRun(sessionID);
|
|
4214
4365
|
const activeGoalIDs = new Set(goals.map((goal) => goal.id));
|
|
4215
|
-
const canPreempt = active && activeGoalIDs.has(active.jobId) && isGoalJob(active.job) && typeof client?.session?.abort === "function";
|
|
4366
|
+
const canPreempt = !compactingSessions.has(sessionID) && active && activeGoalIDs.has(active.jobId) && isGoalJob(active.job) && typeof client?.session?.abort === "function";
|
|
4216
4367
|
let preempted = false;
|
|
4217
4368
|
let abortError = "";
|
|
4218
4369
|
if (canPreempt) {
|
|
@@ -4224,7 +4375,8 @@ function createGoalSteeringRuntime(options = {}) {
|
|
|
4224
4375
|
});
|
|
4225
4376
|
try {
|
|
4226
4377
|
await fireSdk2(client, "session.abort", client.session.abort.bind(client.session), { path: { id: sessionID }, body: {} }, { path: { sessionID }, body: {} }, { sessionID });
|
|
4227
|
-
|
|
4378
|
+
if (getActiveRun(sessionID) === active)
|
|
4379
|
+
clearActiveRun(sessionID);
|
|
4228
4380
|
preempted = true;
|
|
4229
4381
|
} catch (error) {
|
|
4230
4382
|
pendingSteering.delete(sessionID);
|
|
@@ -4245,9 +4397,20 @@ function createGoalSteeringRuntime(options = {}) {
|
|
|
4245
4397
|
const user = userMessageFromEvent(event);
|
|
4246
4398
|
if (!user)
|
|
4247
4399
|
return;
|
|
4248
|
-
|
|
4400
|
+
if (alreadyHandled(user.sessionID, user.messageID))
|
|
4401
|
+
return { handled: false, duplicate: true, ...user };
|
|
4402
|
+
return { handled: false, reason: "untrusted-message-event", ...user };
|
|
4403
|
+
}
|
|
4404
|
+
function setCompacting(sessionID, value = true) {
|
|
4405
|
+
if (typeof sessionID !== "string" || !sessionID)
|
|
4406
|
+
return;
|
|
4407
|
+
if (value)
|
|
4408
|
+
compactingSessions.add(sessionID);
|
|
4409
|
+
else
|
|
4410
|
+
compactingSessions.delete(sessionID);
|
|
4249
4411
|
}
|
|
4250
4412
|
function clearSession(sessionID) {
|
|
4413
|
+
compactingSessions.delete(sessionID);
|
|
4251
4414
|
pendingSteering.delete(sessionID);
|
|
4252
4415
|
const prefix = `${sessionID}\x00`;
|
|
4253
4416
|
for (const key of seenUserMessages.keys())
|
|
@@ -4261,6 +4424,7 @@ function createGoalSteeringRuntime(options = {}) {
|
|
|
4261
4424
|
shouldSuppressIdle,
|
|
4262
4425
|
hasPendingSteering: shouldSuppressIdle,
|
|
4263
4426
|
pendingForSession,
|
|
4427
|
+
setCompacting,
|
|
4264
4428
|
clearSession
|
|
4265
4429
|
};
|
|
4266
4430
|
}
|
|
@@ -4288,6 +4452,7 @@ var executorRuntime = createLoopExecutor({
|
|
|
4288
4452
|
});
|
|
4289
4453
|
var {
|
|
4290
4454
|
clearActiveRun,
|
|
4455
|
+
handleSessionError,
|
|
4291
4456
|
finalizeActiveRun,
|
|
4292
4457
|
maybeRunDueJobs: runDueJobs,
|
|
4293
4458
|
sessionIsIdle,
|
|
@@ -4486,10 +4651,10 @@ var OpenCodeLoopPlugin = async ({ client, directory }) => {
|
|
|
4486
4651
|
"command.execute.before": async (input, output) => {
|
|
4487
4652
|
await handleCommand(directory, client, input, undefined, undefined, output);
|
|
4488
4653
|
},
|
|
4489
|
-
"chat.message": async (input) => {
|
|
4654
|
+
"chat.message": async (input, output) => {
|
|
4490
4655
|
const steering = await goalSteeringRuntime.handleUserMessage(directory, client, {
|
|
4491
4656
|
sessionID: input?.sessionID,
|
|
4492
|
-
messageID: input?.messageID
|
|
4657
|
+
messageID: input?.messageID || output?.message?.id
|
|
4493
4658
|
});
|
|
4494
4659
|
if (steering?.handled && steering.sessionID)
|
|
4495
4660
|
rememberSession(directory, client, steering.sessionID);
|
|
@@ -4501,9 +4666,13 @@ var OpenCodeLoopPlugin = async ({ client, directory }) => {
|
|
|
4501
4666
|
markToolCallFinished(input);
|
|
4502
4667
|
},
|
|
4503
4668
|
"experimental.session.compacting": async (input) => {
|
|
4669
|
+
goalSteeringRuntime.setCompacting(input?.sessionID);
|
|
4504
4670
|
await noteLoopCompactionStarted(directory, input?.sessionID);
|
|
4505
4671
|
},
|
|
4506
4672
|
event: async ({ event }) => {
|
|
4673
|
+
if (["session.compacted", "session.error", "session.deleted"].includes(event?.type)) {
|
|
4674
|
+
goalSteeringRuntime.setCompacting(event?.properties?.sessionID || event?.properties?.info?.id, false);
|
|
4675
|
+
}
|
|
4507
4676
|
if (event.type === "session.compacted")
|
|
4508
4677
|
await noteLoopCompactionCompleted(directory, client, event?.properties?.sessionID);
|
|
4509
4678
|
updateSessionRelationshipFromEvent(event);
|
|
@@ -4517,6 +4686,11 @@ var OpenCodeLoopPlugin = async ({ client, directory }) => {
|
|
|
4517
4686
|
const steering = await goalSteeringRuntime.handleEvent(directory, client, event);
|
|
4518
4687
|
if (steering?.handled && steering.sessionID)
|
|
4519
4688
|
rememberSession(directory, client, steering.sessionID);
|
|
4689
|
+
if (event.type === "session.error") {
|
|
4690
|
+
const handledError = await handleSessionError(directory, client, event);
|
|
4691
|
+
if (handledError && event?.properties?.sessionID)
|
|
4692
|
+
rememberSession(directory, client, event.properties.sessionID);
|
|
4693
|
+
}
|
|
4520
4694
|
const statusUpdate = updateSessionStatusFromEvent(event);
|
|
4521
4695
|
if (statusUpdate?.sessionID)
|
|
4522
4696
|
rememberSession(directory, client, statusUpdate.sessionID);
|