Fix imported agent title hydration
This commit is contained in:
@@ -12,9 +12,11 @@ import type {
|
|||||||
AgentClient,
|
AgentClient,
|
||||||
AgentMode,
|
AgentMode,
|
||||||
AgentModelDefinition,
|
AgentModelDefinition,
|
||||||
|
AgentTimelineItem,
|
||||||
ListModesOptions,
|
ListModesOptions,
|
||||||
ListModelsOptions,
|
ListModelsOptions,
|
||||||
} 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";
|
||||||
@@ -27,6 +29,7 @@ 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>;
|
||||||
|
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>;
|
||||||
@@ -63,6 +66,10 @@ const agentResponseMocks = vi.hoisted(() => ({
|
|||||||
generateStructuredAgentResponseWithFallback: vi.fn(),
|
generateStructuredAgentResponseWithFallback: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const agentMetadataMocks = vi.hoisted(() => ({
|
||||||
|
scheduleAgentMetadataGeneration: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const spawnMocks = vi.hoisted(() => ({
|
const spawnMocks = vi.hoisted(() => ({
|
||||||
execCommand: vi.fn(),
|
execCommand: vi.fn(),
|
||||||
spawnWorkspaceScript: vi.fn(),
|
spawnWorkspaceScript: vi.fn(),
|
||||||
@@ -172,6 +179,14 @@ vi.mock("./agent/agent-response-loop.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("./agent/agent-metadata-generator.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("./agent/agent-metadata-generator.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
scheduleAgentMetadataGeneration: agentMetadataMocks.scheduleAgentMetadataGeneration,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("./worktree-bootstrap.js", async (importOriginal) => {
|
vi.mock("./worktree-bootstrap.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("./worktree-bootstrap.js")>();
|
const actual = await importOriginal<typeof import("./worktree-bootstrap.js")>();
|
||||||
return {
|
return {
|
||||||
@@ -570,6 +585,87 @@ afterEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("session agent import", () => {
|
||||||
|
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 PR status payload normalization", () => {
|
describe("session PR status payload normalization", () => {
|
||||||
test("includes repository identity fields on the wire", () => {
|
test("includes repository identity fields on the wire", () => {
|
||||||
const payload = normalizeCheckoutPrStatusPayload({
|
const payload = normalizeCheckoutPrStatusPayload({
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ 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";
|
||||||
@@ -417,6 +418,19 @@ export function resolveCreateAgentTitles(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
function parseFetchWorkspacesCursorSort(raw: unknown[]): FetchWorkspacesRequestSort[] {
|
function parseFetchWorkspacesCursorSort(raw: unknown[]): FetchWorkspacesRequestSort[] {
|
||||||
const cursorSort: FetchWorkspacesRequestSort[] = [];
|
const cursorSort: FetchWorkspacesRequestSort[] = [];
|
||||||
for (const item of raw) {
|
for (const item of raw) {
|
||||||
@@ -3329,6 +3343,7 @@ export class Session {
|
|||||||
);
|
);
|
||||||
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
|
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
|
||||||
await this.agentManager.hydrateTimelineFromProvider(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 timelineSize = this.agentManager.getTimeline(snapshot.id).length;
|
||||||
const agentPayload = await this.buildAgentPayload(snapshot);
|
const agentPayload = await this.buildAgentPayload(snapshot);
|
||||||
@@ -3365,6 +3380,31 @@ 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,
|
||||||
|
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> {
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ class FakeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
private async emitSlashCommandTurn(slashCommand: {
|
private async emitSlashCommandTurn(slashCommand: {
|
||||||
commandName: string;
|
commandName: string;
|
||||||
args: string;
|
args?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const threadStarted: AgentStreamEvent = {
|
const threadStarted: AgentStreamEvent = {
|
||||||
type: "thread_started",
|
type: "thread_started",
|
||||||
|
|||||||
Reference in New Issue
Block a user