mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(cli): mark synthetic SDK user messages as meta (#1209)
Claude Code injects its own user-role turns for skill bodies and compact continuation summaries. The on-disk transcript flags them `isMeta`, which claudeLocalLauncher drops before they ever reach the web UI. Over stream-json the same event is flagged `isSynthetic` instead, and sdkToLogConverter copied only `message`, dropping the flag entirely. With no `isMeta` on the converted line, every downstream guard let it through: OutgoingMessageQueue forwarded it, isExternalUserMessage classified it as genuine human input (its XML-prefix allowlist does not match a bare-markdown skill body), and the web UI rendered the full skill document as a user bubble. Normalize `isSynthetic` to `isMeta` in the converter so the SDK path carries the same signal as the transcript path and the existing filters fire. Fixes skill injections appearing as user messages in remote mode.
This commit is contained in:
@@ -17,6 +17,13 @@ export interface SDKMessage {
|
||||
export interface SDKUserMessage extends SDKMessage {
|
||||
type: 'user'
|
||||
parent_tool_use_id?: string
|
||||
/**
|
||||
* Set by Claude Code on user-role messages it injects itself (skill bodies,
|
||||
* compact continuation summaries) rather than relaying from the human. The
|
||||
* on-disk transcript spells the same thing `isMeta`.
|
||||
*/
|
||||
isSynthetic?: boolean
|
||||
isMeta?: boolean
|
||||
message: {
|
||||
role: 'user'
|
||||
content: string | Array<{
|
||||
|
||||
@@ -69,6 +69,52 @@ describe('SDKToLogConverter', () => {
|
||||
expect(logMessage?.type).toBe('user')
|
||||
expect((logMessage as any).message.content).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should mark synthetic user messages as meta', () => {
|
||||
// Skill injections arrive over stream-json as plain-text user messages
|
||||
// flagged with isSynthetic. The on-disk transcript flags the same event
|
||||
// with isMeta, which is what every downstream filter looks for.
|
||||
const sdkMessage: SDKUserMessage = {
|
||||
type: 'user',
|
||||
isSynthetic: true,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Base directory for this skill: /home/user/.claude/skills/foo' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const logMessage = converter.convert(sdkMessage)
|
||||
|
||||
expect(logMessage?.type).toBe('user')
|
||||
expect(logMessage?.isMeta).toBe(true)
|
||||
})
|
||||
|
||||
it('should preserve isMeta on user messages', () => {
|
||||
const sdkMessage: SDKUserMessage = {
|
||||
type: 'user',
|
||||
isMeta: true,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: 'injected context'
|
||||
}
|
||||
}
|
||||
|
||||
expect(converter.convert(sdkMessage)?.isMeta).toBe(true)
|
||||
})
|
||||
|
||||
it('should not mark genuine user messages as meta', () => {
|
||||
const sdkMessage: SDKUserMessage = {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: 'Hello Claude'
|
||||
}
|
||||
}
|
||||
|
||||
expect(converter.convert(sdkMessage)?.isMeta).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Assistant messages', () => {
|
||||
|
||||
@@ -271,6 +271,16 @@ export class SDKToLogConverter {
|
||||
message: userMsg.message
|
||||
}
|
||||
|
||||
// Claude Code injects its own user-role turns (skill bodies, compact
|
||||
// continuation summaries). Over stream-json they are flagged
|
||||
// `isSynthetic`, while the on-disk transcript the local launcher reads
|
||||
// flags them `isMeta`. Normalize to `isMeta` so both paths hit the same
|
||||
// downstream filters — otherwise the injected text reaches the web UI
|
||||
// and is rendered as if the human had typed it.
|
||||
if (userMsg.isSynthetic === true || userMsg.isMeta === true) {
|
||||
logMessage.isMeta = true
|
||||
}
|
||||
|
||||
// Check if this is a tool result and add mode if available
|
||||
if (Array.isArray(userMsg.message.content)) {
|
||||
for (const content of userMsg.message.content) {
|
||||
|
||||
Reference in New Issue
Block a user