feat: add retained Worker session observation

This commit is contained in:
2026-09-18 05:53:53 +09:00
30 changed files with 1277 additions and 128 deletions
@@ -0,0 +1,84 @@
import {
isCurrentWorkerSessionRequest,
workerSessionAction,
type WorkerSessionRequestIdentity,
workerSessionRequestInit,
} from "./session-observation";
import type { SessionSnapshot } from "$lib/generated/protocol";
declare const Deno: {
test(name: string, fn: () => Promise<void> | void): void;
};
function assert(condition: boolean, message: string): void {
if (!condition) throw new Error(message);
}
const snapshot: SessionSnapshot = {
pending_submissions: {
revision: 0,
notification_count: 0,
head_id: null,
submissions: [],
},
entries: [],
};
Deno.test("worker session request includes same-origin credentials", () => {
const controller = new AbortController();
const init = workerSessionRequestInit(controller.signal);
assert(
init.credentials === "same-origin",
"session observation must use the Workspace auth cookie boundary",
);
assert(
init.signal === controller.signal,
"request must retain its abort fence",
);
});
Deno.test("worker session observation selects live, retained, and unavailable behavior", () => {
assert(
workerSessionAction({ availability: "live_protocol" }).kind ===
"subscribe_live",
"live observation must subscribe",
);
const retained = workerSessionAction({
availability: "retained_snapshot",
snapshot,
});
assert(
retained.kind === "apply_retained" && retained.snapshot === snapshot,
"retained observation must apply its snapshot",
);
const unavailable = workerSessionAction({
availability: "unavailable",
message: "retention expired",
});
assert(
unavailable.kind === "show_unavailable" &&
unavailable.message === "retention expired",
"unavailable observation must stay recoverable and visible",
);
});
Deno.test("late worker session response cannot overwrite a newer selection", () => {
const request: WorkerSessionRequestIdentity = {
token: 7,
workspaceId: "workspace-a",
runtimeId: "runtime-a",
workerId: "worker-a",
};
assert(
isCurrentWorkerSessionRequest(request, request),
"matching request identity must remain current",
);
assert(
!isCurrentWorkerSessionRequest(request, {
...request,
token: 8,
workerId: "worker-b",
}),
"late response for an older token/worker must be fenced",
);
});
@@ -0,0 +1,54 @@
import type { SessionSnapshot } from "$lib/generated/protocol";
export type WorkerSessionObservation =
| { availability: "live_protocol" }
| { availability: "retained_snapshot"; snapshot: SessionSnapshot }
| { availability: "unavailable"; message: string };
export type WorkerSessionAction =
| { kind: "subscribe_live" }
| { kind: "apply_retained"; snapshot: SessionSnapshot }
| { kind: "show_unavailable"; message: string };
export interface WorkerSessionRequestIdentity {
token: number;
workspaceId: string;
runtimeId: string;
workerId: string;
}
export function workerSessionRequestInit(signal: AbortSignal): RequestInit {
return {
signal,
credentials: "same-origin",
headers: { accept: "application/json" },
};
}
export function workerSessionAction(
observation: WorkerSessionObservation,
): WorkerSessionAction {
switch (observation.availability) {
case "live_protocol":
return { kind: "subscribe_live" };
case "retained_snapshot":
return { kind: "apply_retained", snapshot: observation.snapshot };
case "unavailable":
return {
kind: "show_unavailable",
message: observation.message.slice(0, 512),
};
}
}
export function isCurrentWorkerSessionRequest(
request: WorkerSessionRequestIdentity,
current: WorkerSessionRequestIdentity,
): boolean {
return (
request.token === current.token &&
request.workspaceId === current.workspaceId &&
request.runtimeId === current.runtimeId &&
request.workerId === current.workerId
);
}
@@ -5,6 +5,7 @@ import type {
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
WorkerLaunchWorkerSummary as WorkerSummary,
WorkingDirectoryRepositoryOption as SharedWorkingDirectoryRepositoryOption,
} from "$lib/generated/worker-launch-api";
import type {
@@ -52,6 +52,13 @@
import { pushWorkspaceAlert } from "$lib/workspace/alerts/store";
import { workspaceApiPath } from "$lib/workspace/api/http";
import { workspaceMultiplexer, type WorkspaceMultiplexerSubscription } from "$lib/workspace/multiplexer";
import {
isCurrentWorkerSessionRequest,
workerSessionAction,
workerSessionRequestInit,
type WorkerSessionObservation,
type WorkerSessionRequestIdentity,
} from "$lib/workspace/session-observation";
import type { Diagnostic, Worker } from "$lib/workspace/sidebar/types";
type Props = {
@@ -927,14 +934,108 @@
}
function connectProtocolTransport(
targetWorker: Worker | null,
_targetWorker: Worker | null,
token: number,
target: ConsoleTarget,
) {
if (!targetWorker || targetWorker.state === "stopped") {
protocolState = "closed";
return;
}
protocolState = "connecting";
const controller = new AbortController();
let closeLiveSubscription: (() => void) | undefined;
const requestIdentity: WorkerSessionRequestIdentity = {
token,
workspaceId: target.workspaceId,
runtimeId: target.runtimeId,
workerId: target.workerId,
};
void fetch(
workerApiPath(
`/runtimes/${encodeURIComponent(target.runtimeId)}/workers/${encodeURIComponent(target.workerId)}/session`,
),
workerSessionRequestInit(controller.signal),
)
.then(async (response) => {
if (!response.ok) {
throw new Error(`failed to observe Worker Session (${response.status})`);
}
return (await response.json()) as WorkerSessionObservation;
})
.then((observation) => {
const currentIdentity: WorkerSessionRequestIdentity = {
token: reloadToken,
workspaceId,
runtimeId,
workerId,
};
if (
!isCurrentWorkerSessionRequest(requestIdentity, currentIdentity) ||
controller.signal.aborted
)
return;
const action = workerSessionAction(observation);
if (action.kind === "show_unavailable") {
protocolState = "closed";
streamDiagnostics = [
...streamDiagnostics,
{
code: "retained_session_unavailable",
severity: "warning",
message: action.message,
},
];
return;
}
if (action.kind === "subscribe_live") {
closeLiveSubscription = connectLiveProtocolTransport(token, target);
return;
}
handleIncomingProtocolEvent({
event: "snapshot",
data: {
session: action.snapshot,
greeting: {
worker_name: "retained worker",
cwd: "",
provider: "retained session",
model: "",
scope_summary: "read-only retained session",
tools: [],
context_window: 0,
context_tokens: 0,
},
state: { last_command_id: 0, state: { kind: "idle" } },
in_flight: { responses: [], commands: [] },
internal_workers: [],
},
} as ProtocolEvent);
protocolState = "closed";
streamDiagnostics = [
...streamDiagnostics,
{
code: "retained_session_read_only",
severity: "info",
message: "Showing a read-only retained Session snapshot.",
},
];
})
.catch((error) => {
if (token !== reloadToken || controller.signal.aborted) return;
protocolState = "error";
streamDiagnostics = [
...streamDiagnostics,
{
code: "worker_session_observation_failed",
severity: "warning",
message: error instanceof Error ? error.message : String(error),
},
];
});
return () => {
controller.abort();
closeLiveSubscription?.();
};
}
function connectLiveProtocolTransport(token: number, target: ConsoleTarget) {
protocolState = "connecting";
const subscription = workspaceMultiplexer(target.workspaceId).subscribe(
{