mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Session workers now host a durable Harness over a per-session sqlite storage. The server keeps each session in <sessionDir>/<id>/ with meta.json and session.sqlite and never opens the storage itself. Workers hold retirement while the Harness task graph has live tasks and resume interrupted work on open. Transcript serves the root conversation's durable view directly, Models follows the conversation's agent document, and AgentController maps prompt, steer, follow-up, queue cancellation, abort, and compaction onto the root conversation, plus waitForPrompt. navigate, nextRun, and resume are dropped (see experimental/services/README.md TODO). The durable TUI and the worker share harness-setup.ts. pi-server exports its own SessionMetadata and no longer depends on pi-agent-core. Chord accepts readonly JSON types in service contracts without walking them, which recursive JSON types like pi-ai's made too deep.
157 lines
5.6 KiB
TypeScript
157 lines
5.6 KiB
TypeScript
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
import { WorkerLifecycle } from "../src/experimental/session-worker.ts";
|
|
|
|
const GENERATION = "generation-1";
|
|
|
|
function createLifecycle(options: { initialDemandGraceMs?: number; orphanDemandGraceMs?: number } = {}) {
|
|
const retire = vi.fn();
|
|
const lifecycle = new WorkerLifecycle({
|
|
initialServerConnectionId: GENERATION,
|
|
initialDemandGraceMs: options.initialDemandGraceMs ?? 100,
|
|
orphanDemandGraceMs: options.orphanDemandGraceMs ?? 200,
|
|
onRetire: retire,
|
|
});
|
|
return { lifecycle, retire };
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("Session worker lifecycle", () => {
|
|
test("retires only after client demand and Harness activity are both gone", async () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
lifecycle.setHarnessActive(true);
|
|
lifecycle.setDemand(GENERATION, "attachment-1", false);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
|
|
lifecycle.setHarnessActive(false);
|
|
await vi.runAllTicks();
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("retains the worker until every presentation attachment is released", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
lifecycle.setDemand(GENERATION, "attachment-2", true);
|
|
lifecycle.setDemand(GENERATION, "attachment-1", false);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
lifecycle.setDemand(GENERATION, "attachment-2", false);
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("does not retire on repeated activity updates while the Harness stays active", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
lifecycle.setHarnessActive(true);
|
|
lifecycle.setDemand(GENERATION, "attachment-1", false);
|
|
lifecycle.setHarnessActive(true);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
lifecycle.setHarnessActive(false);
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("an idle Harness update does not retire while demand remains", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
lifecycle.setHarnessActive(false);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("does not retire while a demand acknowledgement holds reconciliation", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
const release = lifecycle.holdRetirement();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", false);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
release();
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("holds retirement only for requests from the active attachment", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
const release = lifecycle.beginRequest(GENERATION, "attachment-1");
|
|
lifecycle.setDemand(GENERATION, "attachment-1", false);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
release();
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
expect(() => lifecycle.beginRequest(GENERATION, "attachment-1")).toThrow(/retiring/);
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("rejects requests from stale generations and attachments", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
expect(() => lifecycle.beginRequest("stale", "attachment-1")).toThrow(/stale server generation/);
|
|
expect(() => lifecycle.beginRequest(GENERATION, "wrong-attachment")).toThrow(/active attachment/);
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("retains disconnected-generation demand for the orphan grace", async () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
lifecycle.serverDisconnected(GENERATION);
|
|
|
|
vi.advanceTimersByTime(199);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
await vi.runAllTicks();
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("allows a replacement generation to retain the worker", async () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
lifecycle.setDemand(GENERATION, "attachment-1", true);
|
|
lifecycle.serverDisconnected(GENERATION);
|
|
lifecycle.serverConnected("generation-2");
|
|
lifecycle.setDemand("generation-2", "attachment-2", true);
|
|
|
|
vi.advanceTimersByTime(200);
|
|
expect(retire).not.toHaveBeenCalled();
|
|
lifecycle.setDemand("generation-2", "attachment-2", false);
|
|
await vi.runAllTicks();
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("retires a launched worker that never receives initial demand", async () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle, retire } = createLifecycle();
|
|
vi.advanceTimersByTime(100);
|
|
await vi.runAllTicks();
|
|
expect(retire).toHaveBeenCalledOnce();
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("rejects demand after retirement has won the race", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle } = createLifecycle();
|
|
vi.advanceTimersByTime(100);
|
|
expect(() => lifecycle.setDemand(GENERATION, "attachment-1", true)).toThrow(/retiring/);
|
|
lifecycle.close();
|
|
});
|
|
|
|
test("rejects demand from a stale server generation", () => {
|
|
vi.useFakeTimers();
|
|
const { lifecycle } = createLifecycle();
|
|
expect(() => lifecycle.setDemand("stale", "attachment-1", true)).toThrow(/stale server generation/);
|
|
lifecycle.close();
|
|
});
|
|
});
|