Skip to content

Commit 375a1a0

Browse files
svelderrainruizGitHub Copilot
andauthored
Runtime daemon: bootstrap allocated worker checkouts into a ready lane state (#997) (#999)
* #997 Add runtime worker readiness bootstrap hook * #997 Address Copilot bootstrap review feedback --------- Co-authored-by: GitHub Copilot <copilot@users.noreply.github.com>
1 parent 62717a0 commit 375a1a0

9 files changed

Lines changed: 521 additions & 13 deletions

File tree

docs/knowledgebase/External-Agent-Runtime.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@ Initial extraction note:
342342
- the observer/adapter surface now also has a worker preparation hook so the
343343
daemon can create or reuse one deterministic checkout per selected lane and
344344
persist `worker-checkout.json` plus `workers/*.json` metadata
345+
- the next worker lifecycle seam now bootstraps an allocated checkout into a
346+
ready lane state and persists `worker-ready.json` plus
347+
`workers-ready/*.json` metadata for resumed daemon turns
345348
- the compare-vi repository wrapper remains at
346349
`tools/priority/runtime-supervisor.mjs`
347350
- that wrapper now includes the first compare-vi scheduler cut: when no manual

packages/runtime-harness/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Adapters may also provide:
5353
step before each daemon cycle
5454
- `prepareWorker(context)` to create or reuse a deterministic worker checkout
5555
for the selected lane before the worker step runs
56+
- `bootstrapWorker(context)` to bootstrap that allocated checkout into a ready
57+
lane state before later worker cycles reuse it
5658

5759
The observer persists scheduler evidence under the runtime directory:
5860

@@ -61,6 +63,8 @@ The observer persists scheduler evidence under the runtime directory:
6163
- `observer-heartbeat.json` now includes the latest scheduler decision summary
6264
- `worker-checkout.json` for the latest prepared worker checkout
6365
- `workers/*.json` for per-lane worker checkout state
66+
- `worker-ready.json` for the latest worker readiness state
67+
- `workers-ready/*.json` for per-lane worker readiness history
6468

6569
The compare-vi repository is the first adapter implementation.
6670

packages/runtime-harness/index.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const STOP_REQUEST_SCHEMA = 'priority/runtime-stop-request@v1';
1414
export const BLOCKER_SCHEMA = 'priority/runtime-blocker@v1';
1515
export const SCHEDULER_DECISION_SCHEMA = 'priority/runtime-scheduler-decision@v1';
1616
export const WORKER_CHECKOUT_SCHEMA = 'priority/runtime-worker-checkout@v1';
17+
export const WORKER_READY_SCHEMA = 'priority/runtime-worker-ready@v1';
1718
export const DEFAULT_RUNTIME_DIR = path.join('tests', 'results', '_agent', 'runtime');
1819
export const DEFAULT_LEASE_SCOPE = 'workspace';
1920
export const ACTIONS = new Set(['status', 'step', 'stop', 'resume']);
@@ -280,6 +281,7 @@ export function createRuntimeAdapter(adapter = {}) {
280281
releaseLease: adapter.releaseLease,
281282
planStep: typeof adapter.planStep === 'function' ? adapter.planStep : null,
282283
prepareWorker: typeof adapter.prepareWorker === 'function' ? adapter.prepareWorker : null,
284+
bootstrapWorker: typeof adapter.bootstrapWorker === 'function' ? adapter.bootstrapWorker : null,
283285
resolveRepository:
284286
typeof adapter.resolveRepository === 'function'
285287
? adapter.resolveRepository
@@ -309,6 +311,19 @@ function summarizeWorker(workerRecord) {
309311
};
310312
}
311313

314+
function summarizeWorkerReady(workerReadyRecord) {
315+
if (!workerReadyRecord) return null;
316+
return {
317+
laneId: workerReadyRecord.laneId,
318+
checkoutPath: workerReadyRecord.checkoutPath,
319+
status: workerReadyRecord.status,
320+
bootstrapCommand: workerReadyRecord.bootstrapCommand,
321+
preparedAt: workerReadyRecord.preparedAt,
322+
readyAt: workerReadyRecord.readyAt,
323+
refreshed: Boolean(workerReadyRecord.refreshed)
324+
};
325+
}
326+
312327
function normalizeWorkerRecord(worker, now) {
313328
if (!worker || typeof worker !== 'object') return null;
314329
const checkoutPath = normalizeText(worker.checkoutPath) || null;
@@ -337,6 +352,36 @@ function normalizeWorkerRecord(worker, now) {
337352
};
338353
}
339354

355+
function normalizeWorkerReadyRecord(workerReady, now) {
356+
if (!workerReady || typeof workerReady !== 'object') return null;
357+
const laneId = normalizeText(workerReady.laneId) || null;
358+
const checkoutPath = normalizeText(workerReady.checkoutPath) || null;
359+
const status = normalizeText(workerReady.status).toLowerCase() || 'ready';
360+
const source = normalizeText(workerReady.source) || null;
361+
const reason = normalizeText(workerReady.reason) || null;
362+
const bootstrapCommand = Array.isArray(workerReady.bootstrapCommand)
363+
? workerReady.bootstrapCommand.map((entry) => String(entry))
364+
: [];
365+
const bootstrapExitCode = Number.isInteger(workerReady.bootstrapExitCode) ? workerReady.bootstrapExitCode : 0;
366+
const preparedAt = normalizeText(workerReady.preparedAt) || toIso(now);
367+
const readyAt = normalizeText(workerReady.readyAt) || toIso(now);
368+
const artifacts = workerReady.artifacts && typeof workerReady.artifacts === 'object' ? workerReady.artifacts : {};
369+
return {
370+
schema: WORKER_READY_SCHEMA,
371+
laneId,
372+
checkoutPath,
373+
status,
374+
source,
375+
reason,
376+
bootstrapCommand,
377+
bootstrapExitCode,
378+
preparedAt,
379+
readyAt,
380+
refreshed: workerReady.refreshed === true || status === 'reused',
381+
artifacts
382+
};
383+
}
384+
340385
function buildActiveLaneSummary(laneRecord) {
341386
if (!laneRecord) return null;
342387
return {
@@ -348,6 +393,7 @@ function buildActiveLaneSummary(laneRecord) {
348393
prUrl: laneRecord.prUrl,
349394
blockerClass: laneRecord.blocker?.blockerClass ?? 'none',
350395
worker: summarizeWorker(laneRecord.worker),
396+
workerReady: summarizeWorkerReady(laneRecord.workerReady),
351397
updatedAt: laneRecord.updatedAt
352398
};
353399
}
@@ -434,6 +480,7 @@ function buildLaneRecord(options, now) {
434480
`lane-${sanitizeSegment(options.branch || 'active')}`;
435481
const blockerClass = options.blockerClass || 'none';
436482
const worker = normalizeWorkerRecord(options.worker, now);
483+
const workerReady = normalizeWorkerReadyRecord(options.workerReady, now);
437484
return {
438485
schema: LANE_SCHEMA,
439486
laneId,
@@ -452,6 +499,7 @@ function buildLaneRecord(options, now) {
452499
observedAt: toIso(now)
453500
},
454501
worker,
502+
workerReady,
455503
createdAt: toIso(now),
456504
updatedAt: toIso(now)
457505
};
@@ -682,6 +730,8 @@ async function runStepAction(context) {
682730
blockerPath,
683731
workerPath: laneRecord?.worker?.checkoutPath ?? null,
684732
workerArtifactPath: laneRecord?.worker?.artifacts?.lanePath ?? null,
733+
workerReadyPath: laneRecord?.workerReady?.artifacts?.latestPath ?? null,
734+
workerReadyArtifactPath: laneRecord?.workerReady?.artifacts?.lanePath ?? null,
685735
statePath: runtimePaths.statePath,
686736
eventsPath: runtimePaths.eventsPath
687737
}
@@ -705,6 +755,7 @@ async function runStepAction(context) {
705755
report.outcome = outcome;
706756
report.turnPath = turnPath;
707757
report.worker = summarizeWorker(laneRecord?.worker);
758+
report.workerReady = summarizeWorkerReady(laneRecord?.workerReady);
708759
report.state = state;
709760
} finally {
710761
const leaseId = report.lease?.acquire?.leaseId ?? null;

0 commit comments

Comments
 (0)