@runuai/host 0.9.149 → 0.9.151
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/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/meta/_journal.json +14 -0
- package/db/schema.ts +188 -0
- package/lib/agent-cli.ts +11 -13
- package/lib/agent.ts +177 -7
- package/lib/agents/durable-proc.ts +56 -44
- package/lib/agents/registry.ts +19 -3
- package/lib/agents/session-state.ts +23 -10
- package/lib/agents/transport.ts +101 -69
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +4 -3
- package/lib/attachments.ts +19 -17
- package/lib/capture-flow.ts +319 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2544 -0
- package/lib/cloud-credential-runtime.ts +1035 -0
- package/lib/codex-auth.ts +21 -1
- package/lib/container-runtime.ts +76 -0
- package/lib/credential-cleanup-inventory.ts +405 -0
- package/lib/credential-consumers/engine-anchor.ts +1876 -0
- package/lib/credential-consumers/engines.ts +773 -0
- package/lib/credential-consumers/github.ts +594 -0
- package/lib/credential-consumers/mcp.ts +384 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-store-pickup.ts +15 -2
- package/lib/credential-store.ts +361 -33
- package/lib/credentials-activation-authority.ts +546 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/db.ts +11 -0
- package/lib/docker-exec.ts +3 -1
- package/lib/engine-accounts.ts +42 -32
- package/lib/engine-connect.ts +47 -2
- package/lib/engine-credential-env.ts +50 -0
- package/lib/engine-login.ts +83 -1
- package/lib/env-file.ts +62 -8
- package/lib/env.ts +5 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +219 -177
- package/lib/git-identity.ts +59 -14
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +56 -21
- package/lib/host-env.ts +17 -4
- package/lib/launch-mode.ts +25 -0
- package/lib/legacy-runtime-retirement.ts +388 -0
- package/lib/machine-exec.ts +14 -3
- package/lib/machine-provider-aws.ts +50 -8
- package/lib/machine-provider-local.ts +4 -0
- package/lib/machine-provider.ts +4 -0
- package/lib/managed-operation-drain.ts +8 -0
- package/lib/managed-runtime.ts +371 -7
- package/lib/mcp-capture.ts +860 -0
- package/lib/mcp-connections.ts +8 -1
- package/lib/mcp-gateway.ts +168 -10
- package/lib/obs.ts +2 -6
- package/lib/orchestrator.ts +1847 -236
- package/lib/person-capture.ts +327 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +50 -3
- package/lib/runtime-archive.ts +68 -1
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +429 -13
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/task-trust.ts +34 -3
- package/lib/sidecar-resume.ts +18 -5
- package/lib/skills.ts +47 -8
- package/lib/standard-image.ts +157 -103
- package/lib/stopped-task-status-queue.ts +68 -10
- package/lib/task-credential-cleanup.ts +525 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +66 -163
- package/lib/task-env-source.ts +68 -14
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +69 -28
- package/lib/task-environment/docker.ts +101 -20
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +459 -96
- package/lib/task-environment/machine.ts +101 -11
- package/lib/task-environment/self.ts +108 -50
- package/lib/task-environment/types.ts +89 -5
- package/lib/task-environment/workspace-files.ts +623 -25
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-gateway-recovery.ts +4 -1
- package/lib/task-gateway-state.ts +15 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +56 -2
- package/lib/token-broker-local.ts +28 -4
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +368 -28
- package/lib/token-lanes/openai-oauth.ts +7 -3
- package/lib/token-lanes/xai-oauth.ts +7 -3
- package/lib/transcript.ts +9 -5
- package/package.json +6 -2
- package/scripts/agent/task-up.sh +242 -100
- package/src/cloud-credentials.ts +2399 -0
- package/src/event-outbox.ts +8 -6
- package/src/index.ts +1386 -875
- package/src/main.ts +1062 -140
- package/src/outbound-frame.ts +31 -0
- package/src/project-env-policy.ts +125 -10
- package/src/protocol.ts +699 -23
- package/src/ui/server.ts +48 -10
package/lib/runtime-state.ts
CHANGED
|
@@ -46,14 +46,30 @@ export function activeLoaderHostTasks(): HostTask[] {
|
|
|
46
46
|
.all();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** ADR-154: the runs whose runtime survives this process's start running:
|
|
50
|
+
* each task marked running whose admitted run is open and established. */
|
|
51
|
+
export function runningTaskRuns(db: Db = getDb()): Array<{ taskId: string; runEpoch: number }> {
|
|
52
|
+
const rows = db
|
|
53
|
+
.select({ taskId: schema.hostTasks.taskId })
|
|
54
|
+
.from(schema.hostTasks)
|
|
55
|
+
.where(eq(schema.hostTasks.statusMirror, "running"))
|
|
56
|
+
.all();
|
|
57
|
+
return rows.flatMap(({ taskId }) => {
|
|
58
|
+
const run = readTaskRunEpochState(taskId, db);
|
|
59
|
+
return run !== null && run.assignmentOpen && run.runtimeEstablishedEpoch === run.highestRunEpoch && run.highestRunEpoch > 0
|
|
60
|
+
? [{ taskId, runEpoch: run.highestRunEpoch }]
|
|
61
|
+
: [];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/** Remove a provisional first-seen row when admission loses a lifecycle gate. */
|
|
50
|
-
export function deleteHostTask(taskId: string): void {
|
|
66
|
+
export function deleteHostTask(taskId: string, db: Db = getDb()): void {
|
|
51
67
|
// Row deletions are rare and load-bearing; a silent one cost a live
|
|
52
68
|
// machine-task forensic session (2026-08-27). Name the caller.
|
|
53
69
|
console.warn(
|
|
54
70
|
`[runtime-state] deleting host task row ${taskId}\n${(new Error().stack ?? "").split("\n").slice(2, 5).join("\n")}`,
|
|
55
71
|
);
|
|
56
|
-
|
|
72
|
+
db
|
|
57
73
|
.delete(schema.hostTasks)
|
|
58
74
|
.where(eq(schema.hostTasks.taskId, taskId))
|
|
59
75
|
.run();
|
|
@@ -62,9 +78,10 @@ export function deleteHostTask(taskId: string): void {
|
|
|
62
78
|
/** Final orphan-GC metadata purge. Runtime/filesystem and credential proofs
|
|
63
79
|
* happen before this transaction; a SQLite failure leaves every task-owned
|
|
64
80
|
* row intact rather than acknowledging a partial metadata deletion. */
|
|
65
|
-
export function purgeHostTaskState(taskId: string): void {
|
|
81
|
+
export function purgeHostTaskState(taskId: string, db: Db = getDb()): void {
|
|
66
82
|
console.warn(`[runtime-state] purging all host state for task ${taskId}`);
|
|
67
|
-
|
|
83
|
+
db.transaction((tx) => {
|
|
84
|
+
closeCurrentTaskRunAssignmentIn(tx, taskId);
|
|
68
85
|
tx.delete(schema.hostAgentSessions)
|
|
69
86
|
.where(eq(schema.hostAgentSessions.taskId, taskId))
|
|
70
87
|
.run();
|
|
@@ -172,8 +189,9 @@ export function recordTaskUpResult(
|
|
|
172
189
|
result: TaskUpResult,
|
|
173
190
|
environment: TaskEnvironmentLocator,
|
|
174
191
|
envMode: ProjectEnvMode,
|
|
192
|
+
db: Db = getDb(),
|
|
175
193
|
): void {
|
|
176
|
-
|
|
194
|
+
upsertHostTaskIn(db, taskId, {
|
|
177
195
|
codeServerPort: result.codeServerPort ?? null,
|
|
178
196
|
previewPorts: JSON.stringify(result.previewPorts ?? []),
|
|
179
197
|
composeProject: result.composeProject,
|
|
@@ -188,15 +206,409 @@ export function recordTaskUpResult(
|
|
|
188
206
|
});
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
export
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
export type TaskRunEpochAdmission =
|
|
210
|
+
| { kind: "legacy" }
|
|
211
|
+
| {
|
|
212
|
+
kind: "accepted";
|
|
213
|
+
relation: "advanced" | "retry";
|
|
214
|
+
previousRunEpoch: number;
|
|
215
|
+
hadTask: boolean;
|
|
216
|
+
}
|
|
217
|
+
| {
|
|
218
|
+
kind: "stale";
|
|
219
|
+
currentRunEpoch: number;
|
|
220
|
+
reason: "older" | "closed";
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export interface TaskRunEpochState {
|
|
224
|
+
/** Highest v1 dispatch this runner has ever admitted for the task. */
|
|
225
|
+
highestRunEpoch: number;
|
|
226
|
+
/** False after Stop/teardown; only a strictly higher epoch may reopen it. */
|
|
227
|
+
assignmentOpen: boolean;
|
|
228
|
+
/** Zero until a runtime has successfully completed task-up for this epoch. */
|
|
229
|
+
runtimeEstablishedEpoch: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export type CloseTaskRunAssignmentResult =
|
|
233
|
+
| { kind: "missing" }
|
|
234
|
+
| { kind: "mismatch"; currentRunEpoch: number }
|
|
235
|
+
| { kind: "already_closed"; runEpoch: number }
|
|
236
|
+
| { kind: "closed"; runEpoch: number };
|
|
237
|
+
|
|
238
|
+
export type StoppedTaskRunAssignment =
|
|
239
|
+
| { kind: "legacy" }
|
|
240
|
+
| { kind: "closed"; runEpoch: number };
|
|
241
|
+
|
|
242
|
+
function persistedRunEpoch(value: number): number {
|
|
243
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
244
|
+
throw new Error("persisted assignment run epoch is invalid");
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function taskRunEpochStateIn(
|
|
250
|
+
db: DbExecutor,
|
|
251
|
+
taskId: string,
|
|
252
|
+
): TaskRunEpochState | null {
|
|
253
|
+
const assignment = db
|
|
254
|
+
.select()
|
|
255
|
+
.from(schema.hostTaskRunAssignments)
|
|
256
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
257
|
+
.get();
|
|
258
|
+
if (!assignment) return null;
|
|
259
|
+
const task = db
|
|
260
|
+
.select({ runEpoch: schema.hostTasks.runEpoch })
|
|
261
|
+
.from(schema.hostTasks)
|
|
262
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
263
|
+
.get();
|
|
264
|
+
return {
|
|
265
|
+
highestRunEpoch: persistedRunEpoch(assignment.highestRunEpoch),
|
|
266
|
+
assignmentOpen: assignment.assignmentOpen,
|
|
267
|
+
runtimeEstablishedEpoch: hostTaskRunEpoch(task ?? {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Read all three durable assignment facts from one SQLite snapshot. */
|
|
272
|
+
export function readTaskRunEpochState(
|
|
273
|
+
taskId: string,
|
|
274
|
+
db: Db = getDb(),
|
|
275
|
+
): TaskRunEpochState | null {
|
|
276
|
+
return db.transaction((tx) => taskRunEpochStateIn(tx, taskId));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* A runtime may be reused only when both authorities name this exact run:
|
|
281
|
+
* the assignment is still open at its monotonic floor, and task-up recorded
|
|
282
|
+
* the runtime as fully established. Legacy reuse remains available only on a
|
|
283
|
+
* task that has never crossed the v1 floor.
|
|
284
|
+
*/
|
|
285
|
+
export function canReuseTaskRuntimeForRun(
|
|
286
|
+
taskId: string,
|
|
287
|
+
runEpoch: number | undefined,
|
|
288
|
+
db: Db = getDb(),
|
|
289
|
+
): boolean {
|
|
290
|
+
if (
|
|
291
|
+
runEpoch !== undefined &&
|
|
292
|
+
(!Number.isSafeInteger(runEpoch) || runEpoch < 1)
|
|
293
|
+
) {
|
|
294
|
+
throw new Error("task run epoch must be a positive safe integer");
|
|
295
|
+
}
|
|
296
|
+
return db.transaction((tx) => {
|
|
297
|
+
const state = taskRunEpochStateIn(tx, taskId);
|
|
298
|
+
const task = tx
|
|
299
|
+
.select({
|
|
300
|
+
runEpoch: schema.hostTasks.runEpoch,
|
|
301
|
+
statusMirror: schema.hostTasks.statusMirror,
|
|
302
|
+
})
|
|
303
|
+
.from(schema.hostTasks)
|
|
304
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
305
|
+
.get();
|
|
306
|
+
if (runEpoch === undefined) {
|
|
307
|
+
return (
|
|
308
|
+
state === null &&
|
|
309
|
+
task !== undefined &&
|
|
310
|
+
hostTaskRunEpoch(task) === 0 &&
|
|
311
|
+
task.statusMirror === "running"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return (
|
|
315
|
+
state !== null &&
|
|
316
|
+
state.assignmentOpen &&
|
|
317
|
+
state.highestRunEpoch === runEpoch &&
|
|
318
|
+
state.runtimeEstablishedEpoch === runEpoch &&
|
|
319
|
+
task?.statusMirror === "running"
|
|
320
|
+
);
|
|
197
321
|
});
|
|
198
322
|
}
|
|
199
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Persist the assignment fence as soon as a v1 task-up reaches the runner.
|
|
326
|
+
* The floor is deliberately independent of the removable runtime row. A
|
|
327
|
+
* same-epoch retry is admitted only while the assignment remains open; a
|
|
328
|
+
* higher epoch atomically reopens it, and nothing can lower it.
|
|
329
|
+
*/
|
|
330
|
+
export function admitTaskRunEpoch(
|
|
331
|
+
taskId: string,
|
|
332
|
+
runEpoch: number | undefined,
|
|
333
|
+
db: Db = getDb(),
|
|
334
|
+
): TaskRunEpochAdmission {
|
|
335
|
+
if (
|
|
336
|
+
runEpoch !== undefined &&
|
|
337
|
+
(!Number.isSafeInteger(runEpoch) || runEpoch < 1)
|
|
338
|
+
) {
|
|
339
|
+
throw new Error("task run epoch must be a positive safe integer");
|
|
340
|
+
}
|
|
341
|
+
return db.transaction((tx) => {
|
|
342
|
+
const existingTask = tx
|
|
343
|
+
.select({ taskId: schema.hostTasks.taskId })
|
|
344
|
+
.from(schema.hostTasks)
|
|
345
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
346
|
+
.get();
|
|
347
|
+
const assignment = tx
|
|
348
|
+
.select()
|
|
349
|
+
.from(schema.hostTaskRunAssignments)
|
|
350
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
351
|
+
.get();
|
|
352
|
+
if (runEpoch === undefined) {
|
|
353
|
+
if (assignment !== undefined) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`legacy task-up is refused after run ${String(assignment.highestRunEpoch)} was admitted`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return { kind: "legacy" } as const;
|
|
359
|
+
}
|
|
360
|
+
if (assignment === undefined) {
|
|
361
|
+
tx.insert(schema.hostTaskRunAssignments)
|
|
362
|
+
.values({
|
|
363
|
+
taskId,
|
|
364
|
+
highestRunEpoch: runEpoch,
|
|
365
|
+
assignmentOpen: true,
|
|
366
|
+
updatedAt: Date.now(),
|
|
367
|
+
})
|
|
368
|
+
.run();
|
|
369
|
+
return {
|
|
370
|
+
kind: "accepted",
|
|
371
|
+
relation: "advanced",
|
|
372
|
+
previousRunEpoch: 0,
|
|
373
|
+
hadTask: existingTask !== undefined,
|
|
374
|
+
} as const;
|
|
375
|
+
}
|
|
376
|
+
const currentRunEpoch = persistedRunEpoch(assignment.highestRunEpoch);
|
|
377
|
+
if (runEpoch < currentRunEpoch) {
|
|
378
|
+
return { kind: "stale", currentRunEpoch, reason: "older" } as const;
|
|
379
|
+
}
|
|
380
|
+
if (runEpoch === currentRunEpoch) {
|
|
381
|
+
if (!assignment.assignmentOpen) {
|
|
382
|
+
return { kind: "stale", currentRunEpoch, reason: "closed" } as const;
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
kind: "accepted",
|
|
386
|
+
relation: "retry",
|
|
387
|
+
previousRunEpoch: currentRunEpoch,
|
|
388
|
+
hadTask: existingTask !== undefined,
|
|
389
|
+
} as const;
|
|
390
|
+
}
|
|
391
|
+
tx.update(schema.hostTaskRunAssignments)
|
|
392
|
+
.set({
|
|
393
|
+
highestRunEpoch: runEpoch,
|
|
394
|
+
assignmentOpen: true,
|
|
395
|
+
updatedAt: Date.now(),
|
|
396
|
+
})
|
|
397
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
398
|
+
.run();
|
|
399
|
+
return {
|
|
400
|
+
kind: "accepted",
|
|
401
|
+
relation: "advanced",
|
|
402
|
+
previousRunEpoch: currentRunEpoch,
|
|
403
|
+
hadTask: existingTask !== undefined,
|
|
404
|
+
} as const;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Close one exact assignment. A delayed Stop for an older run is harmless.
|
|
410
|
+
* The established-runtime epoch remains available for exact cleanup and
|
|
411
|
+
* forensics; the independent closed bit is what makes reuse impossible.
|
|
412
|
+
*/
|
|
413
|
+
export function closeTaskRunAssignment(
|
|
414
|
+
taskId: string,
|
|
415
|
+
runEpoch: number,
|
|
416
|
+
db: Db = getDb(),
|
|
417
|
+
): CloseTaskRunAssignmentResult {
|
|
418
|
+
if (!Number.isSafeInteger(runEpoch) || runEpoch < 1) {
|
|
419
|
+
throw new Error("task run epoch must be a positive safe integer");
|
|
420
|
+
}
|
|
421
|
+
return db.transaction((tx) => {
|
|
422
|
+
const assignment = tx
|
|
423
|
+
.select()
|
|
424
|
+
.from(schema.hostTaskRunAssignments)
|
|
425
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
426
|
+
.get();
|
|
427
|
+
if (!assignment) return { kind: "missing" } as const;
|
|
428
|
+
const currentRunEpoch = persistedRunEpoch(assignment.highestRunEpoch);
|
|
429
|
+
if (currentRunEpoch !== runEpoch) {
|
|
430
|
+
return { kind: "mismatch", currentRunEpoch } as const;
|
|
431
|
+
}
|
|
432
|
+
if (!assignment.assignmentOpen) {
|
|
433
|
+
return { kind: "already_closed", runEpoch } as const;
|
|
434
|
+
}
|
|
435
|
+
closeCurrentTaskRunAssignmentIn(tx, taskId);
|
|
436
|
+
return { kind: "closed", runEpoch } as const;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Close the exact assignment named by Stop. While the caller holds the task
|
|
442
|
+
* lifecycle slot, this may also adopt a pre-v1 runtime whose durable task row
|
|
443
|
+
* still has epoch zero. Persisting that adopted epoch closed prevents a
|
|
444
|
+
* delayed same-epoch task-up from reviving the runtime after Stop.
|
|
445
|
+
*/
|
|
446
|
+
export function closeTaskRunAssignmentForStop(
|
|
447
|
+
taskId: string,
|
|
448
|
+
runEpoch: number,
|
|
449
|
+
db: Db = getDb(),
|
|
450
|
+
): CloseTaskRunAssignmentResult {
|
|
451
|
+
if (!Number.isSafeInteger(runEpoch) || runEpoch < 1) {
|
|
452
|
+
throw new Error("task run epoch must be a positive safe integer");
|
|
453
|
+
}
|
|
454
|
+
return db.transaction((tx) => {
|
|
455
|
+
const assignment = tx
|
|
456
|
+
.select()
|
|
457
|
+
.from(schema.hostTaskRunAssignments)
|
|
458
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
459
|
+
.get();
|
|
460
|
+
if (assignment) {
|
|
461
|
+
const currentRunEpoch = persistedRunEpoch(assignment.highestRunEpoch);
|
|
462
|
+
if (currentRunEpoch !== runEpoch) {
|
|
463
|
+
return { kind: "mismatch", currentRunEpoch } as const;
|
|
464
|
+
}
|
|
465
|
+
if (!assignment.assignmentOpen) {
|
|
466
|
+
return { kind: "already_closed", runEpoch } as const;
|
|
467
|
+
}
|
|
468
|
+
closeCurrentTaskRunAssignmentIn(tx, taskId);
|
|
469
|
+
return { kind: "closed", runEpoch } as const;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const task = tx
|
|
473
|
+
.select({ runEpoch: schema.hostTasks.runEpoch })
|
|
474
|
+
.from(schema.hostTasks)
|
|
475
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
476
|
+
.get();
|
|
477
|
+
if (task === undefined || hostTaskRunEpoch(task) !== 0) {
|
|
478
|
+
return { kind: "missing" } as const;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const now = Date.now();
|
|
482
|
+
tx.insert(schema.hostTaskRunAssignments)
|
|
483
|
+
.values({
|
|
484
|
+
taskId,
|
|
485
|
+
highestRunEpoch: runEpoch,
|
|
486
|
+
assignmentOpen: false,
|
|
487
|
+
createdAt: now,
|
|
488
|
+
updatedAt: now,
|
|
489
|
+
})
|
|
490
|
+
.run();
|
|
491
|
+
return { kind: "closed", runEpoch } as const;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Close and identify the assignment whose runtime was just proven stopped.
|
|
497
|
+
* The caller MUST hold the task lifecycle slot: that is what makes "current"
|
|
498
|
+
* the exact run associated with the physical stop, rather than whichever run
|
|
499
|
+
* happens to be current when a later status publisher wakes up.
|
|
500
|
+
*/
|
|
501
|
+
export function closeCurrentTaskRunAssignmentForStoppedRuntime(
|
|
502
|
+
taskId: string,
|
|
503
|
+
db: Db = getDb(),
|
|
504
|
+
): StoppedTaskRunAssignment {
|
|
505
|
+
return db.transaction((tx) => {
|
|
506
|
+
const assignment = tx
|
|
507
|
+
.select()
|
|
508
|
+
.from(schema.hostTaskRunAssignments)
|
|
509
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
510
|
+
.get();
|
|
511
|
+
if (!assignment) return { kind: "legacy" } as const;
|
|
512
|
+
const runEpoch = persistedRunEpoch(assignment.highestRunEpoch);
|
|
513
|
+
if (assignment.assignmentOpen) {
|
|
514
|
+
closeCurrentTaskRunAssignmentIn(tx, taskId);
|
|
515
|
+
}
|
|
516
|
+
return { kind: "closed", runEpoch } as const;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function closeCurrentTaskRunAssignmentIn(
|
|
521
|
+
db: DbExecutor,
|
|
522
|
+
taskId: string,
|
|
523
|
+
): void {
|
|
524
|
+
const now = Date.now();
|
|
525
|
+
db.update(schema.hostTaskRunAssignments)
|
|
526
|
+
.set({ assignmentOpen: false, updatedAt: now })
|
|
527
|
+
.where(eq(schema.hostTaskRunAssignments.taskId, taskId))
|
|
528
|
+
.run();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Record successful task-up only for the exact still-open assignment. */
|
|
532
|
+
export function recordTaskRuntimeEstablished(
|
|
533
|
+
taskId: string,
|
|
534
|
+
runEpoch: number,
|
|
535
|
+
db: Db = getDb(),
|
|
536
|
+
): void {
|
|
537
|
+
if (!Number.isSafeInteger(runEpoch) || runEpoch < 1) {
|
|
538
|
+
throw new Error("task run epoch must be a positive safe integer");
|
|
539
|
+
}
|
|
540
|
+
db.transaction((tx) => {
|
|
541
|
+
const state = taskRunEpochStateIn(tx, taskId);
|
|
542
|
+
if (
|
|
543
|
+
state === null ||
|
|
544
|
+
!state.assignmentOpen ||
|
|
545
|
+
state.highestRunEpoch !== runEpoch
|
|
546
|
+
) {
|
|
547
|
+
throw new Error("task runtime does not match the open assignment");
|
|
548
|
+
}
|
|
549
|
+
const task = tx
|
|
550
|
+
.select({ statusMirror: schema.hostTasks.statusMirror })
|
|
551
|
+
.from(schema.hostTasks)
|
|
552
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
553
|
+
.get();
|
|
554
|
+
if (task?.statusMirror !== "running") {
|
|
555
|
+
throw new Error("task runtime has not completed task-up");
|
|
556
|
+
}
|
|
557
|
+
tx.update(schema.hostTasks)
|
|
558
|
+
.set({ runEpoch, updatedAt: Date.now() })
|
|
559
|
+
.where(eq(schema.hostTasks.taskId, taskId))
|
|
560
|
+
.run();
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function recordTaskStarting(
|
|
565
|
+
taskId: string,
|
|
566
|
+
runEpoch?: number,
|
|
567
|
+
db: Db = getDb(),
|
|
568
|
+
): void {
|
|
569
|
+
if (
|
|
570
|
+
runEpoch !== undefined &&
|
|
571
|
+
(!Number.isSafeInteger(runEpoch) || runEpoch < 1)
|
|
572
|
+
) {
|
|
573
|
+
throw new Error("task run epoch must be a positive safe integer");
|
|
574
|
+
}
|
|
575
|
+
db.transaction((tx) => {
|
|
576
|
+
const assignment = taskRunEpochStateIn(tx, taskId);
|
|
577
|
+
if (runEpoch === undefined) {
|
|
578
|
+
if (assignment !== null) {
|
|
579
|
+
throw new Error("legacy task start is refused after v1 admission");
|
|
580
|
+
}
|
|
581
|
+
} else if (
|
|
582
|
+
assignment === null ||
|
|
583
|
+
!assignment.assignmentOpen ||
|
|
584
|
+
assignment.highestRunEpoch !== runEpoch
|
|
585
|
+
) {
|
|
586
|
+
throw new Error("task run epoch does not match the open assignment");
|
|
587
|
+
}
|
|
588
|
+
upsertHostTaskIn(tx, taskId, {
|
|
589
|
+
statusMirror: "starting",
|
|
590
|
+
lockedAt: Date.now(),
|
|
591
|
+
startedAt: Date.now(),
|
|
592
|
+
endedAt: null,
|
|
593
|
+
// A starting row is not proof that provisioning completed. The exact
|
|
594
|
+
// epoch is installed only by recordTaskRuntimeEstablished.
|
|
595
|
+
...(runEpoch === undefined ? {} : { runEpoch: 0 }),
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** The successfully established runtime epoch; zero includes legacy rows and
|
|
601
|
+
* v1 rows still provisioning. Anything malformed fails closed. */
|
|
602
|
+
export function hostTaskRunEpoch(task: {
|
|
603
|
+
runEpoch?: number | null;
|
|
604
|
+
}): number {
|
|
605
|
+
const value = task.runEpoch ?? 0;
|
|
606
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
607
|
+
throw new Error("persisted task run epoch is invalid");
|
|
608
|
+
}
|
|
609
|
+
return value;
|
|
610
|
+
}
|
|
611
|
+
|
|
200
612
|
/** Persist the provider-owned crash-recovery identity before provisioning may
|
|
201
613
|
* create worktrees, containers, networks, or volumes. Status remains
|
|
202
614
|
* `starting`; the enriched locator is written by `recordTaskUpResult`. */
|
|
@@ -212,9 +624,13 @@ export function recordTaskEnvironmentPrepared(
|
|
|
212
624
|
});
|
|
213
625
|
}
|
|
214
626
|
|
|
215
|
-
export function recordTaskDown(
|
|
216
|
-
|
|
627
|
+
export function recordTaskDown(
|
|
628
|
+
taskId: string,
|
|
629
|
+
status: string | undefined,
|
|
630
|
+
db: Db = getDb(),
|
|
631
|
+
): void {
|
|
217
632
|
db.transaction((tx) => {
|
|
633
|
+
closeCurrentTaskRunAssignmentIn(tx, taskId);
|
|
218
634
|
upsertHostTaskIn(tx, taskId, {
|
|
219
635
|
codeServerPort: null,
|
|
220
636
|
previewPorts: "[]",
|