From a108338ce54d6babef429c8f286db8a32c19e0fa Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Fri, 31 Jul 2026 11:38:12 +0800 Subject: [PATCH] fix: preserve session title state invariants --- .../agent-core-v2/docs/state-manifest.d.ts | 5 +- .../src/agent/rpc/prompt-metadata.ts | 13 ++- .../sessionLifecycleService.ts | 2 +- .../sessionMetadata/sessionMetadata.ts | 5 +- .../sessionMetadata/sessionMetadataService.ts | 100 ++++++++++++------ .../sessionTitle/sessionTitleService.ts | 17 +-- .../test/agent/rpc/prompt-metadata.test.ts | 1 + .../sessionLifecycle/sessionLifecycle.test.ts | 54 +++++++++- .../sessionMetadata/sessionMetadata.test.ts | 43 ++++++-- .../sessionTitle/sessionTitleService.test.ts | 40 ++----- packages/klient/src/contract/global/events.ts | 4 +- .../klient/src/contract/session/metadata.ts | 9 +- packages/node-sdk/src/v2/session-mapper.ts | 2 +- 13 files changed, 187 insertions(+), 108 deletions(-) diff --git a/packages/agent-core-v2/docs/state-manifest.d.ts b/packages/agent-core-v2/docs/state-manifest.d.ts index a83e5a269..998e6ea16 100644 --- a/packages/agent-core-v2/docs/state-manifest.d.ts +++ b/packages/agent-core-v2/docs/state-manifest.d.ts @@ -335,8 +335,7 @@ export interface SessionStateSnapshot { readonly id: string; readonly version?: number; readonly title?: string; - readonly titleSource?: 'prompt' | 'generated' | 'custom'; - readonly isCustomTitle?: boolean; + readonly titleKind?: 'replaceable' | 'generated' | 'custom'; readonly lastPrompt?: string; readonly createdAt: number; readonly updatedAt: number; @@ -1009,7 +1008,7 @@ export interface AgentStateSnapshot { 'llmRequester.lastConfigLogSignature': string | undefined; 'llmRequester.mediaDegradedTurns': Set; 'llmRequester.mediaStrippedTurns': Map; 'llmRequester.turnConfigs': Map { if (text === undefined) return; const current = await target.metadata.read(); - const patch: { lastPrompt: string; title?: string; isCustomTitle?: boolean } = { + const patch: { lastPrompt: string; title?: string; titleKind?: SessionTitleKind } = { lastPrompt: text, }; - if (!current.isCustomTitle && isUntitled(current.title)) { + if (current.titleKind !== 'custom' && isUntitled(current.title)) { patch.title = titleFromPromptMetadataText(text); - patch.isCustomTitle = false; + patch.titleKind = 'replaceable'; } await target.metadata.update(patch); target.eventService.publish({ @@ -77,7 +80,7 @@ export async function applyPromptMetadataUpdate( title: patch.title, patch: { title: patch.title, - isCustomTitle: patch.isCustomTitle, + isCustomTitle: patch.titleKind === undefined ? undefined : false, lastPrompt: text, }, }, diff --git a/packages/agent-core-v2/src/app/sessionLifecycle/sessionLifecycleService.ts b/packages/agent-core-v2/src/app/sessionLifecycle/sessionLifecycleService.ts index accd49cfd..5e18f6373 100644 --- a/packages/agent-core-v2/src/app/sessionLifecycle/sessionLifecycleService.ts +++ b/packages/agent-core-v2/src/app/sessionLifecycle/sessionLifecycleService.ts @@ -428,7 +428,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec const title = opts.title ?? `Fork: ${sourceMeta?.title || sourceId}`; await targetMeta.update({ title, - isCustomTitle: opts.title !== undefined ? true : sourceMeta?.isCustomTitle === true, + titleKind: opts.title === undefined ? 'replaceable' : 'custom', forkedFrom: sourceId, archived: false, lastPrompt: sourceMeta?.lastPrompt, diff --git a/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadata.ts b/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadata.ts index 223fc8721..49df4c898 100644 --- a/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadata.ts +++ b/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadata.ts @@ -25,12 +25,13 @@ export interface AgentMeta { export const SESSION_META_VERSION = 2; +export type SessionTitleKind = 'replaceable' | 'generated' | 'custom'; + export interface SessionMeta { readonly id: string; readonly version?: number; readonly title?: string; - readonly titleSource?: 'prompt' | 'generated' | 'custom'; - readonly isCustomTitle?: boolean; + readonly titleKind?: SessionTitleKind; readonly lastPrompt?: string; readonly createdAt: number; readonly updatedAt: number; diff --git a/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts b/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts index 24010c5e2..4eb80b68e 100644 --- a/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts +++ b/packages/agent-core-v2/src/session/sessionMetadata/sessionMetadataService.ts @@ -44,6 +44,7 @@ import { type SessionMeta, type SessionMetadataChangedEvent, type SessionMetaPatch, + type SessionTitleKind, } from './sessionMetadata'; const META_KEY = 'state.json'; @@ -109,14 +110,14 @@ export class SessionMetadata extends Disposable implements ISessionMetadata { } async setTitle(title: string): Promise { - await this.update({ title, titleSource: 'custom', isCustomTitle: true }); + await this.update({ title, titleKind: 'custom' }); } async setGeneratedTitleIfUncustomized(title: string): Promise { return this.enqueueUpdate(async () => { await this.ready; - if (this.data.isCustomTitle === true) return false; - await this.applyUpdate({ title, titleSource: 'generated', isCustomTitle: false }); + if (this.data.titleKind === 'custom') return false; + await this.applyUpdate({ title, titleKind: 'generated' }); return true; }); } @@ -174,7 +175,11 @@ export class SessionMetadata extends Disposable implements ISessionMetadata { const existing = await this.store.get(this.scope, META_KEY); if (existing !== undefined) { this.data = normalizeSessionMeta(existing, this.ctx.sessionId); - if (this.data.agents === undefined || this.data.custom === undefined) { + if ( + this.data.agents === undefined || + this.data.custom === undefined || + sessionMetaTitleNeedsMigration(existing, this.data) + ) { this.data = { ...this.data, agents: this.data.agents ?? {}, @@ -220,45 +225,74 @@ function recordEquals(a: AgentMeta['labels'], b: AgentMeta['labels']): boolean { } export function normalizeSessionMeta(raw: SessionMeta, sessionId: string): SessionMeta { - const clean = { ...raw }; - const legacy = raw as unknown as { - createdAt?: unknown; - updatedAt?: unknown; - workDir?: unknown; - customTitle?: unknown; - }; + const legacy = raw as unknown as LegacySessionMeta; + const normalizedTitle = normalizeSessionTitle(legacy); + const { + createdAt: legacyCreatedAt, + updatedAt: legacyUpdatedAt, + workDir: legacyWorkDir, + titleSource: _legacyTitleSource, + isCustomTitle: _legacyIsCustomTitle, + customTitle: _legacyCustomTitle, + ...clean + } = legacy; const cwd = - clean.cwd ?? (typeof legacy.workDir === 'string' && legacy.workDir.length > 0 - ? legacy.workDir + clean.cwd ?? (typeof legacyWorkDir === 'string' && legacyWorkDir.length > 0 + ? legacyWorkDir : undefined); - const legacyCustomTitle = - typeof legacy.customTitle === 'string' ? legacy.customTitle : undefined; - const hasModernTitleState = - typeof clean.title === 'string' && typeof clean.isCustomTitle === 'boolean'; - const title = hasModernTitleState ? clean.title : (legacyCustomTitle ?? clean.title); - const isCustomTitle = hasModernTitleState - ? clean.isCustomTitle - : legacyCustomTitle === undefined - ? clean.isCustomTitle - : true; - if (clean.version === SESSION_META_VERSION) { - if (cwd === clean.cwd && title === clean.title && isCustomTitle === clean.isCustomTitle) { - return clean; - } - return { ...clean, cwd, title, isCustomTitle }; - } + const { title, titleKind } = normalizedTitle; return { ...clean, - id: sessionId, + id: clean.version === SESSION_META_VERSION ? clean.id : sessionId, version: SESSION_META_VERSION, cwd, title, - isCustomTitle, - createdAt: toEpochMs(legacy.createdAt), - updatedAt: toEpochMs(legacy.updatedAt), + titleKind, + createdAt: toEpochMs(legacyCreatedAt), + updatedAt: toEpochMs(legacyUpdatedAt), }; } +type LegacySessionMeta = Omit & { + readonly createdAt?: unknown; + readonly updatedAt?: unknown; + readonly workDir?: unknown; + readonly titleSource?: unknown; + readonly isCustomTitle?: unknown; + readonly customTitle?: unknown; +}; + +function normalizeSessionTitle( + raw: LegacySessionMeta, +): Pick { + const title = typeof raw.title === 'string' ? raw.title : undefined; + if (title !== undefined && isSessionTitleKind(raw.titleKind)) { + return { title, titleKind: raw.titleKind }; + } + if (title !== undefined && typeof raw.isCustomTitle === 'boolean') { + return { title, titleKind: raw.isCustomTitle ? 'custom' : 'replaceable' }; + } + if (typeof raw.customTitle === 'string') { + return { title: raw.customTitle, titleKind: 'custom' }; + } + return title === undefined ? {} : { title, titleKind: 'replaceable' }; +} + +function isSessionTitleKind(value: unknown): value is SessionTitleKind { + return value === 'replaceable' || value === 'generated' || value === 'custom'; +} + +function sessionMetaTitleNeedsMigration(raw: SessionMeta, normalized: SessionMeta): boolean { + const record = raw as unknown as Record; + return ( + raw.title !== normalized.title || + raw.titleKind !== normalized.titleKind || + Object.hasOwn(record, 'titleSource') || + Object.hasOwn(record, 'isCustomTitle') || + Object.hasOwn(record, 'customTitle') + ); +} + export function toEpochMs(value: unknown): number { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { diff --git a/packages/agent-core-v2/src/session/sessionTitle/sessionTitleService.ts b/packages/agent-core-v2/src/session/sessionTitle/sessionTitleService.ts index a0d7d97d8..32d4ca9f5 100644 --- a/packages/agent-core-v2/src/session/sessionTitle/sessionTitleService.ts +++ b/packages/agent-core-v2/src/session/sessionTitle/sessionTitleService.ts @@ -65,8 +65,8 @@ export class SessionTitleService implements ISessionTitleService { async generateTitle(options?: { readonly force?: boolean }): Promise { const current = await this.metadata.read(); - if (hasCustomTitle(current)) return undefined; - if (current.titleSource === 'generated' && options?.force !== true) return undefined; + if (current.titleKind === 'custom') return undefined; + if (current.titleKind === 'generated' && options?.force !== true) return undefined; const main = this.agentLifecycle.get(MAIN_AGENT_ID); const prompts = main === undefined @@ -89,7 +89,7 @@ export class SessionTitleService implements ISessionTitleService { private async generateAndApplyOnce(chatContent: string): Promise { const current = await this.metadata.read(); - if (hasCustomTitle(current)) return undefined; + if (current.titleKind === 'custom') return undefined; const provider = this.providers.get(KIMI_CODE_PROVIDER_NAME); if ( provider === undefined || @@ -158,17 +158,6 @@ export class SessionTitleService implements ISessionTitleService { } } -function hasCustomTitle(metadata: { - readonly title?: unknown; - readonly isCustomTitle?: unknown; - readonly customTitle?: unknown; -}): boolean { - if (typeof metadata.title === 'string' && typeof metadata.isCustomTitle === 'boolean') { - return metadata.isCustomTitle; - } - return metadata.isCustomTitle === true || typeof metadata.customTitle === 'string'; -} - function titleInputFromPrompts(prompts: readonly string[]): string | undefined { if (prompts.length === 0) return undefined; return prompts diff --git a/packages/agent-core-v2/test/agent/rpc/prompt-metadata.test.ts b/packages/agent-core-v2/test/agent/rpc/prompt-metadata.test.ts index bfcea3270..2fd05e8d4 100644 --- a/packages/agent-core-v2/test/agent/rpc/prompt-metadata.test.ts +++ b/packages/agent-core-v2/test/agent/rpc/prompt-metadata.test.ts @@ -97,6 +97,7 @@ describe('applyPromptMetadataUpdate', () => { expect(readMeta().lastPrompt).toBe('第二条'); expect(readMeta().title).toBe('第一条'); + expect(readMeta().titleKind).toBe('replaceable'); }); it('updates metadata for slash activations', async () => { diff --git a/packages/agent-core-v2/test/app/sessionLifecycle/sessionLifecycle.test.ts b/packages/agent-core-v2/test/app/sessionLifecycle/sessionLifecycle.test.ts index eca1bd241..3c3c91daa 100644 --- a/packages/agent-core-v2/test/app/sessionLifecycle/sessionLifecycle.test.ts +++ b/packages/agent-core-v2/test/app/sessionLifecycle/sessionLifecycle.test.ts @@ -34,7 +34,10 @@ import { ISessionLifecycleService } from '#/app/sessionLifecycle/sessionLifecycl import { SessionLifecycleService } from '#/app/sessionLifecycle/sessionLifecycleService'; import { IAgentActivityView } from '#/agent/activityView/activityView'; import { ISessionExternalHooksService } from '#/session/externalHooks/externalHooks'; -import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata'; +import { + ISessionMetadata, + type SessionMetaPatch, +} from '#/session/sessionMetadata/sessionMetadata'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionToolPolicy } from '#/session/sessionToolPolicy/sessionToolPolicy'; import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; @@ -1181,6 +1184,55 @@ describe('SessionLifecycleService', () => { }); } + it('marks the default fork title as replaceable', async () => { + const updates: SessionMetaPatch[] = []; + const svc = build([ + workspaceGetStub(), + stubPair(ISessionMetadata, { + ...metadataStub(), + read: () => + Promise.resolve({ + title: 'generated source', + titleKind: 'generated', + agents: {}, + } as never), + update: (patch) => { + updates.push(patch); + return Promise.resolve(); + }, + }), + ]); + await svc.create({ sessionId: 'src', workDir: '/tmp/proj' }); + + await svc.fork({ sourceSessionId: 'src', newSessionId: 'dst' }); + + expect(updates).toContainEqual( + expect.objectContaining({ title: 'Fork: generated source', titleKind: 'replaceable' }), + ); + }); + + it('marks an explicit fork title as custom', async () => { + const updates: SessionMetaPatch[] = []; + const svc = build([ + workspaceGetStub(), + stubPair(ISessionMetadata, { + ...metadataStub(), + read: () => Promise.resolve({ title: 'source', agents: {} } as never), + update: (patch) => { + updates.push(patch); + return Promise.resolve(); + }, + }), + ]); + await svc.create({ sessionId: 'src', workDir: '/tmp/proj' }); + + await svc.fork({ sourceSessionId: 'src', newSessionId: 'dst', title: 'user title' }); + + expect(updates).toContainEqual( + expect.objectContaining({ title: 'user title', titleKind: 'custom' }), + ); + }); + it('copies blobs, plans, background tasks, and media originals into the fork', async () => { const root = await makeTmpRoot(); const svc = build([ diff --git a/packages/agent-core-v2/test/session/sessionMetadata/sessionMetadata.test.ts b/packages/agent-core-v2/test/session/sessionMetadata/sessionMetadata.test.ts index c00a352bd..69575d3e0 100644 --- a/packages/agent-core-v2/test/session/sessionMetadata/sessionMetadata.test.ts +++ b/packages/agent-core-v2/test/session/sessionMetadata/sessionMetadata.test.ts @@ -93,7 +93,7 @@ describe('SessionMetadata', () => { const meta = ix.get(ISessionMetadata); await meta.setTitle('t'); await meta.setArchived(true); - expect(await meta.read()).toMatchObject({ title: 't', titleSource: 'custom', archived: true }); + expect(await meta.read()).toMatchObject({ title: 't', titleKind: 'custom', archived: true }); }); it('sets a generated title while the metadata remains uncustomized', async () => { @@ -102,8 +102,7 @@ describe('SessionMetadata', () => { await expect(meta.setGeneratedTitleIfUncustomized('generated title')).resolves.toBe(true); await expect(meta.read()).resolves.toMatchObject({ title: 'generated title', - titleSource: 'generated', - isCustomTitle: false, + titleKind: 'generated', }); }); @@ -183,13 +182,13 @@ describe('SessionMetadata', () => { const meta = ix.get(ISessionMetadata); await expect(meta.read()).resolves.toMatchObject({ title: 'legacy title', - isCustomTitle: true, + titleKind: 'custom', }); const fresh = createFreshMetadata(ix); await expect(fresh.read()).resolves.toMatchObject({ title: 'legacy title', - isCustomTitle: true, + titleKind: 'custom', }); }); @@ -209,16 +208,44 @@ describe('SessionMetadata', () => { const meta = ix.get(ISessionMetadata); await expect(meta.read()).resolves.toMatchObject({ title: 'renamed title', - isCustomTitle: true, + titleKind: 'custom', }); await meta.update({ archived: true }); const fresh = createFreshMetadata(ix); await expect(fresh.read()).resolves.toMatchObject({ title: 'renamed title', - isCustomTitle: true, + titleKind: 'custom', archived: true, }); + const persisted = await store.get>(META_SCOPE, 'state.json'); + expect(persisted).not.toHaveProperty('isCustomTitle'); + expect(persisted).not.toHaveProperty('customTitle'); + }); + + it('migrates a legacy non-custom title to replaceable title state', async () => { + const store = ix.get(IAtomicDocumentStore); + await store.set(META_SCOPE, 'state.json', { + id: 's1', + version: 2, + createdAt: 1700000000000, + updatedAt: 1700000000000, + archived: false, + title: 'prompt title', + isCustomTitle: false, + agents: {}, + custom: {}, + }); + + const meta = ix.get(ISessionMetadata); + + await expect(meta.read()).resolves.toMatchObject({ + title: 'prompt title', + titleKind: 'replaceable', + }); + const persisted = await store.get>(META_SCOPE, 'state.json'); + expect(persisted).toMatchObject({ title: 'prompt title', titleKind: 'replaceable' }); + expect(persisted).not.toHaveProperty('isCustomTitle'); }); it('keeps a queued custom title when a generated title is enqueued afterward', async () => { @@ -255,7 +282,7 @@ describe('SessionMetadata', () => { await expect(generated).resolves.toBe(false); await expect(meta.read()).resolves.toMatchObject({ title: 'user title', - isCustomTitle: true, + titleKind: 'custom', }); }); diff --git a/packages/agent-core-v2/test/session/sessionTitle/sessionTitleService.test.ts b/packages/agent-core-v2/test/session/sessionTitle/sessionTitleService.test.ts index 71cd9d711..95cd6ffa8 100644 --- a/packages/agent-core-v2/test/session/sessionTitle/sessionTitleService.test.ts +++ b/packages/agent-core-v2/test/session/sessionTitle/sessionTitleService.test.ts @@ -1,6 +1,6 @@ /** * Scenario: on-demand managed chat_title generation through the session-scoped - * service, including OAuth failures, legacy custom titles, request headers, + * service, including OAuth failures, title-state transitions, request headers, * and races. * Wiring: the real title service with contract fakes; only fetch crosses the * external boundary. Run with `pnpm --filter @moonshot-ai/agent-core-v2 exec @@ -100,12 +100,12 @@ class FakeSessionMetadata implements ISessionMetadata { } setTitle(title: string): Promise { - return this.update({ title, titleSource: 'custom', isCustomTitle: true }); + return this.update({ title, titleKind: 'custom' }); } async setGeneratedTitleIfUncustomized(title: string): Promise { - if (this.meta.isCustomTitle === true) return false; - await this.update({ title, titleSource: 'generated', isCustomTitle: false }); + if (this.meta.titleKind === 'custom') return false; + await this.update({ title, titleKind: 'generated' }); return true; } @@ -252,8 +252,7 @@ describe('SessionTitleService', () => { expect(title).toBe('生成的标题'); expect(metadata.meta.title).toBe('生成的标题'); - expect(metadata.meta.isCustomTitle).toBe(false); - expect(metadata.meta.titleSource).toBe('generated'); + expect(metadata.meta.titleKind).toBe('generated'); const [, init] = fetchMock.mock.calls[0]!; expect(JSON.parse(init?.body as string)).toEqual({ @@ -329,7 +328,7 @@ describe('SessionTitleService', () => { await expect(generation).resolves.toBeUndefined(); expect(metadata.meta.title).toBe('user 取的标题'); - expect(metadata.meta.isCustomTitle).toBe(true); + expect(metadata.meta.titleKind).toBe('custom'); }); it('skips generation when the current title was already generated', async () => { @@ -349,7 +348,7 @@ describe('SessionTitleService', () => { '生成的标题', ); expect(metadata.meta.title).toBe('生成的标题'); - expect(metadata.meta.titleSource).toBe('generated'); + expect(metadata.meta.titleKind).toBe('generated'); }); it('never overwrites a custom title even when forced', async () => { @@ -387,7 +386,7 @@ describe('SessionTitleService', () => { it('keeps the current title when the backend request fails', async () => { fetchMock.mockImplementationOnce(async () => new Response('', { status: 500 })); titlePrompts = ['hello']; - await metadata.update({ title: 'hello', isCustomTitle: false }); + await metadata.update({ title: 'hello', titleKind: 'replaceable' }); await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined(); expect(metadata.meta.title).toBe('hello'); @@ -479,29 +478,6 @@ describe('SessionTitleService', () => { expect(resolvedOAuthRefs[0]?.key).not.toBe(MANAGED_PROVIDER.oauth?.key); }); - it('does not generate over a legacy customTitle', async () => { - metadata.meta = { - ...metadata.meta, - customTitle: 'legacy title', - } as SessionMeta; - - await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBeUndefined(); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it('trusts a modern non-custom title over a stale legacy customTitle', async () => { - metadata.meta = { - ...metadata.meta, - title: 'easy title', - isCustomTitle: false, - customTitle: 'stale legacy title', - } as SessionMeta; - titlePrompts = ['hello']; - - await expect(ix.get(ISessionTitleService).generateTitle()).resolves.toBe('生成的标题'); - expect(metadata.meta.title).toBe('生成的标题'); - }); - it('shares an in-flight generation between concurrent requests', async () => { const pendingFetch = createPendingFetch(); fetchMock.mockImplementationOnce(pendingFetch.fetch); diff --git a/packages/klient/src/contract/global/events.ts b/packages/klient/src/contract/global/events.ts index cdf0a9594..20ba36f07 100644 --- a/packages/klient/src/contract/global/events.ts +++ b/packages/klient/src/contract/global/events.ts @@ -30,7 +30,7 @@ export interface SessionMetaUpdatedPayload { readonly patch: { readonly title?: string; readonly isCustomTitle?: boolean; - readonly lastPrompt: string; + readonly lastPrompt?: string; }; } @@ -75,7 +75,7 @@ const sessionMetaUpdatedSchema = z.object({ isCustomTitle: z.boolean().optional(), lastPrompt: z.string().optional(), }), -}); +}) satisfies z.ZodType; export const catalogChangedSchema = z.object({ changed: z.array( diff --git a/packages/klient/src/contract/session/metadata.ts b/packages/klient/src/contract/session/metadata.ts index 9ea5c62a8..ae0112c2e 100644 --- a/packages/klient/src/contract/session/metadata.ts +++ b/packages/klient/src/contract/session/metadata.ts @@ -22,8 +22,7 @@ export const sessionMetaSchema = z.object({ id: z.string(), version: z.number().optional(), title: z.string().optional(), - titleSource: z.enum(['prompt', 'generated', 'custom']).optional(), - isCustomTitle: z.boolean().optional(), + titleKind: z.enum(['replaceable', 'generated', 'custom']).optional(), lastPrompt: z.string().optional(), createdAt: z.number(), updatedAt: z.number(), @@ -38,8 +37,7 @@ export const sessionMetaSchema = z.object({ export const sessionMetaPatchSchema = z.object({ version: z.number().optional(), title: z.string().optional(), - titleSource: z.enum(['prompt', 'generated', 'custom']).optional(), - isCustomTitle: z.boolean().optional(), + titleKind: z.enum(['replaceable', 'generated', 'custom']).optional(), lastPrompt: z.string().optional(), updatedAt: z.number().optional(), archived: z.boolean().optional(), @@ -54,8 +52,7 @@ export const sessionMetaKeySchema = z.enum([ 'id', 'version', 'title', - 'titleSource', - 'isCustomTitle', + 'titleKind', 'lastPrompt', 'createdAt', 'updatedAt', diff --git a/packages/node-sdk/src/v2/session-mapper.ts b/packages/node-sdk/src/v2/session-mapper.ts index 293f67a55..8e70977b6 100644 --- a/packages/node-sdk/src/v2/session-mapper.ts +++ b/packages/node-sdk/src/v2/session-mapper.ts @@ -69,7 +69,7 @@ export function v2MetaToSessionMeta(meta: V2SessionMeta): SessionMeta { createdAt: new Date(meta.createdAt).toISOString(), updatedAt: new Date(meta.updatedAt).toISOString(), title: meta.title ?? '', - isCustomTitle: meta.isCustomTitle ?? false, + isCustomTitle: meta.titleKind === 'custom', lastPrompt: meta.lastPrompt, forkedFrom: meta.forkedFrom, workDir: meta.cwd,