fix: bound relay hermes run count cache (#4278)

This commit is contained in:
Neil 2026-05-31 10:48:16 -07:00 committed by GitHub
parent c3125803a3
commit a7e8471bf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 0 deletions

View File

@ -195,4 +195,46 @@ describe('ExternalAutomationsHandler', () => {
).resolves.toEqual({ total: 2, runs: [] })
expect(readHermesRunRefs).toHaveBeenCalledTimes(2)
})
it('evicts oldest remote Hermes count cache entries when many job ids are observed', async () => {
const { handler, requestHandlers } = createHandlerHarness()
const readHermesRunRefs = vi.fn(async (jobId: string) =>
jobId === 'job-0'
? [{ id: 'job-0:2026-05-15_09-00-00.md', run_at: '2026-05-15T09:00:00' }]
: []
)
const handlerInternals = handler as unknown as {
readHermesRunRefs: typeof readHermesRunRefs
}
handlerInternals.readHermesRunRefs = readHermesRunRefs
await expect(
requestHandlers.get('externalAutomations.runs')?.({
provider: 'hermes',
jobId: 'job-0',
page: 1,
pageSize: 0
})
).resolves.toEqual({ total: 1, runs: [] })
for (let i = 1; i <= 200; i += 1) {
await requestHandlers.get('externalAutomations.runs')?.({
provider: 'hermes',
jobId: `job-${i}`,
page: 1,
pageSize: 0
})
}
await expect(
requestHandlers.get('externalAutomations.runs')?.({
provider: 'hermes',
jobId: 'job-0',
page: 1,
pageSize: 0
})
).resolves.toEqual({ total: 1, runs: [] })
expect(readHermesRunRefs).toHaveBeenCalledTimes(202)
})
})

View File

@ -22,6 +22,7 @@ const HERMES_OUTPUT_FILE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d
const HERMES_RUN_KEY_PATTERN = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/
const MAX_SESSION_OUTPUT_GAP_MS = 24 * 60 * 60 * 1000
const MAX_REFERENCED_LOG_BYTES = 5 * 1024 * 1024
const HERMES_RUN_COUNT_CACHE_MAX_ENTRIES = 200
const FULL_SESSION_LOG_HEADING = '## Full session log'
const REFERENCED_LOG_HEADING = '## Latest log file'
const LATEST_LOG_PATH_PATTERN =
@ -572,6 +573,12 @@ export class ExternalAutomationsHandler {
if (cached && cached.expiresAt > now) {
return cached.promise
}
if (cached) {
this.hermesRunCountCache.delete(jobId)
}
// Why: remote Hermes jobs can churn independently of Orca; relay
// processes are long-lived, so stale job ids need both TTL and a hard cap.
this.pruneHermesRunCountCache(now)
const entry: HermesRunCountCacheEntry = {
promise: this.readHermesRunRefs(jobId).then((refs) => refs.length),
expiresAt: Number.POSITIVE_INFINITY
@ -589,6 +596,21 @@ export class ExternalAutomationsHandler {
}
}
private pruneHermesRunCountCache(now: number): void {
for (const [jobId, entry] of this.hermesRunCountCache) {
if (entry.expiresAt <= now) {
this.hermesRunCountCache.delete(jobId)
}
}
while (this.hermesRunCountCache.size >= HERMES_RUN_COUNT_CACHE_MAX_ENTRIES) {
const oldestJobId = this.hermesRunCountCache.keys().next().value
if (oldestJobId === undefined) {
return
}
this.hermesRunCountCache.delete(oldestJobId)
}
}
private clearHermesRunCountCache(jobId?: string): void {
if (jobId) {
this.hermesRunCountCache.delete(jobId)