refactor(server): extract import session boundary (#854)
This commit is contained in:
@@ -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 { AgentManager, ManagedAgent } from "./agent-manager.js";
|
||||||
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
|
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
|
||||||
import type { FetchRecentProviderSessionsRequestMessage } from "../../shared/messages.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 {
|
import {
|
||||||
ImportSessionsRequestError,
|
ImportSessionsRequestError,
|
||||||
|
importProviderSession,
|
||||||
listImportableProviderSessions,
|
listImportableProviderSessions,
|
||||||
normalizeImportAgentRequest,
|
normalizeImportAgentRequest,
|
||||||
} from "./import-sessions.js";
|
} 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: {
|
function makeDescriptor(args: {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
sessionId: 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(
|
function makeRequest(
|
||||||
overrides: Partial<FetchRecentProviderSessionsRequestMessage> = {},
|
overrides: Partial<FetchRecentProviderSessionsRequestMessage> = {},
|
||||||
): FetchRecentProviderSessionsRequestMessage {
|
): FetchRecentProviderSessionsRequestMessage {
|
||||||
@@ -263,3 +320,144 @@ test("normalizeImportAgentRequest accepts new and legacy import handle shapes",
|
|||||||
providerHandleId: "thread-2",
|
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 { z } from "zod";
|
||||||
|
import type { Logger } from "pino";
|
||||||
import type { ProviderDefinition } from "./provider-registry.js";
|
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 { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
|
||||||
import type {
|
import type {
|
||||||
AgentPersistenceHandle,
|
AgentPersistenceHandle,
|
||||||
AgentProvider,
|
AgentProvider,
|
||||||
|
AgentSessionConfig,
|
||||||
|
AgentTimelineItem,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "./agent-sdk-types.js";
|
} 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 { toRecentProviderSessionDescriptorPayload } from "./agent-projections.js";
|
||||||
import type {
|
import type {
|
||||||
FetchRecentProviderSessionsRequestMessage,
|
FetchRecentProviderSessionsRequestMessage,
|
||||||
ImportAgentRequestMessageSchema,
|
ImportAgentRequestMessageSchema,
|
||||||
RecentProviderSessionDescriptorPayload,
|
RecentProviderSessionDescriptorPayload,
|
||||||
} from "../../shared/messages.js";
|
} from "../../shared/messages.js";
|
||||||
|
import type { WorkspaceGitService } from "../workspace-git-service.js";
|
||||||
|
|
||||||
type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
|
type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
|
||||||
|
|
||||||
@@ -49,6 +56,23 @@ export interface ListImportableProviderSessionsResult {
|
|||||||
filteredAlreadyImportedCount: number;
|
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
|
// COMPAT(import-agent-request-v1): accept legacy {provider, sessionId} shape
|
||||||
// alongside the new {providerId, providerHandleId} shape. Old clients
|
// alongside the new {providerId, providerHandleId} shape. Old clients
|
||||||
// (< target daemon floor) send the legacy fields. Drop the fallbacks and the
|
// (< target daemon floor) send the legacy fields. Drop the fallbacks and the
|
||||||
@@ -118,6 +142,99 @@ export async function listImportableProviderSessions(
|
|||||||
return { entries, filteredAlreadyImportedCount };
|
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 {
|
function parseRecentProviderSessionsSince(since: string | undefined): number | null {
|
||||||
if (!since) {
|
if (!since) {
|
||||||
return null;
|
return null;
|
||||||
@@ -129,6 +246,54 @@ function parseRecentProviderSessionsSince(since: string | undefined): number | n
|
|||||||
return timestamp;
|
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(
|
async function collectImportedProviderSessionHandles(
|
||||||
agentManager: Pick<AgentManager, "listAgents">,
|
agentManager: Pick<AgentManager, "listAgents">,
|
||||||
agentStorage: Pick<AgentStorage, "list">,
|
agentStorage: Pick<AgentStorage, "list">,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
import { resolveCreateAgentTitles } from "./session.js";
|
import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
|
||||||
|
|
||||||
describe("resolveCreateAgentTitles", () => {
|
describe("resolveCreateAgentTitles", () => {
|
||||||
test("derives a provisional title from prompt when explicit title is absent", () => {
|
test("derives a provisional title from prompt when explicit title is absent", () => {
|
||||||
|
|||||||
@@ -14,12 +14,9 @@ import type {
|
|||||||
AgentClient,
|
AgentClient,
|
||||||
AgentMode,
|
AgentMode,
|
||||||
AgentModelDefinition,
|
AgentModelDefinition,
|
||||||
AgentTimelineItem,
|
|
||||||
ListModesOptions,
|
ListModesOptions,
|
||||||
ListModelsOptions,
|
ListModelsOptions,
|
||||||
PersistedAgentDescriptor,
|
|
||||||
} from "./agent/agent-sdk-types.js";
|
} from "./agent/agent-sdk-types.js";
|
||||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
|
||||||
import type { ProviderDefinition } from "./agent/provider-registry.js";
|
import type { ProviderDefinition } from "./agent/provider-registry.js";
|
||||||
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
||||||
import type { SessionOptions } from "./session.js";
|
import type { SessionOptions } from "./session.js";
|
||||||
@@ -70,8 +67,6 @@ interface SessionHandlerInternals {
|
|||||||
handleCheckoutPullRequest(params: unknown): Promise<unknown>;
|
handleCheckoutPullRequest(params: unknown): Promise<unknown>;
|
||||||
handleCheckoutPushRequest(params: unknown): Promise<unknown>;
|
handleCheckoutPushRequest(params: unknown): Promise<unknown>;
|
||||||
handleCheckoutStatusRequest(params: unknown): Promise<unknown>;
|
handleCheckoutStatusRequest(params: unknown): Promise<unknown>;
|
||||||
handleFetchRecentProviderSessions(params: unknown): Promise<unknown>;
|
|
||||||
handleImportAgentRequest(params: unknown): Promise<unknown>;
|
|
||||||
describeWorkspaceRecord(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
|
describeWorkspaceRecord(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
|
||||||
describeWorkspaceRecordWithGitData(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
|
describeWorkspaceRecordWithGitData(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
|
||||||
handleValidateBranchRequest(params: unknown): Promise<unknown>;
|
handleValidateBranchRequest(params: unknown): Promise<unknown>;
|
||||||
@@ -934,262 +929,6 @@ afterEach(() => {
|
|||||||
vi.clearAllMocks();
|
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", () => {
|
describe("session provider refresh cwd routing", () => {
|
||||||
test("routes no-cwd provider snapshot refreshes through settings refresh", async () => {
|
test("routes no-cwd provider snapshot refreshes through settings refresh", async () => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -97,13 +97,13 @@ import type {
|
|||||||
ManagedAgent,
|
ManagedAgent,
|
||||||
} from "./agent/agent-manager.js";
|
} from "./agent/agent-manager.js";
|
||||||
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
||||||
|
import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
|
||||||
import {
|
import {
|
||||||
buildStoredAgentPayload,
|
buildStoredAgentPayload,
|
||||||
resolveEffectiveThinkingOptionId,
|
resolveEffectiveThinkingOptionId,
|
||||||
resolveStoredAgentPayloadUpdatedAt,
|
resolveStoredAgentPayloadUpdatedAt,
|
||||||
toAgentPayload,
|
toAgentPayload,
|
||||||
} from "./agent/agent-projections.js";
|
} from "./agent/agent-projections.js";
|
||||||
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent/agent-title-limits.js";
|
|
||||||
import {
|
import {
|
||||||
appendTimelineItemIfAgentKnown,
|
appendTimelineItemIfAgentKnown,
|
||||||
emitLiveTimelineItemIfAgentKnown,
|
emitLiveTimelineItemIfAgentKnown,
|
||||||
@@ -128,13 +128,13 @@ import type {
|
|||||||
AgentRunOptions,
|
AgentRunOptions,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
|
||||||
ProviderSnapshotEntry,
|
ProviderSnapshotEntry,
|
||||||
} from "./agent/agent-sdk-types.js";
|
} from "./agent/agent-sdk-types.js";
|
||||||
import type { StoredAgentRecord } from "./agent/agent-storage.js";
|
import type { StoredAgentRecord } from "./agent/agent-storage.js";
|
||||||
import type { AgentStorage } from "./agent/agent-storage.js";
|
import type { AgentStorage } from "./agent/agent-storage.js";
|
||||||
import {
|
import {
|
||||||
ImportSessionsRequestError,
|
ImportSessionsRequestError,
|
||||||
|
importProviderSession,
|
||||||
listImportableProviderSessions,
|
listImportableProviderSessions,
|
||||||
normalizeImportAgentRequest,
|
normalizeImportAgentRequest,
|
||||||
} from "./agent/import-sessions.js";
|
} from "./agent/import-sessions.js";
|
||||||
@@ -239,7 +239,6 @@ import {
|
|||||||
} from "./worktree-session.js";
|
} from "./worktree-session.js";
|
||||||
import { toWorktreeWireError } from "./worktree-errors.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__";
|
const WORKSPACE_GIT_WATCH_REMOVED_STATE_KEY = "__removed__";
|
||||||
|
|
||||||
interface ResolveKnownProjectRootForConfigInput {
|
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;
|
const FETCH_AGENTS_SORT_KEYS = ["status_priority", "created_at", "updated_at", "title"] as const;
|
||||||
|
|
||||||
export function resolveWaitForFinishError(options: {
|
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(
|
function convertPCMToWavBuffer(
|
||||||
pcmBuffer: Buffer,
|
pcmBuffer: Buffer,
|
||||||
sampleRate: number,
|
sampleRate: number,
|
||||||
@@ -3199,39 +3112,22 @@ export class Session {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { provider, providerHandleId, cwd, labels, requestId } = normalized;
|
const { provider, providerHandleId, requestId } = normalized;
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{ providerHandleId, provider },
|
{ providerHandleId, provider },
|
||||||
`Importing agent ${providerHandleId} (${provider})`,
|
`Importing agent ${providerHandleId} (${provider})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const descriptor = await this.agentManager.findPersistedAgent(provider, providerHandleId);
|
const { snapshot, timelineSize } = await importProviderSession({
|
||||||
if (!descriptor && provider === "opencode" && !cwd) {
|
request: normalized,
|
||||||
throw new Error(
|
agentManager: this.agentManager,
|
||||||
"OpenCode sessions require --cwd when the session cannot be found in persisted agents",
|
agentStorage: this.agentStorage,
|
||||||
);
|
workspaceGitService: this.workspaceGitService,
|
||||||
}
|
paseoHome: this.paseoHome,
|
||||||
|
logger: this.sessionLogger,
|
||||||
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);
|
|
||||||
await this.forwardAgentUpdate(snapshot);
|
await this.forwardAgentUpdate(snapshot);
|
||||||
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
|
|
||||||
const agentPayload = await this.buildAgentPayload(snapshot);
|
const agentPayload = await this.buildAgentPayload(snapshot);
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "status",
|
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(
|
private async handleRefreshAgentRequest(
|
||||||
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
|
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user