Skip to content

Commit 9cc0f2f

Browse files
committed
fix: preserve fork title across metadata updates
1 parent b7fdfe2 commit 9cc0f2f

4 files changed

Lines changed: 103 additions & 48 deletions

File tree

hub/src/socket/handlers/cli/sessionHandlers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto'
44
import type { CodexCollaborationMode, PermissionMode } from '@hapi/protocol/types'
55
import type { Store, StoredSession } from '../../../store'
66
import type { SyncEvent } from '../../../sync/syncEngine'
7+
import { mergeSessionMetadata } from '../../../sync/sessionMetadata'
78
import { extractTodoWriteTodosFromMessageContent } from '../../../sync/todos'
89
import { extractTeamStateFromMessageContent, applyTeamStateDelta } from '../../../sync/teams'
910
import type { CliSocketWithData } from '../../socketTypes'
@@ -154,9 +155,12 @@ export function registerSessionHandlers(socket: CliSocketWithData, deps: Session
154155
return
155156
}
156157

158+
const currentSession = store.sessions.getSessionByNamespace(sid, sessionAccess.value.namespace)
159+
const mergedMetadata = mergeSessionMetadata(currentSession?.metadata ?? null, metadata)
160+
157161
const result = store.sessions.updateSessionMetadata(
158162
sid,
159-
metadata,
163+
mergedMetadata,
160164
expectedVersion,
161165
sessionAccess.value.namespace
162166
)
@@ -176,7 +180,7 @@ export function registerSessionHandlers(socket: CliSocketWithData, deps: Session
176180
body: {
177181
t: 'update-session' as const,
178182
sid,
179-
metadata: { version: result.version, value: metadata },
183+
metadata: { version: result.version, value: mergedMetadata },
180184
agentState: null
181185
}
182186
}

hub/src/sync/sessionCache.ts

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { CodexCollaborationMode, PermissionMode, Session } from '@hapi/prot
33
import type { Store } from '../store'
44
import { clampAliveTime } from './aliveTime'
55
import { EventPublisher } from './eventPublisher'
6+
import { mergeSessionMetadata } from './sessionMetadata'
67
import { extractTodoWriteTodosFromMessageContent, TodosSchema } from './todos'
78

