Skip to content

Commit 1bf961c

Browse files
committed
Publish v3.2.1
1 parent f60947f commit 1bf961c

6 files changed

Lines changed: 96 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [3.2.1] — 2026-05-20
11+
12+
### Fixed
13+
14+
- `ctvs gascity backfill --all` now gives large city session discovery and
15+
transcript fetches up to two minutes before timing out, and reports timeout
16+
failures clearly instead of the raw aborted fetch message.
17+
1018
## [3.2.0] — 2026-05-20
1119

1220
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "collectivus",
3-
"version": "3.2.0",
3+
"version": "3.2.1",
44
"description": "OTLP collector and pass-through LLM proxy",
55
"author": "Hyperparam",
66
"homepage": "https://hyperparam.app",

src/cli/gascity.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,7 @@ Options:
761761
--config <path> collectivus config (default: ~/.hyp/collectivus.json)
762762
--help, -h Show this help
763763
`
764-
const BACKFILL_DISCOVERY_TIMEOUT_MS = 5000
764+
const BACKFILL_DISCOVERY_TIMEOUT_MS = 120_000
765765

766766
/**
767767
* @typedef {object} BackfillParseResult
@@ -1013,6 +1013,11 @@ async function fetchSupervisorSessions(args) {
10131013
let response
10141014
try {
10151015
response = await args.fetchFn(url, { signal: ac.signal })
1016+
} catch (err) {
1017+
if (ac.signal.aborted || isAbortError(err)) {
1018+
throw new Error(`session discovery timed out after ${formatTimeout(BACKFILL_DISCOVERY_TIMEOUT_MS)}`)
1019+
}
1020+
throw err
10161021
} finally {
10171022
clearTimeout(timer)
10181023
}
@@ -1100,6 +1105,25 @@ function pickString(obj, key) {
11001105
return typeof value === 'string' ? value : undefined
11011106
}
11021107

1108+
/**
1109+
* @param {unknown} err
1110+
* @returns {boolean}
1111+
*/
1112+
function isAbortError(err) {
1113+
if (err === null || typeof err !== 'object') return false
1114+
return /** @type {{ name?: unknown }} */ (err).name === 'AbortError'
1115+
}
1116+
1117+
/**
1118+
* @param {number} ms
1119+
* @returns {string}
1120+
*/
1121+
function formatTimeout(ms) {
1122+
if (ms % 60_000 === 0) return `${ms / 60_000}m`
1123+
if (ms % 1000 === 0) return `${ms / 1000}s`
1124+
return `${ms}ms`
1125+
}
1126+
11031127
/**
11041128
* Parse a duration like `7d`, `12h`, `30m`, `45s`. Returns the equivalent
11051129
* milliseconds, or undefined for malformed input.

src/gascity/backfill.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { cursorsDir } from './paths.js'
77
* @import { SessionCursor, SessionContext } from './types.d.ts'
88
*/
99

10-
const POLL_TIMEOUT_MS = 5000
10+
const POLL_TIMEOUT_MS = 120_000
1111

1212
/**
1313
* Run a one-shot backfill against the supervisor's `/transcript` endpoint
@@ -128,6 +128,11 @@ export async function backfillSession(args) {
128128
let response
129129
try {
130130
response = await args.fetchFn(url, { signal: ac.signal })
131+
} catch (err) {
132+
if (ac.signal.aborted || isAbortError(err)) {
133+
throw new Error(`transcript fetch timed out after ${formatTimeout(POLL_TIMEOUT_MS)}`)
134+
}
135+
throw err
131136
} finally {
132137
clearTimeout(timer)
133138
}
@@ -210,3 +215,22 @@ function wrapProviderFrames(frames, provider) {
210215
function formatError(err) {
211216
return err instanceof Error ? err.message : String(err)
212217
}
218+
219+
/**
220+
* @param {unknown} err
221+
* @returns {boolean}
222+
*/
223+
function isAbortError(err) {
224+
if (err === null || typeof err !== 'object') return false
225+
return /** @type {{ name?: unknown }} */ (err).name === 'AbortError'
226+
}
227+
228+
/**
229+
* @param {number} ms
230+
* @returns {string}
231+
*/
232+
function formatTimeout(ms) {
233+
if (ms % 60_000 === 0) return `${ms / 60_000}m`
234+
if (ms % 1000 === 0) return `${ms / 1000}s`
235+
return `${ms}ms`
236+
}

test/cli/gascity.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,25 @@ describe('runBackfill', () => {
508508
expect(stdout.value()).toMatch(/Discovered 2 sessions/)
509509
})
510510

511+
it('reports --all session discovery timeouts clearly', async () => {
512+
const paths = buildPaths()
513+
await writeConfig(paths.configPath, {
514+
gascity: [{ name: 'hyptown', api_url: 'http://127.0.0.1:8372' }],
515+
})
516+
const abort = new Error('This operation was aborted')
517+
abort.name = 'AbortError'
518+
const fetchFn = vi.fn(async () => { throw abort })
519+
const stderr = memo()
520+
const code = await runBackfill(['hyptown', '--all', '--config', paths.configPath], {
521+
stdout: memo(), stderr,
522+
sinkRoot: paths.sinkRoot,
523+
fetchFn: /** @type {typeof fetch} */ (/** @type {unknown} */ (fetchFn)),
524+
})
525+
expect(code).toBe(1)
526+
expect(stderr.value()).toMatch(/session discovery timed out after 2m/)
527+
expect(stderr.value()).not.toMatch(/This operation was aborted/)
528+
})
529+
511530
it('skips sessions whose cursor is older than --since', async () => {
512531
const paths = buildPaths()
513532
await writeConfig(paths.configPath, {

test/gascity/backfill.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,24 @@ describe('backfillCity', () => {
160160
expect(stderr.value()).toMatch(/backfill_session_failed.*hy-1.*HTTP 500/)
161161
})
162162

163+
it('logs transcript timeouts as explicit timeout failures', async () => {
164+
await writeCursorFile('hyptown', 'hy-1', { last_uuid: 'u-0', retired: false })
165+
const stderr = memoStream()
166+
const abort = new Error('This operation was aborted')
167+
abort.name = 'AbortError'
168+
const fetchFn = vi.fn(async () => { throw abort })
169+
const result = await backfillCity({
170+
city: { name: 'hyptown', api_url: 'http://h:8372' },
171+
sinkRoot,
172+
dispatcher: new NormalizerDispatcher({ stderr: memoStream() }),
173+
stderr,
174+
fetchFn,
175+
})
176+
expect(result).toEqual({ sessionsAttempted: 1, framesDispatched: 0, sessionsFailed: 1 })
177+
expect(stderr.value()).toMatch(/backfill_session_failed.*hy-1.*transcript fetch timed out after 2m/)
178+
expect(stderr.value()).not.toMatch(/This operation was aborted/)
179+
})
180+
163181
it('treats HTTP 404 as a quiet skip (session retired upstream)', async () => {
164182
await writeCursorFile('hyptown', 'hy-1', { last_uuid: 'u-0', retired: false })
165183
const stderr = memoStream()

0 commit comments

Comments
 (0)