Preserve authoritative agent title across refresh
This commit is contained in:
@@ -408,6 +408,36 @@ describe("AgentManager", () => {
|
||||
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 () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-reload-active-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
|
||||
@@ -1434,6 +1434,10 @@ export class AgentManager {
|
||||
if (this.agents.has(resolvedAgentId)) {
|
||||
throw new Error(`Agent with id ${resolvedAgentId} already exists`);
|
||||
}
|
||||
const initialPersistedTitle = await this.resolveInitialPersistedTitle(
|
||||
resolvedAgentId,
|
||||
config
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
const initialTimeline = options?.timeline ? [...options.timeline] : [];
|
||||
@@ -1494,7 +1498,7 @@ export class AgentManager {
|
||||
this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
|
||||
await this.refreshRuntimeInfo(managed);
|
||||
await this.persistSnapshot(managed, {
|
||||
title: config.title ?? null,
|
||||
title: initialPersistedTitle,
|
||||
});
|
||||
this.emitState(managed);
|
||||
|
||||
@@ -1506,6 +1510,20 @@ export class AgentManager {
|
||||
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(
|
||||
items: readonly AgentTimelineItem[],
|
||||
startSeq: number,
|
||||
|
||||
@@ -138,6 +138,9 @@ function buildSerializableConfig(
|
||||
config: AgentSessionConfig
|
||||
): SerializableAgentConfig | null {
|
||||
const serializable: SerializableAgentConfig = {};
|
||||
if (Object.prototype.hasOwnProperty.call(config, "title")) {
|
||||
serializable.title = config.title ?? null;
|
||||
}
|
||||
if (config.modeId) {
|
||||
serializable.modeId = config.modeId;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ function createManagedAgent(
|
||||
const config: AgentSessionConfig = {
|
||||
provider,
|
||||
cwd,
|
||||
title: configOverrides.title,
|
||||
modeId: configOverrides.modeId ?? "plan",
|
||||
model: configOverrides.model ?? "gpt-5.1",
|
||||
extra: configOverrides.extra ?? { claude: { maxThinkingTokens: 1024 } },
|
||||
@@ -118,6 +119,7 @@ describe("AgentStorage", () => {
|
||||
currentModeId: "coding",
|
||||
lifecycle: "idle",
|
||||
config: {
|
||||
title: "Initial title",
|
||||
modeId: "coding",
|
||||
model: "gpt-5.1",
|
||||
systemPrompt: "Be terse and explicit.",
|
||||
@@ -137,6 +139,7 @@ describe("AgentStorage", () => {
|
||||
expect(records).toHaveLength(1);
|
||||
const [record] = records;
|
||||
expect(record.provider).toBe("claude");
|
||||
expect(record.config?.title).toBe("Initial title");
|
||||
expect(record.config?.modeId).toBe("coding");
|
||||
expect(record.config?.model).toBe("gpt-5.1");
|
||||
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
|
||||
.object({
|
||||
title: z.string().nullable().optional(),
|
||||
modeId: z.string().nullable().optional(),
|
||||
model: z.string().nullable().optional(),
|
||||
thinkingOptionId: z.string().nullable().optional(),
|
||||
@@ -64,7 +65,13 @@ const STORED_AGENT_SCHEMA = z.object({
|
||||
|
||||
export type SerializableAgentConfig = Pick<
|
||||
AgentSessionConfig,
|
||||
"modeId" | "model" | "thinkingOptionId" | "extra" | "systemPrompt" | "mcpServers"
|
||||
| "title"
|
||||
| "modeId"
|
||||
| "model"
|
||||
| "thinkingOptionId"
|
||||
| "extra"
|
||||
| "systemPrompt"
|
||||
| "mcpServers"
|
||||
>;
|
||||
|
||||
export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>;
|
||||
|
||||
@@ -145,8 +145,9 @@ describe("persistence hooks", () => {
|
||||
|
||||
test("buildConfigOverrides carries systemPrompt and mcpServers", () => {
|
||||
const record = createRecord({
|
||||
title: "Voice agent",
|
||||
title: "Voice agent (current)",
|
||||
config: {
|
||||
title: "Voice agent (created)",
|
||||
modeId: "default",
|
||||
model: "gpt-5.1-codex-mini",
|
||||
thinkingOptionId: "minimal",
|
||||
@@ -166,7 +167,7 @@ describe("persistence hooks", () => {
|
||||
modeId: "plan",
|
||||
model: "gpt-5.1-codex-mini",
|
||||
thinkingOptionId: "minimal",
|
||||
title: "Voice agent",
|
||||
title: "Voice agent (created)",
|
||||
systemPrompt: "Use speak first.",
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
@@ -181,7 +182,9 @@ describe("persistence hooks", () => {
|
||||
test("buildSessionConfig includes persisted systemPrompt and mcpServers", () => {
|
||||
const record = createRecord({
|
||||
provider: "codex",
|
||||
title: "Renamed title",
|
||||
config: {
|
||||
title: "Creation title",
|
||||
modeId: "default",
|
||||
model: "gpt-5.1-codex-mini",
|
||||
systemPrompt: "Confirm and speak first.",
|
||||
@@ -200,6 +203,7 @@ describe("persistence hooks", () => {
|
||||
cwd: "/tmp/project",
|
||||
modeId: "plan",
|
||||
model: "gpt-5.1-codex-mini",
|
||||
title: "Creation title",
|
||||
systemPrompt: "Confirm and speak first.",
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
|
||||
@@ -54,7 +54,7 @@ export function buildConfigOverrides(
|
||||
modeId: record.lastModeId ?? record.config?.modeId ?? undefined,
|
||||
model: record.config?.model ?? undefined,
|
||||
thinkingOptionId: record.config?.thinkingOptionId ?? undefined,
|
||||
title: record.title ?? undefined,
|
||||
title: record.config?.title ?? undefined,
|
||||
extra: record.config?.extra ?? undefined,
|
||||
systemPrompt: record.config?.systemPrompt ?? undefined,
|
||||
mcpServers: record.config?.mcpServers ?? undefined,
|
||||
|
||||
@@ -1725,7 +1725,6 @@ export class Session {
|
||||
}
|
||||
|
||||
await this.agentStorage.applySnapshot(liveAgent, {
|
||||
title: liveAgent.config.title ?? null,
|
||||
internal: liveAgent.internal,
|
||||
})
|
||||
archivedRecord = await this.agentStorage.get(agentId)
|
||||
@@ -2397,7 +2396,17 @@ export class Session {
|
||||
private async handleCreateAgentRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: 'create_agent_request' }>
|
||||
): Promise<void> {
|
||||
const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } =
|
||||
const {
|
||||
config,
|
||||
worktreeName,
|
||||
requestId,
|
||||
initialPrompt,
|
||||
clientMessageId,
|
||||
outputSchema,
|
||||
git,
|
||||
images,
|
||||
labels,
|
||||
} =
|
||||
msg
|
||||
this.sessionLogger.info(
|
||||
{ cwd: config.cwd, provider: config.provider, worktreeName },
|
||||
@@ -2432,7 +2441,7 @@ export class Session {
|
||||
await this.handleSendAgentMessage(
|
||||
snapshot.id,
|
||||
trimmedPrompt,
|
||||
uuidv4(),
|
||||
clientMessageId ?? uuidv4(),
|
||||
images,
|
||||
outputSchema ? { outputSchema } : undefined
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user