89
export class SessionCache {
@@ -340,7 +341,7 @@ export class SessionCache {
340341
throw new Error('Session not found')
341342
}
342343

343-
const mergedMetadata = this.mergeSessionMetadata(source.metadata ?? null, target.metadata ?? null)
344+
const mergedMetadata = mergeSessionMetadata(source.metadata ?? null, target.metadata ?? null)
344345
if (mergedMetadata === target.metadata) {
345346
return
346347
}
@@ -399,7 +400,7 @@ export class SessionCache {
399400

400401
this.store.messages.mergeSessionMessages(oldSessionId, newSessionId)
401402

402-
const mergedMetadata = this.mergeSessionMetadata(oldStored.metadata, newStored.metadata)
403+
const mergedMetadata = mergeSessionMetadata(oldStored.metadata, newStored.metadata)
403404
if (mergedMetadata !== null && mergedMetadata !== newStored.metadata) {
404405
for (let attempt = 0; attempt < 2; attempt += 1) {
405406
const latest = this.store.sessions.getSessionByNamespace(newSessionId, namespace)
@@ -470,48 +471,4 @@ export class SessionCache {
470471

471472
this.refreshSession(newSessionId)
472473
}
473-
474-
private mergeSessionMetadata(oldMetadata: unknown | null, newMetadata: unknown | null): unknown | null {
475-
if (!oldMetadata || typeof oldMetadata !== 'object') {
476-
return newMetadata
477-
}
478-
if (!newMetadata || typeof newMetadata !== 'object') {
479-
return oldMetadata
480-
}
481-
482-
const oldObj = oldMetadata as Record<string, unknown>
483-
const newObj = newMetadata as Record<string, unknown>
484-
const merged: Record<string, unknown> = { ...newObj }
485-
let changed = false
486-
487-
if (typeof oldObj.name === 'string' && typeof newObj.name !== 'string') {
488-
merged.name = oldObj.name
489-
changed = true
490-
}
491-
492-
const oldSummary = oldObj.summary as { text?: unknown; updatedAt?: unknown } | undefined
493-
const newSummary = newObj.summary as { text?: unknown; updatedAt?: unknown } | undefined
494-
const oldUpdatedAt = typeof oldSummary?.updatedAt === 'number' ? oldSummary.updatedAt : null
495-
const newUpdatedAt = typeof newSummary?.updatedAt === 'number' ? newSummary.updatedAt : null
496-
if (oldUpdatedAt !== null && (newUpdatedAt === null || oldUpdatedAt > newUpdatedAt)) {
497-
merged.summary = oldSummary
498-
changed = true
499-
}
500-
501-
if (oldObj.worktree && !newObj.worktree) {
502-
merged.worktree = oldObj.worktree
503-
changed = true
504-
}
505-
506-
if (typeof oldObj.path === 'string' && typeof newObj.path !== 'string') {
507-
merged.path = oldObj.path
508-
changed = true
509-
}
510-
if (typeof oldObj.host === 'string' && typeof newObj.host !== 'string') {
511-
merged.host = oldObj.host
512-
changed = true
513-
}
514-
515-
return changed ? merged : newMetadata
516-
}
517474
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import { mergeSessionMetadata } from './sessionMetadata'
3+
4+
describe('mergeSessionMetadata', () => {
5+
it('preserves custom name when new metadata omits it', () => {
6+
expect(mergeSessionMetadata(
7+
{ path: '/tmp/project', host: 'localhost', name: '自定义标题' },
8+
{ path: '/tmp/project', host: 'localhost', codexSessionId: 'thread-2' }
9+
)).toEqual({
10+
path: '/tmp/project',
11+
host: 'localhost',
12+
codexSessionId: 'thread-2',
13+
name: '自定义标题'
14+
})
15+
})
16+
17+
it('preserves newer summary when new metadata omits it', () => {
18+
expect(mergeSessionMetadata(
19+
{
20+
path: '/tmp/project',
21+
host: 'localhost',
22+
summary: { text: '原标题', updatedAt: 200 }
23+
},
24+
{ path: '/tmp/project', host: 'localhost', codexSessionId: 'thread-2' }
25+
)).toEqual({
26+
path: '/tmp/project',
27+
host: 'localhost',
28+
codexSessionId: 'thread-2',
29+
summary: { text: '原标题', updatedAt: 200 }
30+
})
31+
})
32+
33+
it('keeps newer incoming summary when it is fresher', () => {
34+
expect(mergeSessionMetadata(
35+
{
36+
path: '/tmp/project',
37+
host: 'localhost',
38+
summary: { text: '旧标题', updatedAt: 100 }
39+
},
40+
{
41+
path: '/tmp/project',
42+
host: 'localhost',
43+
summary: { text: '新标题', updatedAt: 200 }
44+
}
45+
)).toEqual({
46+
path: '/tmp/project',
47+
host: 'localhost',
48+
summary: { text: '新标题', updatedAt: 200 }
49+
})
50+
})
51+
})

hub/src/sync/sessionMetadata.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export function mergeSessionMetadata(oldMetadata: unknown | null, newMetadata: unknown | null): unknown | null {
2+
if (!oldMetadata || typeof oldMetadata !== 'object') {
3+
return newMetadata
4+
}
5+
if (!newMetadata || typeof newMetadata !== 'object') {
6+
return oldMetadata
7+
}
8+
9+
const oldObj = oldMetadata as Record<string, unknown>
10+
const newObj = newMetadata as Record<string, unknown>
11+
const merged: Record<string, unknown> = { ...newObj }
12+
let changed = false
13+
14+
if (typeof oldObj.name === 'string' && typeof newObj.name !== 'string') {
15+
merged.name = oldObj.name
16+
changed = true
17+
}
18+
19+
const oldSummary = oldObj.summary as { text?: unknown; updatedAt?: unknown } | undefined
20+
const newSummary = newObj.summary as { text?: unknown; updatedAt?: unknown } | undefined
21+
const oldUpdatedAt = typeof oldSummary?.updatedAt === 'number' ? oldSummary.updatedAt : null
22+
const newUpdatedAt = typeof newSummary?.updatedAt === 'number' ? newSummary.updatedAt : null
23+
if (oldUpdatedAt !== null && (newUpdatedAt === null || oldUpdatedAt > newUpdatedAt)) {
24+
merged.summary = oldSummary
25+
changed = true
26+
}
27+
28+
if (oldObj.worktree && !newObj.worktree) {
29+
merged.worktree = oldObj.worktree
30+
changed = true
31+
}
32+
33+
if (typeof oldObj.path === 'string' && typeof newObj.path !== 'string') {
34+
merged.path = oldObj.path
35+
changed = true
36+
}
37+
if (typeof oldObj.host === 'string' && typeof newObj.host !== 'string') {
38+
merged.host = oldObj.host
39+
changed = true
40+
}
41+
42+
return changed ? merged : newMetadata
43+
}

0 commit comments

Comments
 (0)