fix: sync dev branch with main timeline/schema changes and remove stale providers
Forward-port main's timeline epoch tracking, projected window selection, and schema updates into dev. Remove aider/amp/gemini providers and provider-history-compatibility layers that are no longer needed.
This commit is contained in:
@@ -2032,15 +2032,24 @@ describe("DaemonClient", () => {
|
||||
agentId: "agent_cli",
|
||||
agent: null,
|
||||
direction: "tail",
|
||||
startSeq: 1,
|
||||
endSeq: 1,
|
||||
projection: "projected",
|
||||
epoch: "epoch-1",
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window: { minSeq: 1, maxSeq: 1, nextSeq: 2 },
|
||||
startCursor: { epoch: "epoch-1", seq: 1 },
|
||||
endCursor: { epoch: "epoch-1", seq: 1 },
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
entries: [
|
||||
{
|
||||
timestamp: "2026-02-08T20:20:00.000Z",
|
||||
provider: "codex",
|
||||
seq: 1,
|
||||
seqStart: 1,
|
||||
seqEnd: 1,
|
||||
sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }],
|
||||
collapsed: [],
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call_cli_snapshot",
|
||||
@@ -2117,15 +2126,24 @@ describe("DaemonClient", () => {
|
||||
agentId: "agent_cli",
|
||||
agent: null,
|
||||
direction: "tail",
|
||||
startSeq: 1,
|
||||
endSeq: 1,
|
||||
projection: "projected",
|
||||
epoch: "epoch-1",
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window: { minSeq: 1, maxSeq: 1, nextSeq: 2 },
|
||||
startCursor: { epoch: "epoch-1", seq: 1 },
|
||||
endCursor: { epoch: "epoch-1", seq: 1 },
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
entries: [
|
||||
{
|
||||
timestamp: "2026-02-08T20:20:00.000Z",
|
||||
provider: "codex",
|
||||
seq: 1,
|
||||
seqStart: 1,
|
||||
seqEnd: 1,
|
||||
sourceSeqRanges: [{ startSeq: 1, endSeq: 1 }],
|
||||
collapsed: [],
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call_cli_invalid",
|
||||
|
||||
@@ -152,6 +152,7 @@ export type DaemonEvent =
|
||||
event: AgentStreamEventPayload;
|
||||
timestamp: string;
|
||||
seq?: number;
|
||||
epoch?: string;
|
||||
}
|
||||
| { type: "status"; payload: { status: string } & Record<string, unknown> }
|
||||
| { type: "agent_deleted"; agentId: string }
|
||||
@@ -336,13 +337,13 @@ type ScheduleDeletePayload = Extract<
|
||||
export type FetchAgentTimelinePayload = FetchAgentTimelineResponseMessage["payload"];
|
||||
|
||||
export type FetchAgentTimelineDirection = FetchAgentTimelinePayload["direction"];
|
||||
export type FetchAgentTimelineCursor = NonNullable<
|
||||
Extract<SessionInboundMessage, { type: "fetch_agent_timeline_request" }>["cursor"]
|
||||
>;
|
||||
export type FetchAgentTimelineProjection = FetchAgentTimelinePayload["projection"];
|
||||
export type FetchAgentTimelineCursor = NonNullable<FetchAgentTimelinePayload["startCursor"]>;
|
||||
export type FetchAgentTimelineOptions = {
|
||||
direction?: FetchAgentTimelineDirection;
|
||||
cursor?: FetchAgentTimelineCursor;
|
||||
limit?: number;
|
||||
projection?: FetchAgentTimelineProjection;
|
||||
requestId?: string;
|
||||
};
|
||||
|
||||
@@ -1673,6 +1674,7 @@ export class DaemonClient {
|
||||
...(options.direction ? { direction: options.direction } : {}),
|
||||
...(options.cursor ? { cursor: options.cursor } : {}),
|
||||
...(typeof options.limit === "number" ? { limit: options.limit } : {}),
|
||||
...(options.projection ? { projection: options.projection } : {}),
|
||||
});
|
||||
|
||||
const payload = await this.sendRequest({
|
||||
@@ -3899,6 +3901,7 @@ export class DaemonClient {
|
||||
event: msg.payload.event,
|
||||
timestamp: msg.payload.timestamp,
|
||||
...(typeof msg.payload.seq === "number" ? { seq: msg.payload.seq } : {}),
|
||||
...(typeof msg.payload.epoch === "string" ? { epoch: msg.payload.epoch } : {}),
|
||||
};
|
||||
case "status":
|
||||
return { type: "status", payload: msg.payload };
|
||||
|
||||
@@ -1273,7 +1273,7 @@ describe("AgentManager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchTimeline returns committed rows after a known seq without reset metadata", async () => {
|
||||
test("fetchTimeline returns full timeline with reset when cursor epoch is stale", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-timeline-stale-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
@@ -1291,42 +1291,40 @@ describe("AgentManager", () => {
|
||||
cwd: workdir,
|
||||
});
|
||||
|
||||
for (let seq = 1; seq <= 120; seq += 1) {
|
||||
await manager.appendTimelineItem(snapshot.id, {
|
||||
type: "assistant_message",
|
||||
text: `committed row ${seq}`,
|
||||
});
|
||||
}
|
||||
|
||||
const baseline = await manager.fetchTimeline(snapshot.id, {
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
});
|
||||
expect(baseline.rows).toHaveLength(120);
|
||||
|
||||
await manager.emitLiveTimelineItem(snapshot.id, {
|
||||
await manager.appendTimelineItem(snapshot.id, {
|
||||
type: "assistant_message",
|
||||
text: "partial reply",
|
||||
text: "one",
|
||||
});
|
||||
await manager.appendTimelineItem(snapshot.id, {
|
||||
type: "assistant_message",
|
||||
text: "finalized reply",
|
||||
text: "two",
|
||||
});
|
||||
await manager.appendTimelineItem(snapshot.id, {
|
||||
type: "assistant_message",
|
||||
text: "three",
|
||||
});
|
||||
|
||||
const result = await manager.fetchTimeline(snapshot.id, {
|
||||
const baseline = manager.fetchTimeline(snapshot.id, {
|
||||
direction: "tail",
|
||||
limit: 2,
|
||||
});
|
||||
expect(baseline.rows).toHaveLength(2);
|
||||
|
||||
const result = manager.fetchTimeline(snapshot.id, {
|
||||
direction: "after",
|
||||
cursor: {
|
||||
seq: 120,
|
||||
epoch: "stale-epoch",
|
||||
seq: baseline.rows[baseline.rows.length - 1]!.seq,
|
||||
},
|
||||
limit: 0,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
expect(result.rows).toHaveLength(1);
|
||||
expect(result.rows[0]?.seq).toBe(121);
|
||||
expect(result.rows[0]?.item).toEqual({
|
||||
type: "assistant_message",
|
||||
text: "finalized reply",
|
||||
});
|
||||
expect(result.reset).toBe(true);
|
||||
expect(result.staleCursor).toBe(true);
|
||||
expect(result.gap).toBe(false);
|
||||
expect(result.rows).toHaveLength(3);
|
||||
expect(result.rows[0]?.seq).toBe(1);
|
||||
expect(result.rows[result.rows.length - 1]?.seq).toBe(3);
|
||||
});
|
||||
|
||||
test("getTimelineRows falls back to the in-memory timeline when no durable store is configured", async () => {
|
||||
@@ -1448,6 +1446,7 @@ describe("AgentManager", () => {
|
||||
|
||||
const streamEvents: Array<{
|
||||
seq?: number;
|
||||
epoch?: string;
|
||||
eventType?: string;
|
||||
itemType?: string;
|
||||
text?: string;
|
||||
@@ -1459,6 +1458,7 @@ describe("AgentManager", () => {
|
||||
}
|
||||
streamEvents.push({
|
||||
seq: event.seq,
|
||||
epoch: event.epoch,
|
||||
eventType: event.event.type,
|
||||
itemType: event.event.type === "timeline" ? event.event.item.type : undefined,
|
||||
text:
|
||||
@@ -1487,12 +1487,14 @@ describe("AgentManager", () => {
|
||||
itemType: "assistant_message",
|
||||
text: "final ",
|
||||
seq: 1,
|
||||
epoch: expect.any(String),
|
||||
});
|
||||
expect(assistantTimelineEvents[1]).toMatchObject({
|
||||
eventType: "timeline",
|
||||
itemType: "assistant_message",
|
||||
text: "reply",
|
||||
seq: 2,
|
||||
epoch: expect.any(String),
|
||||
});
|
||||
|
||||
expect(manager.getTimeline(snapshot.id)).toEqual([
|
||||
@@ -1510,6 +1512,8 @@ describe("AgentManager", () => {
|
||||
limit: 0,
|
||||
});
|
||||
expect(fetched.rows).toHaveLength(2);
|
||||
expect(assistantTimelineEvents[0]?.epoch).toBe(fetched.epoch);
|
||||
expect(assistantTimelineEvents[1]?.epoch).toBe(fetched.epoch);
|
||||
expect(fetched.rows[0]?.item).toEqual({
|
||||
type: "assistant_message",
|
||||
text: "final ",
|
||||
|
||||
@@ -63,6 +63,7 @@ export type AgentManagerEvent =
|
||||
agentId: string;
|
||||
event: AgentStreamEvent;
|
||||
seq?: number;
|
||||
epoch?: string;
|
||||
};
|
||||
|
||||
export type AgentSubscriber = (event: AgentManagerEvent) => void;
|
||||
@@ -584,18 +585,8 @@ export class AgentManager {
|
||||
return this.timelineStore.getRows(id);
|
||||
}
|
||||
|
||||
async fetchTimeline(
|
||||
id: string,
|
||||
options?: AgentTimelineFetchOptions,
|
||||
): Promise<AgentTimelineFetchResult> {
|
||||
fetchTimeline(id: string, options?: AgentTimelineFetchOptions): AgentTimelineFetchResult {
|
||||
this.requireAgent(id);
|
||||
const agent = this.agents.get(id);
|
||||
if (agent && !agent.historyPrimed) {
|
||||
await this.hydrateTimelineFromLegacyProviderHistory(agent);
|
||||
}
|
||||
if (this.durableTimelineStore) {
|
||||
return await this.durableTimelineStore.fetchCommitted(id, options);
|
||||
}
|
||||
return this.timelineStore.fetch(id, options);
|
||||
}
|
||||
|
||||
@@ -1053,6 +1044,7 @@ export class AgentManager {
|
||||
},
|
||||
{
|
||||
seq: row.seq,
|
||||
epoch: this.timelineStore.getEpoch(agentId),
|
||||
},
|
||||
);
|
||||
if (options?.emitState !== false) {
|
||||
@@ -1073,6 +1065,7 @@ export class AgentManager {
|
||||
},
|
||||
{
|
||||
seq: row.seq,
|
||||
epoch: this.timelineStore.getEpoch(agentId),
|
||||
},
|
||||
);
|
||||
await this.persistSnapshot(agent);
|
||||
@@ -2404,6 +2397,7 @@ export class AgentManager {
|
||||
timelineRow
|
||||
? {
|
||||
seq: timelineRow.seq,
|
||||
epoch: this.timelineStore.getEpoch(agent.id),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
@@ -2445,6 +2439,7 @@ export class AgentManager {
|
||||
},
|
||||
{
|
||||
seq: row.seq,
|
||||
epoch: this.timelineStore.getEpoch(agent.id),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -2604,7 +2599,7 @@ export class AgentManager {
|
||||
private dispatchStream(
|
||||
agentId: string,
|
||||
event: AgentStreamEvent,
|
||||
metadata?: { seq?: number },
|
||||
metadata?: { seq?: number; epoch?: string },
|
||||
): void {
|
||||
this.dispatch({ type: "agent_stream", agentId, event, ...metadata });
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type AgentTimelineRow = {
|
||||
};
|
||||
|
||||
export type AgentTimelineCursor = {
|
||||
epoch: string;
|
||||
seq: number;
|
||||
};
|
||||
|
||||
@@ -30,7 +31,11 @@ export type AgentTimelineWindow = {
|
||||
};
|
||||
|
||||
export type AgentTimelineFetchResult = {
|
||||
epoch: string;
|
||||
direction: AgentTimelineFetchDirection;
|
||||
reset: boolean;
|
||||
staleCursor: boolean;
|
||||
gap: boolean;
|
||||
window: AgentTimelineWindow;
|
||||
hasOlder: boolean;
|
||||
hasNewer: boolean;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
||||
import type {
|
||||
AgentTimelineFetchOptions,
|
||||
@@ -8,11 +9,13 @@ import type {
|
||||
export type SeedAgentTimelineOptions = {
|
||||
items?: readonly AgentTimelineItem[];
|
||||
rows?: readonly AgentTimelineRow[];
|
||||
epoch?: string;
|
||||
nextSeq?: number;
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
type AgentTimelineState = {
|
||||
epoch: string;
|
||||
rows: AgentTimelineRow[];
|
||||
nextSeq: number;
|
||||
};
|
||||
@@ -45,6 +48,7 @@ export class InMemoryAgentTimelineStore {
|
||||
: this.buildRowsFromItems(options?.items ?? [], options?.nextSeq ?? 1, timestamp);
|
||||
const nextSeq = options?.nextSeq ?? (rows.length ? rows[rows.length - 1]!.seq + 1 : 1);
|
||||
this.states.set(agentId, {
|
||||
epoch: options?.epoch ?? randomUUID(),
|
||||
rows,
|
||||
nextSeq,
|
||||
});
|
||||
@@ -62,6 +66,10 @@ export class InMemoryAgentTimelineStore {
|
||||
return this.requireState(agentId).rows.map(cloneRow);
|
||||
}
|
||||
|
||||
getEpoch(agentId: string): string {
|
||||
return this.requireState(agentId).epoch;
|
||||
}
|
||||
|
||||
fetch(agentId: string, options?: AgentTimelineFetchOptions): AgentTimelineFetchResult {
|
||||
const state = this.requireState(agentId);
|
||||
const direction = options?.direction ?? "tail";
|
||||
@@ -81,9 +89,43 @@ export class InMemoryAgentTimelineStore {
|
||||
nextSeq: state.nextSeq,
|
||||
};
|
||||
|
||||
if (cursor && typeof cursor.epoch === "string" && cursor.epoch !== state.epoch) {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: true,
|
||||
staleCursor: true,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
rows: state.rows.map(cloneRow),
|
||||
};
|
||||
}
|
||||
|
||||
const cloneRows = (items: AgentTimelineRow[]) => items.map(cloneRow);
|
||||
|
||||
if (direction === "after" && cursor && state.rows.length > 0 && cursor.seq < minSeq - 1) {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: true,
|
||||
staleCursor: false,
|
||||
gap: true,
|
||||
window,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
rows: cloneRows(state.rows),
|
||||
};
|
||||
}
|
||||
|
||||
if (state.rows.length === 0) {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
@@ -97,11 +139,15 @@ export class InMemoryAgentTimelineStore {
|
||||
? state.rows
|
||||
: state.rows.slice(state.rows.length - limit);
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: selected.length > 0 && selected[0]!.seq > minSeq,
|
||||
hasNewer: false,
|
||||
rows: selected.map(cloneRow),
|
||||
rows: cloneRows(selected),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +156,11 @@ export class InMemoryAgentTimelineStore {
|
||||
const startIdx = state.rows.findIndex((row) => row.seq > baseSeq);
|
||||
if (startIdx < 0) {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: baseSeq >= minSeq,
|
||||
hasNewer: false,
|
||||
@@ -123,11 +173,15 @@ export class InMemoryAgentTimelineStore {
|
||||
: state.rows.slice(startIdx, startIdx + limit);
|
||||
const lastSelected = selected[selected.length - 1];
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: selected[0]!.seq > minSeq,
|
||||
hasNewer: Boolean(lastSelected && lastSelected.seq < maxSeq),
|
||||
rows: selected.map(cloneRow),
|
||||
rows: cloneRows(selected),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,11 +193,15 @@ export class InMemoryAgentTimelineStore {
|
||||
? boundedRows
|
||||
: boundedRows.slice(boundedRows.length - limit);
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
direction,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window,
|
||||
hasOlder: selected.length > 0 && selected[0]!.seq > minSeq,
|
||||
hasNewer: endExclusive >= 0,
|
||||
rows: selected.map(cloneRow),
|
||||
rows: cloneRows(selected),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -141,27 +141,6 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
||||
defaultModel: "gpt-5.4-mini",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
label: "Gemini CLI",
|
||||
description: "Google's coding agent CLI",
|
||||
defaultModeId: null,
|
||||
modes: [],
|
||||
},
|
||||
{
|
||||
id: "amp",
|
||||
label: "AMP",
|
||||
description: "Sourcegraph's coding agent CLI",
|
||||
defaultModeId: null,
|
||||
modes: [],
|
||||
},
|
||||
{
|
||||
id: "aider",
|
||||
label: "Aider",
|
||||
description: "Paul Gauthier's coding assistant CLI",
|
||||
defaultModeId: null,
|
||||
modes: [],
|
||||
},
|
||||
{
|
||||
id: "copilot",
|
||||
label: "Copilot",
|
||||
|
||||
@@ -20,13 +20,9 @@ import type {
|
||||
ProviderProfileModel,
|
||||
ProviderRuntimeSettings,
|
||||
} from "./provider-launch-config.js";
|
||||
|
||||
import { AiderAgentClient } from "./providers/aider-agent.js";
|
||||
import { AmpAgentClient } from "./providers/amp-agent.js";
|
||||
import { ClaudeAgentClient } from "./providers/claude-agent.js";
|
||||
import { CodexAppServerAgentClient } from "./providers/codex-app-server-agent.js";
|
||||
import { CopilotACPAgentClient } from "./providers/copilot-acp-agent.js";
|
||||
import { GeminiAgentClient } from "./providers/gemini-agent.js";
|
||||
import { GenericACPAgentClient } from "./providers/generic-acp-agent.js";
|
||||
import { OpenCodeAgentClient, OpenCodeServerManager } from "./providers/opencode-agent.js";
|
||||
import { PiACPAgentClient } from "./providers/pi-acp-agent.js";
|
||||
@@ -72,9 +68,6 @@ const PROVIDER_CLIENT_FACTORIES: Record<string, ProviderClientFactory> = {
|
||||
runtimeSettings,
|
||||
}),
|
||||
codex: (logger, runtimeSettings) => new CodexAppServerAgentClient(logger, runtimeSettings),
|
||||
gemini: (_logger, runtimeSettings) => new GeminiAgentClient(runtimeSettings),
|
||||
amp: (_logger, runtimeSettings) => new AmpAgentClient(runtimeSettings),
|
||||
aider: (_logger, runtimeSettings) => new AiderAgentClient(runtimeSettings),
|
||||
copilot: (logger, runtimeSettings) =>
|
||||
new CopilotACPAgentClient({
|
||||
logger,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentLaunchContext,
|
||||
AgentModelDefinition,
|
||||
AgentPersistenceHandle,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
ListModelsOptions,
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
findExecutable,
|
||||
isProviderCommandAvailable,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
|
||||
const AIDER_PROVIDER = "aider" as const;
|
||||
|
||||
const AIDER_CAPABILITIES: AgentCapabilityFlags = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: false,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
};
|
||||
|
||||
function resolveAiderBinary(): string {
|
||||
const found = findExecutable("aider");
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
throw new Error(
|
||||
"Aider binary not found. Install Aider and ensure 'aider' is available in your shell PATH.",
|
||||
);
|
||||
}
|
||||
|
||||
function createUnsupportedSessionError(): Error {
|
||||
return new Error("Aider does not support session-backed agents in Paseo.");
|
||||
}
|
||||
|
||||
export class AiderAgentClient implements AgentClient {
|
||||
readonly provider = AIDER_PROVIDER;
|
||||
readonly capabilities = AIDER_CAPABILITIES;
|
||||
|
||||
constructor(private readonly runtimeSettings?: ProviderRuntimeSettings) {}
|
||||
|
||||
async createSession(
|
||||
_config: AgentSessionConfig,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
throw createUnsupportedSessionError();
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
_overrides?: Partial<AgentSessionConfig>,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
throw createUnsupportedSessionError();
|
||||
}
|
||||
|
||||
async listModels(_options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
if (this.runtimeSettings?.command?.mode === "replace") {
|
||||
return existsSync(this.runtimeSettings.command.argv[0]);
|
||||
}
|
||||
return isProviderCommandAvailable(this.runtimeSettings?.command, resolveAiderBinary);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentLaunchContext,
|
||||
AgentModelDefinition,
|
||||
AgentPersistenceHandle,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
ListModelsOptions,
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
findExecutable,
|
||||
isProviderCommandAvailable,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
|
||||
const AMP_PROVIDER = "amp" as const;
|
||||
|
||||
const AMP_CAPABILITIES: AgentCapabilityFlags = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: false,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
};
|
||||
|
||||
function resolveAmpBinary(): string {
|
||||
const found = findExecutable("amp");
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
throw new Error(
|
||||
"AMP binary not found. Install AMP and ensure 'amp' is available in your shell PATH.",
|
||||
);
|
||||
}
|
||||
|
||||
function createUnsupportedSessionError(): Error {
|
||||
return new Error("AMP does not support session-backed agents in Paseo.");
|
||||
}
|
||||
|
||||
export class AmpAgentClient implements AgentClient {
|
||||
readonly provider = AMP_PROVIDER;
|
||||
readonly capabilities = AMP_CAPABILITIES;
|
||||
|
||||
constructor(private readonly runtimeSettings?: ProviderRuntimeSettings) {}
|
||||
|
||||
async createSession(
|
||||
_config: AgentSessionConfig,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
throw createUnsupportedSessionError();
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
_overrides?: Partial<AgentSessionConfig>,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
throw createUnsupportedSessionError();
|
||||
}
|
||||
|
||||
async listModels(_options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
if (this.runtimeSettings?.command?.mode === "replace") {
|
||||
return existsSync(this.runtimeSettings.command.argv[0]);
|
||||
}
|
||||
return isProviderCommandAvailable(this.runtimeSettings?.command, resolveAmpBinary);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentLaunchContext,
|
||||
AgentModelDefinition,
|
||||
AgentPersistenceHandle,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
ListModelsOptions,
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
findExecutable,
|
||||
isProviderCommandAvailable,
|
||||
type ProviderRuntimeSettings,
|
||||
} from "../provider-launch-config.js";
|
||||
|
||||
const GEMINI_PROVIDER = "gemini" as const;
|
||||
|
||||
const GEMINI_CAPABILITIES: AgentCapabilityFlags = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: false,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
};
|
||||
|
||||
function resolveGeminiBinary(): string {
|
||||
const found = findExecutable("gemini");
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
throw new Error(
|
||||
"Gemini CLI binary not found. Install Gemini CLI and ensure 'gemini' is available in your shell PATH.",
|
||||
);
|
||||
}
|
||||
|
||||
function createUnsupportedSessionError(): Error {
|
||||
return new Error("Gemini CLI does not support session-backed agents in Paseo.");
|
||||
}
|
||||
|
||||
export class GeminiAgentClient implements AgentClient {
|
||||
readonly provider = GEMINI_PROVIDER;
|
||||
readonly capabilities = GEMINI_CAPABILITIES;
|
||||
|
||||
constructor(private readonly runtimeSettings?: ProviderRuntimeSettings) {}
|
||||
|
||||
async createSession(
|
||||
_config: AgentSessionConfig,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
throw createUnsupportedSessionError();
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
_overrides?: Partial<AgentSessionConfig>,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
throw createUnsupportedSessionError();
|
||||
}
|
||||
|
||||
async listModels(_options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
if (this.runtimeSettings?.command?.mode === "replace") {
|
||||
return existsSync(this.runtimeSettings.command.argv[0]);
|
||||
}
|
||||
return isProviderCommandAvailable(this.runtimeSettings?.command, resolveGeminiBinary);
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,10 @@ async function waitFor(
|
||||
}
|
||||
}
|
||||
|
||||
function isSeqLessAssistantTimeline(
|
||||
function isLiveAssistantTimeline(
|
||||
message: SessionOutboundMessage,
|
||||
agentId: string,
|
||||
epoch?: string,
|
||||
text?: string,
|
||||
): boolean {
|
||||
return (
|
||||
@@ -40,6 +41,8 @@ function isSeqLessAssistantTimeline(
|
||||
message.payload.event.type === "timeline" &&
|
||||
message.payload.event.item.type === "assistant_message" &&
|
||||
message.payload.seq === undefined &&
|
||||
typeof message.payload.epoch === "string" &&
|
||||
(epoch === undefined || message.payload.epoch === epoch) &&
|
||||
(text === undefined || message.payload.event.item.text === text)
|
||||
);
|
||||
}
|
||||
@@ -73,6 +76,14 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
text: `committed row ${seq}`,
|
||||
});
|
||||
}
|
||||
const baseline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
projection: "canonical",
|
||||
});
|
||||
const epoch = baseline.epoch;
|
||||
expect(epoch).not.toBe("");
|
||||
expect(baseline.endCursor?.epoch).toBe(epoch);
|
||||
|
||||
primaryCollector.clear();
|
||||
await ctx.daemon.daemon.agentManager.emitLiveTimelineItem(agent.id, {
|
||||
@@ -81,7 +92,7 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
});
|
||||
await waitFor(() =>
|
||||
primaryCollector.messages.some((message) =>
|
||||
isSeqLessAssistantTimeline(message, agent.id, "partial before disconnect"),
|
||||
isLiveAssistantTimeline(message, agent.id, epoch, "partial before disconnect"),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -105,18 +116,26 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
|
||||
expect(
|
||||
reconnectCollector.messages.some((message) =>
|
||||
isSeqLessAssistantTimeline(message, agent.id),
|
||||
isLiveAssistantTimeline(message, agent.id, epoch),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const catchUp = await reconnectClient.fetchAgentTimeline(agent.id, {
|
||||
direction: "after",
|
||||
cursor: { seq: 120 },
|
||||
cursor: { epoch, seq: 120 },
|
||||
limit: 0,
|
||||
projection: "canonical",
|
||||
});
|
||||
|
||||
expect(catchUp.epoch).toBe(epoch);
|
||||
expect(catchUp.reset).toBe(false);
|
||||
expect(catchUp.staleCursor).toBe(false);
|
||||
expect(catchUp.gap).toBe(false);
|
||||
expect(catchUp.entries).toHaveLength(1);
|
||||
expect(catchUp.entries[0]?.seq).toBe(121);
|
||||
expect(catchUp.startCursor).toEqual({ epoch, seq: 121 });
|
||||
expect(catchUp.endCursor).toEqual({ epoch, seq: 121 });
|
||||
expect(catchUp.entries[0]?.seqStart).toBe(121);
|
||||
expect(catchUp.entries[0]?.seqEnd).toBe(121);
|
||||
expect(catchUp.entries[0]?.item).toEqual({
|
||||
type: "assistant_message",
|
||||
text: "finalized while disconnected",
|
||||
@@ -149,6 +168,14 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
text: `committed row ${seq}`,
|
||||
});
|
||||
}
|
||||
const baseline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
limit: 0,
|
||||
projection: "canonical",
|
||||
});
|
||||
const epoch = baseline.epoch;
|
||||
expect(epoch).not.toBe("");
|
||||
expect(baseline.endCursor?.epoch).toBe(epoch);
|
||||
|
||||
primaryCollector.clear();
|
||||
await ctx.daemon.daemon.agentManager.emitLiveTimelineItem(agent.id, {
|
||||
@@ -157,7 +184,7 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
});
|
||||
await waitFor(() =>
|
||||
primaryCollector.messages.some((message) =>
|
||||
isSeqLessAssistantTimeline(message, agent.id, "partial before disconnect"),
|
||||
isLiveAssistantTimeline(message, agent.id, epoch, "partial before disconnect"),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -176,17 +203,24 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
|
||||
expect(
|
||||
reconnectCollector.messages.some((message) =>
|
||||
isSeqLessAssistantTimeline(message, agent.id),
|
||||
isLiveAssistantTimeline(message, agent.id, epoch),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const catchUp = await reconnectClient.fetchAgentTimeline(agent.id, {
|
||||
direction: "after",
|
||||
cursor: { seq: 120 },
|
||||
cursor: { epoch, seq: 120 },
|
||||
limit: 0,
|
||||
projection: "canonical",
|
||||
});
|
||||
|
||||
expect(catchUp.epoch).toBe(epoch);
|
||||
expect(catchUp.reset).toBe(false);
|
||||
expect(catchUp.staleCursor).toBe(false);
|
||||
expect(catchUp.gap).toBe(false);
|
||||
expect(catchUp.entries).toHaveLength(0);
|
||||
expect(catchUp.startCursor).toBeNull();
|
||||
expect(catchUp.endCursor).toBeNull();
|
||||
|
||||
reconnectCollector.clear();
|
||||
await ctx.daemon.daemon.agentManager.emitLiveTimelineItem(agent.id, {
|
||||
@@ -195,7 +229,7 @@ describe("daemon E2E - timeline reconnect contract", () => {
|
||||
});
|
||||
await waitFor(() =>
|
||||
reconnectCollector.messages.some((message) =>
|
||||
isSeqLessAssistantTimeline(message, agent.id, "fresh live after reconnect"),
|
||||
isLiveAssistantTimeline(message, agent.id, epoch, "fresh live after reconnect"),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -38,6 +38,7 @@ describe("daemon E2E - timeline window", () => {
|
||||
const timeline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
limit: 1,
|
||||
projection: "canonical",
|
||||
});
|
||||
|
||||
const assistantTexts = timeline.entries
|
||||
@@ -45,7 +46,7 @@ describe("daemon E2E - timeline window", () => {
|
||||
.map((entry) => entry.item.text);
|
||||
|
||||
expect(assistantTexts).toEqual([expected]);
|
||||
expect(timeline.startSeq).toBe(timeline.endSeq);
|
||||
expect(timeline.startCursor?.seq).toBe(timeline.endCursor?.seq);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
@@ -71,6 +72,7 @@ describe("daemon E2E - timeline window", () => {
|
||||
const timeline = await ctx.client.fetchAgentTimeline(agent.id, {
|
||||
direction: "tail",
|
||||
limit: 1,
|
||||
projection: "canonical",
|
||||
});
|
||||
|
||||
const assistantTexts = timeline.entries
|
||||
@@ -79,7 +81,7 @@ describe("daemon E2E - timeline window", () => {
|
||||
|
||||
expect(assistantTexts.join("")).toBe(expected);
|
||||
expect(timeline.hasOlder).toBe(true);
|
||||
expect(timeline.startSeq).toBeGreaterThan(1);
|
||||
expect(timeline.startCursor?.seq).toBeGreaterThan(1);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
||||
import type { StoredAgentRecord } from "./agent/agent-storage.js";
|
||||
import {
|
||||
attachAgentStoragePersistence,
|
||||
@@ -173,16 +174,16 @@ describe("persistence hooks", () => {
|
||||
|
||||
test("buildSessionConfig accepts providers from the canonical manifest", () => {
|
||||
const record = createRecord({
|
||||
provider: "gemini",
|
||||
provider: "claude",
|
||||
persistence: {
|
||||
provider: "gemini",
|
||||
provider: "claude",
|
||||
sessionId: "session-123",
|
||||
},
|
||||
config: {},
|
||||
});
|
||||
|
||||
expect(buildSessionConfig(record)).toMatchObject({
|
||||
provider: "gemini",
|
||||
provider: "claude",
|
||||
cwd: "/tmp/project",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("agent loading boundary", () => {
|
||||
test("session runtime code does not directly hydrate provider history", () => {
|
||||
const sessionSource = readFileSync(new URL("./session.ts", import.meta.url), "utf8");
|
||||
const agentLoadingSource = readFileSync(
|
||||
new URL("./agent-loading-service.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(sessionSource).not.toMatch(/hydrateTimelineFromProvider\s*\(/);
|
||||
expect(agentLoadingSource).not.toMatch(/hydrateTimelineFromProvider\s*\(/);
|
||||
});
|
||||
});
|
||||
@@ -1,333 +0,0 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { AgentLoadingService } from "./agent-loading-service.js";
|
||||
import { AgentManager } from "./agent/agent-manager.js";
|
||||
import { AgentStorage, type StoredAgentRecord } from "./agent/agent-storage.js";
|
||||
import { createTestAgentClients } from "./test-utils/fake-agent-client.js";
|
||||
|
||||
function createStoredAgentRecord(overrides?: Partial<StoredAgentRecord>): StoredAgentRecord {
|
||||
const now = "2026-03-25T00:00:00.000Z";
|
||||
return {
|
||||
id: "agent-compat-1",
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
title: null,
|
||||
labels: {},
|
||||
lastStatus: "idle",
|
||||
config: {
|
||||
model: "gpt-5.1-codex-mini",
|
||||
},
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function createCompatibilitySnapshot(overrides?: Partial<Record<string, unknown>>) {
|
||||
return {
|
||||
id: "agent-compat-1",
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("AgentLoadingService", () => {
|
||||
test("ensureAgentLoaded succeeds when provider history is absent", async () => {
|
||||
const workspaceRoot = mkdtempSync(path.join(os.tmpdir(), "provider-history-compat-empty-"));
|
||||
const logger = pino({ level: "silent" });
|
||||
|
||||
try {
|
||||
const storage = new AgentStorage(path.join(workspaceRoot, "agents"), logger);
|
||||
const manager = new AgentManager({
|
||||
clients: createTestAgentClients(),
|
||||
registry: storage,
|
||||
logger,
|
||||
idFactory: () => "00000000-0000-4000-8000-000000000302",
|
||||
});
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: manager as any,
|
||||
agentStorage: storage as any,
|
||||
logger,
|
||||
});
|
||||
|
||||
const snapshot = await manager.createAgent({
|
||||
provider: "codex",
|
||||
cwd: workspaceRoot,
|
||||
model: "gpt-5.1-codex-mini",
|
||||
});
|
||||
await manager.flush();
|
||||
await storage.flush();
|
||||
await manager.closeAgent(snapshot.id);
|
||||
|
||||
const loaded = await service.ensureAgentLoaded({ agentId: snapshot.id });
|
||||
|
||||
expect(loaded.id).toBe(snapshot.id);
|
||||
expect(manager.getTimeline(snapshot.id)).toEqual([]);
|
||||
} finally {
|
||||
rmSync(workspaceRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("ensureAgentLoaded dedupes concurrent cold-load bootstrap", async () => {
|
||||
const deferred = createDeferred<any>();
|
||||
let currentAgent: any = null;
|
||||
const snapshot = createCompatibilitySnapshot({ id: "agent-compat-dedupe" });
|
||||
const agentStorage = {
|
||||
get: vi.fn(async () =>
|
||||
createStoredAgentRecord({
|
||||
id: "agent-compat-dedupe",
|
||||
cwd: "/tmp/dedupe",
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-dedupe",
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
const agentManager = {
|
||||
getAgent: vi.fn(() => currentAgent),
|
||||
resumeAgentFromPersistence: vi.fn(async () => deferred.promise),
|
||||
createAgent: vi.fn(),
|
||||
reloadAgentSession: vi.fn(),
|
||||
};
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: agentManager as any,
|
||||
agentStorage: agentStorage as any,
|
||||
logger: logger as any,
|
||||
});
|
||||
|
||||
const firstLoad = service.ensureAgentLoaded({ agentId: "agent-compat-dedupe" });
|
||||
const secondLoad = service.ensureAgentLoaded({ agentId: "agent-compat-dedupe" });
|
||||
deferred.resolve(snapshot);
|
||||
|
||||
const [firstResult, secondResult] = await Promise.all([firstLoad, secondLoad]);
|
||||
|
||||
expect(firstResult).toEqual(snapshot);
|
||||
expect(secondResult).toEqual(snapshot);
|
||||
expect(agentStorage.get).toHaveBeenCalledTimes(1);
|
||||
expect(agentManager.resumeAgentFromPersistence).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("resumeAgent delegates to manager resume", async () => {
|
||||
const snapshot = createCompatibilitySnapshot({ id: "agent-compat-resume" });
|
||||
const agentManager = {
|
||||
getAgent: vi.fn(() => null),
|
||||
resumeAgentFromPersistence: vi.fn(async () => snapshot),
|
||||
createAgent: vi.fn(),
|
||||
reloadAgentSession: vi.fn(),
|
||||
};
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: agentManager as any,
|
||||
agentStorage: {
|
||||
get: async () => null,
|
||||
} as any,
|
||||
logger: logger as any,
|
||||
});
|
||||
|
||||
const result = await service.resumeAgent({
|
||||
handle: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-resume",
|
||||
},
|
||||
overrides: {
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
});
|
||||
|
||||
expect(agentManager.resumeAgentFromPersistence).toHaveBeenCalledWith(
|
||||
{
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-resume",
|
||||
},
|
||||
{
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(snapshot);
|
||||
});
|
||||
|
||||
test("refreshAgent reloads loaded persisted agents", async () => {
|
||||
const existing = createCompatibilitySnapshot({ id: "agent-compat-refresh-loaded" });
|
||||
const reloaded = createCompatibilitySnapshot({ id: "agent-compat-refresh-loaded" });
|
||||
let currentAgent: any = existing;
|
||||
const agentManager = {
|
||||
getAgent: vi.fn(() => currentAgent),
|
||||
resumeAgentFromPersistence: vi.fn(),
|
||||
createAgent: vi.fn(),
|
||||
reloadAgentSession: vi.fn(async () => {
|
||||
currentAgent = reloaded;
|
||||
return reloaded;
|
||||
}),
|
||||
};
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: agentManager as any,
|
||||
agentStorage: {
|
||||
get: async () => null,
|
||||
} as any,
|
||||
logger: logger as any,
|
||||
});
|
||||
|
||||
const result = await service.refreshAgent({ agentId: "agent-compat-refresh-loaded" });
|
||||
|
||||
expect(agentManager.reloadAgentSession).toHaveBeenCalledWith("agent-compat-refresh-loaded");
|
||||
expect(result).toEqual(reloaded);
|
||||
});
|
||||
|
||||
test("refreshAgent keeps loaded non-persisted agents without reloading", async () => {
|
||||
const existing = createCompatibilitySnapshot({
|
||||
id: "agent-compat-refresh-live",
|
||||
persistence: null,
|
||||
});
|
||||
const agentManager = {
|
||||
getAgent: vi.fn(() => existing),
|
||||
resumeAgentFromPersistence: vi.fn(),
|
||||
createAgent: vi.fn(),
|
||||
reloadAgentSession: vi.fn(),
|
||||
};
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: agentManager as any,
|
||||
agentStorage: {
|
||||
get: async () => null,
|
||||
} as any,
|
||||
logger: logger as any,
|
||||
});
|
||||
|
||||
const result = await service.refreshAgent({ agentId: "agent-compat-refresh-live" });
|
||||
|
||||
expect(agentManager.reloadAgentSession).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(existing);
|
||||
});
|
||||
|
||||
test("refreshAgent resumes unloaded persisted agents", async () => {
|
||||
const snapshot = createCompatibilitySnapshot({ id: "agent-compat-refresh-cold" });
|
||||
const record = createStoredAgentRecord({
|
||||
id: "agent-compat-refresh-cold",
|
||||
cwd: "/tmp/refresh-cold",
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-refresh-cold",
|
||||
},
|
||||
});
|
||||
const agentManager = {
|
||||
getAgent: vi.fn(() => null),
|
||||
resumeAgentFromPersistence: vi.fn(async () => snapshot),
|
||||
createAgent: vi.fn(),
|
||||
reloadAgentSession: vi.fn(),
|
||||
};
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: agentManager as any,
|
||||
agentStorage: {
|
||||
get: vi.fn(async () => record),
|
||||
} as any,
|
||||
logger: logger as any,
|
||||
});
|
||||
|
||||
const result = await service.refreshAgent({ agentId: "agent-compat-refresh-cold" });
|
||||
|
||||
expect(agentManager.resumeAgentFromPersistence).toHaveBeenCalledWith(
|
||||
{
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-refresh-cold",
|
||||
nativeHandle: undefined,
|
||||
metadata: undefined,
|
||||
},
|
||||
{
|
||||
cwd: "/tmp/refresh-cold",
|
||||
modeId: undefined,
|
||||
model: "gpt-5.1-codex-mini",
|
||||
thinkingOptionId: undefined,
|
||||
title: undefined,
|
||||
extra: undefined,
|
||||
systemPrompt: undefined,
|
||||
mcpServers: undefined,
|
||||
},
|
||||
"agent-compat-refresh-cold",
|
||||
{
|
||||
createdAt: new Date("2026-03-25T00:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-25T00:00:00.000Z"),
|
||||
lastUserMessageAt: null,
|
||||
labels: {},
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(snapshot);
|
||||
});
|
||||
|
||||
test("refreshAgent preserves the unloaded no-persistence error", async () => {
|
||||
const service = new AgentLoadingService({
|
||||
agentManager: {
|
||||
getAgent: vi.fn(() => null),
|
||||
resumeAgentFromPersistence: vi.fn(),
|
||||
createAgent: vi.fn(),
|
||||
reloadAgentSession: vi.fn(),
|
||||
} as any,
|
||||
agentStorage: {
|
||||
get: async () =>
|
||||
createStoredAgentRecord({
|
||||
id: "agent-compat-no-persistence",
|
||||
persistence: null,
|
||||
}),
|
||||
} as any,
|
||||
logger: {
|
||||
child: () => ({
|
||||
child: () => null,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
}),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
} as any,
|
||||
});
|
||||
|
||||
await expect(service.refreshAgent({ agentId: "agent-compat-no-persistence" })).rejects.toThrow(
|
||||
"Agent agent-compat-no-persistence cannot be refreshed because it lacks persistence",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,389 +0,0 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { Session } from "./session.js";
|
||||
|
||||
function createStoredAgentRecord(overrides?: Partial<Record<string, unknown>>) {
|
||||
return {
|
||||
id: "agent-1",
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
createdAt: "2026-03-24T00:00:00.000Z",
|
||||
updatedAt: "2026-03-24T00:00:00.000Z",
|
||||
title: null,
|
||||
labels: {},
|
||||
lastStatus: "idle",
|
||||
config: null,
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
archivedAt: null,
|
||||
requiresAttention: false,
|
||||
attentionReason: null,
|
||||
attentionTimestamp: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createCompatibilitySnapshot(overrides?: Partial<Record<string, unknown>>) {
|
||||
return {
|
||||
id: "agent-1",
|
||||
provider: "codex",
|
||||
cwd: "/tmp/project",
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionForOwnershipTests(options?: {
|
||||
agentLoadingService?: {
|
||||
ensureAgentLoaded?: (options: { agentId: string }) => Promise<any>;
|
||||
resumeAgent?: (options: {
|
||||
handle: { provider: string; sessionId: string };
|
||||
overrides?: Record<string, unknown>;
|
||||
}) => Promise<any>;
|
||||
refreshAgent?: (options: { agentId: string }) => Promise<any>;
|
||||
};
|
||||
storedRecord?: Record<string, unknown> | null;
|
||||
loadedAgent?: Record<string, unknown> | null;
|
||||
timelineRows?: Array<{ seq: number; item: Record<string, unknown>; timestamp: Date }>;
|
||||
}) {
|
||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
trace: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const agentManager = {
|
||||
subscribe: () => () => {},
|
||||
listAgents: () => [],
|
||||
getAgent: vi.fn(() => options?.loadedAgent ?? null),
|
||||
createAgent: vi.fn(async () => {
|
||||
throw new Error("Session should delegate unloaded bootstrap to AgentLoadingService");
|
||||
}),
|
||||
resumeAgentFromPersistence: vi.fn(async () => {
|
||||
throw new Error("Session should delegate persistence resume to AgentLoadingService");
|
||||
}),
|
||||
reloadAgentSession: vi.fn(async () => {
|
||||
throw new Error("Session should delegate refresh reload to AgentLoadingService");
|
||||
}),
|
||||
hydrateTimelineFromProvider: vi.fn(async () => {
|
||||
throw new Error("Session should not call hydrateTimelineFromProvider directly");
|
||||
}),
|
||||
fetchTimeline: vi.fn(async () => ({
|
||||
rows: options?.timelineRows ?? [],
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
})),
|
||||
recordUserMessage: vi.fn(),
|
||||
waitForAgentRunStart: vi.fn(async () => undefined),
|
||||
getTimeline: vi.fn(() => []),
|
||||
};
|
||||
|
||||
const session = new Session({
|
||||
clientId: "test-client",
|
||||
onMessage: (message) => emitted.push(message as any),
|
||||
logger: logger as any,
|
||||
downloadTokenStore: {} as any,
|
||||
pushTokenStore: {} as any,
|
||||
paseoHome: "/tmp/paseo-test",
|
||||
agentManager: agentManager as any,
|
||||
agentStorage: {
|
||||
list: async () => (options?.storedRecord ? [options.storedRecord as any] : []),
|
||||
get: async () => (options?.storedRecord as any) ?? null,
|
||||
} as any,
|
||||
projectRegistry: {
|
||||
initialize: async () => {},
|
||||
existsOnDisk: async () => true,
|
||||
list: async () => [],
|
||||
get: async () => null,
|
||||
upsert: async () => {},
|
||||
archive: async () => {},
|
||||
remove: async () => {},
|
||||
} as any,
|
||||
workspaceRegistry: {
|
||||
initialize: async () => {},
|
||||
existsOnDisk: async () => true,
|
||||
list: async () => [],
|
||||
get: async () => null,
|
||||
upsert: async () => {},
|
||||
archive: async () => {},
|
||||
remove: async () => {},
|
||||
} as any,
|
||||
createAgentMcpTransport: async () => {
|
||||
throw new Error("not used");
|
||||
},
|
||||
stt: null,
|
||||
tts: null,
|
||||
terminalManager: null,
|
||||
agentLoadingService: options?.agentLoadingService,
|
||||
} as any) as any;
|
||||
|
||||
return { session, emitted, agentManager };
|
||||
}
|
||||
|
||||
describe("provider history compatibility ownership", () => {
|
||||
test("fetch_agent_timeline_request delegates unloaded bootstrap through the compatibility seam", async () => {
|
||||
const ensureAgentLoaded = vi.fn(async () => createCompatibilitySnapshot());
|
||||
const { session, emitted } = createSessionForOwnershipTests({
|
||||
storedRecord: createStoredAgentRecord(),
|
||||
timelineRows: [
|
||||
{
|
||||
seq: 1,
|
||||
item: { type: "assistant_message", text: "rehydrated from provider history" },
|
||||
timestamp: new Date("2026-03-24T00:00:01.000Z"),
|
||||
},
|
||||
],
|
||||
agentLoadingService: {
|
||||
ensureAgentLoaded,
|
||||
},
|
||||
});
|
||||
|
||||
session.buildAgentPayload = vi.fn(async () => ({ id: "agent-1" }));
|
||||
|
||||
await session.handleMessage({
|
||||
type: "fetch_agent_timeline_request",
|
||||
requestId: "req-fetch",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
expect(ensureAgentLoaded).toHaveBeenCalledWith({ agentId: "agent-1" });
|
||||
expect(emitted).toContainEqual({
|
||||
type: "fetch_agent_timeline_response",
|
||||
payload: expect.objectContaining({
|
||||
requestId: "req-fetch",
|
||||
agentId: "agent-1",
|
||||
error: null,
|
||||
entries: [
|
||||
expect.objectContaining({
|
||||
seq: 1,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("fetch_agent_timeline_request emits legacy compatibility fields for older clients", async () => {
|
||||
const ensureAgentLoaded = vi.fn(async () => createCompatibilitySnapshot());
|
||||
const { session, emitted } = createSessionForOwnershipTests({
|
||||
storedRecord: createStoredAgentRecord(),
|
||||
timelineRows: [
|
||||
{
|
||||
seq: 7,
|
||||
item: { type: "assistant_message", text: "compat row" },
|
||||
timestamp: new Date("2026-03-24T00:00:07.000Z"),
|
||||
},
|
||||
],
|
||||
agentLoadingService: {
|
||||
ensureAgentLoaded,
|
||||
},
|
||||
});
|
||||
|
||||
session.buildAgentPayload = vi.fn(async () => ({ id: "agent-1" }));
|
||||
|
||||
await session.handleMessage({
|
||||
type: "fetch_agent_timeline_request",
|
||||
requestId: "req-compat",
|
||||
agentId: "agent-1",
|
||||
direction: "tail",
|
||||
});
|
||||
|
||||
expect(emitted).toContainEqual({
|
||||
type: "fetch_agent_timeline_response",
|
||||
payload: expect.objectContaining({
|
||||
requestId: "req-compat",
|
||||
agentId: "agent-1",
|
||||
epoch: "compat:agent-1",
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
projection: "projected",
|
||||
startCursor: { seq: 7, epoch: "compat:agent-1" },
|
||||
endCursor: { seq: 7, epoch: "compat:agent-1" },
|
||||
window: {
|
||||
minSeq: 7,
|
||||
maxSeq: 7,
|
||||
nextSeq: 8,
|
||||
startSeq: 7,
|
||||
endSeq: 7,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
},
|
||||
entries: [
|
||||
expect.objectContaining({
|
||||
seq: 7,
|
||||
seqStart: 7,
|
||||
seqEnd: 7,
|
||||
sourceSeqRanges: [{ startSeq: 7, endSeq: 7 }],
|
||||
collapsed: [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("send_agent_message_request delegates unloaded bootstrap before recording and streaming", async () => {
|
||||
const ensureAgentLoaded = vi.fn(async () => createCompatibilitySnapshot());
|
||||
const { session, agentManager, emitted } = createSessionForOwnershipTests({
|
||||
storedRecord: createStoredAgentRecord(),
|
||||
agentLoadingService: {
|
||||
ensureAgentLoaded,
|
||||
},
|
||||
});
|
||||
|
||||
session.resolveAgentIdentifier = vi.fn(async () => ({ ok: true, agentId: "agent-1" }));
|
||||
session.unarchiveAgentState = vi.fn(async () => true);
|
||||
session.buildAgentPrompt = vi.fn((text: string) => text);
|
||||
session.startAgentStream = vi.fn(() => ({ ok: true }));
|
||||
|
||||
await session.handleMessage({
|
||||
type: "send_agent_message_request",
|
||||
requestId: "req-send",
|
||||
agentId: "agent-1",
|
||||
text: "hello",
|
||||
images: [],
|
||||
messageId: "msg-1",
|
||||
});
|
||||
|
||||
expect(ensureAgentLoaded).toHaveBeenCalledWith({ agentId: "agent-1" });
|
||||
expect(ensureAgentLoaded.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
agentManager.recordUserMessage.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(agentManager.recordUserMessage).toHaveBeenCalledWith("agent-1", "hello", {
|
||||
messageId: "msg-1",
|
||||
emitState: false,
|
||||
});
|
||||
expect(session.startAgentStream).toHaveBeenCalledWith("agent-1", "hello");
|
||||
expect(emitted).toContainEqual({
|
||||
type: "send_agent_message_response",
|
||||
payload: {
|
||||
requestId: "req-send",
|
||||
agentId: "agent-1",
|
||||
accepted: true,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("resume_agent_request delegates persistence bootstrap through the compatibility seam", async () => {
|
||||
const resumeAgent = vi.fn(async () => createCompatibilitySnapshot());
|
||||
const { session, emitted } = createSessionForOwnershipTests({
|
||||
agentLoadingService: {
|
||||
resumeAgent,
|
||||
},
|
||||
});
|
||||
|
||||
session.unarchiveAgentByHandle = vi.fn(async () => undefined);
|
||||
session.unarchiveAgentState = vi.fn(async () => true);
|
||||
session.forwardAgentUpdate = vi.fn(async () => undefined);
|
||||
session.getAgentPayloadById = vi.fn(async () => ({ id: "agent-1" }));
|
||||
|
||||
await session.handleMessage({
|
||||
type: "resume_agent_request",
|
||||
requestId: "req-resume",
|
||||
handle: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
overrides: {
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
});
|
||||
|
||||
expect(resumeAgent).toHaveBeenCalledWith({
|
||||
handle: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
overrides: {
|
||||
model: "gpt-5.4",
|
||||
},
|
||||
});
|
||||
expect(emitted).toContainEqual({
|
||||
type: "status",
|
||||
payload: expect.objectContaining({
|
||||
status: "agent_resumed",
|
||||
requestId: "req-resume",
|
||||
agentId: "agent-1",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("refresh_agent_request delegates loaded persisted refresh through the compatibility seam", async () => {
|
||||
const refreshAgent = vi.fn(async () =>
|
||||
createCompatibilitySnapshot({
|
||||
persistence: {
|
||||
provider: "codex",
|
||||
sessionId: "provider-session-1",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const { session, emitted } = createSessionForOwnershipTests({
|
||||
loadedAgent: createCompatibilitySnapshot(),
|
||||
agentLoadingService: {
|
||||
refreshAgent,
|
||||
},
|
||||
});
|
||||
|
||||
session.unarchiveAgentState = vi.fn(async () => true);
|
||||
session.interruptAgentIfRunning = vi.fn(async () => undefined);
|
||||
session.forwardAgentUpdate = vi.fn(async () => undefined);
|
||||
|
||||
await session.handleMessage({
|
||||
type: "refresh_agent_request",
|
||||
requestId: "req-refresh-loaded",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
expect(session.interruptAgentIfRunning).toHaveBeenCalledWith("agent-1");
|
||||
expect(refreshAgent).toHaveBeenCalledWith({ agentId: "agent-1" });
|
||||
expect(emitted).toContainEqual({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "agent_refreshed",
|
||||
requestId: "req-refresh-loaded",
|
||||
agentId: "agent-1",
|
||||
timelineSize: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("refresh_agent_request delegates unloaded persisted refresh through the compatibility seam", async () => {
|
||||
const refreshAgent = vi.fn(async () => createCompatibilitySnapshot());
|
||||
const { session, emitted } = createSessionForOwnershipTests({
|
||||
storedRecord: createStoredAgentRecord(),
|
||||
agentLoadingService: {
|
||||
refreshAgent,
|
||||
},
|
||||
});
|
||||
|
||||
session.unarchiveAgentState = vi.fn(async () => true);
|
||||
session.interruptAgentIfRunning = vi.fn(async () => undefined);
|
||||
session.forwardAgentUpdate = vi.fn(async () => undefined);
|
||||
|
||||
await session.handleMessage({
|
||||
type: "refresh_agent_request",
|
||||
requestId: "req-refresh-unloaded",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
|
||||
expect(session.interruptAgentIfRunning).not.toHaveBeenCalled();
|
||||
expect(refreshAgent).toHaveBeenCalledWith({ agentId: "agent-1" });
|
||||
expect(emitted).toContainEqual({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "agent_refreshed",
|
||||
requestId: "req-refresh-unloaded",
|
||||
agentId: "agent-1",
|
||||
timelineSize: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,11 @@ import {
|
||||
appendTimelineItemIfAgentKnown,
|
||||
emitLiveTimelineItemIfAgentKnown,
|
||||
} from "./agent/timeline-append.js";
|
||||
import {
|
||||
projectTimelineRows,
|
||||
selectTimelineWindowByProjectedLimit,
|
||||
type TimelineProjectionMode,
|
||||
} from "./agent/timeline-projection.js";
|
||||
import {
|
||||
DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
|
||||
StructuredAgentFallbackError,
|
||||
@@ -1143,6 +1148,7 @@ export class Session {
|
||||
event: serializedEvent,
|
||||
timestamp: new Date().toISOString(),
|
||||
...(typeof event.seq === "number" ? { seq: event.seq } : {}),
|
||||
...(typeof event.epoch === "string" ? { epoch: event.epoch } : {}),
|
||||
} as const;
|
||||
|
||||
this.emit({
|
||||
@@ -1705,7 +1711,7 @@ export class Session {
|
||||
break;
|
||||
|
||||
case "cancel_agent_request":
|
||||
await this.handleCancelAgentRequest(msg.agentId);
|
||||
await this.handleCancelAgentRequest(msg.agentId, msg.requestId);
|
||||
break;
|
||||
|
||||
case "restart_server_request":
|
||||
@@ -1886,7 +1892,7 @@ export class Session {
|
||||
break;
|
||||
|
||||
case "clear_agent_attention":
|
||||
await this.handleClearAgentAttention(msg.agentId);
|
||||
await this.handleClearAgentAttention(msg.agentId, msg.requestId);
|
||||
break;
|
||||
|
||||
case "client_heartbeat":
|
||||
@@ -3337,11 +3343,23 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCancelAgentRequest(agentId: string): Promise<void> {
|
||||
private async handleCancelAgentRequest(agentId: string, requestId?: string): Promise<void> {
|
||||
this.sessionLogger.info({ agentId }, `Cancel request received for agent ${agentId}`);
|
||||
|
||||
try {
|
||||
await this.interruptAgentIfRunning(agentId);
|
||||
if (requestId) {
|
||||
const agent = this.agentManager.getAgent(agentId);
|
||||
const payload = agent ? await this.buildAgentPayload(agent) : null;
|
||||
this.emit({
|
||||
type: "cancel_agent_response",
|
||||
payload: {
|
||||
requestId,
|
||||
agentId,
|
||||
agent: payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleAgentRunError(agentId, error, "Failed to cancel running agent on request");
|
||||
}
|
||||
@@ -4005,11 +4023,32 @@ export class Session {
|
||||
/**
|
||||
* Handle clearing agent attention flag
|
||||
*/
|
||||
private async handleClearAgentAttention(agentId: string | string[]): Promise<void> {
|
||||
private async handleClearAgentAttention(
|
||||
agentId: string | string[],
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const agentIds = Array.isArray(agentId) ? agentId : [agentId];
|
||||
|
||||
try {
|
||||
await Promise.all(agentIds.map((id) => this.agentManager.clearAgentAttention(id)));
|
||||
if (requestId) {
|
||||
const agents = (
|
||||
await Promise.all(
|
||||
agentIds.map(async (id) => {
|
||||
const agent = this.agentManager.getAgent(id);
|
||||
return agent ? this.buildAgentPayload(agent) : null;
|
||||
}),
|
||||
)
|
||||
).filter((payload): payload is NonNullable<typeof payload> => payload !== null);
|
||||
this.emit({
|
||||
type: "clear_agent_attention_response",
|
||||
payload: {
|
||||
requestId,
|
||||
agentId,
|
||||
agents,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.sessionLogger.error({ err: error, agentIds }, "Failed to clear agent attention");
|
||||
// Don't throw - this is not critical
|
||||
@@ -6884,10 +6923,17 @@ export class Session {
|
||||
msg: Extract<SessionInboundMessage, { type: "fetch_agent_timeline_request" }>,
|
||||
): Promise<void> {
|
||||
const direction: AgentTimelineFetchDirection = msg.direction ?? (msg.cursor ? "after" : "tail");
|
||||
const projection: TimelineProjectionMode = msg.projection ?? "projected";
|
||||
const requestedLimit = msg.limit;
|
||||
const limit = requestedLimit ?? (direction === "after" ? 0 : undefined);
|
||||
const shouldLimitByProjectedWindow =
|
||||
projection === "canonical" &&
|
||||
direction === "tail" &&
|
||||
typeof requestedLimit === "number" &&
|
||||
requestedLimit > 0;
|
||||
const cursor: AgentTimelineCursor | undefined = msg.cursor
|
||||
? {
|
||||
epoch: msg.cursor.epoch,
|
||||
seq: msg.cursor.seq,
|
||||
}
|
||||
: undefined;
|
||||
@@ -6895,46 +6941,86 @@ export class Session {
|
||||
try {
|
||||
const snapshot = await this.ensureAgentLoaded(msg.agentId);
|
||||
const agentPayload = await this.buildAgentPayload(snapshot);
|
||||
const timeline = await this.agentManager.fetchTimeline(msg.agentId, {
|
||||
|
||||
let timeline = this.agentManager.fetchTimeline(msg.agentId, {
|
||||
direction,
|
||||
cursor,
|
||||
limit,
|
||||
limit:
|
||||
shouldLimitByProjectedWindow && typeof requestedLimit === "number"
|
||||
? Math.max(1, Math.floor(requestedLimit))
|
||||
: limit,
|
||||
});
|
||||
const firstRow = timeline.rows[0];
|
||||
const lastRow = timeline.rows[timeline.rows.length - 1];
|
||||
const timelineWindow =
|
||||
timeline.window ??
|
||||
(() => {
|
||||
const minSeq = firstRow?.seq ?? 0;
|
||||
const maxSeq = lastRow?.seq ?? 0;
|
||||
return {
|
||||
minSeq,
|
||||
maxSeq,
|
||||
nextSeq: maxSeq > 0 ? maxSeq + 1 : 0,
|
||||
};
|
||||
})();
|
||||
const epoch = `compat:${msg.agentId}`;
|
||||
const startCursor = firstRow ? { seq: firstRow.seq, epoch } : null;
|
||||
const endCursor = lastRow ? { seq: lastRow.seq, epoch } : null;
|
||||
const window = {
|
||||
minSeq: timelineWindow.minSeq,
|
||||
maxSeq: timelineWindow.maxSeq,
|
||||
nextSeq: timelineWindow.nextSeq,
|
||||
startSeq: firstRow?.seq ?? null,
|
||||
endSeq: lastRow?.seq ?? null,
|
||||
hasOlder: timeline.hasOlder,
|
||||
hasNewer: timeline.hasNewer,
|
||||
};
|
||||
const entries = timeline.rows.map((row) => ({
|
||||
provider: snapshot.provider,
|
||||
item: row.item,
|
||||
timestamp: row.timestamp,
|
||||
seq: row.seq,
|
||||
seqStart: row.seq,
|
||||
seqEnd: row.seq,
|
||||
sourceSeqRanges: [{ startSeq: row.seq, endSeq: row.seq }],
|
||||
collapsed: [],
|
||||
}));
|
||||
let hasOlder = timeline.hasOlder;
|
||||
let hasNewer = timeline.hasNewer;
|
||||
let startCursor: { epoch: string; seq: number } | null = null;
|
||||
let endCursor: { epoch: string; seq: number } | null = null;
|
||||
let entries: ReturnType<typeof projectTimelineRows>;
|
||||
|
||||
if (shouldLimitByProjectedWindow) {
|
||||
const projectedLimit = Math.max(1, Math.floor(requestedLimit));
|
||||
let fetchLimit = projectedLimit;
|
||||
let projectedWindow = selectTimelineWindowByProjectedLimit({
|
||||
rows: timeline.rows,
|
||||
provider: snapshot.provider,
|
||||
direction,
|
||||
limit: projectedLimit,
|
||||
collapseToolLifecycle: false,
|
||||
});
|
||||
|
||||
while (timeline.hasOlder) {
|
||||
const needsMoreProjectedEntries =
|
||||
projectedWindow.projectedEntries.length < projectedLimit;
|
||||
const firstLoadedRow = timeline.rows[0];
|
||||
const firstSelectedRow = projectedWindow.selectedRows[0];
|
||||
const startsAtLoadedBoundary =
|
||||
firstLoadedRow != null &&
|
||||
firstSelectedRow != null &&
|
||||
firstSelectedRow.seq === firstLoadedRow.seq;
|
||||
const boundaryIsAssistantChunk =
|
||||
startsAtLoadedBoundary && firstLoadedRow.item.type === "assistant_message";
|
||||
|
||||
if (!needsMoreProjectedEntries && !boundaryIsAssistantChunk) {
|
||||
break;
|
||||
}
|
||||
|
||||
const maxRows = Math.max(0, timeline.window.maxSeq - timeline.window.minSeq + 1);
|
||||
const nextFetchLimit = Math.min(maxRows, fetchLimit * 2);
|
||||
if (nextFetchLimit <= fetchLimit) {
|
||||
break;
|
||||
}
|
||||
|
||||
fetchLimit = nextFetchLimit;
|
||||
timeline = this.agentManager.fetchTimeline(msg.agentId, {
|
||||
direction,
|
||||
cursor,
|
||||
limit: fetchLimit,
|
||||
});
|
||||
projectedWindow = selectTimelineWindowByProjectedLimit({
|
||||
rows: timeline.rows,
|
||||
provider: snapshot.provider,
|
||||
direction,
|
||||
limit: projectedLimit,
|
||||
collapseToolLifecycle: false,
|
||||
});
|
||||
}
|
||||
|
||||
const selectedRows = projectedWindow.selectedRows;
|
||||
|
||||
entries = projectTimelineRows(selectedRows, snapshot.provider, projection);
|
||||
|
||||
if (projectedWindow.minSeq !== null && projectedWindow.maxSeq !== null) {
|
||||
startCursor = { epoch: timeline.epoch, seq: projectedWindow.minSeq };
|
||||
endCursor = { epoch: timeline.epoch, seq: projectedWindow.maxSeq };
|
||||
hasOlder = projectedWindow.minSeq > timeline.window.minSeq;
|
||||
hasNewer = false;
|
||||
}
|
||||
} else {
|
||||
const firstRow = timeline.rows[0];
|
||||
const lastRow = timeline.rows[timeline.rows.length - 1];
|
||||
startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
|
||||
endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
|
||||
entries = projectTimelineRows(timeline.rows, snapshot.provider, projection);
|
||||
}
|
||||
|
||||
this.emit({
|
||||
type: "fetch_agent_timeline_response",
|
||||
@@ -6943,24 +7029,21 @@ export class Session {
|
||||
agentId: msg.agentId,
|
||||
agent: agentPayload,
|
||||
direction,
|
||||
startSeq: firstRow?.seq ?? null,
|
||||
endSeq: lastRow?.seq ?? null,
|
||||
hasOlder: timeline.hasOlder,
|
||||
hasNewer: timeline.hasNewer,
|
||||
entries,
|
||||
error: null,
|
||||
epoch,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window,
|
||||
projection,
|
||||
epoch: timeline.epoch,
|
||||
reset: timeline.reset,
|
||||
staleCursor: timeline.staleCursor,
|
||||
gap: timeline.gap,
|
||||
window: timeline.window,
|
||||
startCursor,
|
||||
endCursor,
|
||||
projection: msg.projection ?? "projected",
|
||||
hasOlder,
|
||||
hasNewer,
|
||||
entries,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const epoch = `compat:${msg.agentId}`;
|
||||
this.sessionLogger.error(
|
||||
{ err: error, agentId: msg.agentId },
|
||||
"Failed to handle fetch_agent_timeline_request",
|
||||
@@ -6972,28 +7055,18 @@ export class Session {
|
||||
agentId: msg.agentId,
|
||||
agent: null,
|
||||
direction,
|
||||
startSeq: null,
|
||||
endSeq: null,
|
||||
projection,
|
||||
epoch: "",
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window: { minSeq: 0, maxSeq: 0, nextSeq: 0 },
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
entries: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
epoch,
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window: {
|
||||
minSeq: 0,
|
||||
maxSeq: 0,
|
||||
nextSeq: 0,
|
||||
startSeq: null,
|
||||
endSeq: null,
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
},
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
projection: msg.projection ?? "projected",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -935,9 +935,6 @@ export function createTestAgentClients(): Record<string, AgentClient> {
|
||||
return {
|
||||
claude: new FakeAgentClient("claude"),
|
||||
codex: new FakeAgentClient("codex"),
|
||||
gemini: new FakeAgentClient("gemini"),
|
||||
amp: new FakeAgentClient("amp"),
|
||||
aider: new FakeAgentClient("aider"),
|
||||
opencode: new FakeAgentClient("opencode"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
AgentStreamMessageSchema,
|
||||
FetchAgentTimelineRequestMessageSchema,
|
||||
FetchAgentTimelineResponseMessageSchema,
|
||||
SessionInboundMessageSchema,
|
||||
SessionOutboundMessageSchema,
|
||||
@@ -18,55 +17,16 @@ describe("shared messages stream parsing", () => {
|
||||
agentId: "agent_live",
|
||||
agent: null,
|
||||
direction: "tail",
|
||||
startSeq: 1,
|
||||
endSeq: 2,
|
||||
projection: "projected",
|
||||
epoch: "epoch-1",
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window: { minSeq: 1, maxSeq: 2, nextSeq: 3 },
|
||||
startCursor: { epoch: "epoch-1", seq: 1 },
|
||||
endCursor: { epoch: "epoch-1", seq: 2 },
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
entries: [
|
||||
{
|
||||
provider: "codex",
|
||||
item: { type: "assistant_message", text: "hello" },
|
||||
timestamp: "2026-02-08T20:10:00.000Z",
|
||||
seq: 2,
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload.entries).toHaveLength(1);
|
||||
expect(parsed.payload.entries[0]?.item.type).toBe("assistant_message");
|
||||
});
|
||||
|
||||
it("parses legacy fetch timeline request baggage at the parser boundary", () => {
|
||||
const parsed = FetchAgentTimelineRequestMessageSchema.parse({
|
||||
type: "fetch_agent_timeline_request",
|
||||
agentId: "agent_live",
|
||||
requestId: "req-legacy",
|
||||
direction: "after",
|
||||
cursor: {
|
||||
seq: 12,
|
||||
epoch: "legacy-epoch",
|
||||
},
|
||||
projection: "canonical",
|
||||
});
|
||||
|
||||
expect(parsed.cursor?.epoch).toBe("legacy-epoch");
|
||||
expect(parsed.projection).toBe("canonical");
|
||||
});
|
||||
|
||||
it("parses legacy fetch timeline response baggage at the parser boundary", () => {
|
||||
const parsed = FetchAgentTimelineResponseMessageSchema.parse({
|
||||
type: "fetch_agent_timeline_response",
|
||||
payload: {
|
||||
requestId: "req-1",
|
||||
agentId: "agent_live",
|
||||
agent: null,
|
||||
direction: "tail",
|
||||
reset: false,
|
||||
startCursor: { seq: 1, epoch: "legacy-epoch" },
|
||||
endCursor: { seq: 2, epoch: "legacy-epoch" },
|
||||
projection: "canonical",
|
||||
entries: [
|
||||
{
|
||||
provider: "codex",
|
||||
@@ -82,10 +42,8 @@ describe("shared messages stream parsing", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload.startSeq).toBe(1);
|
||||
expect(parsed.payload.endSeq).toBe(2);
|
||||
expect(parsed.payload.projection).toBe("canonical");
|
||||
expect(parsed.payload.entries[0]?.seq).toBe(2);
|
||||
expect(parsed.payload.entries).toHaveLength(1);
|
||||
expect(parsed.payload.entries[0]?.item.type).toBe("assistant_message");
|
||||
});
|
||||
|
||||
it("parses explicit shutdown and restart lifecycle request payloads as distinct message types", () => {
|
||||
@@ -112,7 +70,6 @@ describe("shared messages stream parsing", () => {
|
||||
payload: {
|
||||
agentId: "agent_live",
|
||||
timestamp: "2026-02-08T20:10:00.000Z",
|
||||
seq: 12,
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
@@ -140,27 +97,6 @@ describe("shared messages stream parsing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("parses legacy agent_stream baggage at the parser boundary", () => {
|
||||
const parsed = AgentStreamMessageSchema.parse({
|
||||
type: "agent_stream",
|
||||
payload: {
|
||||
agentId: "agent_live",
|
||||
timestamp: "2026-02-08T20:10:00.000Z",
|
||||
epoch: "legacy-epoch",
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "assistant_message",
|
||||
text: "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload.epoch).toBe("legacy-epoch");
|
||||
});
|
||||
|
||||
it("parses representative sub_agent tool_call event", () => {
|
||||
const parsed = AgentStreamMessageSchema.parse({
|
||||
type: "agent_stream",
|
||||
|
||||
@@ -908,6 +908,7 @@ export const RefreshAgentRequestMessageSchema = z.object({
|
||||
export const CancelAgentRequestMessageSchema = z.object({
|
||||
type: z.literal("cancel_agent_request"),
|
||||
agentId: z.string(),
|
||||
requestId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const RestartServerRequestMessageSchema = z.object({
|
||||
@@ -922,9 +923,8 @@ export const ShutdownServerRequestMessageSchema = z.object({
|
||||
});
|
||||
|
||||
export const AgentTimelineCursorSchema = z.object({
|
||||
epoch: z.string(),
|
||||
seq: z.number().int().nonnegative(),
|
||||
// COMPAT(timeline): retain legacy cursor epoch for older clients.
|
||||
epoch: z.string().optional(),
|
||||
});
|
||||
|
||||
export const FetchAgentTimelineRequestMessageSchema = z.object({
|
||||
@@ -935,8 +935,8 @@ export const FetchAgentTimelineRequestMessageSchema = z.object({
|
||||
cursor: AgentTimelineCursorSchema.optional(),
|
||||
// 0 means "all matching rows for this query window".
|
||||
limit: z.number().int().nonnegative().optional(),
|
||||
// COMPAT(timeline): retain removed projection so older clients can still send it.
|
||||
projection: z.enum(["canonical", "projected"]).optional(),
|
||||
// Default should be projected for app timeline loading.
|
||||
projection: z.enum(["projected", "canonical"]).optional(),
|
||||
});
|
||||
|
||||
export const SetAgentModeRequestMessageSchema = z.object({
|
||||
@@ -1367,6 +1367,7 @@ export const FileDownloadTokenRequestSchema = z.object({
|
||||
export const ClearAgentAttentionMessageSchema = z.object({
|
||||
type: z.literal("clear_agent_attention"),
|
||||
agentId: z.union([z.string(), z.array(z.string())]),
|
||||
requestId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ClientHeartbeatMessageSchema = z.object({
|
||||
@@ -2036,9 +2037,8 @@ export const AgentStreamMessageSchema = z.object({
|
||||
agentId: z.string(),
|
||||
event: AgentStreamEventPayloadSchema,
|
||||
timestamp: z.string(),
|
||||
// Present only for committed timeline events.
|
||||
// Present for timeline events. Maps 1:1 to canonical in-memory timeline rows.
|
||||
seq: z.number().int().nonnegative().optional(),
|
||||
// COMPAT(timeline): retain removed epoch for older clients.
|
||||
epoch: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
@@ -2196,72 +2196,63 @@ export const FetchAgentResponseMessageSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const TimelineProjectionKindSchema = z.enum(["assistant_merge", "tool_lifecycle"]);
|
||||
|
||||
const TimelineSeqRangeSchema = z.object({
|
||||
const AgentTimelineSeqRangeSchema = z.object({
|
||||
startSeq: z.number().int().nonnegative(),
|
||||
endSeq: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const AgentTimelineEntryPayloadSchema = z
|
||||
.object({
|
||||
provider: AgentProviderSchema,
|
||||
item: AgentTimelineItemPayloadSchema,
|
||||
timestamp: z.string(),
|
||||
seq: z.number().int().nonnegative().optional(),
|
||||
// COMPAT(timeline): retain legacy projected timeline fields.
|
||||
seqStart: z.number().int().nonnegative().optional(),
|
||||
seqEnd: z.number().int().nonnegative().optional(),
|
||||
sourceSeqRanges: z.array(TimelineSeqRangeSchema).optional(),
|
||||
collapsed: z.array(TimelineProjectionKindSchema).optional(),
|
||||
})
|
||||
.transform((entry, ctx) => {
|
||||
const seq = entry.seq ?? entry.seqEnd ?? entry.seqStart;
|
||||
if (typeof seq !== "number") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Agent timeline entry must include seq or legacy seqStart/seqEnd",
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
return {
|
||||
...entry,
|
||||
seq,
|
||||
};
|
||||
});
|
||||
export const AgentTimelineEntryPayloadSchema = z.object({
|
||||
provider: AgentProviderSchema,
|
||||
item: AgentTimelineItemPayloadSchema,
|
||||
timestamp: z.string(),
|
||||
seqStart: z.number().int().nonnegative(),
|
||||
seqEnd: z.number().int().nonnegative(),
|
||||
sourceSeqRanges: z.array(AgentTimelineSeqRangeSchema),
|
||||
collapsed: z.array(z.enum(["assistant_merge", "tool_lifecycle"])),
|
||||
});
|
||||
|
||||
export const FetchAgentTimelineResponseMessageSchema = z.object({
|
||||
type: z.literal("fetch_agent_timeline_response"),
|
||||
payload: z
|
||||
.object({
|
||||
requestId: z.string(),
|
||||
agentId: z.string(),
|
||||
agent: AgentSnapshotPayloadSchema.nullable(),
|
||||
direction: z.enum(["tail", "before", "after"]),
|
||||
startSeq: z.number().int().nonnegative().nullable().optional(),
|
||||
endSeq: z.number().int().nonnegative().nullable().optional(),
|
||||
hasOlder: z.boolean().optional(),
|
||||
hasNewer: z.boolean().optional(),
|
||||
entries: z.array(AgentTimelineEntryPayloadSchema),
|
||||
error: z.string().nullable(),
|
||||
// COMPAT(timeline): retain legacy response baggage for older clients.
|
||||
epoch: z.string().optional(),
|
||||
reset: z.boolean().optional(),
|
||||
staleCursor: z.boolean().optional(),
|
||||
gap: z.unknown().optional(),
|
||||
window: z.unknown().optional(),
|
||||
startCursor: AgentTimelineCursorSchema.nullable().optional(),
|
||||
endCursor: AgentTimelineCursorSchema.nullable().optional(),
|
||||
projection: z.enum(["canonical", "projected"]).optional(),
|
||||
})
|
||||
.transform((payload) => ({
|
||||
...payload,
|
||||
startSeq: payload.startSeq ?? payload.startCursor?.seq ?? null,
|
||||
endSeq: payload.endSeq ?? payload.endCursor?.seq ?? null,
|
||||
hasOlder: payload.hasOlder ?? false,
|
||||
hasNewer: payload.hasNewer ?? false,
|
||||
})),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
agentId: z.string(),
|
||||
agent: AgentSnapshotPayloadSchema.nullable(),
|
||||
direction: z.enum(["tail", "before", "after"]),
|
||||
projection: z.enum(["projected", "canonical"]),
|
||||
epoch: z.string(),
|
||||
reset: z.boolean(),
|
||||
staleCursor: z.boolean(),
|
||||
gap: z.boolean(),
|
||||
window: z.object({
|
||||
minSeq: z.number().int().nonnegative(),
|
||||
maxSeq: z.number().int().nonnegative(),
|
||||
nextSeq: z.number().int().nonnegative(),
|
||||
}),
|
||||
startCursor: AgentTimelineCursorSchema.nullable(),
|
||||
endCursor: AgentTimelineCursorSchema.nullable(),
|
||||
hasOlder: z.boolean(),
|
||||
hasNewer: z.boolean(),
|
||||
entries: z.array(AgentTimelineEntryPayloadSchema),
|
||||
error: z.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const CancelAgentResponseMessageSchema = z.object({
|
||||
type: z.literal("cancel_agent_response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
agentId: z.string(),
|
||||
agent: AgentSnapshotPayloadSchema.nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ClearAgentAttentionResponseMessageSchema = z.object({
|
||||
type: z.literal("clear_agent_attention_response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
agentId: z.string().or(z.array(z.string())),
|
||||
agents: z.array(AgentSnapshotPayloadSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
export const SendAgentMessageResponseMessageSchema = z.object({
|
||||
@@ -2960,6 +2951,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ArchiveWorkspaceResponseMessageSchema,
|
||||
FetchAgentResponseMessageSchema,
|
||||
FetchAgentTimelineResponseMessageSchema,
|
||||
CancelAgentResponseMessageSchema,
|
||||
ClearAgentAttentionResponseMessageSchema,
|
||||
SendAgentMessageResponseMessageSchema,
|
||||
SetVoiceModeResponseMessageSchema,
|
||||
GetDaemonConfigResponseMessageSchema,
|
||||
@@ -3085,6 +3078,7 @@ export type FetchAgentResponseMessage = z.infer<typeof FetchAgentResponseMessage
|
||||
export type FetchAgentTimelineResponseMessage = z.infer<
|
||||
typeof FetchAgentTimelineResponseMessageSchema
|
||||
>;
|
||||
export type CancelAgentResponseMessage = z.infer<typeof CancelAgentResponseMessageSchema>;
|
||||
export type SendAgentMessageResponseMessage = z.infer<typeof SendAgentMessageResponseMessageSchema>;
|
||||
export type SetVoiceModeResponseMessage = z.infer<typeof SetVoiceModeResponseMessageSchema>;
|
||||
export type SetAgentModeResponseMessage = z.infer<typeof SetAgentModeResponseMessageSchema>;
|
||||
@@ -3256,6 +3250,9 @@ export type FileDownloadTokenResponse = z.infer<typeof FileDownloadTokenResponse
|
||||
export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>;
|
||||
export type ShutdownServerRequestMessage = z.infer<typeof ShutdownServerRequestMessageSchema>;
|
||||
export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessageSchema>;
|
||||
export type ClearAgentAttentionResponseMessage = z.infer<
|
||||
typeof ClearAgentAttentionResponseMessageSchema
|
||||
>;
|
||||
export type ClientHeartbeatMessage = z.infer<typeof ClientHeartbeatMessageSchema>;
|
||||
export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>;
|
||||
export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>;
|
||||
|
||||
Reference in New Issue
Block a user