Fix imported agent title hydration

This commit is contained in:
Mohamed Boudra
2026-04-30 22:42:23 +07:00
parent c783b7b093
commit eee4a772d7
3 changed files with 137 additions and 1 deletions

View File

@@ -12,9 +12,11 @@ import type {
AgentClient,
AgentMode,
AgentModelDefinition,
AgentTimelineItem,
ListModesOptions,
ListModelsOptions,
} 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";
@@ -27,6 +29,7 @@ interface SessionHandlerInternals {
handleCheckoutPullRequest(params: unknown): Promise<unknown>;
handleCheckoutPushRequest(params: unknown): Promise<unknown>;
handleCheckoutStatusRequest(params: unknown): Promise<unknown>;
handleImportAgentRequest(params: unknown): Promise<unknown>;
describeWorkspaceRecord(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
describeWorkspaceRecordWithGitData(...args: unknown[]): Promise<WorkspaceDescriptorPayload>;
handleValidateBranchRequest(params: unknown): Promise<unknown>;
@@ -63,6 +66,10 @@ const agentResponseMocks = vi.hoisted(() => ({
generateStructuredAgentResponseWithFallback: vi.fn(),
}));
const agentMetadataMocks = vi.hoisted(() => ({
scheduleAgentMetadataGeneration: vi.fn(),
}));
const spawnMocks = vi.hoisted(() => ({
execCommand: 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) => {
const actual = await importOriginal<typeof import("./worktree-bootstrap.js")>();
return {
@@ -570,6 +585,87 @@ afterEach(() => {
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", () => {
test("includes repository identity fields on the wire", () => {
const payload = normalizeCheckoutPrStatusPayload({

View File

@@ -133,6 +133,7 @@ import type {
AgentRunOptions,
AgentSessionConfig,
AgentStreamEvent,
AgentTimelineItem,
ProviderSnapshotEntry,
} from "./agent/agent-sdk-types.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[] {
const cursorSort: FetchWorkspacesRequestSort[] = [];
for (const item of raw) {
@@ -3329,6 +3343,7 @@ export class Session {
);
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
await this.applyImportedAgentTitle(snapshot);
await this.forwardAgentUpdate(snapshot);
const timelineSize = this.agentManager.getTimeline(snapshot.id).length;
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(
msg: Extract<SessionInboundMessage, { type: "refresh_agent_request" }>,
): Promise<void> {

View File

@@ -418,7 +418,7 @@ class FakeAgentSession implements AgentSession {
private async emitSlashCommandTurn(slashCommand: {
commandName: string;
args: string;
args?: string;
}): Promise<void> {
const threadStarted: AgentStreamEvent = {
type: "thread_started",