refactor: goose 2 ui used acp session id (#8985)
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockLoadSession = vi.fn();
|
||||
const mockNewSession = vi.fn();
|
||||
const mockSetProvider = vi.fn();
|
||||
const mockSetModel = vi.fn();
|
||||
|
||||
vi.mock("../acpApi", () => ({
|
||||
listProviders: vi.fn(),
|
||||
prompt: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
setModel: (...args: unknown[]) => mockSetModel(...args),
|
||||
setProvider: (...args: unknown[]) => mockSetProvider(...args),
|
||||
listSessions: vi.fn(),
|
||||
loadSession: (...args: unknown[]) => mockLoadSession(...args),
|
||||
newSession: (...args: unknown[]) => mockNewSession(...args),
|
||||
exportSession: vi.fn(),
|
||||
importSession: vi.fn(),
|
||||
forkSession: vi.fn(),
|
||||
@@ -29,29 +34,94 @@ describe("acpLoadSession", () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("restores the prior session mapping when replay loading fails", async () => {
|
||||
it("restores the prior prepared session registration when replay loading fails", async () => {
|
||||
mockLoadSession.mockRejectedValueOnce(new Error("load failed"));
|
||||
|
||||
const sessionTracker = await import("../acpSessionTracker");
|
||||
const sessionRegistry = await import("../acpSessionRegistry");
|
||||
const { acpLoadSession } = await import("../acp");
|
||||
|
||||
sessionTracker.registerSession(
|
||||
"local-session",
|
||||
"goose-session-1",
|
||||
sessionRegistry.registerPreparedSession(
|
||||
"acp-session-1",
|
||||
"goose",
|
||||
"/tmp/original",
|
||||
);
|
||||
|
||||
await expect(
|
||||
acpLoadSession("local-session", "goose-session-2", "/tmp/replay"),
|
||||
acpLoadSession("acp-session-1", "/tmp/replay"),
|
||||
).rejects.toThrow("load failed");
|
||||
|
||||
expect(sessionTracker.getGooseSessionId("local-session")).toBe(
|
||||
"goose-session-1",
|
||||
);
|
||||
expect(sessionTracker.getLocalSessionId("goose-session-1")).toBe(
|
||||
"local-session",
|
||||
);
|
||||
expect(sessionTracker.getLocalSessionId("goose-session-2")).toBeNull();
|
||||
expect(sessionRegistry.isSessionPrepared("acp-session-1")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("acpCreateSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("uses the ACP session id as the UI session id", async () => {
|
||||
mockNewSession.mockResolvedValue({ sessionId: "acp-session-1" });
|
||||
|
||||
const sessionRegistry = await import("../acpSessionRegistry");
|
||||
const { acpCreateSession } = await import("../acp");
|
||||
|
||||
await expect(
|
||||
acpCreateSession("openai", "/tmp/project", {
|
||||
projectId: "project-1",
|
||||
personaId: "persona-1",
|
||||
modelId: "gpt-4.1",
|
||||
}),
|
||||
).resolves.toEqual({ sessionId: "acp-session-1" });
|
||||
|
||||
expect(mockNewSession).toHaveBeenCalledWith(
|
||||
"/tmp/project",
|
||||
"openai",
|
||||
"project-1",
|
||||
"persona-1",
|
||||
);
|
||||
expect(mockLoadSession).not.toHaveBeenCalled();
|
||||
expect(mockSetProvider).toHaveBeenCalledWith("acp-session-1", "openai");
|
||||
expect(mockSetModel).toHaveBeenCalledWith("acp-session-1", "gpt-4.1");
|
||||
expect(sessionRegistry.isSessionPrepared("acp-session-1")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("acpPrepareSession", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("loads the existing ACP session instead of creating a replacement", async () => {
|
||||
mockLoadSession.mockResolvedValue(undefined);
|
||||
|
||||
const sessionRegistry = await import("../acpSessionRegistry");
|
||||
const { acpPrepareSession } = await import("../acp");
|
||||
|
||||
await expect(
|
||||
acpPrepareSession("acp-session-1", "openai", "/tmp/project"),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(mockLoadSession).toHaveBeenCalledWith(
|
||||
"acp-session-1",
|
||||
"/tmp/project",
|
||||
);
|
||||
expect(mockNewSession).not.toHaveBeenCalled();
|
||||
expect(mockSetProvider).toHaveBeenCalledWith("acp-session-1", "openai");
|
||||
expect(sessionRegistry.isSessionPrepared("acp-session-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("surfaces load failures instead of creating a new ACP session", async () => {
|
||||
mockLoadSession.mockRejectedValueOnce(new Error("missing session"));
|
||||
|
||||
const { acpPrepareSession } = await import("../acp");
|
||||
|
||||
await expect(
|
||||
acpPrepareSession("acp-session-1", "openai", "/tmp/project"),
|
||||
).rejects.toThrow("missing session");
|
||||
|
||||
expect(mockNewSession).not.toHaveBeenCalled();
|
||||
expect(mockSetProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,12 +11,11 @@ import {
|
||||
handleSessionNotification,
|
||||
setActiveMessageId,
|
||||
} from "../acpNotificationHandler";
|
||||
import { registerSession } from "../acpSessionTracker";
|
||||
import { registerPreparedSession } from "../acpSessionRegistry";
|
||||
|
||||
function createMcpAppPayload(): McpAppPayload {
|
||||
return {
|
||||
sessionId: "local-session",
|
||||
gooseSessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
toolCallId: "tool-1",
|
||||
toolCallTitle: "mcp_app_bench__inspect_host_info",
|
||||
source: "toolCallUpdateMeta",
|
||||
@@ -34,8 +33,7 @@ function createMcpAppPayload(): McpAppPayload {
|
||||
describe("acpNotificationHandler", () => {
|
||||
beforeEach(() => {
|
||||
clearMessageTracking();
|
||||
clearReplayBuffer("local-session");
|
||||
clearReplayBuffer("goose-session");
|
||||
clearReplayBuffer("acp-session");
|
||||
useChatStore.setState({
|
||||
messagesBySession: {},
|
||||
sessionStateById: {},
|
||||
@@ -49,16 +47,11 @@ describe("acpNotificationHandler", () => {
|
||||
});
|
||||
|
||||
it("keeps tool calls that arrive before the first text chunk on the pending assistant message", async () => {
|
||||
registerSession(
|
||||
"local-session",
|
||||
"goose-session",
|
||||
"goose",
|
||||
"/Users/aharvard",
|
||||
);
|
||||
setActiveMessageId("goose-session", "assistant-1");
|
||||
registerPreparedSession("acp-session", "goose", "/Users/aharvard");
|
||||
setActiveMessageId("acp-session", "assistant-1");
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
@@ -67,7 +60,7 @@ describe("acpNotificationHandler", () => {
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
@@ -94,7 +87,7 @@ describe("acpNotificationHandler", () => {
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
@@ -106,14 +99,13 @@ describe("acpNotificationHandler", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const message =
|
||||
useChatStore.getState().messagesBySession["local-session"]?.[0];
|
||||
useChatStore.getState().messagesBySession["acp-session"]?.[0];
|
||||
expect(message?.content.some((block) => block.type === "mcpApp")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
const [message] =
|
||||
useChatStore.getState().messagesBySession["local-session"];
|
||||
const [message] = useChatStore.getState().messagesBySession["acp-session"];
|
||||
expect(message.id).toBe("assistant-1");
|
||||
expect(message.content.map((block) => block.type)).toEqual([
|
||||
"toolRequest",
|
||||
@@ -146,17 +138,17 @@ describe("acpNotificationHandler", () => {
|
||||
text: "The Host Info inspector is now open.",
|
||||
});
|
||||
expect(
|
||||
useChatStore.getState().getSessionRuntime("local-session")
|
||||
useChatStore.getState().getSessionRuntime("acp-session")
|
||||
.streamingMessageId,
|
||||
).toBe("assistant-1");
|
||||
});
|
||||
|
||||
it("preserves ACP tool kind and locations on tool requests", async () => {
|
||||
registerSession("local-session", "goose-session", "goose", "/Users/test");
|
||||
setActiveMessageId("goose-session", "assistant-1");
|
||||
registerPreparedSession("acp-session", "goose", "/Users/test");
|
||||
setActiveMessageId("acp-session", "assistant-1");
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
@@ -168,7 +160,7 @@ describe("acpNotificationHandler", () => {
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
@@ -177,8 +169,7 @@ describe("acpNotificationHandler", () => {
|
||||
},
|
||||
} as never);
|
||||
|
||||
const [message] =
|
||||
useChatStore.getState().messagesBySession["local-session"];
|
||||
const [message] = useChatStore.getState().messagesBySession["acp-session"];
|
||||
expect(message.content[0]).toMatchObject({
|
||||
type: "toolRequest",
|
||||
id: "tool-1",
|
||||
@@ -190,16 +181,15 @@ describe("acpNotificationHandler", () => {
|
||||
});
|
||||
|
||||
it("preserves structured tool output when ACP provides rawOutput", async () => {
|
||||
registerSession(
|
||||
"local-session",
|
||||
"goose-session",
|
||||
registerPreparedSession(
|
||||
"acp-session",
|
||||
"goose",
|
||||
"/Users/aharvard/.goose/artifacts",
|
||||
);
|
||||
setActiveMessageId("goose-session", "assistant-1");
|
||||
setActiveMessageId("acp-session", "assistant-1");
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
@@ -208,7 +198,7 @@ describe("acpNotificationHandler", () => {
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
@@ -229,8 +219,7 @@ describe("acpNotificationHandler", () => {
|
||||
},
|
||||
} as never);
|
||||
|
||||
const [message] =
|
||||
useChatStore.getState().messagesBySession["local-session"];
|
||||
const [message] = useChatStore.getState().messagesBySession["acp-session"];
|
||||
expect(message.content[1]).toMatchObject({
|
||||
type: "toolResponse",
|
||||
id: "tool-1",
|
||||
@@ -244,7 +233,7 @@ describe("acpNotificationHandler", () => {
|
||||
});
|
||||
|
||||
it("replay keeps tool and MCP app content on an assistant message when tool events arrive before text", async () => {
|
||||
const replaySessionId = "replay-goose-session";
|
||||
const replaySessionId = "replay-acp-session";
|
||||
useChatStore.setState({
|
||||
loadingSessionIds: new Set<string>([replaySessionId]),
|
||||
});
|
||||
@@ -339,7 +328,6 @@ describe("acpNotificationHandler", () => {
|
||||
payload: {
|
||||
...createMcpAppPayload(),
|
||||
sessionId: replaySessionId,
|
||||
gooseSessionId: replaySessionId,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -442,8 +430,8 @@ describe("acpNotificationHandler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("replay preserves gooseSessionId in MCP app payloads before tracker registration", async () => {
|
||||
const replaySessionId = "replay-goose-session-2";
|
||||
it("replay attaches MCP app payloads to tool-only assistant messages", async () => {
|
||||
const replaySessionId = "replay-acp-session-2";
|
||||
const replayCreated = 1_700_000_240;
|
||||
useChatStore.setState({
|
||||
loadingSessionIds: new Set<string>([replaySessionId]),
|
||||
@@ -496,7 +484,7 @@ describe("acpNotificationHandler", () => {
|
||||
expect(mcpAppBlock).toMatchObject({
|
||||
type: "mcpApp",
|
||||
payload: expect.objectContaining({
|
||||
gooseSessionId: replaySessionId,
|
||||
sessionId: replaySessionId,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ describe("ACP session info updates", () => {
|
||||
it("applies generated session info updates to non-user-named sessions", async () => {
|
||||
useChatSessionStore.getState().addSession({
|
||||
id: "goose-session-title",
|
||||
acpSessionId: "goose-session-title",
|
||||
title: "New Chat",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
@@ -62,7 +61,6 @@ describe("ACP session info updates", () => {
|
||||
it("ignores generated titles for user-named sessions", async () => {
|
||||
useChatSessionStore.getState().addSession({
|
||||
id: "goose-session-user-title",
|
||||
acpSessionId: "goose-session-user-title",
|
||||
title: "My Custom Title",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
handleSessionNotification,
|
||||
setActiveMessageId,
|
||||
} from "../acpNotificationHandler";
|
||||
import { registerSession } from "../acpSessionTracker";
|
||||
import { registerPreparedSession } from "../acpSessionRegistry";
|
||||
|
||||
describe("ACP tool call status handling", () => {
|
||||
beforeEach(() => {
|
||||
clearMessageTracking();
|
||||
clearReplayBuffer("replay-failed-tool-session");
|
||||
clearReplayBuffer("goose-session");
|
||||
clearReplayBuffer("acp-session");
|
||||
useChatStore.setState({
|
||||
messagesBySession: {},
|
||||
sessionStateById: {},
|
||||
@@ -76,16 +76,15 @@ describe("ACP tool call status handling", () => {
|
||||
});
|
||||
|
||||
it("marks failed live tool updates as errors", async () => {
|
||||
registerSession(
|
||||
"local-session",
|
||||
"goose-session",
|
||||
registerPreparedSession(
|
||||
"acp-session",
|
||||
"goose",
|
||||
"/Users/aharvard/.goose/artifacts",
|
||||
);
|
||||
setActiveMessageId("goose-session", "assistant-1");
|
||||
setActiveMessageId("acp-session", "assistant-1");
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: "tool-1",
|
||||
@@ -94,7 +93,7 @@ describe("ACP tool call status handling", () => {
|
||||
} as never);
|
||||
|
||||
await handleSessionNotification({
|
||||
sessionId: "goose-session",
|
||||
sessionId: "acp-session",
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: "tool-1",
|
||||
@@ -111,8 +110,7 @@ describe("ACP tool call status handling", () => {
|
||||
},
|
||||
} as never);
|
||||
|
||||
const [message] =
|
||||
useChatStore.getState().messagesBySession["local-session"];
|
||||
const [message] = useChatStore.getState().messagesBySession["acp-session"];
|
||||
expect(message.content[0]).toMatchObject({
|
||||
type: "toolRequest",
|
||||
id: "tool-1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ContentBlock } from "@agentclientprotocol/sdk";
|
||||
import * as directAcp from "./acpApi";
|
||||
import type { AcpSessionInfo } from "./acpApi";
|
||||
import * as sessionTracker from "./acpSessionTracker";
|
||||
import * as sessionRegistry from "./acpSessionRegistry";
|
||||
import {
|
||||
getCatalogEntry,
|
||||
resolveAgentProviderCatalogId,
|
||||
@@ -27,12 +27,9 @@ export interface AcpSendMessageOptions {
|
||||
images?: [string, string][];
|
||||
}
|
||||
|
||||
export interface AcpPrepareSessionOptions {
|
||||
export interface AcpCreateSessionOptions {
|
||||
personaId?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export interface AcpCreateSessionOptions extends AcpPrepareSessionOptions {
|
||||
modelId?: string | null;
|
||||
}
|
||||
|
||||
@@ -85,8 +82,7 @@ export async function acpSendMessage(
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const tStart = performance.now();
|
||||
|
||||
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId);
|
||||
if (!gooseSessionId) {
|
||||
if (!sessionRegistry.isSessionPrepared(sessionId)) {
|
||||
throw new Error("Session not prepared. Call acpPrepareSession first.");
|
||||
}
|
||||
|
||||
@@ -113,7 +109,7 @@ export async function acpSendMessage(
|
||||
}
|
||||
|
||||
const messageId = crypto.randomUUID();
|
||||
setActiveMessageId(gooseSessionId, messageId);
|
||||
setActiveMessageId(sessionId, messageId);
|
||||
|
||||
perfLog(
|
||||
`[perf:send] ${sid} acpSendMessage → prompt(len=${prompt.length}, imgs=${images?.length ?? 0})`,
|
||||
@@ -123,7 +119,7 @@ export async function acpSendMessage(
|
||||
if (personaId) meta.personaId = personaId;
|
||||
try {
|
||||
await directAcp.prompt(
|
||||
gooseSessionId,
|
||||
sessionId,
|
||||
content,
|
||||
Object.keys(meta).length > 0 ? meta : undefined,
|
||||
);
|
||||
@@ -132,7 +128,7 @@ export async function acpSendMessage(
|
||||
`[perf:send] ${sid} prompt() resolved in ${(tDone - tPrompt).toFixed(1)}ms (total acpSendMessage ${(tDone - tStart).toFixed(1)}ms)`,
|
||||
);
|
||||
} finally {
|
||||
clearActiveMessageId(gooseSessionId);
|
||||
clearActiveMessageId(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,24 +137,16 @@ export async function acpPrepareSession(
|
||||
sessionId: string,
|
||||
providerId: string,
|
||||
workingDir: string,
|
||||
options: AcpPrepareSessionOptions = {},
|
||||
): Promise<string> {
|
||||
): Promise<void> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const t0 = performance.now();
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} acpPrepareSession start (provider=${providerId})`,
|
||||
);
|
||||
const gooseSessionId = await sessionTracker.prepareSession(
|
||||
sessionId,
|
||||
providerId,
|
||||
workingDir,
|
||||
options.personaId,
|
||||
options.projectId,
|
||||
);
|
||||
await sessionRegistry.prepareSession(sessionId, providerId, workingDir);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} acpPrepareSession done in ${(performance.now() - t0).toFixed(1)}ms`,
|
||||
);
|
||||
return gooseSessionId;
|
||||
}
|
||||
|
||||
export async function acpCreateSession(
|
||||
@@ -166,31 +154,26 @@ export async function acpCreateSession(
|
||||
workingDir: string,
|
||||
options: AcpCreateSessionOptions = {},
|
||||
): Promise<{ sessionId: string }> {
|
||||
const localSessionId = crypto.randomUUID();
|
||||
const gooseSessionId = await acpPrepareSession(
|
||||
localSessionId,
|
||||
providerId,
|
||||
const response = await directAcp.newSession(
|
||||
workingDir,
|
||||
options,
|
||||
);
|
||||
sessionTracker.registerSession(
|
||||
gooseSessionId,
|
||||
gooseSessionId,
|
||||
providerId,
|
||||
workingDir,
|
||||
options.projectId,
|
||||
options.personaId,
|
||||
);
|
||||
const sessionId = response.sessionId;
|
||||
await directAcp.setProvider(sessionId, providerId);
|
||||
sessionRegistry.registerPreparedSession(sessionId, providerId, workingDir);
|
||||
if (options.modelId) {
|
||||
await directAcp.setModel(gooseSessionId, options.modelId);
|
||||
await directAcp.setModel(sessionId, options.modelId);
|
||||
}
|
||||
return { sessionId: gooseSessionId };
|
||||
return { sessionId };
|
||||
}
|
||||
|
||||
export async function acpSetModel(
|
||||
sessionId: string,
|
||||
modelId: string,
|
||||
): Promise<void> {
|
||||
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId);
|
||||
return directAcp.setModel(gooseSessionId ?? sessionId, modelId);
|
||||
return directAcp.setModel(sessionId, modelId);
|
||||
}
|
||||
|
||||
export type { AcpSessionInfo };
|
||||
@@ -223,21 +206,19 @@ export async function acpSearchSessions(
|
||||
*/
|
||||
export async function acpLoadSession(
|
||||
sessionId: string,
|
||||
gooseSessionId: string,
|
||||
workingDir?: string,
|
||||
): Promise<void> {
|
||||
const effectiveWorkingDir = workingDir ?? "~";
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const t0 = performance.now();
|
||||
const rollbackSessionRegistration = sessionTracker.registerSession(
|
||||
const rollbackSessionRegistration = sessionRegistry.registerPreparedSession(
|
||||
sessionId,
|
||||
gooseSessionId,
|
||||
"goose",
|
||||
effectiveWorkingDir,
|
||||
);
|
||||
try {
|
||||
perfLog(`[perf:load] ${sid} acpLoadSession → client.loadSession`);
|
||||
await directAcp.loadSession(gooseSessionId, effectiveWorkingDir);
|
||||
await directAcp.loadSession(sessionId, effectiveWorkingDir);
|
||||
perfLog(
|
||||
`[perf:load] ${sid} client.loadSession resolved in ${(performance.now() - t0).toFixed(1)}ms`,
|
||||
);
|
||||
@@ -261,17 +242,11 @@ export async function acpImportSession(json: string): Promise<AcpSessionInfo> {
|
||||
export async function acpDuplicateSession(
|
||||
sessionId: string,
|
||||
): Promise<AcpSessionInfo> {
|
||||
const gooseSessionId =
|
||||
sessionTracker.getGooseSessionId(sessionId) ?? sessionId;
|
||||
return directAcp.forkSession(gooseSessionId);
|
||||
return directAcp.forkSession(sessionId);
|
||||
}
|
||||
|
||||
/** Cancel an in-progress ACP session so the backend stops streaming. */
|
||||
export async function acpCancelSession(
|
||||
sessionId: string,
|
||||
personaId?: string,
|
||||
): Promise<boolean> {
|
||||
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId);
|
||||
await directAcp.cancelSession(gooseSessionId ?? sessionId);
|
||||
export async function acpCancelSession(sessionId: string): Promise<boolean> {
|
||||
await directAcp.cancelSession(sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { SessionNotification } from "@agentclientprotocol/sdk";
|
||||
import { useChatStore } from "@/features/chat/stores/chatStore";
|
||||
import {
|
||||
clearReplayBuffer,
|
||||
getAndDeleteReplayBuffer,
|
||||
} from "@/features/chat/hooks/replayBuffer";
|
||||
import { registerSession } from "./acpSessionTracker";
|
||||
import { clearReplayBuffer } from "@/features/chat/hooks/replayBuffer";
|
||||
import {
|
||||
clearMessageTracking,
|
||||
handleSessionNotification,
|
||||
@@ -14,8 +10,8 @@ import {
|
||||
describe("acpNotificationHandler", () => {
|
||||
beforeEach(() => {
|
||||
clearMessageTracking();
|
||||
clearReplayBuffer("draft-session-1");
|
||||
clearReplayBuffer("draft-session-2");
|
||||
clearReplayBuffer("acp-session-1");
|
||||
clearReplayBuffer("acp-session-2");
|
||||
useChatStore.setState({
|
||||
messagesBySession: {},
|
||||
sessionStateById: {},
|
||||
@@ -28,9 +24,9 @@ describe("acpNotificationHandler", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("buffers usage updates until the local session mapping is registered", async () => {
|
||||
it("applies usage updates to the ACP session id", async () => {
|
||||
const notification = {
|
||||
sessionId: "goose-session-1",
|
||||
sessionId: "acp-session-1",
|
||||
update: {
|
||||
sessionUpdate: "usage_update",
|
||||
used: 512,
|
||||
@@ -40,42 +36,9 @@ describe("acpNotificationHandler", () => {
|
||||
|
||||
await handleSessionNotification(notification);
|
||||
|
||||
expect(
|
||||
useChatStore.getState().sessionStateById["draft-session-1"],
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
useChatStore.getState().sessionStateById["goose-session-1"],
|
||||
).toBeUndefined();
|
||||
|
||||
registerSession("draft-session-1", "goose-session-1", "goose", "/tmp");
|
||||
|
||||
const runtime = useChatStore
|
||||
.getState()
|
||||
.getSessionRuntime("draft-session-1");
|
||||
const runtime = useChatStore.getState().getSessionRuntime("acp-session-1");
|
||||
expect(runtime.tokenState.accumulatedTotal).toBe(512);
|
||||
expect(runtime.tokenState.contextLimit).toBe(8192);
|
||||
expect(runtime.hasUsageSnapshot).toBe(true);
|
||||
});
|
||||
|
||||
it("does not buffer non-usage updates before the local session mapping exists", async () => {
|
||||
const notification = {
|
||||
sessionId: "goose-session-2",
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
messageId: "message-1",
|
||||
content: {
|
||||
type: "text",
|
||||
text: "hello from replay",
|
||||
},
|
||||
},
|
||||
} as SessionNotification;
|
||||
|
||||
await handleSessionNotification(notification);
|
||||
registerSession("draft-session-2", "goose-session-2", "goose", "/tmp");
|
||||
|
||||
expect(getAndDeleteReplayBuffer("draft-session-2")).toBeUndefined();
|
||||
expect(
|
||||
useChatStore.getState().messagesBySession["draft-session-2"],
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,14 +31,10 @@ import {
|
||||
} from "./acpReplayAssistant";
|
||||
import { getReplayCreated, getReplayMessageId } from "./acpReplayMetadata";
|
||||
import { handleSessionInfoUpdate } from "./acpSessionInfoUpdate";
|
||||
import {
|
||||
getLocalSessionId,
|
||||
subscribeToSessionRegistration,
|
||||
} from "./acpSessionTracker";
|
||||
import { getToolCallIdentity } from "./acpToolCallIdentity";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
// Pre-set message ID for the next live stream per goose session
|
||||
// Pre-set message ID for the next live stream per session.
|
||||
const presetMessageIds = new Map<string, string>();
|
||||
|
||||
// Per-session perf counters for replay/live streaming.
|
||||
@@ -54,10 +50,6 @@ interface LivePerf {
|
||||
chunkCount: number;
|
||||
}
|
||||
const livePerf = new Map<string, LivePerf>();
|
||||
const pendingUsageUpdates = new Map<
|
||||
string,
|
||||
{ accumulatedTotal: number; contextLimit: number }
|
||||
>();
|
||||
|
||||
const toolCallStatusFromUpdate = (status: string): ToolCallStatus =>
|
||||
status === "failed" ? "error" : "completed";
|
||||
@@ -108,33 +100,20 @@ function toolCallUpdatePatch(
|
||||
};
|
||||
}
|
||||
|
||||
subscribeToSessionRegistration((localSessionId, gooseSessionId) => {
|
||||
const pendingUsage = pendingUsageUpdates.get(gooseSessionId);
|
||||
if (!pendingUsage) {
|
||||
return;
|
||||
}
|
||||
|
||||
useChatStore.getState().updateTokenState(localSessionId, pendingUsage);
|
||||
pendingUsageUpdates.delete(gooseSessionId);
|
||||
});
|
||||
|
||||
export function setActiveMessageId(
|
||||
gooseSessionId: string,
|
||||
messageId: string,
|
||||
): void {
|
||||
presetMessageIds.set(gooseSessionId, messageId);
|
||||
livePerf.set(gooseSessionId, {
|
||||
export function setActiveMessageId(sessionId: string, messageId: string): void {
|
||||
presetMessageIds.set(sessionId, messageId);
|
||||
livePerf.set(sessionId, {
|
||||
sendStartedAt: performance.now(),
|
||||
firstChunkAt: null,
|
||||
chunkCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearActiveMessageId(gooseSessionId: string): void {
|
||||
presetMessageIds.delete(gooseSessionId);
|
||||
const perf = livePerf.get(gooseSessionId);
|
||||
export function clearActiveMessageId(sessionId: string): void {
|
||||
presetMessageIds.delete(sessionId);
|
||||
const perf = livePerf.get(sessionId);
|
||||
if (perf) {
|
||||
const sid = gooseSessionId.slice(0, 8);
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const total = performance.now() - perf.sendStartedAt;
|
||||
const ttft =
|
||||
perf.firstChunkAt !== null
|
||||
@@ -143,16 +122,14 @@ export function clearActiveMessageId(gooseSessionId: string): void {
|
||||
perfLog(
|
||||
`[perf:stream] ${sid} stream ended — ttft=${ttft}ms total=${total.toFixed(1)}ms chunks=${perf.chunkCount}`,
|
||||
);
|
||||
livePerf.delete(gooseSessionId);
|
||||
livePerf.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleSessionNotification(
|
||||
notification: SessionNotification,
|
||||
): Promise<void> {
|
||||
const gooseSessionId = notification.sessionId;
|
||||
const localSessionId = getLocalSessionId(gooseSessionId);
|
||||
const sessionId = localSessionId ?? gooseSessionId;
|
||||
const sessionId = notification.sessionId;
|
||||
const { update } = notification;
|
||||
const isReplay = useChatStore.getState().loadingSessionIds.has(sessionId);
|
||||
|
||||
@@ -167,20 +144,20 @@ export async function handleSessionNotification(
|
||||
}
|
||||
perf.lastAt = now;
|
||||
perf.count += 1;
|
||||
handleReplay(sessionId, gooseSessionId, localSessionId, update);
|
||||
handleReplay(sessionId, update);
|
||||
} else {
|
||||
const perf = livePerf.get(gooseSessionId);
|
||||
const perf = livePerf.get(sessionId);
|
||||
if (perf && update.sessionUpdate === "agent_message_chunk") {
|
||||
perf.chunkCount += 1;
|
||||
if (perf.firstChunkAt === null) {
|
||||
perf.firstChunkAt = performance.now();
|
||||
const sid = gooseSessionId.slice(0, 8);
|
||||
const sid = sessionId.slice(0, 8);
|
||||
perfLog(
|
||||
`[perf:stream] ${sid} first agent_message_chunk at ttft=${(perf.firstChunkAt - perf.sendStartedAt).toFixed(1)}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
handleLive(sessionId, gooseSessionId, localSessionId, update);
|
||||
handleLive(sessionId, update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,12 +179,7 @@ function getChunkMessageId(update: SessionUpdate): string | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
function handleReplay(
|
||||
sessionId: string,
|
||||
gooseSessionId: string,
|
||||
localSessionId: string | null,
|
||||
update: SessionUpdate,
|
||||
): void {
|
||||
function handleReplay(sessionId: string, update: SessionUpdate): void {
|
||||
switch (update.sessionUpdate) {
|
||||
case "agent_message_chunk": {
|
||||
const msg = ensureReplayAssistantMessage(
|
||||
@@ -331,7 +303,6 @@ function handleReplay(
|
||||
update,
|
||||
true,
|
||||
{
|
||||
gooseSessionId,
|
||||
replayMessageId,
|
||||
},
|
||||
);
|
||||
@@ -344,7 +315,7 @@ function handleReplay(
|
||||
case "session_info_update":
|
||||
case "config_option_update":
|
||||
case "usage_update":
|
||||
handleShared(sessionId, gooseSessionId, localSessionId, update);
|
||||
handleShared(sessionId, update);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -352,19 +323,13 @@ function handleReplay(
|
||||
}
|
||||
}
|
||||
|
||||
function handleLive(
|
||||
sessionId: string,
|
||||
gooseSessionId: string,
|
||||
localSessionId: string | null,
|
||||
update: SessionUpdate,
|
||||
): void {
|
||||
function handleLive(sessionId: string, update: SessionUpdate): void {
|
||||
const store = useChatStore.getState();
|
||||
|
||||
switch (update.sessionUpdate) {
|
||||
case "agent_message_chunk": {
|
||||
const messageId = ensureLiveAssistantMessage(
|
||||
sessionId,
|
||||
gooseSessionId,
|
||||
getChunkMessageId(update) ?? undefined,
|
||||
);
|
||||
|
||||
@@ -376,7 +341,7 @@ function handleLive(
|
||||
}
|
||||
|
||||
case "tool_call": {
|
||||
const messageId = ensureLiveAssistantMessage(sessionId, gooseSessionId);
|
||||
const messageId = ensureLiveAssistantMessage(sessionId);
|
||||
const identity = getToolCallIdentity(update);
|
||||
|
||||
const toolRequest: ToolRequestContent = {
|
||||
@@ -395,7 +360,7 @@ function handleLive(
|
||||
}
|
||||
|
||||
case "tool_call_update": {
|
||||
const messageId = ensureLiveAssistantMessage(sessionId, gooseSessionId);
|
||||
const messageId = ensureLiveAssistantMessage(sessionId);
|
||||
const identity = getToolCallIdentity(update);
|
||||
|
||||
const patch = toolCallUpdatePatch(update);
|
||||
@@ -460,9 +425,6 @@ function handleLive(
|
||||
toolRequest?.name ?? update.title ?? "",
|
||||
update,
|
||||
false,
|
||||
{
|
||||
gooseSessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -472,7 +434,7 @@ function handleLive(
|
||||
case "session_info_update":
|
||||
case "config_option_update":
|
||||
case "usage_update":
|
||||
handleShared(sessionId, gooseSessionId, localSessionId, update);
|
||||
handleShared(sessionId, update);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -480,12 +442,7 @@ function handleLive(
|
||||
}
|
||||
}
|
||||
|
||||
function handleShared(
|
||||
sessionId: string,
|
||||
gooseSessionId: string,
|
||||
localSessionId: string | null,
|
||||
update: SessionUpdate,
|
||||
): void {
|
||||
function handleShared(sessionId: string, update: SessionUpdate): void {
|
||||
switch (update.sessionUpdate) {
|
||||
case "session_info_update": {
|
||||
handleSessionInfoUpdate(sessionId, update);
|
||||
@@ -535,15 +492,7 @@ function handleShared(
|
||||
case "usage_update": {
|
||||
const usage = update as SessionUpdate & { sessionUpdate: "usage_update" };
|
||||
|
||||
if (!localSessionId) {
|
||||
pendingUsageUpdates.set(gooseSessionId, {
|
||||
accumulatedTotal: usage.used,
|
||||
contextLimit: usage.size,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
useChatStore.getState().updateTokenState(localSessionId, {
|
||||
useChatStore.getState().updateTokenState(sessionId, {
|
||||
accumulatedTotal: usage.used,
|
||||
contextLimit: usage.size,
|
||||
});
|
||||
@@ -562,7 +511,6 @@ function findStreamingMessageId(sessionId: string): string | null {
|
||||
|
||||
function ensureLiveAssistantMessage(
|
||||
sessionId: string,
|
||||
gooseSessionId: string,
|
||||
preferredMessageId?: string | null,
|
||||
): string {
|
||||
const store = useChatStore.getState();
|
||||
@@ -578,7 +526,7 @@ function ensureLiveAssistantMessage(
|
||||
|
||||
const messageId =
|
||||
preferredMessageId ??
|
||||
presetMessageIds.get(gooseSessionId) ??
|
||||
presetMessageIds.get(sessionId) ??
|
||||
existingStreamingMessageId ??
|
||||
crypto.randomUUID();
|
||||
|
||||
@@ -598,14 +546,13 @@ function ensureLiveAssistantMessage(
|
||||
|
||||
store.setPendingAssistantProvider(sessionId, null);
|
||||
store.setStreamingMessageId(sessionId, messageId);
|
||||
clearActiveMessageId(gooseSessionId);
|
||||
clearActiveMessageId(sessionId);
|
||||
|
||||
return messageId;
|
||||
}
|
||||
|
||||
export function clearMessageTracking(): void {
|
||||
presetMessageIds.clear();
|
||||
pendingUsageUpdates.clear();
|
||||
clearReplayAssistantTracking();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import * as acpApi from "./acpApi";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
interface PreparedSession {
|
||||
providerId: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
const prepared = new Map<string, PreparedSession>();
|
||||
|
||||
export async function prepareSession(
|
||||
sessionId: string,
|
||||
providerId: string,
|
||||
workingDir: string,
|
||||
): Promise<void> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
|
||||
const existing = prepared.get(sessionId);
|
||||
if (existing) {
|
||||
const tReuse = performance.now();
|
||||
let changed = false;
|
||||
if (existing.workingDir !== workingDir) {
|
||||
await acpApi.updateWorkingDir(sessionId, workingDir);
|
||||
existing.workingDir = workingDir;
|
||||
changed = true;
|
||||
}
|
||||
if (existing.providerId !== providerId) {
|
||||
const tProv = performance.now();
|
||||
await acpApi.setProvider(sessionId, providerId);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} reuse setProvider(${providerId}) in ${(performance.now() - tProv).toFixed(1)}ms`,
|
||||
);
|
||||
existing.providerId = providerId;
|
||||
changed = true;
|
||||
}
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} reuse existing session (updates=${changed}) in ${(performance.now() - tReuse).toFixed(1)}ms`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tLoad = performance.now();
|
||||
await acpApi.loadSession(sessionId, workingDir);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} registry loadSession ok in ${(performance.now() - tLoad).toFixed(1)}ms`,
|
||||
);
|
||||
|
||||
const tProv = performance.now();
|
||||
await acpApi.setProvider(sessionId, providerId);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} registry setProvider(${providerId}) in ${(performance.now() - tProv).toFixed(1)}ms`,
|
||||
);
|
||||
|
||||
const entry = { providerId, workingDir };
|
||||
prepared.set(sessionId, entry);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export function isSessionPrepared(sessionId: string): boolean {
|
||||
return prepared.has(sessionId);
|
||||
}
|
||||
|
||||
export function registerPreparedSession(
|
||||
sessionId: string,
|
||||
providerId: string,
|
||||
workingDir: string,
|
||||
): () => void {
|
||||
const previousEntry = prepared.get(sessionId);
|
||||
const entry = { providerId, workingDir };
|
||||
|
||||
prepared.set(sessionId, entry);
|
||||
|
||||
return () => {
|
||||
prepared.delete(sessionId);
|
||||
if (previousEntry) {
|
||||
prepared.set(sessionId, previousEntry);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import * as acpApi from "./acpApi";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
interface PreparedSession {
|
||||
gooseSessionId: string;
|
||||
providerId: string;
|
||||
workingDir: string;
|
||||
}
|
||||
|
||||
type SessionRegistrationListener = (
|
||||
localSessionId: string,
|
||||
gooseSessionId: string,
|
||||
) => void;
|
||||
|
||||
const prepared = new Map<string, PreparedSession>();
|
||||
const gooseToLocal = new Map<string, string>();
|
||||
const registrationListeners = new Set<SessionRegistrationListener>();
|
||||
|
||||
function restoreGooseRegistration(
|
||||
gooseSessionId: string,
|
||||
localSessionId: string | undefined,
|
||||
): void {
|
||||
if (localSessionId === undefined) {
|
||||
gooseToLocal.delete(gooseSessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
gooseToLocal.set(gooseSessionId, localSessionId);
|
||||
}
|
||||
|
||||
function makeKey(sessionId: string, personaId?: string): string {
|
||||
if (personaId && personaId.length > 0) {
|
||||
return `${sessionId}__${personaId}`;
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
function notifySessionRegistered(
|
||||
localSessionId: string,
|
||||
gooseSessionId: string,
|
||||
): void {
|
||||
for (const listener of registrationListeners) {
|
||||
listener(localSessionId, gooseSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeToSessionRegistration(
|
||||
listener: SessionRegistrationListener,
|
||||
): () => void {
|
||||
registrationListeners.add(listener);
|
||||
return () => registrationListeners.delete(listener);
|
||||
}
|
||||
|
||||
export async function prepareSession(
|
||||
sessionId: string,
|
||||
providerId: string,
|
||||
workingDir: string,
|
||||
personaId?: string,
|
||||
projectId?: string,
|
||||
): Promise<string> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const key = makeKey(sessionId, personaId);
|
||||
|
||||
const existing = prepared.get(key) ?? prepared.get(sessionId);
|
||||
if (existing) {
|
||||
const tReuse = performance.now();
|
||||
let changed = false;
|
||||
if (existing.workingDir !== workingDir) {
|
||||
await acpApi.updateWorkingDir(existing.gooseSessionId, workingDir);
|
||||
existing.workingDir = workingDir;
|
||||
changed = true;
|
||||
}
|
||||
if (existing.providerId !== providerId) {
|
||||
const tProv = performance.now();
|
||||
await acpApi.setProvider(existing.gooseSessionId, providerId);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} reuse setProvider(${providerId}) in ${(performance.now() - tProv).toFixed(1)}ms (goose_sid=${existing.gooseSessionId.slice(0, 8)})`,
|
||||
);
|
||||
existing.providerId = providerId;
|
||||
changed = true;
|
||||
}
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} reuse existing session (updates=${changed}) in ${(performance.now() - tReuse).toFixed(1)}ms`,
|
||||
);
|
||||
return existing.gooseSessionId;
|
||||
}
|
||||
|
||||
let gooseSessionId: string | null = null;
|
||||
|
||||
const tLoad = performance.now();
|
||||
try {
|
||||
await acpApi.loadSession(sessionId, workingDir);
|
||||
gooseSessionId = sessionId;
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker loadSession ok in ${(performance.now() - tLoad).toFixed(1)}ms`,
|
||||
);
|
||||
} catch {
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker loadSession failed in ${(performance.now() - tLoad).toFixed(1)}ms → newSession`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!gooseSessionId) {
|
||||
const tNew = performance.now();
|
||||
const response = await acpApi.newSession(
|
||||
workingDir,
|
||||
providerId,
|
||||
projectId,
|
||||
personaId,
|
||||
);
|
||||
gooseSessionId = response.sessionId;
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker newSession done in ${(performance.now() - tNew).toFixed(1)}ms (goose_sid=${gooseSessionId.slice(0, 8)})`,
|
||||
);
|
||||
}
|
||||
|
||||
const gooseSid = gooseSessionId.slice(0, 8);
|
||||
const tProv = performance.now();
|
||||
await acpApi.setProvider(gooseSessionId, providerId);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker setProvider(${providerId}) in ${(performance.now() - tProv).toFixed(1)}ms (goose_sid=${gooseSid})`,
|
||||
);
|
||||
|
||||
const entry = { gooseSessionId, providerId, workingDir };
|
||||
prepared.set(key, entry);
|
||||
prepared.set(sessionId, entry);
|
||||
prepared.set(gooseSessionId, entry);
|
||||
gooseToLocal.set(gooseSessionId, sessionId);
|
||||
notifySessionRegistered(sessionId, gooseSessionId);
|
||||
|
||||
return gooseSessionId;
|
||||
}
|
||||
|
||||
export function getGooseSessionId(
|
||||
sessionId: string,
|
||||
personaId?: string,
|
||||
): string | null {
|
||||
const key = makeKey(sessionId, personaId);
|
||||
return (
|
||||
prepared.get(key)?.gooseSessionId ??
|
||||
prepared.get(sessionId)?.gooseSessionId ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalSessionId(gooseSessionId: string): string | null {
|
||||
return gooseToLocal.get(gooseSessionId) ?? null;
|
||||
}
|
||||
|
||||
export function registerSession(
|
||||
sessionId: string,
|
||||
gooseSessionId: string,
|
||||
providerId: string,
|
||||
workingDir: string,
|
||||
): () => void {
|
||||
const previousEntry = prepared.get(sessionId);
|
||||
const previousGooseSessionLocal = gooseToLocal.get(gooseSessionId);
|
||||
const previousSessionGooseLocal = previousEntry
|
||||
? gooseToLocal.get(previousEntry.gooseSessionId)
|
||||
: undefined;
|
||||
const entry = { gooseSessionId, providerId, workingDir };
|
||||
|
||||
if (
|
||||
previousEntry &&
|
||||
previousEntry.gooseSessionId !== gooseSessionId &&
|
||||
gooseToLocal.get(previousEntry.gooseSessionId) === sessionId
|
||||
) {
|
||||
gooseToLocal.delete(previousEntry.gooseSessionId);
|
||||
}
|
||||
|
||||
prepared.set(sessionId, entry);
|
||||
prepared.set(gooseSessionId, entry);
|
||||
gooseToLocal.set(gooseSessionId, sessionId);
|
||||
notifySessionRegistered(sessionId, gooseSessionId);
|
||||
|
||||
return () => {
|
||||
prepared.delete(sessionId);
|
||||
if (previousEntry) {
|
||||
prepared.set(sessionId, previousEntry);
|
||||
}
|
||||
|
||||
restoreGooseRegistration(gooseSessionId, previousGooseSessionLocal);
|
||||
if (previousEntry && previousEntry.gooseSessionId !== gooseSessionId) {
|
||||
restoreGooseRegistration(
|
||||
previousEntry.gooseSessionId,
|
||||
previousSessionGooseLocal,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function unregisterSession(
|
||||
sessionId: string,
|
||||
gooseSessionId?: string,
|
||||
): void {
|
||||
const entry = prepared.get(sessionId);
|
||||
prepared.delete(sessionId);
|
||||
|
||||
const resolvedGooseSessionId = gooseSessionId ?? entry?.gooseSessionId;
|
||||
if (
|
||||
resolvedGooseSessionId &&
|
||||
gooseToLocal.get(resolvedGooseSessionId) === sessionId
|
||||
) {
|
||||
gooseToLocal.delete(resolvedGooseSessionId);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,6 @@ export function attachMcpAppPayload(
|
||||
update: SessionUpdate,
|
||||
isReplay: boolean,
|
||||
options?: {
|
||||
gooseSessionId?: string | null;
|
||||
replayMessageId?: string | null;
|
||||
},
|
||||
): void {
|
||||
@@ -75,7 +74,6 @@ export function attachMcpAppPayload(
|
||||
toolCallId,
|
||||
toolCallTitle,
|
||||
update,
|
||||
options?.gooseSessionId,
|
||||
);
|
||||
if (!payload) {
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
} from "@aaif/goose-sdk";
|
||||
import type { SessionUpdate } from "@agentclientprotocol/sdk";
|
||||
import type { McpAppPayload } from "@/shared/types/messages";
|
||||
import { getGooseSessionId } from "./acpSessionTracker";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
@@ -29,7 +28,6 @@ export function buildMcpAppPayloadFromToolUpdate(
|
||||
toolCallId: string,
|
||||
toolCallTitle: string,
|
||||
update: SessionUpdate,
|
||||
gooseSessionIdOverride?: string | null,
|
||||
): McpAppPayload | null {
|
||||
const payload = extractMcpAppPayload(update);
|
||||
if (!payload) {
|
||||
@@ -38,8 +36,6 @@ export function buildMcpAppPayloadFromToolUpdate(
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
gooseSessionId:
|
||||
gooseSessionIdOverride ?? getGooseSessionId(sessionId) ?? null,
|
||||
toolCallId,
|
||||
toolCallTitle,
|
||||
source: "toolCallUpdateMeta",
|
||||
|
||||
@@ -118,7 +118,6 @@ export interface ToolResponseContent {
|
||||
|
||||
export interface McpAppPayload {
|
||||
sessionId: string;
|
||||
gooseSessionId: string | null;
|
||||
toolCallId: string;
|
||||
toolCallTitle: string;
|
||||
source: "toolCallUpdateMeta";
|
||||
|
||||
Reference in New Issue
Block a user