Preserve authoritative agent title across refresh

This commit is contained in:
Mohamed Boudra
2026-02-22 14:10:42 +07:00
parent 8ee9e7274d
commit 40f376b824
8 changed files with 82 additions and 8 deletions
@@ -408,6 +408,36 @@ describe("AgentManager", () => {
expect(afterHydrate).toEqual(beforeReload); expect(afterHydrate).toEqual(beforeReload);
}); });
test("reloadAgentSession preserves current title when config title is unset", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-title-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const manager = new AgentManager({
clients: {
codex: new TestAgentClient(),
},
registry: storage,
logger,
idFactory: () => "00000000-0000-4000-8000-000000000126",
});
const snapshot = await manager.createAgent({
provider: "codex",
cwd: workdir,
});
await manager.setTitle(snapshot.id, "Generated title");
const beforeReload = await storage.get(snapshot.id);
expect(beforeReload?.title).toBe("Generated title");
expect(beforeReload?.config?.title).toBeUndefined();
await manager.reloadAgentSession(snapshot.id);
const afterReload = await storage.get(snapshot.id);
expect(afterReload?.title).toBe("Generated title");
expect(afterReload?.config?.title).toBeUndefined();
});
test("reloadAgentSession cancels active run and resumes existing session once thread_started is observed", async () => { test("reloadAgentSession cancels active run and resumes existing session once thread_started is observed", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-active-")); const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-active-"));
const storagePath = join(workdir, "agents"); const storagePath = join(workdir, "agents");
@@ -1434,6 +1434,10 @@ export class AgentManager {
if (this.agents.has(resolvedAgentId)) { if (this.agents.has(resolvedAgentId)) {
throw new Error(`Agent with id ${resolvedAgentId} already exists`); throw new Error(`Agent with id ${resolvedAgentId} already exists`);
} }
const initialPersistedTitle = await this.resolveInitialPersistedTitle(
resolvedAgentId,
config
);
const now = new Date(); const now = new Date();
const initialTimeline = options?.timeline ? [...options.timeline] : []; const initialTimeline = options?.timeline ? [...options.timeline] : [];
@@ -1494,7 +1498,7 @@ export class AgentManager {
this.previousStatuses.set(resolvedAgentId, managed.lifecycle); this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
await this.refreshRuntimeInfo(managed); await this.refreshRuntimeInfo(managed);
await this.persistSnapshot(managed, { await this.persistSnapshot(managed, {
title: config.title ?? null, title: initialPersistedTitle,
}); });
this.emitState(managed); this.emitState(managed);
@@ -1506,6 +1510,20 @@ export class AgentManager {
return { ...managed }; return { ...managed };
} }
private async resolveInitialPersistedTitle(
agentId: string,
config: AgentSessionConfig
): Promise<string | null> {
const existing = await this.registry?.get(agentId);
if (existing) {
return existing.title ?? null;
}
if (Object.prototype.hasOwnProperty.call(config, "title")) {
return config.title ?? null;
}
return null;
}
private buildTimelineRowsFromItems( private buildTimelineRowsFromItems(
items: readonly AgentTimelineItem[], items: readonly AgentTimelineItem[],
startSeq: number, startSeq: number,
@@ -138,6 +138,9 @@ function buildSerializableConfig(
config: AgentSessionConfig config: AgentSessionConfig
): SerializableAgentConfig | null { ): SerializableAgentConfig | null {
const serializable: SerializableAgentConfig = {}; const serializable: SerializableAgentConfig = {};
if (Object.prototype.hasOwnProperty.call(config, "title")) {
serializable.title = config.title ?? null;
}
if (config.modeId) { if (config.modeId) {
serializable.modeId = config.modeId; serializable.modeId = config.modeId;
} }
@@ -36,6 +36,7 @@ function createManagedAgent(
const config: AgentSessionConfig = { const config: AgentSessionConfig = {
provider, provider,
cwd, cwd,
title: configOverrides.title,
modeId: configOverrides.modeId ?? "plan", modeId: configOverrides.modeId ?? "plan",
model: configOverrides.model ?? "gpt-5.1", model: configOverrides.model ?? "gpt-5.1",
extra: configOverrides.extra ?? { claude: { maxThinkingTokens: 1024 } }, extra: configOverrides.extra ?? { claude: { maxThinkingTokens: 1024 } },
@@ -118,6 +119,7 @@ describe("AgentStorage", () => {
currentModeId: "coding", currentModeId: "coding",
lifecycle: "idle", lifecycle: "idle",
config: { config: {
title: "Initial title",
modeId: "coding", modeId: "coding",
model: "gpt-5.1", model: "gpt-5.1",
systemPrompt: "Be terse and explicit.", systemPrompt: "Be terse and explicit.",
@@ -137,6 +139,7 @@ describe("AgentStorage", () => {
expect(records).toHaveLength(1); expect(records).toHaveLength(1);
const [record] = records; const [record] = records;
expect(record.provider).toBe("claude"); expect(record.provider).toBe("claude");
expect(record.config?.title).toBe("Initial title");
expect(record.config?.modeId).toBe("coding"); expect(record.config?.modeId).toBe("coding");
expect(record.config?.model).toBe("gpt-5.1"); expect(record.config?.model).toBe("gpt-5.1");
expect(record.config?.systemPrompt).toBe("Be terse and explicit."); expect(record.config?.systemPrompt).toBe("Be terse and explicit.");
@@ -11,6 +11,7 @@ import type { AgentSessionConfig } from "./agent-sdk-types.js";
const SERIALIZABLE_CONFIG_SCHEMA = z const SERIALIZABLE_CONFIG_SCHEMA = z
.object({ .object({
title: z.string().nullable().optional(),
modeId: z.string().nullable().optional(), modeId: z.string().nullable().optional(),
model: z.string().nullable().optional(), model: z.string().nullable().optional(),
thinkingOptionId: z.string().nullable().optional(), thinkingOptionId: z.string().nullable().optional(),
@@ -64,7 +65,13 @@ const STORED_AGENT_SCHEMA = z.object({
export type SerializableAgentConfig = Pick< export type SerializableAgentConfig = Pick<
AgentSessionConfig, AgentSessionConfig,
"modeId" | "model" | "thinkingOptionId" | "extra" | "systemPrompt" | "mcpServers" | "title"
| "modeId"
| "model"
| "thinkingOptionId"
| "extra"
| "systemPrompt"
| "mcpServers"
>; >;
export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>; export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>;
@@ -145,8 +145,9 @@ describe("persistence hooks", () => {
test("buildConfigOverrides carries systemPrompt and mcpServers", () => { test("buildConfigOverrides carries systemPrompt and mcpServers", () => {
const record = createRecord({ const record = createRecord({
title: "Voice agent", title: "Voice agent (current)",
config: { config: {
title: "Voice agent (created)",
modeId: "default", modeId: "default",
model: "gpt-5.1-codex-mini", model: "gpt-5.1-codex-mini",
thinkingOptionId: "minimal", thinkingOptionId: "minimal",
@@ -166,7 +167,7 @@ describe("persistence hooks", () => {
modeId: "plan", modeId: "plan",
model: "gpt-5.1-codex-mini", model: "gpt-5.1-codex-mini",
thinkingOptionId: "minimal", thinkingOptionId: "minimal",
title: "Voice agent", title: "Voice agent (created)",
systemPrompt: "Use speak first.", systemPrompt: "Use speak first.",
mcpServers: { mcpServers: {
paseo: { paseo: {
@@ -181,7 +182,9 @@ describe("persistence hooks", () => {
test("buildSessionConfig includes persisted systemPrompt and mcpServers", () => { test("buildSessionConfig includes persisted systemPrompt and mcpServers", () => {
const record = createRecord({ const record = createRecord({
provider: "codex", provider: "codex",
title: "Renamed title",
config: { config: {
title: "Creation title",
modeId: "default", modeId: "default",
model: "gpt-5.1-codex-mini", model: "gpt-5.1-codex-mini",
systemPrompt: "Confirm and speak first.", systemPrompt: "Confirm and speak first.",
@@ -200,6 +203,7 @@ describe("persistence hooks", () => {
cwd: "/tmp/project", cwd: "/tmp/project",
modeId: "plan", modeId: "plan",
model: "gpt-5.1-codex-mini", model: "gpt-5.1-codex-mini",
title: "Creation title",
systemPrompt: "Confirm and speak first.", systemPrompt: "Confirm and speak first.",
mcpServers: { mcpServers: {
paseo: { paseo: {
@@ -54,7 +54,7 @@ export function buildConfigOverrides(
modeId: record.lastModeId ?? record.config?.modeId ?? undefined, modeId: record.lastModeId ?? record.config?.modeId ?? undefined,
model: record.config?.model ?? undefined, model: record.config?.model ?? undefined,
thinkingOptionId: record.config?.thinkingOptionId ?? undefined, thinkingOptionId: record.config?.thinkingOptionId ?? undefined,
title: record.title ?? undefined, title: record.config?.title ?? undefined,
extra: record.config?.extra ?? undefined, extra: record.config?.extra ?? undefined,
systemPrompt: record.config?.systemPrompt ?? undefined, systemPrompt: record.config?.systemPrompt ?? undefined,
mcpServers: record.config?.mcpServers ?? undefined, mcpServers: record.config?.mcpServers ?? undefined,
+12 -3
View File
@@ -1725,7 +1725,6 @@ export class Session {
} }
await this.agentStorage.applySnapshot(liveAgent, { await this.agentStorage.applySnapshot(liveAgent, {
title: liveAgent.config.title ?? null,
internal: liveAgent.internal, internal: liveAgent.internal,
}) })
archivedRecord = await this.agentStorage.get(agentId) archivedRecord = await this.agentStorage.get(agentId)
@@ -2397,7 +2396,17 @@ export class Session {
private async handleCreateAgentRequest( private async handleCreateAgentRequest(
msg: Extract<SessionInboundMessage, { type: 'create_agent_request' }> msg: Extract<SessionInboundMessage, { type: 'create_agent_request' }>
): Promise<void> { ): Promise<void> {
const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = const {
config,
worktreeName,
requestId,
initialPrompt,
clientMessageId,
outputSchema,
git,
images,
labels,
} =
msg msg
this.sessionLogger.info( this.sessionLogger.info(
{ cwd: config.cwd, provider: config.provider, worktreeName }, { cwd: config.cwd, provider: config.provider, worktreeName },
@@ -2432,7 +2441,7 @@ export class Session {
await this.handleSendAgentMessage( await this.handleSendAgentMessage(
snapshot.id, snapshot.id,
trimmedPrompt, trimmedPrompt,
uuidv4(), clientMessageId ?? uuidv4(),
images, images,
outputSchema ? { outputSchema } : undefined outputSchema ? { outputSchema } : undefined
) )