@bybrawe/opencode-loop 0.5.33 → 0.5.35
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 +18 -0
- package/README.md +197 -283
- package/commands/loop.md +1 -1
- package/package.json +3 -3
- package/scripts/comprehensive-test.mjs +1 -1
- package/scripts/host-loop-canary.mjs +8 -3
- package/scripts/loop-command-handlers-test.mjs +33 -4
- package/scripts/loop-registration-test.mjs +77 -3
- package/scripts/network-recovery-test.mjs +218 -0
- package/scripts/schedule-policy-test.mjs +114 -0
- package/scripts/schedule-syntax-test.mjs +75 -0
- package/scripts/session-status-idle-recovery-test.mjs +44 -0
- package/scripts/session-status-test.mjs +18 -4
- package/src/index.js +749 -126
- package/src/source/core/continuation.js +71 -0
- package/src/source/core/jobs.js +3 -0
- package/src/source/core/schedule-syntax.js +93 -0
- package/src/source/opencode/loop-commands.js +39 -30
- package/src/source/opencode/loop-registration.js +40 -3
- package/src/source/runtime/companion-goal.js +37 -0
- package/src/source/runtime/executor.js +145 -16
- package/src/source/runtime/loop-diagnostics.js +45 -0
- package/src/source/runtime/network-recovery.js +40 -0
- package/src/source/runtime/run-admission.js +2 -13
- package/src/source/runtime/run-finalization.js +28 -0
- package/src/source/runtime/schedule-policy.js +80 -0
- package/src/source/runtime/scheduler-diagnostics.js +30 -0
- package/src/source/runtime/scheduler.js +10 -23
- package/src/source/runtime/session-status.js +46 -23
- package/src/source/runtime/terminal-guard.js +56 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { normalizeLoopScheduleArgs } from "../src/source/core/schedule-syntax.js"
|
|
3
|
+
|
|
4
|
+
{
|
|
5
|
+
const result = normalizeLoopScheduleArgs("continue the project")
|
|
6
|
+
assert.equal(result.ok, true)
|
|
7
|
+
assert.equal(result.args, "continue the project")
|
|
8
|
+
assert.equal(result.defaults.intervalMs, 0)
|
|
9
|
+
assert.equal(result.defaults.immediate, true)
|
|
10
|
+
assert.equal(result.scheduleMode, "idle")
|
|
11
|
+
assert.equal(result.scheduleSyntax, "idle-shorthand")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
const result = normalizeLoopScheduleArgs("--safe --ask-never continue the project")
|
|
16
|
+
assert.equal(result.ok, true)
|
|
17
|
+
assert.equal(result.defaults.intervalMs, 0)
|
|
18
|
+
assert.equal(result.args, "--safe --ask-never continue the project")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
const result = normalizeLoopScheduleArgs("idle continue forever")
|
|
23
|
+
assert.equal(result.args, "continue forever")
|
|
24
|
+
assert.equal(result.defaults.intervalMs, 0)
|
|
25
|
+
assert.equal(result.defaults.immediate, true)
|
|
26
|
+
assert.equal(result.scheduleMode, "idle")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
const result = normalizeLoopScheduleArgs("every 5m continue forever")
|
|
31
|
+
assert.equal(result.args, "continue forever")
|
|
32
|
+
assert.equal(result.defaults.intervalMs, 300_000)
|
|
33
|
+
assert.equal(result.defaults.immediate, false)
|
|
34
|
+
assert.equal(result.scheduleMode, "interval")
|
|
35
|
+
assert.equal(result.scheduleSyntax, "every")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
const result = normalizeLoopScheduleArgs("after 5m continue once")
|
|
40
|
+
assert.equal(result.args, "continue once")
|
|
41
|
+
assert.equal(result.defaults.intervalMs, 300_000)
|
|
42
|
+
assert.equal(result.defaults.immediate, false)
|
|
43
|
+
assert.equal(result.defaults.maxRuns, 1)
|
|
44
|
+
assert.equal(result.scheduleMode, "once")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
const result = normalizeLoopScheduleArgs("in 30s ping once")
|
|
49
|
+
assert.equal(result.args, "ping once")
|
|
50
|
+
assert.equal(result.defaults.intervalMs, 30_000)
|
|
51
|
+
assert.equal(result.defaults.maxRuns, 1)
|
|
52
|
+
assert.equal(result.scheduleSyntax, "after")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
const result = normalizeLoopScheduleArgs("5m continue legacy")
|
|
57
|
+
assert.equal(result.args, "5m continue legacy")
|
|
58
|
+
assert.equal(result.scheduleMode, "interval")
|
|
59
|
+
assert.equal(result.scheduleSyntax, "legacy")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
const result = normalizeLoopScheduleArgs("--allow-goal-overlap continue")
|
|
64
|
+
assert.equal(result.args, "continue")
|
|
65
|
+
assert.equal(result.allowGoalOverlap, true)
|
|
66
|
+
assert.equal(result.scheduleMode, "idle")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
const result = normalizeLoopScheduleArgs("every nope continue")
|
|
71
|
+
assert.equal(result.ok, false)
|
|
72
|
+
assert.match(result.error, /Invalid every schedule/)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log("schedule syntax tests passed")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { clearSessionActivity } from "../src/source/runtime/session-activity.js"
|
|
3
|
+
import { createSessionStatusRuntime } from "../src/source/runtime/session-status.js"
|
|
4
|
+
|
|
5
|
+
const activeRuns = new Map()
|
|
6
|
+
const logs = []
|
|
7
|
+
let clock = 100_000
|
|
8
|
+
let completion = "completed"
|
|
9
|
+
|
|
10
|
+
const runtime = createSessionStatusRuntime({
|
|
11
|
+
activeRuns,
|
|
12
|
+
now: () => clock,
|
|
13
|
+
sessionStatusCacheMs: 0,
|
|
14
|
+
appendLoopLog: async (...args) => logs.push(args),
|
|
15
|
+
activeRunCompletionFromMessages: async () => completion,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const busyClient = {
|
|
19
|
+
session: {
|
|
20
|
+
status: async () => ({ data: { session: { type: "busy" } } }),
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
runtime.markSessionStatus("session", "busy", clock - 2_000)
|
|
26
|
+
assert.equal(await runtime.sessionStatusType(busyClient, "session", "/repo"), "idle")
|
|
27
|
+
assert.equal(logs.at(-1)[1], "status-message-idle-recovery")
|
|
28
|
+
assert.equal(logs.at(-1)[2].staleStatus, "busy")
|
|
29
|
+
|
|
30
|
+
runtime.clearSessionStatus("session")
|
|
31
|
+
completion = "incomplete"
|
|
32
|
+
clock += 5_000
|
|
33
|
+
assert.equal(await runtime.sessionStatusType(busyClient, "session", "/repo"), "busy", "an unfinished tail must never be force-recovered")
|
|
34
|
+
|
|
35
|
+
runtime.clearSessionStatus("session")
|
|
36
|
+
completion = "unknown"
|
|
37
|
+
clock += 5_000
|
|
38
|
+
assert.equal(await runtime.sessionStatusType(busyClient, "session", "/repo"), "busy", "unknown tail completion must remain conservative")
|
|
39
|
+
|
|
40
|
+
console.log("session status idle recovery tests passed")
|
|
41
|
+
} finally {
|
|
42
|
+
activeRuns.clear()
|
|
43
|
+
clearSessionActivity("session")
|
|
44
|
+
}
|
|
@@ -158,17 +158,31 @@ try {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
{
|
|
161
|
-
const sessionID = "
|
|
161
|
+
const sessionID = "retry-remains-owned"
|
|
162
162
|
track(sessionID)
|
|
163
163
|
runtime.clearSessionStatus(sessionID)
|
|
164
164
|
activeRuns.set(sessionID, { jobId: "job-2", job: { id: "job-2" }, startedAt: 1_000 })
|
|
165
165
|
completion = "unknown"
|
|
166
|
+
const beforeLogs = logs.length
|
|
166
167
|
const client = { session: { status: async () => ({ data: { [sessionID]: { type: "retry" } } }) } }
|
|
167
|
-
assert.equal(await runtime.sessionStatusType(client, sessionID, "/repo"), "
|
|
168
|
-
assert.
|
|
168
|
+
assert.equal(await runtime.sessionStatusType(client, sessionID, "/repo"), "retry")
|
|
169
|
+
assert.equal(logs.length, beforeLogs, "unknown retry must not use stale-busy recovery")
|
|
169
170
|
activeRuns.delete(sessionID)
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
{
|
|
174
|
+
const sessionID = "retry-message-complete"
|
|
175
|
+
track(sessionID)
|
|
176
|
+
runtime.clearSessionStatus(sessionID)
|
|
177
|
+
activeRuns.set(sessionID, { jobId: "job-retry-complete", job: { id: "job-retry-complete", name: "retry-complete" }, startedAt: 1_000 })
|
|
178
|
+
completion = "completed"
|
|
179
|
+
const client = { session: { status: async () => ({ data: { [sessionID]: { type: "retry" } } }) } }
|
|
180
|
+
assert.equal(await runtime.sessionStatusType(client, sessionID, "/repo"), "idle", "a genuinely completed current assistant turn may release stale retry")
|
|
181
|
+
assert.deepEqual(logs.at(-1), ["/repo", "status-message-complete-recovery", { sessionID, job: "retry-complete", startedAt: 1_000 }])
|
|
182
|
+
activeRuns.delete(sessionID)
|
|
183
|
+
completion = "unknown"
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
{
|
|
173
187
|
const sessionID = "fallback-fresh"
|
|
174
188
|
track(sessionID)
|
|
@@ -178,7 +192,7 @@ try {
|
|
|
178
192
|
assert.equal(await runtime.sessionStatusType(client, sessionID, "/repo"), "busy")
|
|
179
193
|
activeRuns.set(sessionID, { jobId: "stale", job: {}, startedAt: clock - 60_000 })
|
|
180
194
|
runtime.clearSessionStatus(sessionID)
|
|
181
|
-
assert.equal(await runtime.sessionStatusType(client, sessionID, "/repo"), "
|
|
195
|
+
assert.equal(await runtime.sessionStatusType(client, sessionID, "/repo"), "busy", "status transport failure must fail closed even for an old active run")
|
|
182
196
|
activeRuns.delete(sessionID)
|
|
183
197
|
}
|
|
184
198
|
|