refactor(server): extract import session boundary (#854)

This commit is contained in:
Mohamed Boudra
2026-05-10 12:08:14 +08:00
committed by GitHub
parent 73d27bd5fe
commit ab4525db5b
6 changed files with 415 additions and 406 deletions
@@ -0,0 +1,37 @@
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent-title-limits.js";
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
function deriveInitialAgentTitle(prompt: string): string | null {
const firstContentLine = prompt
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0);
if (!firstContentLine) {
return null;
}
const normalized = firstContentLine.replace(/\s+/g, " ").trim();
if (!normalized) {
return null;
}
const clamped = normalized.slice(0, MAX_INITIAL_AGENT_TITLE_CHARS).trim();
return clamped.length > 0 ? clamped : null;
}
export function resolveCreateAgentTitles(options: {
configTitle?: string | null;
initialPrompt?: string | null;
}): { explicitTitle: string | null; provisionalTitle: string | null } {
const explicitTitle =
typeof options.configTitle === "string" && options.configTitle.trim().length > 0
? options.configTitle.trim()
: null;
const trimmedPrompt = options.initialPrompt?.trim();
const provisionalTitle =
explicitTitle ?? (trimmedPrompt ? deriveInitialAgentTitle(trimmedPrompt) : null);
return {
explicitTitle,
provisionalTitle,
};
}
@@ -1,14 +1,28 @@
import { expect, test, vi } from "vitest";
import { beforeEach, expect, test, vi } from "vitest";
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { FetchRecentProviderSessionsRequestMessage } from "../../shared/messages.js";
import type { PersistedAgentDescriptor } from "./agent-sdk-types.js";
import type { AgentTimelineItem, PersistedAgentDescriptor } from "./agent-sdk-types.js";
import {
ImportSessionsRequestError,
importProviderSession,
listImportableProviderSessions,
normalizeImportAgentRequest,
} from "./import-sessions.js";
const TEST_CAPABILITIES = {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: true,
};
beforeEach(() => {
vi.clearAllMocks();
});
function makeDescriptor(args: {
provider?: string;
sessionId: string;
@@ -40,6 +54,49 @@ function makeDescriptor(args: {
};
}
function makeManagedAgent(args: {
id?: string;
provider?: string;
cwd: string;
sessionId: string;
nativeHandle?: string;
title?: string | null;
}): ManagedAgent {
const provider = args.provider ?? "codex";
return {
id: args.id ?? "00000000-0000-4000-8000-000000000632",
provider,
cwd: args.cwd,
capabilities: TEST_CAPABILITIES,
config: { provider, cwd: args.cwd, title: args.title },
createdAt: new Date("2026-04-30T00:00:00.000Z"),
updatedAt: new Date("2026-04-30T00:00:00.000Z"),
availableModes: [],
currentModeId: null,
pendingPermissions: new Map(),
bufferedPermissionResolutions: new Map(),
inFlightPermissionResponses: new Set(),
pendingReplacement: false,
persistence: {
provider,
sessionId: args.sessionId,
...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
metadata: { provider, cwd: args.cwd },
},
historyPrimed: true,
lastUserMessageAt: null,
attention: { requiresAttention: false },
foregroundTurnWaiters: new Set(),
finalizedForegroundTurnIds: new Set(),
unsubscribeSession: null,
internal: false,
labels: {},
lifecycle: "closed",
session: null,
activeForegroundTurnId: null,
} satisfies ManagedAgent;
}
function makeRequest(
overrides: Partial<FetchRecentProviderSessionsRequestMessage> = {},
): FetchRecentProviderSessionsRequestMessage {
@@ -263,3 +320,144 @@ test("normalizeImportAgentRequest accepts new and legacy import handle shapes",
providerHandleId: "thread-2",
});
});
test("importProviderSession resumes by provider handle, hydrates the timeline, and applies title metadata", async () => {
const cwd = "/tmp/imported-agent";
const timeline: AgentTimelineItem[] = [
{ type: "user_message", text: "Trace recent provider sessions\n\nkeep it tight" },
{ type: "assistant_message", text: "I will inspect the provider listing." },
];
const snapshot = makeManagedAgent({
id: "00000000-0000-4000-8000-000000000633",
provider: "custom-codex",
cwd,
sessionId: "thread-imported",
nativeHandle: "provider-thread-imported",
title: null,
});
const descriptor = makeDescriptor({
provider: "custom-codex",
sessionId: "thread-imported",
nativeHandle: "provider-thread-imported",
cwd,
title: null,
firstPrompt: "Trace recent provider sessions",
lastActivityAt: "2026-04-30T00:00:00.000Z",
});
const agentManager = {
findPersistedAgent: vi.fn().mockResolvedValue(descriptor),
resumeAgentFromPersistence: vi.fn().mockResolvedValue(snapshot),
hydrateTimelineFromProvider: vi.fn().mockResolvedValue(undefined),
getTimeline: vi.fn().mockReturnValue(timeline),
setTitle: vi.fn().mockResolvedValue(undefined),
notifyAgentState: vi.fn(),
} as unknown as AgentManager;
const agentStorage = {
list: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue(null),
} as unknown as AgentStorage;
const scheduleAgentMetadataGeneration = vi.fn();
const result = await importProviderSession({
request: {
requestId: "import-thread",
provider: "custom-codex",
providerHandleId: "provider-thread-imported",
cwd,
},
agentManager,
agentStorage,
logger: { warn: vi.fn(), error: vi.fn() } as never,
deps: { scheduleAgentMetadataGeneration },
});
expect(agentManager.findPersistedAgent).toHaveBeenCalledWith(
"custom-codex",
"provider-thread-imported",
);
expect(agentManager.resumeAgentFromPersistence).toHaveBeenCalledWith(
descriptor.persistence,
{ cwd },
undefined,
{ labels: undefined },
);
expect(agentManager.hydrateTimelineFromProvider).toHaveBeenCalledWith(snapshot.id);
expect(agentManager.setTitle).toHaveBeenCalledWith(snapshot.id, "Trace recent provider sessions");
expect(scheduleAgentMetadataGeneration).toHaveBeenCalledWith(
expect.objectContaining({
agentManager,
agentId: snapshot.id,
cwd,
initialPrompt: "Trace recent provider sessions\n\nkeep it tight",
explicitTitle: null,
}),
);
expect(result).toEqual({ snapshot, timelineSize: 2 });
});
test("importProviderSession builds a fallback handle when a non-OpenCode provider has no descriptor", async () => {
const cwd = "/tmp/imported-agent";
const snapshot = makeManagedAgent({
provider: "codex",
cwd,
sessionId: "thread-imported",
nativeHandle: "thread-imported",
});
const agentManager = {
findPersistedAgent: vi.fn().mockResolvedValue(null),
resumeAgentFromPersistence: vi.fn().mockResolvedValue(snapshot),
hydrateTimelineFromProvider: vi.fn().mockResolvedValue(undefined),
getTimeline: vi.fn().mockReturnValue([]),
setTitle: vi.fn().mockResolvedValue(undefined),
notifyAgentState: vi.fn(),
} as unknown as AgentManager;
const agentStorage = {
list: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue(null),
} as unknown as AgentStorage;
await importProviderSession({
request: {
requestId: "import-thread",
provider: "codex",
providerHandleId: "thread-imported",
cwd,
},
agentManager,
agentStorage,
logger: { warn: vi.fn(), error: vi.fn() } as never,
});
expect(agentManager.resumeAgentFromPersistence).toHaveBeenCalledWith(
{
provider: "codex",
sessionId: "thread-imported",
nativeHandle: "thread-imported",
metadata: { provider: "codex", cwd },
},
{ cwd },
undefined,
{ labels: undefined },
);
});
test("importProviderSession requires cwd for missing OpenCode descriptors", async () => {
const agentManager = {
findPersistedAgent: vi.fn().mockResolvedValue(null),
} as unknown as AgentManager;
await expect(
importProviderSession({
request: {
requestId: "import-thread",
provider: "opencode",
providerHandleId: "thread-imported",
},
agentManager,
agentStorage: { list: vi.fn() } as unknown as AgentStorage,
logger: { warn: vi.fn(), error: vi.fn() } as never,
}),
).rejects.toThrow(
"OpenCode sessions require --cwd when the session cannot be found in persisted agents",
);
});
@@ -1,18 +1,25 @@
import type { z } from "zod";
import type { Logger } from "pino";
import type { ProviderDefinition } from "./provider-registry.js";
import type { AgentManager } from "./agent-manager.js";
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type {
AgentPersistenceHandle,
AgentProvider,
AgentSessionConfig,
AgentTimelineItem,
PersistedAgentDescriptor,
} from "./agent-sdk-types.js";
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
import { resolveCreateAgentTitles } from "./create-agent-title.js";
import { unarchiveAgentState } from "./agent-prompt.js";
import { toRecentProviderSessionDescriptorPayload } from "./agent-projections.js";
import type {
FetchRecentProviderSessionsRequestMessage,
ImportAgentRequestMessageSchema,
RecentProviderSessionDescriptorPayload,
} from "../../shared/messages.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
@@ -49,6 +56,23 @@ export interface ListImportableProviderSessionsResult {
filteredAlreadyImportedCount: number;
}
export interface ImportProviderSessionInput {
request: NormalizedImportAgentRequest;
agentManager: AgentManager;
agentStorage: AgentStorage;
workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
paseoHome?: string;
logger: Logger;
deps?: {
scheduleAgentMetadataGeneration?: typeof scheduleAgentMetadataGeneration;
};
}
export interface ImportProviderSessionResult {
snapshot: ManagedAgent;
timelineSize: number;
}
// COMPAT(import-agent-request-v1): accept legacy {provider, sessionId} shape
// alongside the new {providerId, providerHandleId} shape. Old clients
// (< target daemon floor) send the legacy fields. Drop the fallbacks and the
@@ -118,6 +142,99 @@ export async function listImportableProviderSessions(
return { entries, filteredAlreadyImportedCount };
}
export async function importProviderSession(
input: ImportProviderSessionInput,
): Promise<ImportProviderSessionResult> {
const { provider, providerHandleId, cwd, labels } = input.request;
const descriptor = await input.agentManager.findPersistedAgent(provider, providerHandleId);
if (!descriptor && provider === "opencode" && !cwd) {
throw new Error(
"OpenCode sessions require --cwd when the session cannot be found in persisted agents",
);
}
const handle = descriptor
? applyImportCwdOverride(descriptor.persistence, cwd)
: buildImportPersistenceHandle({ provider, providerHandleId, cwd });
const overrides = cwd ? ({ cwd } satisfies Partial<AgentSessionConfig>) : undefined;
await unarchiveAgentByHandle(input.agentStorage, input.agentManager, handle);
const snapshot = await input.agentManager.resumeAgentFromPersistence(
handle,
overrides,
undefined,
{
labels,
},
);
await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id);
await input.agentManager.hydrateTimelineFromProvider(snapshot.id);
await applyImportedAgentTitle({
snapshot,
agentManager: input.agentManager,
workspaceGitService: input.workspaceGitService,
paseoHome: input.paseoHome,
logger: input.logger,
scheduleAgentMetadataGeneration:
input.deps?.scheduleAgentMetadataGeneration ?? scheduleAgentMetadataGeneration,
});
return {
snapshot,
timelineSize: input.agentManager.getTimeline(snapshot.id).length,
};
}
async function unarchiveAgentByHandle(
agentStorage: AgentStorage,
agentManager: AgentManager,
handle: AgentPersistenceHandle,
): Promise<void> {
const records = await agentStorage.list();
const matched = records.find(
(record) =>
record.persistence?.provider === handle.provider &&
record.persistence?.sessionId === handle.sessionId,
);
if (!matched) {
return;
}
await unarchiveAgentState(agentStorage, agentManager, matched.id);
}
async function applyImportedAgentTitle(input: {
snapshot: ManagedAgent;
agentManager: AgentManager;
workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
paseoHome?: string;
logger: Logger;
scheduleAgentMetadataGeneration: typeof scheduleAgentMetadataGeneration;
}): Promise<void> {
const initialPrompt = getFirstUserMessageText(input.agentManager.getTimeline(input.snapshot.id));
if (!initialPrompt) {
return;
}
const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
configTitle: input.snapshot.config.title,
initialPrompt,
});
if (!explicitTitle && provisionalTitle) {
await input.agentManager.setTitle(input.snapshot.id, provisionalTitle);
}
input.scheduleAgentMetadataGeneration({
agentManager: input.agentManager,
agentId: input.snapshot.id,
cwd: input.snapshot.cwd,
workspaceGitService: input.workspaceGitService,
initialPrompt,
explicitTitle,
paseoHome: input.paseoHome,
logger: input.logger,
});
}
function parseRecentProviderSessionsSince(since: string | undefined): number | null {
if (!since) {
return null;
@@ -129,6 +246,54 @@ function parseRecentProviderSessionsSince(since: string | undefined): number | n
return timestamp;
}
function buildImportPersistenceHandle(input: {
provider: AgentProvider;
providerHandleId: string;
cwd?: string;
}): AgentPersistenceHandle {
const cwd = input.cwd ?? process.cwd();
return {
provider: input.provider,
sessionId: input.providerHandleId,
nativeHandle: input.providerHandleId,
metadata: {
provider: input.provider,
cwd,
},
};
}
function applyImportCwdOverride(
handle: AgentPersistenceHandle,
cwd: string | undefined,
): AgentPersistenceHandle {
if (!cwd) {
return handle;
}
return {
...handle,
metadata: {
...handle.metadata,
provider: handle.provider,
cwd,
},
};
}
function getFirstUserMessageText(timeline: readonly AgentTimelineItem[]): string | null {
for (const item of timeline) {
if (item.type !== "user_message") {
continue;
}
const text = item.text.trim();
if (text) {
return text;
}
}
return null;
}
async function collectImportedProviderSessionHandles(
agentManager: Pick<AgentManager, "listAgents">,
agentStorage: Pick<AgentStorage, "list">,
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { resolveCreateAgentTitles } from "./session.js";
import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
describe("resolveCreateAgentTitles", () => {
test("derives a provisional title from prompt when explicit title is absent", () => {
-261
View File
@@ -14,12 +14,9 @@ import type {
AgentClient,
AgentMode,
AgentModelDefinition,
AgentTimelineItem,
ListModesOptions,
ListModelsOptions,
PersistedAgentDescriptor,
} from "./agent/agent-sdk-types.js";
import type { ManagedAgent } from "./agent/agent-manager.js";
import type { ProviderDefinition } from "./agent/provider-registry.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import type { SessionOptions } from "./session.js";
@@ -70,8 +67,6 @@ interface SessionHandlerInternals {
handleCheckoutPullRequest(params: unknown): Promise<unknown>;
handleCheckoutPushRequest(params: unknown): Promise<unknown>;
handleCheckoutStatusRequest(params: unknown): Promise<unknown>;
handleFetchRecentProviderSessions(params: unknown): Promise<unknown>;
handleImportAgentRequest(params: unknown): Promise<unknown>;
describeWorkspaceRecord(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
describeWorkspaceRecordWithGitData(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
handleValidateBranchRequest(params: unknown): Promise<unknown>;
@@ -934,262 +929,6 @@ afterEach(() => {
vi.clearAllMocks();
});
describe("session agent import", () => {
test("filters already imported provider sessions by native handle", async () => {
const messages: unknown[] = [];
const cwd = "/tmp/import-filter";
const importedDescriptor: PersistedAgentDescriptor = {
provider: "claude",
sessionId: "provider-session-imported",
cwd,
title: "Already imported",
lastActivityAt: new Date("2026-04-30T12:00:00.000Z"),
persistence: {
provider: "claude",
sessionId: "provider-session-imported",
nativeHandle: "provider-native-imported",
},
timeline: [{ type: "user_message", text: "already imported" }],
};
const visibleDescriptor: PersistedAgentDescriptor = {
provider: "claude",
sessionId: "provider-session-visible",
cwd,
title: "Still importable",
lastActivityAt: new Date("2026-04-30T11:00:00.000Z"),
persistence: {
provider: "claude",
sessionId: "provider-session-visible",
nativeHandle: "provider-native-visible",
},
timeline: [{ type: "user_message", text: "still importable" }],
};
const agentManager = {
listAgents: vi.fn(() => [
{
provider: "claude",
persistence: {
provider: "claude",
sessionId: "stored-session-id",
nativeHandle: "provider-native-imported",
},
} as ManagedAgent,
]),
listImportablePersistedAgents: vi
.fn()
.mockResolvedValue([importedDescriptor, visibleDescriptor]),
subscribe: vi.fn(() => () => {}),
};
const agentStorage = {
list: vi.fn().mockResolvedValue([]),
};
const session = createSessionForTest({ messages });
Object.assign(session, { agentManager, agentStorage });
await asSessionInternals(session).handleFetchRecentProviderSessions({
type: "fetch_recent_provider_sessions_request",
requestId: "recent-provider-sessions",
cwd,
providers: ["claude"],
limit: 15,
});
expect(agentManager.listImportablePersistedAgents).toHaveBeenCalledWith({
limit: 200,
providerFilter: new Set(["claude"]),
cwd,
});
expect(messages).toContainEqual(
expect.objectContaining({
type: "fetch_recent_provider_sessions_response",
payload: expect.objectContaining({
requestId: "recent-provider-sessions",
filteredAlreadyImportedCount: 1,
entries: [
expect.objectContaining({
providerId: "claude",
providerHandleId: "provider-native-visible",
title: "Still importable",
}),
],
}),
}),
);
});
test("imports by provider handle id and hydrates title metadata from the first user message", async () => {
const messages: unknown[] = [];
const cwd = "/tmp/imported-agent";
const timeline: AgentTimelineItem[] = [
{ type: "user_message", text: "Trace recent provider sessions\n\nkeep it tight" },
{ type: "assistant_message", text: "I will inspect the provider listing." },
];
const snapshot = {
id: "00000000-0000-4000-8000-000000000633",
provider: "custom-codex",
cwd,
capabilities: TEST_CAPABILITIES,
config: { provider: "custom-codex", cwd },
createdAt: new Date("2026-04-30T00:00:00.000Z"),
updatedAt: new Date("2026-04-30T00:00:00.000Z"),
availableModes: [],
currentModeId: null,
pendingPermissions: new Map(),
bufferedPermissionResolutions: new Map(),
inFlightPermissionResponses: new Set(),
pendingReplacement: false,
persistence: {
provider: "custom-codex",
sessionId: "thread-imported",
nativeHandle: "provider-thread-imported",
metadata: { provider: "custom-codex", cwd },
},
historyPrimed: true,
lastUserMessageAt: null,
attention: { requiresAttention: false },
foregroundTurnWaiters: new Set(),
finalizedForegroundTurnIds: new Set(),
unsubscribeSession: null,
internal: false,
labels: {},
lifecycle: "closed",
session: null,
activeForegroundTurnId: null,
} satisfies ManagedAgent;
const agentManager = {
listAgents: vi.fn(() => []),
subscribe: vi.fn(() => () => {}),
findPersistedAgent: vi.fn().mockResolvedValue({
provider: "custom-codex",
sessionId: "thread-imported",
cwd,
title: null,
firstPromptPreview: "Trace recent provider sessions",
lastPromptPreview: "Trace recent provider sessions",
lastActivityAt: new Date("2026-04-30T00:00:00.000Z"),
persistence: snapshot.persistence,
}),
resumeAgentFromPersistence: vi.fn().mockResolvedValue(snapshot),
hydrateTimelineFromProvider: vi.fn().mockResolvedValue(undefined),
getTimeline: vi.fn().mockReturnValue(timeline),
setTitle: vi.fn().mockResolvedValue(undefined),
notifyAgentState: vi.fn(),
};
const agentStorage = {
list: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue(null),
};
const session = createSessionForTest({ messages });
Object.assign(session, { agentManager, agentStorage });
await asSessionInternals(session).handleImportAgentRequest({
type: "import_agent_request",
providerId: "custom-codex",
providerHandleId: "provider-thread-imported",
cwd,
requestId: "import-thread",
});
expect(agentManager.findPersistedAgent).toHaveBeenCalledWith(
"custom-codex",
"provider-thread-imported",
);
expect(agentManager.hydrateTimelineFromProvider).toHaveBeenCalledWith(snapshot.id);
expect(agentManager.setTitle).toHaveBeenCalledWith(
snapshot.id,
"Trace recent provider sessions",
);
expect(agentMetadataMocks.scheduleAgentMetadataGeneration).toHaveBeenCalledWith(
expect.objectContaining({
agentManager,
agentId: snapshot.id,
cwd,
initialPrompt: "Trace recent provider sessions\n\nkeep it tight",
explicitTitle: null,
}),
);
});
test("sets a provisional title and schedules auto-title generation from the first hydrated user message", async () => {
const messages: unknown[] = [];
const cwd = "/tmp/imported-agent";
const timeline: AgentTimelineItem[] = [
{ type: "user_message", text: "Investigate flaky checkout status\n\ninclude logs" },
{ type: "assistant_message", text: "I will inspect the checkout flow." },
];
const snapshot = {
id: "00000000-0000-4000-8000-000000000632",
provider: "codex",
cwd,
capabilities: TEST_CAPABILITIES,
config: { provider: "codex", cwd },
createdAt: new Date("2026-04-30T00:00:00.000Z"),
updatedAt: new Date("2026-04-30T00:00:00.000Z"),
availableModes: [],
currentModeId: null,
pendingPermissions: new Map(),
bufferedPermissionResolutions: new Map(),
inFlightPermissionResponses: new Set(),
pendingReplacement: false,
persistence: {
provider: "codex",
sessionId: "thread-imported",
nativeHandle: "thread-imported",
metadata: { provider: "codex", cwd },
},
historyPrimed: true,
lastUserMessageAt: null,
attention: { requiresAttention: false },
foregroundTurnWaiters: new Set(),
finalizedForegroundTurnIds: new Set(),
unsubscribeSession: null,
internal: false,
labels: {},
lifecycle: "closed",
session: null,
activeForegroundTurnId: null,
} satisfies ManagedAgent;
const agentManager = {
listAgents: vi.fn(() => []),
subscribe: vi.fn(() => () => {}),
findPersistedAgent: vi.fn().mockResolvedValue(null),
resumeAgentFromPersistence: vi.fn().mockResolvedValue(snapshot),
hydrateTimelineFromProvider: vi.fn().mockResolvedValue(undefined),
getTimeline: vi.fn().mockReturnValue(timeline),
setTitle: vi.fn().mockResolvedValue(undefined),
notifyAgentState: vi.fn(),
};
const agentStorage = {
list: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue(null),
};
const session = createSessionForTest({ messages });
Object.assign(session, { agentManager, agentStorage });
await asSessionInternals(session).handleImportAgentRequest({
type: "import_agent_request",
provider: "codex",
sessionId: "thread-imported",
cwd,
requestId: "import-thread",
});
expect(agentManager.setTitle).toHaveBeenCalledWith(
snapshot.id,
"Investigate flaky checkout status",
);
expect(agentMetadataMocks.scheduleAgentMetadataGeneration).toHaveBeenCalledWith(
expect.objectContaining({
agentManager,
agentId: snapshot.id,
cwd,
initialPrompt: "Investigate flaky checkout status\n\ninclude logs",
explicitTitle: null,
}),
);
});
});
describe("session provider refresh cwd routing", () => {
test("routes no-cwd provider snapshot refreshes through settings refresh", async () => {
const {
+11 -141
View File
@@ -97,13 +97,13 @@ import type {
ManagedAgent,
} from "./agent/agent-manager.js";
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
import {
buildStoredAgentPayload,
resolveEffectiveThinkingOptionId,
resolveStoredAgentPayloadUpdatedAt,
toAgentPayload,
} from "./agent/agent-projections.js";
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent/agent-title-limits.js";
import {
appendTimelineItemIfAgentKnown,
emitLiveTimelineItemIfAgentKnown,
@@ -128,13 +128,13 @@ import type {
AgentRunOptions,
AgentSessionConfig,
AgentStreamEvent,
AgentTimelineItem,
ProviderSnapshotEntry,
} from "./agent/agent-sdk-types.js";
import type { StoredAgentRecord } from "./agent/agent-storage.js";
import type { AgentStorage } from "./agent/agent-storage.js";
import {
ImportSessionsRequestError,
importProviderSession,
listImportableProviderSessions,
normalizeImportAgentRequest,
} from "./agent/import-sessions.js";
@@ -239,7 +239,6 @@ import {
} from "./worktree-session.js";
import { toWorktreeWireError } from "./worktree-errors.js";
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
interface ResolveKnownProjectRootForConfigInput {
@@ -396,53 +395,6 @@ function beginAgentDeleteIfSupported(agentStorage: AgentStorage, agentId: string
}
}
function deriveInitialAgentTitle(prompt: string): string | null {
const firstContentLine = prompt
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0);
if (!firstContentLine) {
return null;
}
const normalized = firstContentLine.replace(/\s+/g, " ").trim();
if (!normalized) {
return null;
}
const clamped = normalized.slice(0, MAX_INITIAL_AGENT_TITLE_CHARS).trim();
return clamped.length > 0 ? clamped : null;
}
export function resolveCreateAgentTitles(options: {
configTitle?: string | null;
initialPrompt?: string | null;
}): { explicitTitle: string | null; provisionalTitle: string | null } {
const explicitTitle =
typeof options.configTitle === "string" && options.configTitle.trim().length > 0
? options.configTitle.trim()
: null;
const trimmedPrompt = options.initialPrompt?.trim();
const provisionalTitle =
explicitTitle ?? (trimmedPrompt ? deriveInitialAgentTitle(trimmedPrompt) : null);
return {
explicitTitle,
provisionalTitle,
};
}
function getFirstUserMessageText(timeline: readonly AgentTimelineItem[]): string | null {
for (const item of timeline) {
if (item.type !== "user_message") {
continue;
}
const text = item.text.trim();
if (text) {
return text;
}
}
return null;
}
const FETCH_AGENTS_SORT_KEYS = ["status_priority", "created_at", "updated_at", "title"] as const;
export function resolveWaitForFinishError(options: {
@@ -672,45 +624,6 @@ class VoiceFeatureUnavailableError extends Error {
}
}
interface BuildImportPersistenceHandleInput {
provider: AgentProvider;
providerHandleId: string;
cwd?: string;
}
function buildImportPersistenceHandle(
input: BuildImportPersistenceHandleInput,
): AgentPersistenceHandle {
const cwd = input.cwd ?? process.cwd();
return {
provider: input.provider,
sessionId: input.providerHandleId,
nativeHandle: input.providerHandleId,
metadata: {
provider: input.provider,
cwd,
},
};
}
function applyImportCwdOverride(
handle: AgentPersistenceHandle,
cwd: string | undefined,
): AgentPersistenceHandle {
if (!cwd) {
return handle;
}
return {
...handle,
metadata: {
...handle.metadata,
provider: handle.provider,
cwd,
},
};
}
function convertPCMToWavBuffer(
pcmBuffer: Buffer,
sampleRate: number,
@@ -3199,39 +3112,22 @@ export class Session {
});
return;
}
const { provider, providerHandleId, cwd, labels, requestId } = normalized;
const { provider, providerHandleId, requestId } = normalized;
this.sessionLogger.info(
{ providerHandleId, provider },
`Importing agent ${providerHandleId} (${provider})`,
);
try {
const descriptor = await this.agentManager.findPersistedAgent(provider, providerHandleId);
if (!descriptor && provider === "opencode" && !cwd) {
throw new Error(
"OpenCode sessions require --cwd when the session cannot be found in persisted agents",
);
}
const handle = descriptor
? applyImportCwdOverride(descriptor.persistence, cwd)
: buildImportPersistenceHandle({ provider, providerHandleId, cwd });
const overrides = cwd ? ({ cwd } satisfies Partial<AgentSessionConfig>) : undefined;
await this.unarchiveAgentByHandle(handle);
const snapshot = await this.agentManager.resumeAgentFromPersistence(
handle,
overrides,
undefined,
{
labels,
},
);
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
await this.applyImportedAgentTitle(snapshot);
const { snapshot, timelineSize } = await importProviderSession({
request: normalized,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
workspaceGitService: this.workspaceGitService,
paseoHome: this.paseoHome,
logger: this.sessionLogger,
});
await this.forwardAgentUpdate(snapshot);
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
const agentPayload = await this.buildAgentPayload(snapshot);
this.emit({
type: "status",
@@ -3266,32 +3162,6 @@ export class Session {
}
}
private async applyImportedAgentTitle(snapshot: ManagedAgent): Promise<void> {
const initialPrompt = getFirstUserMessageText(this.agentManager.getTimeline(snapshot.id));
if (!initialPrompt) {
return;
}
const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({
configTitle: snapshot.config.title,
initialPrompt,
});
if (!explicitTitle && provisionalTitle) {
await this.agentManager.setTitle(snapshot.id, provisionalTitle);
}
scheduleAgentMetadataGeneration({
agentManager: this.agentManager,
agentId: snapshot.id,
cwd: snapshot.cwd,
workspaceGitService: this.workspaceGitService,
initialPrompt,
explicitTitle,
paseoHome: this.paseoHome,
logger: this.sessionLogger,
});
}
private async handleRefreshAgentRequest(
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
): Promise<void> {