Skip to content

Commit 4f723f4

Browse files
Merge pull request #23 from jkrandom-sudo/fix/remove-all-flag
fix(loop)!: drop --all for Claude Code parity; persist no longer rolls back peer updates (0.7.4)
2 parents c964988 + ac069a1 commit 4f723f4

12 files changed

Lines changed: 202 additions & 257 deletions

‎README.md‎

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Claude Code's `/loop`. Each `/loop` task is bound to the session that created it — never leaks to other sessions.
99

10-
> **Upgrading to 0.4.0?** Two behavior changes to know about: (1) since 0.3.0, tasks die with the opencode process by default (`ephemeralTasks: false` restores persistence) — the upgrade drops the pre-0.3.0 `tasks.json` once; (2) scheduling-like input that used to silently create an Adaptive task (cron syntax, bare intervals like `/loop 5m`, unknown flags) now returns an explicit error pointing at `/loop help`.
11-
1210
## Features
1311

1412
- **`/loop 5m <prompt>`** — fixed interval (s/m/h/d supported)
@@ -17,10 +15,10 @@ A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Cla
1715
- **`/loop 30s --once <prompt>`** — one-shot: fires once, then auto-cancels
1816
- **`/loop help`** — full usage, flags, and examples in the terminal
1917
- **Claude Code-style flags** — `--cancel/--list/--status/--pause/--resume/--stop/--stop-all` map to the matching subcommand
20-
- **Per-session scoping** — tasks are bound to a `sessionID`; other sessions never see or fire them
21-
- **Subcommands** — `list | status | cancel | stop | pause | resume | stop-all` (session-scoped; add `--all` to cross sessions; bare `stop` cancels every task in scope)
22-
- **Internal ticker** — 5s loop drives task firing (no longer depends on `session.idle` events)
23-
- **Prompt fidelity** — flags (`--once`, `--all`, `--jitter=*`) are only recognized before the prompt begins; `--` forces the rest to be treated as prompt text verbatim, and whitespace/newlines are preserved
18+
- **Per-session scoping** — tasks are bound to a `sessionID`; other sessions never see, fire, or manage them
19+
- **Subcommands** — `list | status | cancel | stop | pause | resume | stop-all` (all scoped to the current session; bare `stop` cancels every task in the session)
20+
- **Internal ticker** — 5s loop drives task firing
21+
- **Prompt fidelity** — flags (`--once`, `--jitter=*`) are only recognized before the prompt begins; `--` forces the rest to be treated as prompt text verbatim, and whitespace/newlines are preserved
2422
- **Per-process instance coordination** — plugin instances inside one process (case-variant plugin paths, per-command `opencode run` instances) elect a single leader so tasks never double-fire; a second OpenCode process in the same project fires its own tasks independently, and merge-writes prevent task loss
2523
- **Inflight guard** — double-set at ticker and `fireTask` level prevents double-firing even if opencode hot-reloads the plugin
2624
- **Wall-clock scheduling** — fixed tasks anchor to fire start; model-turn duration never inflates the interval
@@ -29,7 +27,7 @@ A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Cla
2927
- **Configurable Jitter** — deterministic Fixed-task offset, controllable per command, tool call, or programmatic default
3028
- **Auto-expire** — tasks idle for more than 7 days are removed on load (active tasks never expire)
3129
- **Max 50 concurrent tasks**
32-
- **LLM-callable tools** — `loop_schedule`, `loop_status` (session-bound by default)
30+
- **LLM-callable tools** — `loop_schedule`, `loop_status` (scoped to the calling session)
3331
- **Inline results, Claude Code style** — every `/loop` result (create, list, cancel, pause, resume, stop-all, failures) is presented by the model directly in the conversation, in the user's own language — task lists render as a markdown table. No dialogs, no toasts
3432

