Skip to content

Commit 9c374ae

Browse files
svelderrainruizGitHub Copilot
andauthored
Runtime daemon: add adapter scheduler seam (#993) (#994)
* #993 Add runtime daemon scheduler seam * #993 Retrigger Copilot review on push --------- Co-authored-by: GitHub Copilot <copilot@users.noreply.github.com>
1 parent 1b57591 commit 9c374ae

17 files changed

Lines changed: 4857 additions & 0 deletions

docs/knowledgebase/Agent-Handoff-Surfaces.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ entrypoint and machine-generated live state.
4949
- Add current state to generated artifacts, issue comments, or repo docs as
5050
appropriate.
5151
- Do not append dated execution history back into `AGENT_HANDOFF.txt`.
52+
- For the persistent supervisor design that consumes these surfaces, see
53+
[`External-Agent-Runtime.md`](./External-Agent-Runtime.md).
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
<!-- markdownlint-disable-next-line MD041 -->
2+
# External Agent Runtime
3+
4+
This document defines the persistent supervisor needed to make the agent behave
5+
continuously outside a single chat/API turn.
6+
7+
The model does not "run forever". The runtime does. It does that by repeatedly
8+
loading fresh state, giving the model a bounded task packet, executing the
9+
checked-in helper surface, persisting evidence, and immediately scheduling the
10+
next turn until one of two stop conditions is true:
11+
12+
- a human explicitly stops the runtime
13+
- no open upstream epics remain in
14+
`LabVIEW-Community-CI-CD/compare-vi-cli-action`
15+
16+
## Goals
17+
18+
- Drain upstream epic backlog without relying on manual "continue" prompts.
19+
- Keep upstream as the only ship/no-ship authority.
20+
- Reuse repo-native helpers instead of inventing a parallel mutation surface.
21+
- Preserve single-writer safety for files, branches, and GitHub mutations.
22+
- Make every turn restartable from durable state instead of transcript memory.
23+
- Advance a different lane while CI, review, or merge queue waits.
24+
25+
## Non-Goals
26+
27+
- Replacing the checked-in `tools/priority/*` helper layer.
28+
- Bypassing GitHub protections, required checks, or review policy.
29+
- Letting the model hold an unbounded transcript or unmanaged background job.
30+
- Using a single shared mutable checkout for all fork lanes.
31+
32+
## Runtime Topology
33+
34+
| Component | Responsibility | Existing repo surface |
35+
| --- | --- | --- |
36+
| Supervisor | Owns the event loop, stop conditions, ranking, and lease renewal. | `tools/priority/queue-supervisor.mjs` |
37+
| State mirror | Refreshes live GitHub epics, child issues, PRs, checks, and project metadata. | `gh`, `priority:project:portfolio:check` |
38+
| Lease broker | Prevents overlapping writers in the same repo scope. | `tools/priority/agent-writer-lease.mjs` |
39+
| Worker pool | Maintains isolated worktrees per lane/fork. | git worktrees + `priority:develop:sync` |
40+
| Command broker | Executes repo-native helpers and records fallback evidence. | `bootstrap.ps1`, `priority:pr`, `priority:issue:mirror` |
41+
| CI watcher | Watches hosted/self-hosted checks and wakes waiting lanes. | `ci:watch:rest`, `ci:watch:safe` |
42+
| Evidence sink | Writes runtime state, events, and lane checkpoints. | `tests/results/_agent/` |
43+
44+
## Core Idea
45+
46+
The runtime is an external supervisor process, not a longer prompt. It runs a
47+
bounded loop:
48+
49+
1. Acquire the writer lease for the repo and lane scopes.
50+
2. Refresh live GitHub state from upstream and both fork planes.
51+
3. Recompute open epics, child issues, PRs, blockers, and free fork lanes.
52+
4. Select exactly one highest-value next action.
53+
5. Materialize a dedicated worker checkout for that lane.
54+
6. Call the model with a compact task packet for that one action.
55+
7. Execute the resulting helper/tool calls.
56+
8. Persist evidence and update runtime state.
57+
9. Re-enter the scheduler immediately.
58+
59+
The model stays stateless between turns. The runtime owns continuity.
60+
61+
## Durable State
62+
63+
The runtime should keep its own durable state outside the model transcript and
64+
mirror a compact copy into the repository evidence tree.
65+
66+
Recommended persistent store:
67+
68+
- SQLite for scheduler state and event indexes
69+
- NDJSON append log for operator replay
70+
- repo evidence files under `tests/results/_agent/runtime/`
71+
72+
Recommended repo-visible artifacts:
73+
74+
- `tests/results/_agent/runtime/runtime-state.json`
75+
- `tests/results/_agent/runtime/runtime-events.ndjson`
76+
- `tests/results/_agent/runtime/lanes/<lane-id>.json`
77+
- `tests/results/_agent/runtime/turns/<timestamp>-<lane-id>.json`
78+
- `tests/results/_agent/runtime/last-blocker.json`
79+
80+
Each lane record should include:
81+
82+
- upstream issue number
83+
- parent epic number
84+
- fork remote (`origin`, `personal`, or `upstream`)
85+
- branch name
86+
- PR URL if present
87+
- current blocker class
88+
- worker path
89+
- last heartbeat time
90+
- last meaningful change
91+
92+
## Lease Model
93+
94+
`tools/priority/agent-writer-lease.mjs` already provides the base lock
95+
primitive. The runtime should layer named scopes on top of it instead of adding
96+
new locking rules.
97+
98+
Required logical scopes:
99+
100+
- `workspace` for repo-wide bootstrap and develop-sync activity
101+
- `fork/<remote>` for one active PR lane per fork
102+
- `issue/<number>` for one active lane per child issue
103+
- `upstream-promotion` for the single upstream merge/admission lane
104+
105+
Lease rules:
106+
107+
- renew heartbeat every 30 to 60 seconds
108+
- treat stale leases as recoverable after the configured timeout
109+
- on lease loss, stop writes, persist a checkpoint, and reschedule
110+
- never let two workers mutate the same fork lane or issue lane
111+
112+
## Worker Model
113+
114+
Each active lane gets its own checkout rooted under a runtime-managed working
115+
directory. A worker should never share a mutable checkout with another lane.
116+
117+
Recommended layout:
118+
119+
```text
120+
.runtime/
121+
bare/
122+
worktrees/
123+
origin-977/
124+
personal-978/
125+
upstream-981/
126+
```
127+
128+
Worker bootstrap sequence:
129+
130+
1. fetch `upstream`, `origin`, and `personal`
131+
2. sync `develop` with
132+
`node tools/npm/run-script.mjs priority:develop:sync -- --fork-remote <remote>`
133+
3. check out or reattach the lane branch
134+
4. run `pwsh -NoLogo -NoProfile -File tools/priority/bootstrap.ps1`
135+
5. refresh local handoff and standing-priority artifacts
136+
137+
## Model Turn Contract
138+
139+
The runtime should never pass the full historical conversation back to the
140+
model. It should compile a bounded task packet from durable state.
141+
142+
Each task packet should contain:
143+
144+
- repo identity and active lane
145+
- exact objective for this turn
146+
- current branch, PR, and check status
147+
- relevant file paths and prior evidence artifacts
148+
- the last small set of runtime events for this lane
149+
- allowed helper surfaces and known fallbacks
150+
151+
Each turn should have a hard budget, for example:
152+
153+
- one lane
154+
- one primary objective
155+
- one promotion attempt or one blocker fix
156+
- a bounded tool-call count
157+
- a bounded wall-clock timeout
158+
159+
When the budget expires, the runtime persists the result and schedules the next
160+
turn. That is how it behaves continuously without pretending a single reply is
161+
infinite.
162+
163+
## Scheduling Policy
164+
165+
The scheduler should implement the repo's backlog policy directly.
166+
167+
Refresh phase:
168+
169+
1. run bootstrap and standing-priority sync
170+
2. read live upstream issues and PRs
171+
3. repair missing epic or child links before feature work
172+
4. refresh fork mirror issues and fork PR state
173+
174+
Selection phase:
175+
176+
1. candidate epics are all open upstream epics
177+
2. candidate children are all open non-epic issues attached to open epics
178+
3. rank epics and children using the repo's selection algorithm
179+
4. choose one action type in this order:
180+
- fix blocker on an existing upstream PR
181+
- advance a different child issue on a free fork while upstream waits
182+
- finish a fork PR that is one iteration from promotion
183+
- split or repair an open epic with no executable children
184+
- start the highest-ranked unblocked child issue
185+
186+
Execution phase:
187+
188+
1. acquire required scopes
189+
2. run the bounded model turn
190+
3. watch CI asynchronously
191+
4. on any meaningful state change, reschedule immediately
192+
193+
## Command Surface
194+
195+
The runtime should treat repo helpers as the default mutation API.
196+
197+
Preferred commands:
198+
199+
- `pwsh -NoLogo -NoProfile -File tools/priority/bootstrap.ps1`
200+
- `node tools/npm/run-script.mjs priority:develop:sync`
201+
- `node tools/npm/run-script.mjs priority:project:portfolio:apply`
202+
- `node tools/npm/run-script.mjs priority:github:metadata:apply`
203+
- `node tools/npm/run-script.mjs priority:issue:mirror`
204+
- `node tools/npm/run-script.mjs priority:pr`
205+
- `node tools/npm/run-script.mjs priority:queue:supervisor`
206+
- `node tools/npm/run-script.mjs ci:watch:rest`
207+
- `node tools/npm/run-script.mjs ci:watch:safe`
208+
209+
Fallback rule:
210+
211+
- raw `gh` is allowed only when no checked-in helper can perform the action, or
212+
when reproducing a helper bug with explicit evidence written to the runtime
213+
event log
214+
215+
## Event Sources
216+
217+
The supervisor should combine polling with GitHub webhooks.
218+
219+
Wake-up sources:
220+
221+
- issue opened, edited, labeled, or relinked
222+
- PR opened, synchronized, reviewed, merged, or closed
223+
- check suite and workflow run state changes
224+
- queue-supervisor readiness changes
225+
- lease stale or worker crash events
226+
- explicit human stop or priority override
227+
228+
Polling is still required as the recovery path when webhooks are missed.
229+
230+
## Failure Handling
231+
232+
Expected failure classes:
233+
234+
- merge blocker
235+
- review blocker
236+
- CI blocker
237+
- scope blocker
238+
- helper/runtime bug
239+
- auth or policy drift
240+
241+
Runtime response:
242+
243+
- classify the blocker explicitly
244+
- attach it to the correct epic if it reveals new scope
245+
- persist evidence paths and API error details
246+
- start the next-best unblocked lane immediately
247+
248+
If a helper bug is discovered, the runtime should create a focused upstream
249+
issue, parent it under the correct epic, and keep moving.
250+
251+
## Human Control Surface
252+
253+
The external runtime needs explicit controls outside the model:
254+
255+
- `start` with repo, model, token source, and workspace root
256+
- `stop` for graceful drain and checkpoint
257+
- `pause` to stop new mutations but keep observing
258+
- `resume` to continue from durable state
259+
- `status` to print current lane, blockers, leases, and recent events
260+
261+
Recommended stop file or command channel:
262+
263+
- `tests/results/_agent/runtime/stop-request.json`
264+
265+
The runtime should check that file before starting a new turn.
266+
267+
## Deployment Shape
268+
269+
Recommended first deployment:
270+
271+
- one always-on supervisor process for the canonical repo
272+
- one host with git, `gh`, Node.js, PowerShell, and token access
273+
- one bare mirror plus per-lane worktrees
274+
- one durable state directory outside the repo checkout
275+
276+
Suitable hosts:
277+
278+
- a Windows service on the existing self-hosted runner
279+
- a long-lived VM with the repo mounted locally
280+
- a containerized worker host if LabVIEW-specific tasks stay on separate runners
281+
282+
## Incremental Rollout
283+
284+
Phase 1:
285+
286+
- single supervisor
287+
- one active fork lane at a time
288+
- polling only
289+
- JSON evidence + SQLite state
290+
- bounded model auto-resume
291+
292+
Phase 2:
293+
294+
- parallel fork lanes with per-fork mutexes
295+
- upstream-promotion mutex
296+
- CI watcher wake-ups
297+
- blocker issue auto-creation
298+
299+
Phase 3:
300+
301+
- webhook ingestion
302+
- adaptive scheduling from queue and SLO artifacts
303+
- richer operator dashboard
304+
305+
## Minimal Pseudocode
306+
307+
```text
308+
while not stopRequested():
309+
refreshState()
310+
repairMetadata()
311+
lane = selectNextLane()
312+
if lane is None:
313+
if noOpenUpstreamEpics():
314+
exit("no-open-epics")
315+
sleep(shortInterval)
316+
continue
317+
with acquireScopes(lane):
318+
task = compileTaskPacket(lane)
319+
result = runBoundedModelTurn(task)
320+
persist(result)
321+
if result.startedCiWatch:
322+
armWatcher(lane)
323+
```
324+
325+
## Design Decision
326+
327+
The runtime should be built as a thin external supervisor around the existing
328+
repo contracts, not as a new agent framework embedded in prompts. The repository
329+
already has the right mutation helpers, watcher outputs, lease primitive, and
330+
handoff artifacts. The missing piece is the always-on scheduler that keeps
331+
calling them.
332+
333+
Initial extraction note:
334+
335+
- the portable core now starts in `packages/runtime-harness/`
336+
- the worker and observer loop seams now live in
337+
`packages/runtime-harness/worker.mjs` and
338+
`packages/runtime-harness/observer.mjs`
339+
- the observer now calls an adapter scheduler hook before each worker turn and
340+
persists `scheduler-decision.json` plus per-cycle
341+
`scheduler-decisions/*.json` artifacts under the runtime directory
342+
- the compare-vi repository wrapper remains at
343+
`tools/priority/runtime-supervisor.mjs`
344+
- that wrapper now includes the first compare-vi scheduler cut: when no manual
345+
lane is supplied, it plans from the bootstrapped standing-priority cache or
346+
router artifacts instead of acting as a heartbeat-only shell
347+
- the compare-vi Linux-only daemon wrapper lives at
348+
`tools/priority/runtime-daemon.mjs`
349+
- the Docker Desktop Linux launcher now starts at
350+
`tools/priority/Start-RuntimeDaemonInDocker.ps1`
351+
- the Docker Desktop lifecycle controller now lives at
352+
`tools/priority/Manage-RuntimeDaemonInDocker.ps1`
353+
and now acquires the Linux Docker Desktop context automatically under a
354+
host-wide engine lock before running lifecycle commands
355+
- the Docker manager also now classifies detached daemon health from
356+
`observer-heartbeat.json`, persists a health artifact, and restarts stale or
357+
wedged running containers deterministically on `start`
358+
- that same manager now exposes `reconcile`, which scans persisted lane state,
359+
reapplies `start` as the repair primitive per lane, and writes a shared
360+
`docker-daemon-reconcile.json` artifact for operator-free recovery

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@
118118
"priority:schema": "node tools/npm/run-script.mjs schema:validate -- --schema docs/schemas/standing-priority-issue-v1.schema.json --data tests/results/_agent/issue/[0-9]*.json --optional",
119119
"priority:show": "node -e \"console.log(require('fs').readFileSync('tests/results/_agent/issue/router.json','utf8'))\"",
120120
"priority:safe-git:trend": "node tools/priority/summarize-safe-git-telemetry.mjs",
121+
"priority:runtime:daemon": "node tools/priority/runtime-daemon.mjs",
122+
"priority:runtime:daemon:docker": "pwsh -NoLogo -NoProfile -File tools/priority/Manage-RuntimeDaemonInDocker.ps1 -Action start",
123+
"priority:runtime:daemon:docker:logs": "pwsh -NoLogo -NoProfile -File tools/priority/Manage-RuntimeDaemonInDocker.ps1 -Action logs",
124+
"priority:runtime:daemon:docker:reconcile": "pwsh -NoLogo -NoProfile -File tools/priority/Manage-RuntimeDaemonInDocker.ps1 -Action reconcile",
125+
"priority:runtime:daemon:docker:status": "pwsh -NoLogo -NoProfile -File tools/priority/Manage-RuntimeDaemonInDocker.ps1 -Action status",
126+
"priority:runtime:daemon:docker:stop": "pwsh -NoLogo -NoProfile -File tools/priority/Manage-RuntimeDaemonInDocker.ps1 -Action stop",
127+
"priority:runtime:supervisor": "node tools/priority/runtime-supervisor.mjs",
128+
"runtime-harness:test": "node --test packages/runtime-harness/test/*.mjs",
121129
"priority:sync": "node tools/priority/sync-standing-priority.mjs",
122130
"priority:sync:strict": "node tools/priority/sync-standing-priority.mjs --fail-on-missing",
123131
"priority:sync:lane": "node tools/priority/sync-standing-priority.mjs --fail-on-missing --fail-on-multiple --auto-select-next",

0 commit comments

Comments
 (0)