feat(workspace): persist runtime worker projection

This commit is contained in:
2026-09-18 06:20:09 +09:00
parent ae7f0f0446
commit 95636b3d53
13 changed files with 1489 additions and 285 deletions
@@ -232,6 +232,8 @@ export type SubscriptionWorkerIds = Array<SubscriptionWorkerId>;
export type SubscriptionWorkerState = "idle" | "running" | "paused" | "stopped";
export type SubscriptionWorkerAvailability = "observed" | "unavailable";
export type EventSubscriptionSelector = { "topic": "runtime_workers" } | { "topic": "worker_lifecycle", worker_ids: SubscriptionWorkerIds, } | { "topic": "worker_protocol", worker_id: SubscriptionWorkerId, runtime_id?: string | null, } | { "topic": "workspace_workers" } | { "topic": "workspace_workdirs" };
export type SubscriptionWorker = { worker_id: SubscriptionWorkerId,
@@ -245,6 +247,11 @@ runtime_id?: string | null,
* Workspace-facing projections must populate it before publishing the Worker.
*/
resource_key?: string | null,
/**
* Freshness of the Runtime-backed observation carried by this projection.
* `Unavailable` preserves catalog membership without claiming that execution stopped.
*/
availability: SubscriptionWorkerAvailability,
/**
* Producer-owned monotonic revision for this Worker subject.
*/
@@ -2,8 +2,10 @@ import type { WorkerStateSnapshot } from "$lib/generated/protocol";
export function liveWorkerState(worker: {
state: string;
availability?: "observed" | "unavailable";
worker_state?: WorkerStateSnapshot | null;
}): string {
if (worker.availability === "unavailable") return "unknown";
const state = worker.worker_state?.state;
if (!state) {
if (worker.state === "missing" || worker.state === "stopped") {
@@ -20,10 +20,12 @@ function worker(
workerId: string,
revision: number,
hasRunningInternalWorkers = false,
availability: 'observed' | 'unavailable' = 'observed',
): SubscriptionWorker {
return {
worker_id: workerId,
runtime_id: runtimeId,
availability,
subject_revision: revision,
state: 'idle',
has_running_internal_workers: hasRunningInternalWorkers,
@@ -42,12 +44,23 @@ Deno.test('Worker list state uses the authoritative live snapshot separately fro
};
assertEquals(liveWorkerState(active), 'paused');
const unavailable = worker('runtime-a', 'worker-2', 1);
const unavailable = worker('runtime-a', 'worker-2', 1, false, 'unavailable');
unavailable.worker_state = {
last_command_id: 2,
state: { kind: 'busy', state: { kind: 'run', state: 'running' } },
};
assertEquals(liveWorkerState(unavailable), 'unknown');
assertEquals(
liveWorkerState({ ...unavailable, state: 'missing' }),
liveWorkerState({
...unavailable,
availability: 'observed',
worker_state: null,
state: 'missing',
}),
'missing',
);
unavailable.availability = 'observed';
unavailable.worker_state = null;
unavailable.state = 'stopped';
assertEquals(liveWorkerState(unavailable), 'stopped');
});
@@ -100,7 +100,7 @@ function projectWorker(worker: SubscriptionWorker): SidebarWorker {
display_hint: 'Workspace-authorized Runtime Worker',
},
capabilities: {
can_stop: worker.state !== 'stopped',
can_stop: worker.availability !== 'unavailable' && worker.state !== 'stopped',
can_spawn_followup: false,
},
repository_key: worker.repository_key ?? null,