3533
## Requirements
@@ -80,7 +78,7 @@ Server config (`~/.config/opencode/opencode.json`):
8078
"plugin": ["opencode-plugin-loop"],
8179
"command": {
8280
"loop": {
83-
"description": "Run prompts on a schedule. Intervals: s/m/h/d. Subcommands: help | list | status | cancel <id> | pause <id> | resume <id> | stop-all (add --all to cross sessions)",
81+
"description": "Run prompts on a schedule. Intervals: s/m/h/d. Subcommands: help | list | status | cancel <id> | pause <id> | resume <id> | stop-all (all scoped to the current session)",
8482
"template": "$ARGUMENTS",
8583
"agent": "build"
8684
}
@@ -120,10 +118,10 @@ Re-run `npm run build` after editing `src/`, then restart OpenCode to load the r
120118
/loop 30s --once remind me to stretch # one-shot: fires once, then auto-cancels
121119
```
122120

123-
Fixed tasks use deterministic Jitter by default for backward compatibility. Add
121+
Fixed tasks use deterministic Jitter by default. Add
124122
`--jitter=false` for an exact interval or `--jitter=true` to enable it explicitly.
125123
Flags are only recognized **before the prompt begins**: anything after the first
126-
prompt word — including text that looks like `--once`, `--all`, or `--jitter=*` —
124+
prompt word — including text that looks like `--once` or `--jitter=*` —
127125
is part of the prompt and is preserved verbatim (whitespace and newlines
128126
included). Use `--` to force everything after it to be treated as prompt text:
129127

@@ -148,7 +146,7 @@ The natural-language form runs the request immediately in the current model turn
148146

149147
Adaptive-to-Fixed conversion defaults to `jitterEnabled: false`, so an explicit cadence remains exact.
150148

151-
The fallback is written before the prompt is injected. A successful `reschedule` therefore replaces the fallback and is not overwritten after the model finishes. The preferred `delayMs` is relative to tool-call time, avoiding epoch arithmetic. An in-range model delay is stored exactly without Jitter; only an out-of-range request is clamped to the task's configured minimum or maximum delay. Fixed and Maintenance rescheduling remains unchanged. The legacy absolute `nextDueAtMs` remains supported, but passing it together with `delayMs` returns an error without changing the task.
149+
The fallback is written before the prompt is injected. A successful `reschedule` therefore replaces the fallback and is not overwritten after the model finishes. The preferred `delayMs` is relative to tool-call time, avoiding epoch arithmetic. An in-range model delay is stored exactly without Jitter; only an out-of-range request is clamped to the task's configured minimum or maximum delay. Fixed and Maintenance rescheduling remains unchanged. An absolute `nextDueAtMs` is also accepted, but passing it together with `delayMs` returns an error without changing the task.
152150

153151
### Bare `/loop` — custom default prompt
154152
Create `.opencode/loop.md` (project) or `<user>/.opencode/loop.md` (user) with your maintenance instructions:
@@ -160,25 +158,21 @@ address each one. If everything is green, say so in one line.
160158

161159
### Subcommands
162160

163-
All subcommands are **session-scoped by default**. Add `--all` to operate across all sessions.
161+
All subcommands are **scoped to the current session** — tasks created in other sessions are invisible to them, exactly like Claude Code's per-session `/loop` jobs.
164162

165163
```
166164
/loop help # full usage, flags, and examples
167165
/loop list # show tasks in current session
168-
/loop list --all # show all sessions (with [s:xxxx] tags)
169166
/loop status # alias for list
170167
/loop cancel <taskId> # cancel one task in current session
171-
/loop cancel <taskId> --all # override scope
172168
/loop stop <taskId> # alias for cancel
173169
/loop stop # cancel ALL tasks in current session
174-
/loop stop --all # cancel ALL tasks across sessions
175170
/loop pause <taskId> # pause one
176171
/loop resume <taskId> # resume one (re-arms per mode)
177172
/loop stop-all # cancel all tasks in current session
178-
/loop stop-all --all # cancel ALL tasks across sessions
179173
```
180174

181-
If you try `cancel <id>` for a task owned by another session, you'll get a refusal with a hint to add `--all`. The same strict scoping applies to `loop_schedule` and `loop_status` tools.
175+
Trying to manage a task owned by another session reports "No task `<id>` in this session" — switch to that session to manage it. The same strict scoping applies to the `loop_schedule` and `loop_status` tools.
182176

183177
### Migrating from Claude Code
184178

@@ -189,7 +183,7 @@ If you try `cancel <id>` for a task owned by another session, you'll get a refus
189183
| cancel/list via cron tools | `/loop cancel <id>`, `/loop list` |
190184
| `--cancel`, `--list`, `--stop` | accepted — mapped to `cancel`, `list`, `stop` |
191185
| one-off reminder ("in 30m tell me X") | `/loop 30s --once <prompt>` |
192-
| jobs die when the session ends | same default since 0.3.0 (`ephemeralTasks: false` opts out) |
186+
| jobs die when the session ends | same default (`ephemeralTasks: false` opts out) |
193187
| cron expressions (`*/5 * * * *`) | not supported — use `5m` form (explicit error) |
194188

195189
Two behavioral differences worth knowing: tasks only fire for the **currently active session** (switch sessions and the others wait; switch back and they catch up once), and fixed tasks fire on a 5-second ticker rather than exact wall-clock cron times (up to one ticker period late).
@@ -207,7 +201,7 @@ No dialogs, no toasts — the conversation is the only output surface.
207201

208202
### Programmatic (LLM tools)
209203

210-
The plugin registers two LLM-callable tools. Both are session-bound by default; pass `all: true` to cross.
204+
The plugin registers two LLM-callable tools. Both are scoped to the calling session.
211205

212206
```typescript
213207
loop_schedule({
@@ -220,7 +214,7 @@ loop_schedule({
220214
loop_schedule({
221215
action: "cancel",
222216
taskId: "abc12345",
223-
// refuses if taskId belongs to another session (pass all: true to override)
217+
// only works for tasks created in the calling session
224218
})
225219

226220
loop_schedule({
@@ -236,8 +230,7 @@ loop_schedule({
236230
jitterEnabled: false, // default for Adaptive-to-Fixed conversion
237231
})
238232

239-
loop_status({}) // current session only
240-
loop_status({ all: true }) // all sessions
233+
loop_status({}) // current session
241234
```
242235

243236
## Configuration
@@ -262,13 +255,10 @@ task records its owning process (`ownerPid` + start time) in `tasks.json`. On
262255
load, tasks whose owner process is confirmed dead — e.g. after that OpenCode
263256
process exits — are dropped, so loop tasks never outlive the process that
264257
created them (the same lifecycle as Claude Code's `/loop`). Tasks owned by
265-
other **live** OpenCode processes in the same project are kept: they remain
266-
visible and manageable via `--all`, and each process fires only its own tasks.
267-
Same-process plugin reloads keep their tasks. Pass `{ ephemeralTasks: false }`
268-
in the plugin options to restore the previous behavior of persisting tasks
269-
across process restarts. Note that upgrading from a release without per-task
270-
owner tracking drops tasks that carry no owner identity once, since their
271-
writer cannot be verified.
258+
other **live** OpenCode processes in the same project are left untouched, and
259+
each process fires only its own tasks. Same-process plugin reloads keep their
260+
tasks. Set `ephemeralTasks: false` in the plugin options to persist tasks
261+
across process restarts.
272262

273263
Adaptive minimum and maximum delays are persisted on each task. The random fallback
274264
and any model-requested `reschedule` are both constrained by that task's bounds. Jitter
@@ -277,8 +267,8 @@ is not added to a model-selected Adaptive time.
277267
For programmatic composition, `LoopConfig.defaultJitterEnabled` controls newly
278268
created Fixed tasks and defaults to `true`. An explicit command
279269
`--jitter=true|false` or tool argument `jitterEnabled` overrides that default.
280-
Existing persisted Fixed tasks without a `jitterEnabled` field retain the legacy
281-
Jitter-on behavior. Because the ticker checks every 5 seconds, actual prompt
270+
Persisted Fixed tasks without a `jitterEnabled` field are treated as
271+
Jitter-on. Because the ticker checks every 5 seconds, actual prompt
282272
injection can occur up to one ticker period after an exact due time.
283273

284274
## Per-session architecture
@@ -292,7 +282,7 @@ Each `/loop` task carries a `sessionID` field:
292282
| User runs `/loop` in session B | Session B becomes active; A's task waits |
293283
| `session.deleted` for session A | All A's tasks cancelled automatically |
294284
| Plugin reload (`opencode` hot-reload) | Old tickers stop, new ticker starts; in-flight tasks guarded by `inflight` Set |
295-
| Process restart (new pid) | With `ephemeralTasks` enabled (default), tasks whose owner process is dead are dropped on load, while tasks owned by other live processes are kept; with it disabled, tasks resume as before |
285+
| Process restart (new pid) | With `ephemeralTasks` enabled (default), tasks whose owner process is dead are dropped on load, while tasks owned by other live processes are left untouched; with it disabled, tasks resume as before |
296286
| Old `tasks.json` without `sessionID` | Dropped on load (with log message) |
297287

298288
## Storage
@@ -303,15 +293,15 @@ Tasks persist to `.opencode/cache/loop/tasks.json` (per project). Fire history i
303293

304294
### Package entrypoints
305295

306-
Current releases expose separate `opencode-plugin-loop/server` and `opencode-plugin-loop/tui` entrypoints so OpenCode installs that auto-load both keep working (the TUI entrypoint is a no-op since results are presented inline). The root export remains the v1-compatible server module for backward compatibility. Programmatic consumers should use the named factory:
296+
The package exposes separate `opencode-plugin-loop/server` and `opencode-plugin-loop/tui` entrypoints so OpenCode installs that auto-load both keep working (the TUI entrypoint is a no-op since results are presented inline). The root export is the same server module. Programmatic consumers should use the named factory:
307297

308298
```typescript
309299
import { LoopPlugin } from "opencode-plugin-loop"
310300
```
311301

312-
### Task lines overlap the input area
302+
### Results presentation
313303

314-
Older releases wrote `/loop` results directly to the terminal or into native dialogs/toasts. Upgrade to the current release: every result is presented inline by the model, and runtime diagnostics go to OpenCode's structured application log.
304+
Every `/loop` result is presented inline by the model — nothing is written directly to the terminal, and no dialogs or toasts are used. Runtime diagnostics go to OpenCode's structured application log.
315305

316306
Also make sure the plugin is installed from only one source. OpenCode loads npm plugins from `opencode.json` and copied plugins under `~/.config/opencode/plugins/` independently, even when they have the same package name.
317307

‎commands/loop.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
description: Run prompts on a schedule. Natural-language Adaptive requests run immediately and the model decides the next check; explicit intervals support --jitter=true|false and --once. Subcommands add --all to cross sessions. See /loop help.
3-
argument-hint: "[5m] [--jitter=true|false] [--once] [prompt text... | help | list | cancel <id> | pause <id> | resume <id> | stop-all] [--all]"
2+
description: Run prompts on a schedule. Natural-language Adaptive requests run immediately and the model decides the next check; explicit intervals support --jitter=true|false and --once. Tasks are scoped to the current session. See /loop help.
3+
argument-hint: "[5m] [--jitter=true|false] [--once] [prompt text... | help | list | cancel <id> | pause <id> | resume <id> | stop-all]"
44
agent: build
55
---
66

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-plugin-loop",
3-
"version": "0.7.3",
3+
"version": "0.7.4",
44
"description": "/loop command for opencode — run prompts on a schedule (fixed, adaptive, or maintenance), modeled after Claude Code's /loop",
55
"type": "module",
66
"main": "./dist/index.js",

‎src/index.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
* /loop check the deploy status — adaptive interval (1min–1hr)
99
* /loop — bare: read .opencode/loop.md or default maintenance
1010
*
11-
* Subcommands (all session-scoped; add `--all` to cross sessions):
12-
* /loop list | status [--all] — show tasks
13-
* /loop cancel | stop <id> [--all] — cancel one
14-
* /loop pause <id> [--all] — pause one
15-
* /loop resume <id> [--all] — resume one
16-
* /loop stop-all [--all] — cancel all
11+
* Subcommands (all scoped to the current session):
12+
* /loop list | status — show tasks
13+
* /loop cancel | stop <id> — cancel one
14+
* /loop pause <id> — pause one
15+
* /loop resume <id> — resume one
16+
* /loop stop | stop-all — cancel all tasks in this session
1717
*
1818
* Per-session architecture:
1919
* - chat.message hook tracks the current active sessionID

0 commit comments

Comments
 (0)