@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 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.8 is a reliability hotfix for the new loop types and Experimental Goal Mode.** It adds a real due timer in addition to OpenCode idle/status events, so delayed jobs such as `/loop 1m --no-now ...`, `/loop-command 1m --no-now /compact`, `/loop-ask 1h ...`, and `/loop-shell 10m ...` do not depend on another user action to wake the scheduler. The TUI loop still waits for the session to become idle before starting work, debounces idle events, and avoids starting a new run while OpenCode is busy or queued.
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.8 quick behavior guide
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.8 keeps these command templates short and asks the model to reply `OK`. The actual loop scheduling is handled locally by the plugin.
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.8",
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 "session.compact"
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
- try {
398
- await executeTuiCommand(client, "session.compact")
399
- return true
400
- } catch (error) {
401
- await log(client, "warn", "tui session.compact failed", { error: sdkErrorMessage(error) })
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") sessionStatuses.set(sessionID, type)
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
- if (cached && cached !== "idle") return cached
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 executeTuiCommand(client, tuiCommand)
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 } })