You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add target option so a loop can wake the parent session
A loop could only ever re-prompt the session that created it, so a background
child session had no way to hand control back to the session that spawned it.
When the thing it was watching finally happened, the parent sat idle until a
human noticed.
create_loop now takes target: "self" | "parent". "self" is the default and
behaves exactly as before. "parent" resolves the creating session's parentID
once, at creation, and the scheduler injects iterations there instead.
The resolved id is stored as loop.targetSessionID (null for self loops, so
existing persisted records decode unchanged and keep targeting their own
session). loopTarget() is the single accessor the scheduler reads, which keeps
the change to a handful of call sites in both the V1 and V2 paths: busy
deferral, restricted-agent deferral, dynamicPending registration, the prompt
itself, and post-send bookkeeping all follow the target.
Two cases need the target rather than the creator to stay correct. The
iteration prompt tells the receiving session to call stop_loop and
schedule_next_run, so requireSessionLoop now accepts the target session as
well; otherwise a parent-targeted loop could never be stopped by the session
actually running it. Dynamic loops register under the target with sawBusy
false when targeting a parent, because the creating child's idle event would
otherwise settle the loop before the parent was ever prompted.
Creating a parent-targeted loop from a root session fails at create time
rather than silently never firing.
Copy file name to clipboardExpand all lines: README.md
+17Lines changed: 17 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,7 @@ The OpenCode Loop Plugin adds:
13
13
-`/loop <interval> <instruction>` and `/loop <instruction>` (dynamic pacing) as an OpenCode command for TUI, desktop, and web.
14
14
- A server-side scheduler with per-loop timers that injects a synthetic iteration prompt only when the session is idle, with busy backoff.
15
15
- Dynamic loops where the agent itself picks the delay before each next iteration via `schedule_next_run`, mirroring Claude Code's self-paced `/loop`.
16
+
- Parent wake-up: `create_loop` takes `target: "self" | "parent"`, so a background child session can hand control back to the session that spawned it instead of only re-waking itself.
- Persistent loop state that survives OpenCode restarts, with atomic writes and owner-only file permissions.
18
19
- A TUI sidebar with live countdowns and a `Loops` command-palette entry to run, pause, resume, or stop loops.
@@ -120,6 +121,22 @@ After creating a loop, the agent immediately performs the first iteration in the
120
121
121
122
A dynamic loop mirrors Claude Code's self-paced `/loop`: at the end of each iteration the agent calls `schedule_next_run` with a delay in seconds and a one-sentence reason ("watching CI run"), or calls `stop_loop` to end the loop. If an iteration ends without doing either, the loop ends — exactly like omitting `ScheduleWakeup` in Claude Code.
122
123
124
+
### Waking the parent session
125
+
126
+
By default a loop wakes the session that created it. Pass `target: "parent"` to `create_loop` and iterations are injected into that session's parent instead:
127
+
128
+
```
129
+
create_loop(
130
+
instruction: "Check whether PR #42 has merged. If it has, say so and call stop_loop.",
131
+
interval: "5m",
132
+
target: "parent"
133
+
)
134
+
```
135
+
136
+
This is for background work. A subagent spawned to babysit CI can keep polling on its own schedule and then hand control back to the session that spawned it, instead of that session sitting idle until a human notices. One tool, one flag — `"self"` re-wakes this session, `"parent"` wakes the one above it.
137
+
138
+
The parent is resolved once, when the loop is created. Creating a parent-targeted loop from a root session fails immediately rather than silently never firing. Both the creating session and the woken parent can `list_loops` and `stop_loop` it, and the loop is stopped if the parent session is deleted.
139
+
123
140
### How iterations are scheduled
124
141
125
142
- Iterations only run while the session is idle. If a loop comes due while the session is busy, it is deferred with a short backoff and retried when the session goes idle.
@@ -510,6 +516,7 @@ Preserve each loop's id, cadence, instruction, and status in the compacted conte
510
516
}
511
517
512
518
// src/server.ts
519
+
varTARGET_DESCRIPTION='Whose session the iteration wakes. "self" (default) re-prompts this session on the cadence. "parent" prompts the session that spawned this one, so a background child can hand control back to its parent when the instruction fires.';
513
520
varDEFAULT_COMMAND_NAME="loop";
514
521
varDEFAULT_BUSY_BACKOFF_SECONDS=60;
515
522
varDEFAULT_FAILURE_BACKOFF_SECONDS=60;
@@ -646,50 +653,51 @@ var server = async ({ client }, options) => {
646
653
awaitstopLoop(loopID,`expired after ${Math.round(maxLoopAgeMs/86400000)} days`);
thrownewError(`session "${sessionID}" has no parent session, so target "parent" has nothing to wake; create the loop from a child session`);
749
+
}
750
+
returnparentID;
751
+
}
735
752
awaitrehydrate().catch((error)=>log("error","Failed to rehydrate loops",{error: errorinstanceofError ? error.message : String(error)}));
736
753
return{
737
754
asyncdispose(){
@@ -747,26 +764,29 @@ var server = async ({ client }, options) => {
747
764
},
748
765
tool: {
749
766
create_loop: {
750
-
description: 'Create a recurring loop for this session only when explicitly requested (for example via the /loop command). The scheduler re-injects the instruction while the session is idle. Pass interval for fixed cadence (like "10m"); omit it for a dynamic loop where the agent schedules each next run with schedule_next_run.',
767
+
description: 'Create a recurring loop only when explicitly requested (for example via the /loop command). One flag decides whose session wakes up: target "self" (default) re-injects the instruction into THIS session while it is idle; target "parent" prompts the session that spawned this one instead, so a background child can wake its parent when the instruction fires. Pass interval for fixed cadence (like "10m"); omit it for a dynamic loop where the agent schedules each next run with schedule_next_run.',
751
768
args: {
752
769
instruction: z.string().min(1).max(MAX_PROMPT_CHARS).describe("The instruction to perform on each iteration."),
753
770
interval: z.string().optional().describe('Fixed cadence like "30s", "10m", "2h", or "1d". Omit for a dynamically paced loop.'),
754
-
max_runs: z.number().int().positive().optional().describe("Optional maximum number of iterations before the loop completes.")
771
+
max_runs: z.number().int().positive().optional().describe("Optional maximum number of iterations before the loop completes."),
@@ -1263,7 +1294,7 @@ function loopToolsV2(services) {
1263
1294
return[
1264
1295
{
1265
1296
name: "create_loop",
1266
-
description: 'Create a recurring loop for this session only when explicitly requested (for example via the /loop command). The scheduler re-injects the instruction while the session is idle. Pass interval for fixed cadence (like "10m"); omit it for a dynamic loop where the agent schedules each next run with schedule_next_run.',
1297
+
description: 'Create a recurring loop only when explicitly requested (for example via the /loop command). One flag decides whose session wakes up: target "self" (default) re-injects the instruction into THIS session while it is idle; target "parent" prompts the session that spawned this one instead, so a background child can wake its parent when the instruction fires. Pass interval for fixed cadence (like "10m"); omit it for a dynamic loop where the agent schedules each next run with schedule_next_run.',
1267
1298
input: v2ObjectSchema({
1268
1299
instruction: {
1269
1300
type: "string",
@@ -1279,23 +1310,30 @@ function loopToolsV2(services) {
1279
1310
type: "integer",
1280
1311
minimum: 1,
1281
1312
description: "Optional maximum number of iterations before the loop completes."
0 commit comments