@bojackduy/opencode-loopd 1.7.0 → 1.7.2
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 +21 -0
- package/README.md +28 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/scripts/fix-lease.mjs +0 -15
- package/scripts/manual-tick.mjs +0 -15
- package/scripts/patch-schedule.mjs +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 (2026-08-25) — Scheduled Intervals
|
|
4
|
+
|
|
5
|
+
> Interval requeue for repetitive dialogues — one `/goal` covers `monitor deploy` `periodic tests` `keep going` without manual re-prompt. `5s` `createScheduleWorker` resurrects `complete → active` with `Scheduled tick N/M`.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Scheduled intervals** — `GoalConfig.schedule {everyMs, maxRuns}` (`src/domain/goal.ts:100`), `scheduleEveryMs`/`scheduleMaxRuns` args on `loopd_create_goal` (`src/server/goal-tools.ts:56`), min 1000, `maxRuns` total incl initial (`src/server/goal-tools.ts:83` validation).
|
|
10
|
+
- **Schedule worker** — `src/application/schedule-worker.ts:28` `createScheduleWorker` `intervalMs 5000` scans `complete` scheduled goals, `skip-if-running` (lease/`phase` `running/queued/compacting` + `workspaceWrite` single-writer gate `src/application/goal-service.ts:83`), resurrects `complete→active` `phase=idle` `lastScheduleAt`, injects inbox `Scheduled tick N/M — re-execute objective`, then `continueTurn`.
|
|
11
|
+
- **State v6** — `CURRENT_VERSION 6` `src/infrastructure/state-repository.ts:12`, `scheduleRunCount`/`nextRunAt`/`lastScheduleAt` on `GoalRuntimeState` (`src/domain/runtime.ts:115`), `v5→v6` migration with validation, `createRuntimeState` init `scheduleRunCount 0` (`src/application/goal-service.ts:225`), `complete_goal` counts `scheduleRunCount` and sets `nextRunAt=now+everyMs` while `count < maxRuns` and clears stale lease `leaseExpiresAt`/`activeRunID` (`src/server/goal-tools.ts:377` fix).
|
|
12
|
+
- **Plugin wiring** — `createScheduleWorker` started lazily with `engine`+`worker` in `src/server/plugin.ts:31`, `dispose` stops it.
|
|
13
|
+
- **Owner visibility** — `inspect_background_goal` exposes `config.schedule` + `runtime scheduleRunCount/nextRunAt/lastScheduleAt` (`src/server/owner-tools.ts:105`, `src/server/goal-tools.ts:540`).
|
|
14
|
+
- **Tests** — `test/application/schedule-worker.test.ts:1` 6 tests (resurrect due, skip-if-running, maxRuns, writer serialization, concurrent `workspaceWrite:false`, future `nextRunAt`); `test/infrastructure/state-store.test.ts:42` updated to `version 6`; total `165` pass.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Stale lease blocked schedule** — `complete_goal` now clears `leaseExpiresAt`/`turnStartedAt`/`activeRunID`/`activePromptMessageID` on `complete`; previous left `5m` lease valid while `phase idle`, so `schedule.tick` saw `leaseIsValid` and skipped until expiry.
|
|
19
|
+
|
|
20
|
+
### Live proof
|
|
21
|
+
|
|
22
|
+
- `sched-live-verify` `09cb837c` `10s` `maxRuns 3` `workspaceWrite:false` → `tick.txt` `3` lines `02:00:56` `02:03:43` `02:05:47` `runCount 3` `scheduleRunCount 3` `nextRunAt None` `165` tests still pass; `schedule.tick` events `02:01:29` `02:04:29`.
|
|
23
|
+
|
|
3
24
|
## 1.6.0 (2026-08-24) — Hardened Lifecycle
|
|
4
25
|
|
|
5
26
|
> Merge `feat/goal-evaluation` (43 files, +5516 / -1001 since `1.5.3` `5461e0d`). Hardens prompt correlation, maintenance recovery, workspace serialization, and evaluator verification.
|
package/README.md
CHANGED
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
- **Codex-style engine-driven loop** — idle detection → continuation steering with accumulated context (progress history, transcript tail, inbox). No prompt spam in the parent.
|
|
20
20
|
- **Parent ↔ child visibility** — `list/inspect/read_transcript/send_input` give the parent full observability. Bidirectional inbox lets you steer mid-run.
|
|
21
21
|
- **Safe by default** — per-goal artifact isolation (`.opencode/loopd/goals/<id>/`), `maxTurns`/`maxFailures`/`maxNoProgress`, force-finish → semantic `complete_goal` summary → parent notification via wake-up injection.
|
|
22
|
+
- **Scheduled intervals** — `scheduleEveryMs`/`scheduleMaxRuns` auto-requeues the same goal every N ms (e.g., `10s` monitor, `1h` report) without manual `/goal` spam — `5s` poll, `skip-if-running`, `workspaceWrite` serialization, inbox `Scheduled tick N/M`.
|
|
22
23
|
- **Modal TUI dashboard** — `<leader>d` or `/loop` opens a focused dialog (no leak to chat prompt). Vim-style navigation, live running indicator, per-status borders.
|
|
23
24
|
|
|
24
25
|
Keywords: `opencode` `opencode-plugin` `background-agent` `autonomous` `subagent` `loop` `goal` `tui` `codex` `claude-code` `worker`
|
|
25
26
|
|
|
26
27
|
## What it is
|
|
27
28
|
|
|
28
|
-
opencode-loopd is an **OpenCode plugin** (server + TUI) that adds **background goals** to OpenCode. Each goal owns a **worker (child) subagent** that loops autonomously; the **loop engine** drives continuations; the **dashboard** and **owner tools** keep the parent in control. Think *Codex goals* or *Claude Code loop*, but native to OpenCode's session model.
|
|
29
|
+
opencode-loopd is an **OpenCode plugin** (server + TUI) that adds **background goals** to OpenCode. Each goal owns a **worker (child) subagent** that loops autonomously; the **loop engine** drives continuations; the **schedule worker** requeues the same goal on an interval (`scheduleEveryMs`); the **dashboard** and **owner tools** keep the parent in control. Think *Codex goals* or *Claude Code loop*, but native to OpenCode's session model — with scheduled intervals for repetitive dialogues.
|
|
29
30
|
|
|
30
31
|
## Install
|
|
31
32
|
|
|
@@ -211,6 +212,7 @@ Completion is semantic: the child writes the summary; the plugin forwards it to
|
|
|
211
212
|
```
|
|
212
213
|
|
|
213
214
|
- **Engine-driven loop** (like Codex): `session.idle` → lease + retry + polling → re-prompt child. Not a parent-driven re-prompt machine.
|
|
215
|
+
- **Scheduled requeue** (1.7): `createScheduleWorker` `5s` scans `complete` goals with `scheduleEveryMs`/`maxRuns`; `complete→active` resurrect with `Scheduled tick N/M` inbox, `skip-if-running` and `workspaceWrite` gate.
|
|
214
216
|
- **Accumulated context**: each continuation includes progress history, transcript tail, inbox messages, and artifact dir (unless objective names another dir).
|
|
215
217
|
- **Parent wake-up**: when the child calls `complete_goal`/`block_goal`, `tool.execute.after` injects the child's semantic summary into the owner's session — you see "Loop goal … completed: …" without polling.
|
|
216
218
|
|
|
@@ -245,6 +247,8 @@ Goals accept:
|
|
|
245
247
|
| `maxNoProgress` | `3` | Turns without progress before force-finish |
|
|
246
248
|
| `timeoutMs` | `300000` | Per-turn lease (5 min) |
|
|
247
249
|
| `compactEvery` | — | Compact child every N turns |
|
|
250
|
+
| `scheduleEveryMs` | — | Interval in ms to auto-requeue the same goal after each completion. Minimum 1000. Enables repetitive dialogue reduction without manual re-prompt |
|
|
251
|
+
| `scheduleMaxRuns` | — | Maximum total runs including the initial run. Undefined = unlimited. Requires `scheduleEveryMs` |
|
|
248
252
|
| `checks` | `[]` | Shell commands that must pass for `complete_goal` |
|
|
249
253
|
| `checkCwd` | artifact directory | Directory where completion checks run; workspace writes default to project root |
|
|
250
254
|
| `workspaceWrite` | `true` | Marks shared-workspace mutation; only one active workspace writer is allowed. Set `false` explicitly for artifact-only/read-only work |
|
|
@@ -259,6 +263,29 @@ prevent concurrent agents or `git stash` operations from overwriting each
|
|
|
259
263
|
other. Artifact-only research goals may run concurrently by setting
|
|
260
264
|
`workspaceWrite: false` explicitly.
|
|
261
265
|
|
|
266
|
+
## Scheduled goals — repetitive dialogues without manual re-prompt
|
|
267
|
+
|
|
268
|
+
One `/goal` can now loop on an interval — the engine requeues the **same goal** after each `complete`:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
loopd_create_goal({
|
|
272
|
+
name: "hourly-report",
|
|
273
|
+
objective: "Append date -u to tick.txt each run",
|
|
274
|
+
agent: "smart-agent",
|
|
275
|
+
checks: ["test -f tick.txt"],
|
|
276
|
+
scheduleEveryMs: 3600000, // 1h, min 1000
|
|
277
|
+
scheduleMaxRuns: 24, // total incl initial, omit for unlimited
|
|
278
|
+
workspaceWrite: false
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- **Poll:** `5s` `createScheduleWorker` scans `complete` scheduled goals; `skip-if-running` (lease/`phase`/`workspaceWrite` gate).
|
|
283
|
+
- **Resurrect:** `complete → active` `phase=idle`, injects inbox `Scheduled tick N/M — re-execute objective`, then `continueTurn`.
|
|
284
|
+
- **State:** `scheduleRunCount`/`nextRunAt`/`lastScheduleAt` (`v6` `state.json`), `schedule.tick` events, `schedule.resurrected` server log.
|
|
285
|
+
- **Live proof:** `sched-live-verify` `09cb837c` `10s` `maxRuns 3` → `tick.txt` `3` lines `02:00:56` `02:03:43` `02:05:47` with `runCount 3`.
|
|
286
|
+
|
|
287
|
+
Without schedule you’d send `/goal` 3× manually; with schedule one creation covers `monitor deploy`, `periodic tests`, `keep going until done`.
|
|
288
|
+
|
|
262
289
|
## Dashboard tips
|
|
263
290
|
|
|
264
291
|
- Press `:` then type bare text → sent as `send_goal_input` to the selected goal.
|
package/dist/server.js
CHANGED
|
@@ -499,7 +499,7 @@ function resolveGoalCreationConfig(input) {
|
|
|
499
499
|
const agent = explicitAgent || defaultAgent || undefined;
|
|
500
500
|
const workspaceWrite = requested.workspaceWrite ?? true;
|
|
501
501
|
const explicitChecks = cleanList(requested.checks);
|
|
502
|
-
const defaultChecks = workspaceWrite ? cleanList(defaults.defaultChecks) : [];
|
|
502
|
+
const defaultChecks = workspaceWrite ? cleanList(defaults.defaultChecks || ["bun test"]) : [];
|
|
503
503
|
const checks = explicitChecks.length > 0 ? explicitChecks : defaultChecks;
|
|
504
504
|
if (workspaceWrite && checks.length === 0) {
|
|
505
505
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-loopd",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.2",
|
|
5
5
|
"description": "Codex-inspired background goal engine for OpenCode — autonomous subagents, engine-driven loop, child worker sessions and modal TUI dashboard. Like Claude Code loop for OpenCode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "AGPL-3.0-only",
|
package/scripts/fix-lease.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { mutateState } from "../src/infrastructure/state-repository.ts"
|
|
2
|
-
const dir = "/Users/duytrinh/Code/opencode-loopd"
|
|
3
|
-
const id = "54118aa8-ff7b-471c-b9f4-eab9cf537c41"
|
|
4
|
-
await mutateState(dir, "fix-lease", async (s)=>{
|
|
5
|
-
const r = s.runtimes.find(x=>x.goalID===id)
|
|
6
|
-
if (r) {
|
|
7
|
-
r.leaseExpiresAt = undefined
|
|
8
|
-
r.turnStartedAt = undefined
|
|
9
|
-
r.activeRunID = undefined
|
|
10
|
-
r.activePromptMessageID = undefined
|
|
11
|
-
console.log("cleared lease", r.phase, r.leaseExpiresAt)
|
|
12
|
-
}
|
|
13
|
-
return s
|
|
14
|
-
})
|
|
15
|
-
console.log("fixed")
|
package/scripts/manual-tick.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { readState } from "../src/infrastructure/state-repository.ts"
|
|
2
|
-
import { createScheduleWorker } from "../src/application/schedule-worker.ts"
|
|
3
|
-
import { createGoalService } from "../src/application/goal-service.ts"
|
|
4
|
-
const dir = "/Users/duytrinh/Code/opencode-loopd"
|
|
5
|
-
const mockHost = { createWorker: async()=>{}, promptWorker: async()=>{}, sessionStatus: async()=>"idle", abortSession: async()=>{}, readMessages: async()=>[], compactSession: async()=>{}, notifyOwner: async(e)=>console.log("notify",e)}
|
|
6
|
-
const svc = createGoalService(mockHost)
|
|
7
|
-
svc.continueTurn = async (d, id)=>{ console.log("continueTurn", id)}
|
|
8
|
-
const w = createScheduleWorker({directory: dir, goalService: svc})
|
|
9
|
-
const state = await readState(dir)
|
|
10
|
-
for (const g of state.goals) if (g.id==="54118aa8-ff7b-471c-b9f4-eab9cf537c41") console.log("goal", g.config.schedule, state.runtimes.find(r=>r.goalID===g.id))
|
|
11
|
-
console.log("tick...")
|
|
12
|
-
const n = await w.tick()
|
|
13
|
-
console.log("resurrected", n)
|
|
14
|
-
const after = await readState(dir)
|
|
15
|
-
console.log("after", after.goals.find(g=>g.id==="54118aa8-ff7b-471c-b9f4-eab9cf537c41").status, after.runtimes.find(r=>r.goalID==="54118aa8-ff7b-471c-b9f4-eab9cf537c41"))
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { readState, mutateState } from "../src/infrastructure/state-repository.ts"
|
|
2
|
-
const dir = "/Users/duytrinh/Code/opencode-loopd"
|
|
3
|
-
const id = "54118aa8-ff7b-471c-b9f4-eab9cf537c41"
|
|
4
|
-
await mutateState(dir, "patch-schedule", async (s)=>{
|
|
5
|
-
const g = s.goals.find(x=>x.id===id)
|
|
6
|
-
if (g) {
|
|
7
|
-
g.config.schedule = { everyMs: 10000, maxRuns: 3 }
|
|
8
|
-
console.log("patched", g.config.schedule)
|
|
9
|
-
}
|
|
10
|
-
const r = s.runtimes.find(x=>x.goalID===id)
|
|
11
|
-
if (r) {
|
|
12
|
-
r.scheduleRunCount = 0
|
|
13
|
-
console.log("runtime before", r.scheduleRunCount)
|
|
14
|
-
}
|
|
15
|
-
return s
|
|
16
|
-
})
|
|
17
|
-
const state = await readState(dir)
|
|
18
|
-
const g = state.goals.find(x=>x.id===id)
|
|
19
|
-
console.log("after", g.config.schedule)
|