mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
fix(desktop): friendlier toast when a remote attachment exceeds the 16MB cap
Remote attachments read their bytes through the readFileDataUrl IPC, which is hard-capped at 16MB and rejects with a raw "file is too large (N bytes; limit M bytes)" string straight into the failure toast (helix4u review note on #43109). Translate that into "<file> is too large to upload to the remote gateway (max 16 MB)", parsing the limit out of the message so it tracks the real cap. Applies to both the image and non-image remote read paths; non-cap errors pass through unchanged. Adds unit coverage for both.
This commit is contained in:
parent
b021497bc8
commit
29147afd63
2 changed files with 89 additions and 5 deletions
|
|
@ -7,7 +7,7 @@ import { $composerAttachments, type ComposerAttachment } from '@/store/composer'
|
|||
import { $connection, $sessions, setSessions } from '@/store/session'
|
||||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import { usePromptActions } from './use-prompt-actions'
|
||||
import { uploadComposerAttachment, usePromptActions } from './use-prompt-actions'
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getProfiles: vi.fn(async () => ({ profiles: [] })),
|
||||
|
|
@ -703,3 +703,52 @@ describe('usePromptActions eager attachment upload (drop-time)', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('uploadComposerAttachment remote read failures', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('turns the raw 16MB IPC cap error into a friendly remote-gateway message', async () => {
|
||||
// electron/hardening.cjs rejects the readFileDataUrl IPC with this exact
|
||||
// shape when a file exceeds DATA_URL_READ_MAX_BYTES.
|
||||
Object.defineProperty(window, 'hermesDesktop', {
|
||||
configurable: true,
|
||||
value: {
|
||||
readFileDataUrl: vi.fn(async () => {
|
||||
throw new Error('File preview failed: file is too large (20971520 bytes; limit 16777216 bytes).')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
|
||||
await expect(
|
||||
uploadComposerAttachment(
|
||||
{ id: 'file:big', kind: 'file', label: 'huge.csv', path: '/abs/huge.csv' },
|
||||
{ remote: true, requestGateway, sessionId: RUNTIME_SESSION_ID }
|
||||
)
|
||||
).rejects.toThrow('huge.csv is too large to upload to the remote gateway (max 16 MB).')
|
||||
|
||||
// The cap is hit before any gateway round-trip.
|
||||
expect(requestGateway).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes non-cap read errors through unchanged', async () => {
|
||||
Object.defineProperty(window, 'hermesDesktop', {
|
||||
configurable: true,
|
||||
value: {
|
||||
readFileDataUrl: vi.fn(async () => {
|
||||
throw new Error('ENOENT: no such file')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await expect(
|
||||
uploadComposerAttachment(
|
||||
{ id: 'file:gone', kind: 'file', label: 'gone.csv', path: '/abs/gone.csv' },
|
||||
{ remote: true, requestGateway: vi.fn(async () => ({}) as never), sessionId: RUNTIME_SESSION_ID }
|
||||
)
|
||||
).rejects.toThrow('ENOENT: no such file')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,27 @@ async function readFileDataUrlForAttach(filePath: string): Promise<string | null
|
|||
return dataUrl || null
|
||||
}
|
||||
|
||||
// The readFileDataUrl IPC base64-loads the whole file into memory and is
|
||||
// hard-capped (DATA_URL_READ_MAX_BYTES, 16 MB) in electron/hardening.cjs, which
|
||||
// rejects with a raw "file is too large (N bytes; limit M bytes)" string. In
|
||||
// remote mode every attachment's bytes go through that read, so a big file
|
||||
// surfaces that internal message verbatim in the failure toast. Translate it
|
||||
// into a friendly "too large to upload to the remote gateway" line, parsing the
|
||||
// limit out of the message so it tracks the real cap. Non-cap errors pass
|
||||
// through unchanged.
|
||||
function friendlyRemoteAttachError(err: unknown, label: string): Error {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
|
||||
if (!/too large/i.test(message)) {
|
||||
return err instanceof Error ? err : new Error(message)
|
||||
}
|
||||
|
||||
const limitBytes = Number(message.match(/limit (\d+) bytes/)?.[1])
|
||||
const cap = Number.isFinite(limitBytes) && limitBytes > 0 ? ` (max ${Math.floor(limitBytes / (1024 * 1024))} MB)` : ''
|
||||
|
||||
return new Error(`${label} is too large to upload to the remote gateway${cap}.`)
|
||||
}
|
||||
|
||||
type GatewayRequest = <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
|
||||
/**
|
||||
|
|
@ -142,7 +163,13 @@ export async function uploadComposerAttachment(
|
|||
let result: ImageAttachResponse
|
||||
|
||||
if (remote) {
|
||||
const payload = await readImageForRemoteAttach(path)
|
||||
let payload: Awaited<ReturnType<typeof readImageForRemoteAttach>>
|
||||
|
||||
try {
|
||||
payload = await readImageForRemoteAttach(path)
|
||||
} catch (err) {
|
||||
throw friendlyRemoteAttachError(err, label)
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw new Error(`Could not read ${label}`)
|
||||
|
|
@ -176,10 +203,18 @@ export async function uploadComposerAttachment(
|
|||
}
|
||||
|
||||
// Non-image file.
|
||||
const dataUrl = remote ? await readFileDataUrlForAttach(path) : null
|
||||
let dataUrl: string | null = null
|
||||
|
||||
if (remote && !dataUrl) {
|
||||
throw new Error(`Could not read ${label}`)
|
||||
if (remote) {
|
||||
try {
|
||||
dataUrl = await readFileDataUrlForAttach(path)
|
||||
} catch (err) {
|
||||
throw friendlyRemoteAttachError(err, label)
|
||||
}
|
||||
|
||||
if (!dataUrl) {
|
||||
throw new Error(`Could not read ${label}`)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await requestGateway<FileAttachResponse>('file.attach', {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue