mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(ai-vault): scope graph session title metadata
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
import {
|
||||
applyGraphSessionTitle,
|
||||
cloneGraphSessionTitleState,
|
||||
type GraphSessionAgent,
|
||||
type GraphSessionTitleState
|
||||
} from './session-scanner-graph-session-title'
|
||||
import {
|
||||
@@ -174,7 +175,7 @@ export function rovoPartsText(parts: unknown[], role: 'user' | 'assistant'): str
|
||||
// Agents whose transcripts are append-only message-graph JSONL (session +
|
||||
// model_change + message records). OMP and Prime Agent are Pi forks and
|
||||
// share the format.
|
||||
export type MessageGraphAgent = 'openclaw' | 'pi' | 'omp' | 'prime-agent'
|
||||
export type MessageGraphAgent = GraphSessionAgent
|
||||
|
||||
export async function parseMessageGraphSessionFile(
|
||||
agent: MessageGraphAgent,
|
||||
@@ -255,6 +256,7 @@ export function createMessageGraphSessionResumeState(
|
||||
file: FileWithMtime
|
||||
): ResumableSessionParseState {
|
||||
const resume = createMessageGraphResumeFrom({
|
||||
agent,
|
||||
accumulator: createAccumulator({ agent, file, sessionId: sessionIdFromFileName(file.path) }),
|
||||
source: null
|
||||
})
|
||||
|
||||
@@ -31,6 +31,8 @@ const firstPrompt = {
|
||||
message: { role: 'user', content: 'first user prompt' }
|
||||
}
|
||||
|
||||
const piFamilyAgents = ['pi', 'openclaw', 'prime-agent'] as const
|
||||
|
||||
describe('message-graph harness session titles', () => {
|
||||
it('uses the latest Pi session_info.name ahead of the first user prompt', async () => {
|
||||
const session = await parseGraph('pi', [
|
||||
@@ -60,6 +62,31 @@ describe('message-graph harness session titles', () => {
|
||||
expect(session?.title).toBe('first user prompt')
|
||||
})
|
||||
|
||||
it.each(piFamilyAgents)(
|
||||
'uses %s session_info.name but ignores OMP-only title records',
|
||||
async (agent) => {
|
||||
const session = await parseGraph(agent, [
|
||||
{ type: 'session', id: `${agent}-session`, timestamp: '2026-05-01T10:00:00.000Z' },
|
||||
firstPrompt,
|
||||
{ type: 'session_info', name: `${agent} name` },
|
||||
{ type: 'title', title: 'wrong slot title', source: 'user' },
|
||||
{ type: 'session', id: `${agent}-session`, title: 'wrong header title' },
|
||||
{ type: 'title_change', title: 'wrong changed title', source: 'user' },
|
||||
{ type: 'session_info', title: 'wrong session info title' }
|
||||
])
|
||||
expect(session?.title).toBe(`${agent} name`)
|
||||
}
|
||||
)
|
||||
|
||||
it('accepts legacy OMP session_info.name but rejects session_info.title', async () => {
|
||||
const [unsupported, legacy] = await Promise.all([
|
||||
parseGraph('omp', [firstPrompt, { type: 'session_info', title: 'unsupported title' }]),
|
||||
parseGraph('omp', [firstPrompt, { type: 'session_info', name: 'legacy OMP name' }])
|
||||
])
|
||||
expect(unsupported?.title).toBe('first user prompt')
|
||||
expect(legacy?.title).toBe('legacy OMP name')
|
||||
})
|
||||
|
||||
it('uses the OMP session header title ahead of the first user prompt', async () => {
|
||||
const session = await parseGraph('omp', [
|
||||
{
|
||||
@@ -127,7 +154,26 @@ describe('message-graph harness session titles', () => {
|
||||
expect(session?.title).toBe('useful title')
|
||||
})
|
||||
|
||||
it('uses a user title slot even after a later first prompt', async () => {
|
||||
it('lets a later automatic OMP title update a source-less legacy header', async () => {
|
||||
const session = await parseGraph('omp', [
|
||||
{
|
||||
type: 'session',
|
||||
id: 'omp-session',
|
||||
title: 'legacy automatic title',
|
||||
timestamp: '2026-05-01T10:00:00.000Z'
|
||||
},
|
||||
firstPrompt,
|
||||
{
|
||||
type: 'title_change',
|
||||
title: 'new automatic title',
|
||||
source: 'auto',
|
||||
timestamp: '2026-05-01T10:00:30.000Z'
|
||||
}
|
||||
])
|
||||
expect(session?.title).toBe('new automatic title')
|
||||
})
|
||||
|
||||
it('keeps a user title slot ahead of a stale source-less header', async () => {
|
||||
const session = await parseGraph('omp', [
|
||||
{
|
||||
type: 'title',
|
||||
@@ -142,6 +188,7 @@ describe('message-graph harness session titles', () => {
|
||||
version: 3,
|
||||
id: 'omp-session',
|
||||
cwd: '/tmp/omp',
|
||||
title: 'stale automatic title',
|
||||
timestamp: '2026-05-01T10:00:00.000Z'
|
||||
},
|
||||
firstPrompt
|
||||
|
||||
@@ -2,28 +2,29 @@ import { cloneSessionAccumulator } from './session-scanner-accumulator'
|
||||
import type { SessionAccumulator } from './session-scanner-types'
|
||||
import { extractString, normalizeTitleText } from './session-scanner-values'
|
||||
|
||||
export type GraphSessionAgent = 'openclaw' | 'pi' | 'omp' | 'prime-agent'
|
||||
export type GraphSessionTitleSource = 'user' | 'auto'
|
||||
|
||||
export type GraphSessionTitleState = {
|
||||
agent: GraphSessionAgent
|
||||
accumulator: SessionAccumulator
|
||||
source: GraphSessionTitleSource | null
|
||||
}
|
||||
|
||||
export function cloneGraphSessionTitleState(state: GraphSessionTitleState): GraphSessionTitleState {
|
||||
return {
|
||||
agent: state.agent,
|
||||
accumulator: cloneSessionAccumulator(state.accumulator),
|
||||
source: state.source
|
||||
}
|
||||
}
|
||||
|
||||
// Why: Pi `/name` and OMP `/rename` persist as harness metadata, not as the
|
||||
// first user prompt. A later user title must replace auto/prompt titles, and
|
||||
// an automatic title must never clobber a user title.
|
||||
// Harness names are persisted metadata; explicit names outrank automatic and prompt fallbacks.
|
||||
export function applyGraphSessionTitle(
|
||||
state: GraphSessionTitleState,
|
||||
record: Record<string, unknown>
|
||||
): void {
|
||||
const incoming = graphSessionTitleFromRecord(record)
|
||||
const incoming = graphSessionTitleFromRecord(state.agent, record)
|
||||
if (!incoming) {
|
||||
return
|
||||
}
|
||||
@@ -35,16 +36,15 @@ export function applyGraphSessionTitle(
|
||||
}
|
||||
|
||||
function graphSessionTitleFromRecord(
|
||||
agent: GraphSessionAgent,
|
||||
record: Record<string, unknown>
|
||||
): { title: string; source: GraphSessionTitleSource } | null {
|
||||
const type = extractString(record.type)
|
||||
if (type === 'session_info') {
|
||||
const title = normalizeTitleText(
|
||||
extractString(record.name) ?? extractString(record.title) ?? ''
|
||||
)
|
||||
const title = normalizeTitleText(extractString(record.name) ?? '')
|
||||
return title ? { title, source: 'user' } : null
|
||||
}
|
||||
if (type !== 'title' && type !== 'title_change' && type !== 'session') {
|
||||
if (agent !== 'omp' || (type !== 'title' && type !== 'title_change' && type !== 'session')) {
|
||||
return null
|
||||
}
|
||||
const title = normalizeTitleText(extractString(record.title) ?? '')
|
||||
@@ -52,5 +52,8 @@ function graphSessionTitleFromRecord(
|
||||
return null
|
||||
}
|
||||
const raw = extractString(record.source) ?? extractString(record.titleSource)
|
||||
if (raw !== null && raw !== 'user' && raw !== 'auto') {
|
||||
return null
|
||||
}
|
||||
return { title, source: raw === 'user' ? 'user' : 'auto' }
|
||||
}
|
||||
|
||||
@@ -169,6 +169,13 @@ export function piFixture(): IncrementalAgentFixture {
|
||||
})
|
||||
],
|
||||
appendLines: [
|
||||
JSON.stringify({
|
||||
type: 'session_info',
|
||||
id: 'pi-name-1',
|
||||
parentId: null,
|
||||
name: 'Pi renamed session',
|
||||
timestamp: '2026-05-01T10:00:20.000Z'
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'model_change',
|
||||
modelId: 'pi-2',
|
||||
@@ -192,8 +199,8 @@ export function piFixture(): IncrementalAgentFixture {
|
||||
|
||||
// OMP is a Pi fork sharing the message-graph format, but keys the model on
|
||||
// `model` (not Pi's `modelId`); its own fixture exercises the registry's 'omp'
|
||||
// branch and that model-key difference. The header title outranks the first
|
||||
// user prompt, matching a real `/rename`.
|
||||
// branch and that model-key difference. The appended title change mirrors a
|
||||
// persisted `/rename` after the initial scan.
|
||||
export function ompFixture(): IncrementalAgentFixture {
|
||||
return {
|
||||
agent: 'omp',
|
||||
@@ -204,7 +211,8 @@ export function ompFixture(): IncrementalAgentFixture {
|
||||
version: 3,
|
||||
id: 'cccccccc-dddd-4eee-8fff-000000000000',
|
||||
cwd: '/repo/app',
|
||||
title: 'OMP session title',
|
||||
title: 'OMP automatic title',
|
||||
titleSource: 'auto',
|
||||
timestamp: '2026-05-01T10:00:00.000Z'
|
||||
}),
|
||||
JSON.stringify({
|
||||
@@ -219,6 +227,15 @@ export function ompFixture(): IncrementalAgentFixture {
|
||||
})
|
||||
],
|
||||
appendLines: [
|
||||
JSON.stringify({
|
||||
type: 'title_change',
|
||||
id: 'omp-title-1',
|
||||
parentId: null,
|
||||
title: 'OMP renamed session',
|
||||
previousTitle: 'OMP automatic title',
|
||||
source: 'user',
|
||||
timestamp: '2026-05-01T10:00:30.000Z'
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'message',
|
||||
message: {
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
codexFixture,
|
||||
codexWorkerFixtureLines
|
||||
} from './session-scanner-codex-fixtures'
|
||||
import { allIncrementalAgentFixtures } from './session-scanner-incremental-fixtures'
|
||||
import {
|
||||
allIncrementalAgentFixtures,
|
||||
ompFixture,
|
||||
piFixture
|
||||
} from './session-scanner-incremental-fixtures'
|
||||
import {
|
||||
createSessionParseStats,
|
||||
parseAgentSessionFileCached,
|
||||
@@ -18,6 +22,11 @@ import type { SessionFileCandidate } from './session-scanner-types'
|
||||
|
||||
let tempRoots: string[] = []
|
||||
|
||||
const titleMetadataFixtures = [
|
||||
{ name: 'pi', fixture: piFixture(), expectedTitle: 'Pi renamed session' },
|
||||
{ name: 'omp', fixture: ompFixture(), expectedTitle: 'OMP renamed session' }
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
resetSessionParseCacheForTests()
|
||||
})
|
||||
@@ -136,6 +145,78 @@ describe.each(allIncrementalAgentFixtures())('incremental parse parity: $agent',
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(titleMetadataFixtures)('incremental title metadata: $name', (entry) => {
|
||||
it('adopts a harness rename appended after the cached scan', async () => {
|
||||
const root = await makeTempDir()
|
||||
const path = join(root, entry.fixture.fileName)
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
await writeFile(path, `${entry.fixture.seedLines.join('\n')}\n`)
|
||||
await parseAgentSessionFileCached(
|
||||
await candidateFor(entry.fixture.agent, path),
|
||||
process.platform
|
||||
)
|
||||
|
||||
await appendFile(path, `${entry.fixture.appendLines.join('\n')}\n`)
|
||||
const grownCandidate = await candidateFor(entry.fixture.agent, path)
|
||||
const stats = createSessionParseStats()
|
||||
const incremental = await parseAgentSessionFileCached(grownCandidate, process.platform, stats)
|
||||
expect(stats.incremental).toBe(1)
|
||||
expect(incremental?.title).toBe(entry.expectedTitle)
|
||||
expect(incremental).toEqual(await parseAgentSessionFile(grownCandidate, process.platform))
|
||||
})
|
||||
})
|
||||
|
||||
describe('incremental OMP title precedence', () => {
|
||||
it('keeps a user title ahead of an appended automatic title', async () => {
|
||||
const root = await makeTempDir()
|
||||
const path = join(root, ompFixture().fileName)
|
||||
await writeFile(
|
||||
path,
|
||||
`${[
|
||||
JSON.stringify({
|
||||
type: 'title',
|
||||
v: 1,
|
||||
title: 'Persisted user title',
|
||||
source: 'user',
|
||||
updatedAt: '2026-05-01T10:00:00.000Z',
|
||||
pad: ''
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'session',
|
||||
id: 'omp-session',
|
||||
cwd: '/repo/app',
|
||||
title: 'Stale source-less title',
|
||||
timestamp: '2026-05-01T10:00:00.000Z'
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'message',
|
||||
message: { role: 'user', content: 'first prompt' },
|
||||
timestamp: '2026-05-01T10:00:05.000Z'
|
||||
})
|
||||
].join('\n')}\n`
|
||||
)
|
||||
await parseAgentSessionFileCached(await candidateFor('omp', path), process.platform)
|
||||
|
||||
await appendFile(
|
||||
path,
|
||||
`${JSON.stringify({
|
||||
type: 'title_change',
|
||||
title: 'Later automatic title',
|
||||
source: 'auto',
|
||||
timestamp: '2026-05-01T10:00:30.000Z'
|
||||
})}\n`
|
||||
)
|
||||
const stats = createSessionParseStats()
|
||||
const incremental = await parseAgentSessionFileCached(
|
||||
await candidateFor('omp', path),
|
||||
process.platform,
|
||||
stats
|
||||
)
|
||||
expect(stats.incremental).toBe(1)
|
||||
expect(incremental?.title).toBe('Persisted user title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('codex-specific resume behavior', () => {
|
||||
it('keeps rejecting worker sessions across incremental appends', async () => {
|
||||
const root = await makeTempDir()
|
||||
|
||||
Reference in New Issue
Block a user