Preserve authoritative agent title across refresh
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user