@bybrawe/opencode-loop 0.5.8 → 0.5.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/CHANGELOG.md +7 -0
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/index.js +33 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.9
|
|
4
|
+
|
|
5
|
+
- Fixed stale-busy recovery for TUI sessions. Cached `busy` / `retry` status is now short-lived, so due jobs no longer get stuck at `due in every idle` after `/loop-status` or other short custom-command turns.
|
|
6
|
+
- Improved due-timer behavior when OpenCode misses or delays the final idle event after a command turn.
|
|
7
|
+
- Fixed `/compact` routing for current OpenCode TUI builds by trying `session_compact` first, then `session.compact`, then the session summarize fallback.
|
|
8
|
+
- Kept the command markdown templates short; OpenCode still creates a tiny `OK` turn for custom commands because markdown commands are prompt templates.
|
|
9
|
+
|
|
3
10
|
## 0.5.8
|
|
4
11
|
|
|
5
12
|
- Added a real due timer for TUI loop jobs, so delayed jobs can wake without waiting for another OpenCode event.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ NPM package name: **@bybrawe/opencode-loop**
|
|
|
11
11
|
|
|
12
12
|
## Current status
|
|
13
13
|
|
|
14
|
-
**v0.5.
|
|
14
|
+
**v0.5.9 is a reliability hotfix for stale-busy status and `/compact` routing.** It keeps the real due timer from v0.5.8, but no longer trusts a cached `busy` / `retry` session status forever. This fixes the case where `/loop-status` or another short custom-command turn could leave a loop showing `due in every idle` without firing. It also routes scheduled `/compact` through OpenCode's current TUI alias first, with fallback paths for older builds.
|
|
15
15
|
|
|
16
16
|
The known update-related symptoms from older builds are fixed:
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ The known update-related symptoms from older builds are fixed:
|
|
|
27
27
|
|
|
28
28
|
The TUI loop is still intentionally session-bound: it runs while OpenCode is open and the current session emits status/idle events. For long-running background work after closing the terminal or OpenCode, use `opencode-loopd`.
|
|
29
29
|
|
|
30
|
-
## v0.5.
|
|
30
|
+
## v0.5.9 quick behavior guide
|
|
31
31
|
|
|
32
32
|
OpenCode Loop has two triggers now:
|
|
33
33
|
|
|
@@ -74,7 +74,7 @@ Run a shell command every 10 minutes when idle.
|
|
|
74
74
|
|
|
75
75
|
Experimental persistent goal mode: keep working until the goal is complete, blocked, paused, cleared, or a safety limit is reached.
|
|
76
76
|
|
|
77
|
-
> Note: OpenCode custom command markdown still creates a tiny assistant turn for slash commands. v0.5.
|
|
77
|
+
> Note: OpenCode custom command markdown still creates a tiny assistant turn for slash commands. v0.5.9 keeps these command templates short and asks the model to reply `OK`. The actual loop scheduling is handled locally by the plugin. If that short turn briefly makes OpenCode look busy, the loop now recovers by refreshing stale busy status on the next due retry.
|
|
78
78
|
|
|
79
79
|
## Why this exists
|
|
80
80
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Claude Code/Codex style /loop and experimental goal mode for OpenCode: reliable timers, idle-safe loops, scheduled commands, compact scheduling, verification, checkpoints, and persistent coding goals.",
|
|
3
|
+
"version": "0.5.9",
|
|
4
|
+
"description": "Claude Code/Codex style /loop and experimental goal mode for OpenCode: reliable timers, stale-busy recovery, idle-safe loops, scheduled commands, compact scheduling, verification, checkpoints, and persistent coding goals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const STATE_DIR = ".opencode/opencode-loop"
|
|
|
7
7
|
const DEFAULT_ACTIVE_GUARD_MS = 60_000
|
|
8
8
|
const IDLE_DEBOUNCE_MS = 1_200
|
|
9
9
|
const BUSY_RETRY_MS = 5_000
|
|
10
|
+
const SESSION_STATUS_CACHE_MS = 1_500
|
|
10
11
|
const MIN_DUE_TIMER_MS = 250
|
|
11
12
|
const MAX_DUE_TIMER_MS = 2_147_000_000
|
|
12
13
|
const MAX_SCAN_FILES = 200
|
|
@@ -19,6 +20,7 @@ const handledCommands = new Map()
|
|
|
19
20
|
const idleTimers = new Map()
|
|
20
21
|
const dueTimers = new Map()
|
|
21
22
|
const sessionStatuses = new Map()
|
|
23
|
+
const sessionStatusSeenAt = new Map()
|
|
22
24
|
|
|
23
25
|
const DEFAULT_PROGRESS_MD = `# Progress
|
|
24
26
|
|
|
@@ -389,16 +391,22 @@ async function executeTuiCommand(client, command) {
|
|
|
389
391
|
|
|
390
392
|
function compactTuiCommandName(command = "compact") {
|
|
391
393
|
const normalized = String(command || "compact").replace(/^\/+/, "").trim().toLowerCase()
|
|
392
|
-
if (normalized === "compact" || normalized === "summarize") return "
|
|
394
|
+
if (normalized === "compact" || normalized === "summarize") return "session_compact"
|
|
393
395
|
return undefined
|
|
394
396
|
}
|
|
395
397
|
|
|
396
398
|
async function compactSession(client, sessionID) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
399
|
+
// OpenCode's TUI API accepts legacy keybind aliases (session_compact) in
|
|
400
|
+
// current builds, while some older docs/examples mention the event value
|
|
401
|
+
// (session.compact). Try the alias first, then the event value, then the
|
|
402
|
+
// session summarize endpoint as a last resort.
|
|
403
|
+
for (const command of ["session_compact", "session.compact"]) {
|
|
404
|
+
try {
|
|
405
|
+
await executeTuiCommand(client, command)
|
|
406
|
+
return true
|
|
407
|
+
} catch (error) {
|
|
408
|
+
await log(client, "warn", `tui ${command} failed`, { error: sdkErrorMessage(error) })
|
|
409
|
+
}
|
|
402
410
|
}
|
|
403
411
|
try {
|
|
404
412
|
await sdkCall(client.session.summarize.bind(client.session), { sessionID }, { path: { id: sessionID }, body: {} })
|
|
@@ -734,12 +742,16 @@ function updateSessionStatusFromEvent(event) {
|
|
|
734
742
|
if (typeof sessionID !== "string") return undefined
|
|
735
743
|
if (event?.type === "session.idle") {
|
|
736
744
|
sessionStatuses.set(sessionID, "idle")
|
|
745
|
+
sessionStatusSeenAt.set(sessionID, now())
|
|
737
746
|
return { sessionID, idle: true }
|
|
738
747
|
}
|
|
739
748
|
if (event?.type === "session.status") {
|
|
740
749
|
const status = event?.properties?.status
|
|
741
750
|
const type = status && typeof status === "object" ? status.type : undefined
|
|
742
|
-
if (typeof type === "string")
|
|
751
|
+
if (typeof type === "string") {
|
|
752
|
+
sessionStatuses.set(sessionID, type)
|
|
753
|
+
sessionStatusSeenAt.set(sessionID, now())
|
|
754
|
+
}
|
|
743
755
|
return { sessionID, idle: type === "idle" }
|
|
744
756
|
}
|
|
745
757
|
return undefined
|
|
@@ -747,18 +759,30 @@ function updateSessionStatusFromEvent(event) {
|
|
|
747
759
|
|
|
748
760
|
async function sessionStatusType(client, sessionID) {
|
|
749
761
|
const cached = sessionStatuses.get(sessionID)
|
|
750
|
-
|
|
762
|
+
const seenAt = sessionStatusSeenAt.get(sessionID) || 0
|
|
763
|
+
|
|
764
|
+
// Idle is safe to trust until OpenCode tells us otherwise. Busy/retry is only
|
|
765
|
+
// trusted briefly: OpenCode custom commands such as /loop-status create their
|
|
766
|
+
// own short assistant turn, and some TUI builds do not always emit the final
|
|
767
|
+
// idle event after that turn. If we cache busy forever, due loop work can get
|
|
768
|
+
// stuck at "due in every idle" until the user types another command.
|
|
769
|
+
if (cached === "idle") return cached
|
|
770
|
+
if (cached && now() - seenAt < SESSION_STATUS_CACHE_MS) return cached
|
|
771
|
+
|
|
751
772
|
try {
|
|
752
773
|
const data = await sdkCall(client.session.status.bind(client.session), {}, undefined)
|
|
753
774
|
const status = data?.[sessionID]
|
|
754
775
|
const type = status && typeof status === "object" ? status.type : undefined
|
|
755
776
|
if (typeof type === "string") {
|
|
756
777
|
sessionStatuses.set(sessionID, type)
|
|
778
|
+
sessionStatusSeenAt.set(sessionID, now())
|
|
757
779
|
return type
|
|
758
780
|
}
|
|
759
781
|
} catch {}
|
|
782
|
+
|
|
760
783
|
const fallback = activeRuns.has(sessionID) ? "busy" : "idle"
|
|
761
784
|
sessionStatuses.set(sessionID, fallback)
|
|
785
|
+
sessionStatusSeenAt.set(sessionID, now())
|
|
762
786
|
return fallback
|
|
763
787
|
}
|
|
764
788
|
|
|
@@ -1077,7 +1101,7 @@ async function fireAction(directory, client, sessionID, job) {
|
|
|
1077
1101
|
}
|
|
1078
1102
|
const tuiCommand = compactTuiCommandName(command)
|
|
1079
1103
|
if (tuiCommand) {
|
|
1080
|
-
await
|
|
1104
|
+
await compactSession(client, sessionID)
|
|
1081
1105
|
return { startsAssistantTurn: true }
|
|
1082
1106
|
}
|
|
1083
1107
|
await sdkCall(client.session.command.bind(client.session), { sessionID, command, arguments: argumentsText }, { path: { id: sessionID }, body: { command, arguments: argumentsText } })
|