mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Preserve goal command breadcrumbs for history restore
Desktop history can only render the original /goal prompt if the session API returns the raw system local_command transcript entries. The prior fix handled frontend rendering, but the server dropped those entries because they do not carry message.role. This keeps only /goal local command input and goal-like output records in getSessionMessages while leaving other internal command breadcrumbs filtered. Constraint: Real CLI transcripts store /goal input/output as system local_command records without message.role. Rejected: Expose all local_command records | would leak unrelated internal command breadcrumbs such as /exit. Confidence: high Scope-risk: narrow Directive: Keep this allowlist goal-specific unless another command explicitly needs visible history restore. Tested: bun test src/server/__tests__/sessions.test.ts Tested: bun test src/commands/goal/goal.test.tsx src/goals/goalState.test.ts src/goals/goalEvaluator.test.ts Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts src/components/chat/MessageList.test.tsx src/pages/ActiveSession.test.tsx Tested: cd desktop && bun run lint Tested: real sessionService.getSessionMessages for 3e9117d5-b792-43c9-bf57-7aec2b124f7e returns initial /goal local_command entries Not-tested: Packaged app rebuild after this server API fix.
This commit is contained in:
parent
1e0e5bca0f
commit
052cc4da5d
@ -739,6 +739,61 @@ describe('SessionService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep /goal local command transcript entries for desktop history restore', async () => {
|
||||
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
await writeSessionFile('-tmp-project', sessionId, [
|
||||
makeSnapshotEntry(),
|
||||
{
|
||||
parentUuid: null,
|
||||
isSidechain: false,
|
||||
type: 'system',
|
||||
subtype: 'local_command',
|
||||
content: '<command-name>/goal</command-name>\n<command-message>goal</command-message>\n<command-args>ship persisted goal</command-args>',
|
||||
level: 'info',
|
||||
timestamp: '2026-01-01T00:00:01.000Z',
|
||||
uuid: 'goal-command',
|
||||
},
|
||||
{
|
||||
parentUuid: 'goal-command',
|
||||
isSidechain: false,
|
||||
type: 'system',
|
||||
subtype: 'local_command',
|
||||
content: [
|
||||
'<local-command-stdout>',
|
||||
'Goal created.',
|
||||
'Goal: active',
|
||||
'Objective: ship persisted goal',
|
||||
'Budget: 0 / unlimited tokens',
|
||||
'Elapsed: 0s',
|
||||
'Continuations: 0',
|
||||
'</local-command-stdout>',
|
||||
].join('\n'),
|
||||
level: 'info',
|
||||
timestamp: '2026-01-01T00:00:02.000Z',
|
||||
uuid: 'goal-output',
|
||||
},
|
||||
makeAssistantEntry('正常助手消息', crypto.randomUUID()),
|
||||
])
|
||||
|
||||
const messages = await service.getSessionMessages(sessionId)
|
||||
|
||||
expect(messages).toMatchObject([
|
||||
{
|
||||
id: 'goal-command',
|
||||
type: 'system',
|
||||
content: expect.stringContaining('<command-name>/goal</command-name>'),
|
||||
},
|
||||
{
|
||||
id: 'goal-output',
|
||||
type: 'system',
|
||||
content: expect.stringContaining('Goal created.'),
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should hide task-notification turns and their automatic responses from history', async () => {
|
||||
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
const firstUserId = crypto.randomUUID()
|
||||
|
||||
@ -161,6 +161,8 @@ export type TranscriptContextEstimate = {
|
||||
/** Raw entry parsed from a single JSONL line */
|
||||
type RawEntry = {
|
||||
type?: string
|
||||
subtype?: string
|
||||
content?: unknown
|
||||
uuid?: string
|
||||
messageId?: string
|
||||
parentUuid?: string | null
|
||||
@ -556,6 +558,49 @@ export class SessionService {
|
||||
return false
|
||||
}
|
||||
|
||||
private isGoalLocalCommandOutput(output: string): boolean {
|
||||
const trimmed = output.trim()
|
||||
return (
|
||||
trimmed.startsWith('Goal created.\n') ||
|
||||
trimmed.startsWith('Goal replaced.\n') ||
|
||||
trimmed.startsWith('Goal: ') ||
|
||||
trimmed === 'Goal cleared.' ||
|
||||
trimmed === 'Goal marked complete.' ||
|
||||
trimmed === 'No active goal.' ||
|
||||
trimmed === 'No goal to resume.'
|
||||
)
|
||||
}
|
||||
|
||||
private isGoalLocalCommandEntry(entry: RawEntry): boolean {
|
||||
if (
|
||||
entry.type !== 'system' ||
|
||||
entry.subtype !== 'local_command' ||
|
||||
typeof entry.content !== 'string'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const commandName = this.readXmlTag(entry.content, 'command-name')?.replace(/^\//, '')
|
||||
if (commandName) return commandName === 'goal'
|
||||
|
||||
const output =
|
||||
this.readXmlTag(entry.content, 'local-command-stdout') ??
|
||||
this.readXmlTag(entry.content, 'local-command-stderr')
|
||||
return output ? this.isGoalLocalCommandOutput(output) : false
|
||||
}
|
||||
|
||||
private goalLocalCommandEntryToMessage(entry: RawEntry): MessageEntry | null {
|
||||
if (!this.isGoalLocalCommandEntry(entry)) return null
|
||||
return {
|
||||
id: entry.uuid || crypto.randomUUID(),
|
||||
type: 'system',
|
||||
content: entry.content,
|
||||
timestamp: entry.timestamp || new Date().toISOString(),
|
||||
parentUuid: entry.parentUuid ?? undefined,
|
||||
isSidechain: entry.isSidechain,
|
||||
}
|
||||
}
|
||||
|
||||
private extractAgentToolUseId(entry: RawEntry): string | undefined {
|
||||
const content = entry.message?.content
|
||||
if (!Array.isArray(content)) return undefined
|
||||
@ -1790,6 +1835,12 @@ export class SessionService {
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const goalLocalCommandMessage = this.goalLocalCommandEntryToMessage(entry)
|
||||
if (goalLocalCommandMessage) {
|
||||
messages.push(goalLocalCommandMessage)
|
||||
continue
|
||||
}
|
||||
|
||||
// Only process transcript entries (user / assistant / system with messages)
|
||||
if (!entry.message?.role) continue
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user