fix(apiproxy): harden cold session metadata probing
This commit is contained in:
18 files changed
+138
-131
No files matched your search
@@ -563,40 +563,40 @@ function summarize(session: Session, running: boolean): SessionSummary {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a possibly blank cold Session only when its physical artifact is
|
||||
* within the configured per-Session read bound. A stale `blank: true`, an
|
||||
* Verify a possibly blank cold Session only when its physical artifact passes
|
||||
* the configured per-Session size check. A stale `blank: true`, an
|
||||
* absent cache row, a large or location-less artifact, and read failures all
|
||||
* resolve to visible (`false`); listing must never hide a conversation on a
|
||||
* cache hint or an unavailable optimization.
|
||||
*/
|
||||
async function probeColdSessionBlank(
|
||||
async function probeColdSessionMetadata(
|
||||
ctx: Context,
|
||||
persistence: SessionPersistence,
|
||||
meta: SessionHeader,
|
||||
maxBytes: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<boolean> {
|
||||
if (maxBytes === 0) return false
|
||||
): Promise<SessionListMetadata | undefined> {
|
||||
if (maxBytes === 0) return undefined
|
||||
signal?.throwIfAborted()
|
||||
const location = persistence.locate(meta)
|
||||
if (location === undefined) return false
|
||||
if (location === undefined) return undefined
|
||||
signal?.throwIfAborted()
|
||||
let size: number
|
||||
try {
|
||||
size = (await stat(location.path)).size
|
||||
} catch {
|
||||
signal?.throwIfAborted()
|
||||
return false
|
||||
return undefined
|
||||
}
|
||||
if (size > maxBytes) return false
|
||||
if (size > maxBytes) return undefined
|
||||
try {
|
||||
const { events } = await persistence.readFrom(meta.id, 0, signal)
|
||||
signal?.throwIfAborted()
|
||||
return !events.some(event => event.type === 'turn/start')
|
||||
return sessionListMetadata(events)
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted()
|
||||
ctx.logger.warn(`session.list: blank probe for "${meta.id}" failed (serving it as visible): ${String(error)}`)
|
||||
return false
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,14 +609,14 @@ async function summarizeCold(
|
||||
blankProbeMaxBytes: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SessionSummary> {
|
||||
const blank = metadata?.blank === false
|
||||
? false
|
||||
: await probeColdSessionBlank(ctx, persistence, meta, blankProbeMaxBytes, signal)
|
||||
const probed = metadata?.blank === false
|
||||
? undefined
|
||||
: await probeColdSessionMetadata(ctx, persistence, meta, blankProbeMaxBytes, signal)
|
||||
return {
|
||||
sessionId: meta.id,
|
||||
updatedAt: sessionListUpdatedAt(meta, metadata),
|
||||
updatedAt: sessionListUpdatedAt(meta, probed ?? metadata),
|
||||
running: false,
|
||||
blank,
|
||||
blank: metadata?.blank === false ? false : probed?.blank ?? false,
|
||||
// Header-only: reading the log for a blank-window preset switch would
|
||||
// defeat the same index read, and attaching the session replaces this row
|
||||
// with `summarize()`, which resolves the switch from the events.
|
||||
@@ -1724,14 +1724,15 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
*/
|
||||
async function listVisibleSessionSummaries(signal?: AbortSignal): Promise<SessionSummary[]> {
|
||||
signal?.throwIfAborted()
|
||||
const items = ctx.sessions.list().map((session) => {
|
||||
const summarizeAttached = (session: Session): SessionSummary => {
|
||||
const agent = ctx.agents.get(session.id)
|
||||
const projections = listProjectionsFor(ctx, session.header, session)
|
||||
return {
|
||||
...summarize(session, agent?.status === 'running'),
|
||||
...projections === undefined ? {} : { projections },
|
||||
}
|
||||
})
|
||||
}
|
||||
const items = ctx.sessions.list().map(summarizeAttached)
|
||||
signal?.throwIfAborted()
|
||||
const attached = new Set(items.map(item => item.sessionId))
|
||||
const persistence = ctx.get('sessionPersistence')
|
||||
@@ -1745,17 +1746,20 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const settled = await Promise.allSettled(
|
||||
batch.map(async (meta) => {
|
||||
// Projection hints remain optional. Blank verification may read
|
||||
// this Session's artifact only when it fits the configured bound.
|
||||
// this Session's artifact only when it passes the configured size check.
|
||||
const projections = listProjectionsFor(ctx, meta, undefined)
|
||||
const summary = await summarizeCold(
|
||||
ctx,
|
||||
persistence,
|
||||
meta,
|
||||
projections?.values.sessionListMetadata,
|
||||
coldBlankProbeMaxBytes,
|
||||
signal,
|
||||
)
|
||||
const attachedSession = ctx.sessions.get(meta.id)
|
||||
if (attachedSession !== undefined) return summarizeAttached(attachedSession)
|
||||
return {
|
||||
...await summarizeCold(
|
||||
ctx,
|
||||
persistence,
|
||||
meta,
|
||||
projections?.values.sessionListMetadata,
|
||||
coldBlankProbeMaxBytes,
|
||||
signal,
|
||||
),
|
||||
...summary,
|
||||
...projections === undefined ? {} : { projections },
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user