fix: remove dead experimental protocol code

This commit is contained in:
Armin Ronacher 2026-08-31 19:23:56 +02:00
parent 6f6fa205ca
commit fbf2db6353
16 changed files with 46 additions and 85 deletions

View file

@ -5,10 +5,10 @@ import {
defineFacet,
type FacetHost,
type FacetLoader,
type JsonValue,
type LoadedFacets,
} from "@earendil-works/chord";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import type { JsonValue } from "@earendil-works/pi-protocol";
import {
CombinedAutocompleteProvider,
type Component,
@ -331,7 +331,7 @@ export class ExperimentalClientTui implements Component {
this.#selectedServerId = feature.serverId;
this.#sessionId = prepared.summary.sessionId;
this.#updateAutocomplete();
await this.#openLane(feature, prepared.summary.sessionId);
await this.#openLane(feature);
this.#screen = "chat";
this.#status = "";
this.#rebuild();
@ -464,9 +464,8 @@ export class ExperimentalClientTui implements Component {
#handleAttachmentState(feature: SessionFeature, state: SessionAttachmentState): void {
if (this.#closed || this.#selectedServerId !== feature.serverId || this.#sessionId === undefined) return;
if (state.status === "attached" && state.sessionId === this.#sessionId) {
const sessionId = this.#sessionId;
this.#queueRecovery(async () => {
if (this.#laneReplica === undefined) await this.#openLane(feature, sessionId);
if (this.#laneReplica === undefined) await this.#openLane(feature);
this.#busy = false;
this.#status = "";
this.#rebuild();
@ -493,9 +492,9 @@ export class ExperimentalClientTui implements Component {
});
}
async #openLane(feature: SessionFeature, sessionId: string): Promise<void> {
async #openLane(feature: SessionFeature): Promise<void> {
await this.#closeLane();
const replica = await openLaneReplica(feature.transcript, sessionId);
const replica = await openLaneReplica(feature.transcript);
const view = new ExperimentalChatView(this.#ui, process.cwd());
view.apply(replica.state());
this.#laneReplica = replica;

View file

@ -82,7 +82,7 @@ export async function runClient(command: ClientCommand, options: RunClientOption
const agent = match.agent;
const completedText = new Map<string, string>();
const replica = await openLaneReplica(match.transcript, sessionId, async (event) => {
const replica = await openLaneReplica(match.transcript, async (event) => {
if (event.type === "message_end" && event.runId !== undefined && event.message.role === "assistant") {
completedText.set(event.runId, messageText(event.message));
}

View file

@ -2,7 +2,6 @@ import type { LaneSnapshot, LaneWatchEvent } from "@earendil-works/pi-agent-core
import type { Transcript } from "./services/transcript.ts";
export interface LaneReplica {
readonly sessionId: string;
state(): LaneSnapshot;
subscribe(listener: () => void): () => void;
close(): Promise<void>;
@ -11,7 +10,6 @@ export interface LaneReplica {
/** Open a presentation-local replica from the Session's Transcript state. */
export async function openLaneReplica(
transcript: Transcript,
sessionId: string,
onEvent?: (event: LaneWatchEvent) => void | Promise<void>,
): Promise<LaneReplica> {
const listeners = new Set<() => void>();
@ -51,7 +49,6 @@ export async function openLaneReplica(
}
return {
sessionId,
state() {
if (snapshot === undefined) throw new Error("Transcript replica has no snapshot");
return snapshot;

View file

@ -1,11 +1,10 @@
import { combineFacetLoaders, type FacetLoader } from "@earendil-works/chord";
import { combineFacetLoaders, type FacetLoader, type JsonValue } from "@earendil-works/chord";
import {
createFacetBundleArtifactLoader,
createFacetBundleLoader,
type FacetBundleArtifact,
readFacetBundleManifest,
} from "@earendil-works/chord/node";
import type { JsonValue } from "@earendil-works/pi-protocol";
const PRESENTATION_FACET_BUNDLES_KEY = "presentationFacetBundles";
const PI_PLUGIN_API = "@earendil-works/pi-coding-agent/experimental/plugin";

View file

@ -1,5 +1,4 @@
import { type Context, defineService } from "@earendil-works/chord";
import type { JsonValue } from "@earendil-works/pi-protocol";
import { type Context, defineService, type JsonValue } from "@earendil-works/chord";
/** Server-built plugin generations available to presentations. */
export interface PresentationPlugins {

View file

@ -1,6 +1,12 @@
import { type Context, RemoteServiceProvider, replicatedState, type ServiceSubscription } from "@earendil-works/chord";
import {
type Context,
type JsonValue,
RemoteServiceProvider,
replicatedState,
type ServiceSubscription,
} from "@earendil-works/chord";
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { decodeServiceControlCall, type JsonValue } from "@earendil-works/pi-protocol";
import { decodeServiceControlCall } from "@earendil-works/pi-protocol";
import type { RoutedServerServiceAttachment, RoutedServerServiceHost } from "@earendil-works/pi-server";
import { PresentationPlugins } from "./plugins.ts";
import {

View file

@ -1,6 +1,5 @@
import { defineFacet, type Facet } from "@earendil-works/chord";
import { defineFacet, type Facet, type JsonValue } from "@earendil-works/chord";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { JsonValue } from "@earendil-works/pi-protocol";
import { AgentController } from "./agent-controller.ts";
import { type ModelSummary, Models, type Models as ModelsService } from "./models.ts";
import { PresentationPlugins, SessionPlugins } from "./plugins.ts";

View file

@ -5,19 +5,14 @@ import {
defineFacet,
type FacetHost,
type FacetLoader,
type ServiceCatalogueEntry,
isJsonValue,
type JsonValue,
type ServiceProviderUpdate,
type ServiceSubscription,
} from "@earendil-works/chord";
import type { AgentHarness, AgentLane } from "@earendil-works/pi-agent-core";
import {
decodeServiceControlCall,
type JsonValue,
JsonValueSchema,
type ProtocolRpcCall,
} from "@earendil-works/pi-protocol";
import { decodeServiceControlCall, type ProtocolRpcCall } from "@earendil-works/pi-protocol";
import Type, { type Static } from "typebox";
import { Check } from "typebox/value";
import type { ModelRuntime } from "../../core/model-runtime.ts";
import type { SettingsManager } from "../../core/settings-manager.ts";
import { AgentController } from "./agent-controller.ts";
@ -26,8 +21,10 @@ import { createModelsServiceFacet } from "./models-provider.ts";
import { SessionPlugins } from "./plugins.ts";
import { createTranscriptServiceFacet } from "./transcript-provider.ts";
const OpaqueJsonValueSchema = Type.Unsafe<JsonValue>(Type.Unknown());
export const ServiceOperationResultSchema = Type.Object(
{ result: Type.Optional(JsonValueSchema) },
{ result: Type.Optional(OpaqueJsonValueSchema) },
{ additionalProperties: false },
);
export type ServiceOperationResult = Static<typeof ServiceOperationResultSchema>;
@ -51,10 +48,8 @@ interface WorkerServiceSubscription {
}
export interface SessionWorkerServices {
readonly catalogue: readonly ServiceCatalogueEntry[];
invoke(call: ProtocolRpcCall, scope: WorkerServiceScope, context: Context): Promise<JsonValue | undefined>;
removeSubscriptions(matches: (scope: WorkerServiceScope) => boolean): void;
reload(): Promise<void>;
dispose(): Promise<void>;
}
@ -130,7 +125,6 @@ export async function createSessionWorkerServices(options: {
};
return {
catalogue: provider.catalogue,
async invoke(call, scope, context) {
const controlCall = decodeServiceControlCall(call);
if (controlCall?.type === "catalogue") return toProtocolJsonValue(provider.catalogue);
@ -155,7 +149,6 @@ export async function createSessionWorkerServices(options: {
return provider.invoke(call, context);
},
removeSubscriptions,
reload: () => reloadPlugins(),
async dispose() {
removeSubscriptions(() => true);
await reloadTail;
@ -178,6 +171,6 @@ function scopedSubscriptionKey(scope: WorkerServiceScope, subscriptionId: string
}
function toProtocolJsonValue(value: unknown): JsonValue {
if (!Check(JsonValueSchema, value)) throw new Error("Service produced invalid JSON");
if (!isJsonValue(value)) throw new Error("Service produced invalid JSON");
return value;
}

View file

@ -37,7 +37,7 @@ describe("experimental lane replica", () => {
const staleEvent = { type: "run_start", lane: "main", runId: "stale", startedAt: 1 } as const;
const state = replicatedState<TranscriptState>({ snapshot: snapshot(), event: staleEvent });
const onEvent = vi.fn();
const replica = await openLaneReplica({ state }, "session-1", onEvent);
const replica = await openLaneReplica({ state }, onEvent);
const changed = vi.fn();
replica.subscribe(changed);
expect(onEvent).not.toHaveBeenCalled();
@ -57,7 +57,7 @@ describe("experimental lane replica", () => {
test("waits for the first initialized transcript value", async () => {
const state = replicatedState<TranscriptState>({ snapshot: null, event: null });
const opening = openLaneReplica({ state }, "session-1");
const opening = openLaneReplica({ state });
state.state.snapshot = snapshot("ready");
state.publish(BACKGROUND_CONTEXT);
const replica = await opening;

View file

@ -1,6 +1,7 @@
import { defineFacet, type FacetLoader } from "@earendil-works/chord";
import type { AgentLane, LaneSnapshot } from "@earendil-works/pi-agent-core";
import { type AgentLane, BACKGROUND_CONTEXT, type LaneSnapshot } from "@earendil-works/pi-agent-core";
import { describe, expect, test, vi } from "vitest";
import { SessionPlugins } from "../src/experimental/services/plugins.ts";
import { createSessionWorkerServices } from "../src/experimental/services/worker.ts";
describe("experimental plugin reload", () => {
@ -78,7 +79,11 @@ describe("experimental plugin reload", () => {
});
try {
expect(activations).toEqual([1]);
await services.reload();
await services.invoke(
{ serviceId: SessionPlugins.id, member: "reload", args: [] },
{ serverConnectionId: "server-1", attachmentId: "attachment-1" },
BACKGROUND_CONTEXT,
);
expect(activations).toEqual([1, 2]);
expect(disposals).toEqual([1]);
} finally {

View file

@ -1,13 +1,7 @@
import { isJsonValue } from "@earendil-works/chord";
import { Check } from "typebox/value";
import { decodeCbor, encodeCbor } from "./cbor/index.ts";
import {
assertCompleteFrame,
DEFAULT_MAX_FRAME_LENGTH,
encodeFrame,
FrameDecoder,
type FrameDecoderOptions,
} from "./framing.ts";
import { DEFAULT_MAX_FRAME_LENGTH, encodeFrame, FrameDecoder, type FrameDecoderOptions } from "./framing.ts";
import {
type ClientMessage,
ClientMessageSchema,
@ -51,9 +45,7 @@ function encodeProtocolMessage<T>(
const validated = parse(value);
try {
const maxFrameLength = options?.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH;
const frame = encodeFrame(encodeCbor(validated, { maxByteLength: maxFrameLength }));
assertCompleteFrame(frame, { maxFrameLength });
return frame;
return encodeFrame(encodeCbor(validated, { maxByteLength: maxFrameLength }));
} catch (error) {
if (error instanceof ProtocolValidationError) throw error;
throw new ProtocolValidationError(`Unable to encode ${kind} protocol message: ${boundedErrorMessage(error)}`);

View file

@ -38,20 +38,6 @@ export function encodeFrame(payload: Uint8Array): Uint8Array {
return frame;
}
/** Validates that bytes contain exactly one complete frame within the configured limit. */
export function assertCompleteFrame(frame: Uint8Array, options?: FrameDecoderOptions): void {
if (!(frame instanceof Uint8Array)) throw new TypeError("Frame must be a Uint8Array");
if (frame.byteLength < FRAME_HEADER_LENGTH) throw new FrameError("Frame does not contain a complete length prefix");
const length = frame[0]! * 0x1_000_000 + frame[1]! * 0x1_0000 + frame[2]! * 0x100 + frame[3]!;
const maxFrameLength = resolveMaxFrameLength(options);
if (length > maxFrameLength) {
throw new FrameError(`Frame length ${length} exceeds configured limit of ${maxFrameLength}`);
}
if (frame.byteLength !== FRAME_HEADER_LENGTH + length) {
throw new FrameError("Frame must contain exactly one complete payload");
}
}
type DecoderState = "open" | "ended" | "failed";
/** Incrementally splits arbitrary byte chunks into length-prefixed payloads. */

View file

@ -1,7 +1,6 @@
export * from "./cbor/index.ts";
export * from "./codec.ts";
export * from "./framing.ts";
export * from "./json-value.ts";
export {
type AttachmentEnvelope,
type CancelEnvelope,

View file

@ -1,7 +0,0 @@
import type { JsonValue } from "@earendil-works/chord";
import Type from "typebox";
/** Static JSON contract. Protocol codecs perform recursive validation at the wire boundary. */
export const JsonValueSchema = Type.Unsafe<JsonValue>(Type.Unknown());
export type { JsonValue } from "@earendil-works/chord";

View file

@ -1,11 +1,12 @@
import { isJsonValue } from "@earendil-works/chord";
import { isJsonValue, type JsonValue } from "@earendil-works/chord";
import type { WireOp } from "@earendil-works/chord/delta";
import Type, { type Static } from "typebox";
import { Check } from "typebox/value";
import { type JsonValue, JsonValueSchema } from "./json-value.ts";
export const PROTOCOL_VERSION = 8 as const;
const OpaqueJsonValueSchema = Type.Unsafe<JsonValue>(Type.Unknown());
const IdSchema = Type.String({ minLength: 1 });
const StrictObject = <const T extends Parameters<typeof Type.Object>[0]>(properties: T) =>
Type.Object(properties, { additionalProperties: false });
@ -45,7 +46,7 @@ export const ProtocolRpcCallSchema = StrictObject({
serviceId: Type.String({ minLength: 1 }),
instance: Type.Optional(ServiceInstanceAddressSchema),
member: Type.String({ minLength: 1 }),
args: Type.Array(JsonValueSchema),
args: Type.Array(OpaqueJsonValueSchema),
});
export type ProtocolRpcCall = Static<typeof ProtocolRpcCallSchema>;
export type ProtocolRpcResult = JsonValue | undefined;
@ -58,10 +59,10 @@ const DeltaNonEmptyPathRefSchema = Type.Union([DeltaNonEmptyPathSchema, Type.Int
const DeltaOpsSchema = Type.Array(
Type.Unsafe<WireOp>(
Type.Union([
Type.Tuple([Type.Literal("r"), JsonValueSchema]),
Type.Tuple([Type.Literal("r"), OpaqueJsonValueSchema]),
Type.Tuple([Type.Literal("#"), Type.Integer({ minimum: 0 }), DeltaPathSchema]),
Type.Tuple([Type.Literal("s"), DeltaNonEmptyPathRefSchema, JsonValueSchema]),
Type.Tuple([Type.Literal("s"), JsonValueSchema]),
Type.Tuple([Type.Literal("s"), DeltaNonEmptyPathRefSchema, OpaqueJsonValueSchema]),
Type.Tuple([Type.Literal("s"), OpaqueJsonValueSchema]),
Type.Tuple([Type.Literal("d"), DeltaNonEmptyPathRefSchema]),
Type.Tuple([Type.Literal("d")]),
Type.Tuple([Type.Literal("a"), DeltaNonEmptyPathRefSchema, Type.String()]),
@ -73,13 +74,13 @@ const DeltaOpsSchema = Type.Array(
DeltaPathRefSchema,
Type.Integer({ minimum: 0 }),
Type.Integer({ minimum: 0 }),
Type.Array(JsonValueSchema),
Type.Array(OpaqueJsonValueSchema),
]),
Type.Tuple([
Type.Literal("p"),
Type.Integer({ minimum: 0 }),
Type.Integer({ minimum: 0 }),
Type.Array(JsonValueSchema),
Type.Array(OpaqueJsonValueSchema),
]),
]),
),
@ -279,7 +280,7 @@ const ResponseEnvelopeSchema = Type.Union([
type: Type.Literal("response"),
id: IdSchema,
ok: Type.Literal(true),
result: Type.Optional(JsonValueSchema),
result: Type.Optional(OpaqueJsonValueSchema),
}),
StrictObject({
type: Type.Literal("response"),

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { assertCompleteFrame, DEFAULT_MAX_FRAME_LENGTH, encodeFrame, FrameDecoder, FrameError } from "../src/index.ts";
import { DEFAULT_MAX_FRAME_LENGTH, encodeFrame, FrameDecoder, FrameError } from "../src/index.ts";
function concatenate(...chunks: Uint8Array[]): Uint8Array {
const result = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.byteLength, 0));
@ -19,13 +19,6 @@ describe("binary framing", () => {
expect(encodeFrame(new Uint8Array())).toEqual(new Uint8Array([0, 0, 0, 0]));
});
test("validates one complete bounded frame without accepting trailing or partial bytes", () => {
expect(() => assertCompleteFrame(new Uint8Array([0, 0, 0, 2, 1, 2]), { maxFrameLength: 2 })).not.toThrow();
expect(() => assertCompleteFrame(new Uint8Array([0, 0, 0, 2, 1]))).toThrow(/complete/i);
expect(() => assertCompleteFrame(new Uint8Array([0, 0, 0, 1, 1, 2]))).toThrow(/exactly/i);
expect(() => assertCompleteFrame(new Uint8Array([0, 0, 0, 3, 1, 2, 3]), { maxFrameLength: 2 })).toThrow(/limit/i);
});
test("decodes fragmented, coalesced, and empty frames in order", () => {
const wire = concatenate(
encodeFrame(new Uint8Array([1, 2, 3])),