Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/desktop/src/plugins/hermes-bots/group-chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,22 @@ describe('gateway mirror', () => {
expect(log.length).toBeLessThanOrEqual(16)
expect(log.at(-1)?.text).toMatch(/^99:/)
expect(log.at(-1)?.text.length).toBeLessThanOrEqual(1200)
expect(log.at(-1)?.truncated).toBe(true)
expect(log.at(-1)?.text).toContain('[truncated]')
expect(log.at(-1)?.text).not.toContain(long)
})

it('marks truncated sync text instead of silently slicing it', async () => {
const { chat } = await loadRoom()
const long = `plan:${'x'.repeat(2000)}`
const compacted = chat.compactGroupChatSyncText(long)

expect(compacted.truncated).toBe(true)
expect(compacted.text).toContain('[truncated]')
expect(compacted.text.length).toBeLessThanOrEqual(1200)
expect(compacted.text.startsWith('plan:')).toBe(true)
expect(compacted.text).not.toBe(long)
expect(chat.compactGroupChatSyncText('short').truncated).toBeUndefined()
})

it('preserves threads and budgets escaped Unicode', async () => {
Expand Down
69 changes: 49 additions & 20 deletions apps/desktop/src/plugins/hermes-bots/group-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const GROUP_CHAT_SYNC_META_KEY = 'hermes-bots-groups'
const GROUP_CHAT_SYNC_MAX_BYTES = 48000
const GROUP_CHAT_SYNC_MESSAGES = 16
const GROUP_CHAT_SYNC_TEXT_CHARS = 1200
const GROUP_CHAT_SYNC_TRUNCATION_MARK = '… [truncated]'
const GROUP_CHAT_SYNC_IMAGE_CHARS = 24000
let groupChatSyncTimer: ReturnType<typeof setTimeout> | null = null

Expand Down Expand Up @@ -89,6 +90,25 @@ const groupChatSyncRetryTimers = new Map<string, ReturnType<typeof setTimeout>>(
const groupChatSyncRetryCounts = new Map<string, number>()
export let groupChatSyncDisposed = false

/** Cut one sync-projection line to the per-message budget and mark the cut.
* Receivers used to see a silent mid-sentence slice with no signal that the
* body continued. Keep the mark inside the same char budget so CJK/envelope
* accounting does not grow. */
export function compactGroupChatSyncText(text: string, limit = GROUP_CHAT_SYNC_TEXT_CHARS) {
const raw = String(text || '')

if (raw.length <= limit) {
return { text: raw }
}

const budget = Math.max(0, limit - GROUP_CHAT_SYNC_TRUNCATION_MARK.length)

return {
text: `${raw.slice(0, budget)}${GROUP_CHAT_SYNC_TRUNCATION_MARK}`,
truncated: true as const
}
}

/** Conservative byte count for the gateway's ensure_ascii JSON encoding.
* Python also inserts separator spaces, so reserve one extra byte per JS
* structural separator on top of escaped Unicode code-point widths. */
Expand Down Expand Up @@ -214,29 +234,38 @@ export function groupChatSyncSnapshot(
}

for (const [name, room] of ranked) {
const log: GroupMessage[] = room.log.slice(-GROUP_CHAT_SYNC_MESSAGES).map(entry => ({
...(entry?.id
? {
id: String(entry.id).slice(0, 160)
}
: {}),
from: {
kind: entry?.from?.kind === 'member' ? 'member' : 'user',
name: String(entry?.from?.name || (entry?.from?.kind === 'member' ? 'Bot' : 'You')).slice(0, 128),
...(entry?.from?.source
const log: GroupMessage[] = room.log.slice(-GROUP_CHAT_SYNC_MESSAGES).map(entry => {
const compacted = compactGroupChatSyncText(String(entry?.text || ''))

return {
...(entry?.id
? {
source: String(entry.from.source).slice(0, 128)
id: String(entry.id).slice(0, 160)
}
: {}),
from: {
kind: entry?.from?.kind === 'member' ? 'member' : 'user',
name: String(entry?.from?.name || (entry?.from?.kind === 'member' ? 'Bot' : 'You')).slice(0, 128),
...(entry?.from?.source
? {
source: String(entry.from.source).slice(0, 128)
}
: {})
},
text: compacted.text,
at: Number(entry?.at || 0),
...(entry?.thread
? {
thread: String(entry.thread).slice(0, 128)
}
: {}),
...(compacted.truncated
? {
truncated: true
}
: {})
},
text: String(entry?.text || '').slice(0, GROUP_CHAT_SYNC_TEXT_CHARS),
at: Number(entry?.at || 0),
...(entry?.thread
? {
thread: String(entry.thread).slice(0, 128)
}
: {})
}))
}
})

const compact: GroupChatSyncRoom = {
name: String(name).slice(0, 64),
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/plugins/hermes-bots/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export interface GroupMessage {
text: string
/** Messages predating threading carry the sentinel thread `'legacy'`. */
thread?: string
/** Set on the ui_meta projection when `text` was cut to the sync budget. */
truncated?: boolean
}

export interface GroupHold {
Expand Down
Loading