mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
Recover first prompt for resume sessions
This commit is contained in:
@@ -22,7 +22,16 @@ describe('isExternalUserMessage', () => {
|
||||
expect(isExternalUserMessage({ ...baseUserMsg, isSidechain: true })).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when content is an array (tool results)', () => {
|
||||
it('returns true when content is an array of text blocks', () => {
|
||||
expect(
|
||||
isExternalUserMessage({
|
||||
...baseUserMsg,
|
||||
message: { role: 'user', content: [{ type: 'text', text: 'hello array' }] },
|
||||
} as never)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when content is a non-text array (tool results)', () => {
|
||||
expect(
|
||||
isExternalUserMessage({
|
||||
...baseUserMsg,
|
||||
|
||||
@@ -48,6 +48,28 @@ const SYSTEM_INJECTION_PREFIXES = [
|
||||
'<system-reminder>',
|
||||
]
|
||||
|
||||
function extractRawUserTextContent(content: unknown): string | null {
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
if (!Array.isArray(content)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parts = content
|
||||
.map((block) => {
|
||||
if (!block || typeof block !== 'object' || Array.isArray(block)) return null
|
||||
const record = block as Record<string, unknown>
|
||||
return record.type === 'text' && typeof record.text === 'string'
|
||||
? record.text
|
||||
: null
|
||||
})
|
||||
.filter((text): text is string => text !== null)
|
||||
|
||||
return parts.length > 0 ? parts.join('\n') : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a JSONL message should be classified as a user-role message
|
||||
* (i.e., text typed by a real human) rather than an agent-role message.
|
||||
@@ -58,13 +80,14 @@ const SYSTEM_INJECTION_PREFIXES = [
|
||||
* genuine user messages, so the only reliable signal is the message content
|
||||
* itself: injected messages always start with a well-known XML tag.
|
||||
*/
|
||||
export function isExternalUserMessage(body: RawJSONLines): body is Extract<RawJSONLines, { type: 'user' }> & { message: { content: string } } {
|
||||
export function isExternalUserMessage(body: RawJSONLines): body is Extract<RawJSONLines, { type: 'user' }> {
|
||||
if (body.type !== 'user') return false
|
||||
if (typeof body.message.content !== 'string') return false
|
||||
const text = extractRawUserTextContent(body.message.content)
|
||||
if (text === null) return false
|
||||
if (body.isSidechain === true) return false
|
||||
if (body.isMeta === true) return false
|
||||
|
||||
const trimmed = body.message.content.trimStart()
|
||||
const trimmed = text.trimStart()
|
||||
for (const prefix of SYSTEM_INJECTION_PREFIXES) {
|
||||
if (trimmed.startsWith(prefix)) return false
|
||||
}
|
||||
@@ -440,7 +463,7 @@ export class ApiSessionClient extends EventEmitter {
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: body.message.content
|
||||
text: extractRawUserTextContent(body.message.content) ?? ''
|
||||
},
|
||||
meta: {
|
||||
sentFrom: 'cli'
|
||||
|
||||
@@ -1034,6 +1034,54 @@ describe('session model', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('recovers first user message from stored Claude user output events', () => {
|
||||
const store = new Store(':memory:')
|
||||
const engine = new SyncEngine(
|
||||
store,
|
||||
{} as never,
|
||||
new RpcRegistry(),
|
||||
{ broadcast() {} } as never
|
||||
)
|
||||
|
||||
try {
|
||||
const session = engine.getOrCreateSession(
|
||||
'local-resume-first-claude-output',
|
||||
{
|
||||
path: '/tmp/project',
|
||||
host: 'localhost',
|
||||
machineId: 'machine-1',
|
||||
flavor: 'claude',
|
||||
claudeSessionId: '11111111-1111-4111-8111-111111111111',
|
||||
name: 'Generated title'
|
||||
},
|
||||
null,
|
||||
'default'
|
||||
)
|
||||
store.messages.addMessage(session.id, {
|
||||
role: 'agent',
|
||||
content: {
|
||||
type: 'output',
|
||||
data: {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'First Claude prompt' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const sessions = engine.listLocalResumableSessions('default', { machineId: 'machine-1' })
|
||||
|
||||
expect(sessions.find((item) => item.sessionId === session.id)).toMatchObject({
|
||||
name: 'Generated title',
|
||||
firstUserMessage: 'First Claude prompt'
|
||||
})
|
||||
} finally {
|
||||
engine.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('local handoff succeeds immediately for inactive sessions', async () => {
|
||||
const store = new Store(':memory:')
|
||||
const engine = new SyncEngine(
|
||||
|
||||
@@ -98,6 +98,19 @@ function extractUserMessageText(content: unknown): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function extractClaudeUserMessageTextFromAgentOutput(content: unknown): string | undefined {
|
||||
const record = asRecord(content)
|
||||
if (record?.type !== 'output') return undefined
|
||||
|
||||
const data = asRecord(record.data)
|
||||
if (data?.type !== 'user') return undefined
|
||||
|
||||
const message = asRecord(data.message)
|
||||
if (message?.role !== 'user') return undefined
|
||||
|
||||
return extractUserMessageText(message.content)
|
||||
}
|
||||
|
||||
export class SyncEngine {
|
||||
private readonly eventPublisher: EventPublisher
|
||||
private readonly sessionCache: SessionCache
|
||||
@@ -569,9 +582,11 @@ export class SyncEngine {
|
||||
private resolveFirstUserMessage(sessionId: string): string | undefined {
|
||||
for (const message of this.store.messages.getFirstMessages(sessionId, 50)) {
|
||||
const roleWrapped = unwrapRoleWrappedRecordEnvelope(message.content)
|
||||
if (roleWrapped?.role !== 'user') continue
|
||||
|
||||
const text = extractUserMessageText(roleWrapped.content)
|
||||
const text = roleWrapped?.role === 'user'
|
||||
? extractUserMessageText(roleWrapped.content)
|
||||
: roleWrapped?.role === 'agent'
|
||||
? extractClaudeUserMessageTextFromAgentOutput(roleWrapped.content)
|
||||
: undefined
|
||||
if (text) return text
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user