From 352ea6cf4f96d656f896f8a18519503d4bfbb917 Mon Sep 17 00:00:00 2001 From: Hypatia May Date: Sat, 11 Jul 2026 10:19:34 +0800 Subject: [PATCH] fix(session-query): checkpoint review round 3 --- docs/core-data-structures/session-query.md | 47 ++++++++++++++++++- docs/event-producer-consumer.md | 6 +-- .../cordis/tool-cordis/src/api-catalog.ts | 10 +++- packages/core/session/tests/session.spec.ts | 6 +-- .../tests/coordinator-contract.ts | 3 +- .../session-query/session-query/README.md | 6 +++ .../session-query/session-query/src/config.ts | 36 +++++++++++++- .../session-query/session-query/src/index.ts | 2 +- .../session-query/src/provider.ts | 9 ++-- .../session-query/session-query/src/types.ts | 16 ++++++- .../session-query/tests/session-query.spec.ts | 35 ++++++++------ scripts/gen-doc-graphs.ts | 11 +++-- scripts/type-equiv.manifest.json | 3 ++ vendor/README.md | 2 +- vendor/cordis/src/events.ts | 12 ++--- 15 files changed, 153 insertions(+), 51 deletions(-) diff --git a/docs/core-data-structures/session-query.md b/docs/core-data-structures/session-query.md index 8ad0f0cbf5..b824e99f1e 100644 --- a/docs/core-data-structures/session-query.md +++ b/docs/core-data-structures/session-query.md @@ -95,6 +95,20 @@ export interface SessionEventSearchRequest extends SessionSearchPageRequest { } ``` +The service resolves caller requests before crossing the provider seam, so provider implementations always receive a validated page limit. + +```ts type-equiv +export interface SessionSearchSpec extends SessionSearchRequest { + limit: number +} +``` + +```ts type-equiv +export interface SessionEventSearchSpec extends SessionEventSearchRequest { + limit: number +} +``` + ```ts type-equiv export interface SessionEventSearchHit extends SessionEventRecord { snippet: string @@ -115,6 +129,35 @@ export interface SessionSearchPage { } ``` +## Errors + +The service exposes a closed machine-routable error taxonomy; messages and causes provide detail but do not add codes. + +```ts type-equiv +export type SessionQueryErrorCode = + | 'SESSION_QUERY_ABORTED' + | 'SESSION_QUERY_DUPLICATE_EXTRACTOR' + | 'SESSION_QUERY_DUPLICATE_PROVIDER' + | 'SESSION_QUERY_EVENT_NOT_FOUND' + | 'SESSION_QUERY_INDEX_FAILED' + | 'SESSION_QUERY_INVALID_CONFIG' + | 'SESSION_QUERY_INVALID_EXTRACTOR' + | 'SESSION_QUERY_INVALID_FILTER' + | 'SESSION_QUERY_INVALID_LIMIT' + | 'SESSION_QUERY_INVALID_LINEAGE' + | 'SESSION_QUERY_INVALID_QUERY' + | 'SESSION_QUERY_INVALID_SURFACE' + | 'SESSION_QUERY_INVALID_WINDOW' + | 'SESSION_QUERY_PERSISTENCE_FAILED' + | 'SESSION_QUERY_PROVIDER_AMBIGUOUS' + | 'SESSION_QUERY_PROVIDER_CONFIGURED_MISSING' + | 'SESSION_QUERY_PROVIDER_CONFIGURED_UNAVAILABLE' + | 'SESSION_QUERY_PROVIDER_ERROR' + | 'SESSION_QUERY_PROVIDER_UNAVAILABLE' + | 'SESSION_QUERY_SESSION_NOT_FOUND' + | 'SESSION_QUERY_SOURCE_CONFLICT' +``` + ## Event reads and traces An event read returns the full target plus a bounded raw-log window. Trace records retain lightweight seq links so callers choose which related event bodies to read. @@ -215,7 +258,7 @@ export interface SessionSearchProvider { removePersisted(sessionId: SessionId): Promise replaceLive(snapshot: SessionIndexSnapshot): Promise removeLive(sessionId: SessionId): Promise - searchSessions(request: SessionSearchRequest, exec?: SessionQueryExecContext): Promise> - searchEvents(request: SessionEventSearchRequest, exec?: SessionQueryExecContext): Promise> + searchSessions(request: SessionSearchSpec, exec?: SessionQueryExecContext): Promise> + searchEvents(request: SessionEventSearchSpec, exec?: SessionQueryExecContext): Promise> } ``` diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 23e3844375..0b8c4fce02 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -23,11 +23,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:138`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:109`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) | -| `session/created` | `emit` | [`packages/core/session/src/index.ts:39`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`emit`) | [`invariants`](../packages/support/invariants), [`session-persistence`](../packages/session-persistence/session-persistence) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`emit`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) | +| `session/created` | `emit` | [`packages/core/session/src/index.ts:39`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`emit`) | [`invariants`](../packages/support/invariants), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`emit`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`stdio-agent`](../packages/ui/stdio-agent) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`parallel`) | [`session-persistence`](../packages/session-persistence/session-persistence) | | `session/persisted` | `parallel` | [`packages/session-persistence/session-persistence/src/index.ts:50`](../packages/session-persistence/session-persistence/src/index.ts) | [`session-persistence`](../packages/session-persistence/session-persistence) (`parallel`) | [`session-query`](../packages/session-query/session-query) | -| `session/removed` | `parallel` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`parallel`) | - | +| `session/removed` | `parallel` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`parallel`) | [`session-query`](../packages/session-query/session-query) | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:98`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:72`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:83`](../packages/subagent/subagent/src/index.ts) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 7b3cd1297f..12872d689f 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -737,6 +737,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionEventSearchRequest', declaration: 'export interface SessionEventSearchRequest extends SessionSearchPageRequest {\n sessionId: SessionId;\n query: string;\n filters?: readonly SessionEventResultFilter[];\n}', }, + { + name: 'SessionEventSearchSpec', + declaration: 'export interface SessionEventSearchSpec extends SessionEventSearchRequest {\n limit: number;\n}', + }, { name: 'SessionEventSurface', declaration: 'export type SessionEventSurface = \'current\' | \'shadowed\' | \'log-only\';', @@ -819,7 +823,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionSearchProvider', - declaration: 'export interface SessionSearchProvider {\n readonly id: string;\n status(): SessionSearchProviderStatus;\n persistedInventory(): Promise;\n setPersistedActive(active: boolean): Promise;\n replacePersisted(snapshot: SessionIndexSnapshot): Promise;\n removePersisted(sessionId: SessionId): Promise;\n replaceLive(snapshot: SessionIndexSnapshot): Promise;\n removeLive(sessionId: SessionId): Promise;\n searchSessions(request: SessionSearchRequest, exec?: SessionQueryExecContext): Promise>;\n searchEvents(request: SessionEventSearchRequest, exec?: SessionQueryExecContext): Promise>;\n}', + declaration: 'export interface SessionSearchProvider {\n readonly id: string;\n status(): SessionSearchProviderStatus;\n persistedInventory(): Promise;\n setPersistedActive(active: boolean): Promise;\n replacePersisted(snapshot: SessionIndexSnapshot): Promise;\n removePersisted(sessionId: SessionId): Promise;\n replaceLive(snapshot: SessionIndexSnapshot): Promise;\n removeLive(sessionId: SessionId): Promise;\n searchSessions(request: SessionSearchSpec, exec?: SessionQueryExecContext): Promise>;\n searchEvents(request: SessionEventSearchSpec, exec?: SessionQueryExecContext): Promise>;\n}', }, { name: 'SessionSearchProviderStatus', @@ -829,6 +833,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionSearchRequest', declaration: 'export interface SessionSearchRequest extends SessionSearchPageRequest {\n query: string;\n sessionFilters?: readonly SessionResultFilter[];\n eventFilters?: readonly SessionEventResultFilter[];\n}', }, + { + name: 'SessionSearchSpec', + declaration: 'export interface SessionSearchSpec extends SessionSearchRequest {\n limit: number;\n}', + }, { name: 'StreamChunk', declaration: 'export type StreamChunk = {\n type: \'block-start\';\n index: number;\n blockType: ContentBlockType;\n} | {\n type: \'text-delta\';\n index: number;\n text: string;\n} | {\n type: \'reasoning-delta\';\n index: number;\n text: string;\n} | {\n type: \'tool-call-delta\';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n} | {\n type: \'block-end\';\n index: number;\n block: ContentBlock;\n} | {\n type: \'usage\';\n usage: TokenUsage;\n} | {\n type: \'finish\';\n reason: FinishReason;\n};', diff --git a/packages/core/session/tests/session.spec.ts b/packages/core/session/tests/session.spec.ts index b32ae07fdd..e51ed923df 100644 --- a/packages/core/session/tests/session.spec.ts +++ b/packages/core/session/tests/session.spec.ts @@ -374,12 +374,9 @@ describe('SessionStore', () => { expect(observations).toHaveLength(1) }) - it('contains failing session/removed listeners without starving later observers', async () => { + it('contains rejected session/removed listeners during teardown', async () => { const ctx = new Context() await ctx.plugin(SessionStore) - const observed: SessionId[] = [] - ctx.on('session/removed', () => { throw new Error('synchronous observer failed') }) - ctx.on('session/removed', header => void observed.push(header.id)) ctx.on('session/removed', () => Promise.reject(new Error('observer failed'))) const session = ctx.sessions.prepare(SessionId('contained')) const detach = ctx.sessions.enter(session) @@ -388,7 +385,6 @@ describe('SessionStore', () => { await Promise.resolve() await Promise.resolve() expect(ctx.sessions.get(session.id)).toBeUndefined() - expect(observed).toEqual([session.id]) }) it('rolls back the session (and onAppend) when a session/created listener throws (P1-1)', async () => { diff --git a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts index 4cf4e6ff67..6d95fbb4b2 100644 --- a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts +++ b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts @@ -128,12 +128,11 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< const fix = await makeFixture() const { ctx, fiber } = await freshCtx(fix) const observed: Array<{ headerId: SessionId; change: SessionPersistedChange }> = [] - ctx.on('session/persisted', () => { throw new Error('synchronous derived read model failed') }) ctx.on('session/persisted', (header, change) => { observed.push({ headerId: header.id, change: structuredClone(change) }) header.createdAt = -1 + return Promise.reject(new Error('derived read model failed')) }) - ctx.on('session/persisted', () => Promise.reject(new Error('asynchronous derived read model failed'))) try { const m = meta('notifications', WORK) await ctx.sessionPersistence.create(m) diff --git a/packages/session-query/session-query/README.md b/packages/session-query/session-query/README.md index 70b57dad07..5dc5aecc34 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -28,6 +28,12 @@ The service feeds providers two independent layers: a durable persisted base (`p Persisted snapshots carry a SHA-256 fingerprint over canonical header/events plus the versions of relevant extractors. Reconciliation still loads and hashes canonical logs, but a provider replacement occurs only for a new or changed fingerprint; stale durable inventory entries are removed only while persistence is active and authoritative. +Providers receive resolved `SessionSearchSpec` and `SessionEventSearchSpec` values whose `limit` is required after service defaulting and validation. Public service callers use `SessionSearchRequest` and `SessionEventSearchRequest`, where `limit` remains optional. + +## Errors + +`SessionQueryError.code` is the closed `SessionQueryErrorCode` union: `SESSION_QUERY_ABORTED`, `SESSION_QUERY_DUPLICATE_EXTRACTOR`, `SESSION_QUERY_DUPLICATE_PROVIDER`, `SESSION_QUERY_EVENT_NOT_FOUND`, `SESSION_QUERY_INDEX_FAILED`, `SESSION_QUERY_INVALID_CONFIG`, `SESSION_QUERY_INVALID_EXTRACTOR`, `SESSION_QUERY_INVALID_FILTER`, `SESSION_QUERY_INVALID_LIMIT`, `SESSION_QUERY_INVALID_LINEAGE`, `SESSION_QUERY_INVALID_QUERY`, `SESSION_QUERY_INVALID_SURFACE`, `SESSION_QUERY_INVALID_WINDOW`, `SESSION_QUERY_PERSISTENCE_FAILED`, `SESSION_QUERY_PROVIDER_AMBIGUOUS`, `SESSION_QUERY_PROVIDER_CONFIGURED_MISSING`, `SESSION_QUERY_PROVIDER_CONFIGURED_UNAVAILABLE`, `SESSION_QUERY_PROVIDER_ERROR`, `SESSION_QUERY_PROVIDER_UNAVAILABLE`, `SESSION_QUERY_SESSION_NOT_FOUND`, and `SESSION_QUERY_SOURCE_CONFLICT`. + ## Text extractors Core extraction indexes semantic message text and reasoning, tool names/arguments/results, blocked prompts, context and steering, todos, and error/status detail. Stream chunks, request headers, and structural-only events contribute no document. Unknown event and content-block types contribute no text until their owner registers a versioned extractor with `registerEventTextExtractor()` or `registerContentTextExtractor()`. diff --git a/packages/session-query/session-query/src/config.ts b/packages/session-query/session-query/src/config.ts index 6f8443aeb1..c70eb52988 100644 --- a/packages/session-query/session-query/src/config.ts +++ b/packages/session-query/session-query/src/config.ts @@ -25,5 +25,37 @@ export interface Config { readWindowMax?: number } -/** Typed session-query failure with a stable machine-routable code. */ -export class SessionQueryError extends HarnessError {} +/** Complete stable machine-routable failure taxonomy for session-query. */ +export type SessionQueryErrorCode = + | 'SESSION_QUERY_ABORTED' + | 'SESSION_QUERY_DUPLICATE_EXTRACTOR' + | 'SESSION_QUERY_DUPLICATE_PROVIDER' + | 'SESSION_QUERY_EVENT_NOT_FOUND' + | 'SESSION_QUERY_INDEX_FAILED' + | 'SESSION_QUERY_INVALID_CONFIG' + | 'SESSION_QUERY_INVALID_EXTRACTOR' + | 'SESSION_QUERY_INVALID_FILTER' + | 'SESSION_QUERY_INVALID_LIMIT' + | 'SESSION_QUERY_INVALID_LINEAGE' + | 'SESSION_QUERY_INVALID_QUERY' + | 'SESSION_QUERY_INVALID_SURFACE' + | 'SESSION_QUERY_INVALID_WINDOW' + | 'SESSION_QUERY_PERSISTENCE_FAILED' + | 'SESSION_QUERY_PROVIDER_AMBIGUOUS' + | 'SESSION_QUERY_PROVIDER_CONFIGURED_MISSING' + | 'SESSION_QUERY_PROVIDER_CONFIGURED_UNAVAILABLE' + | 'SESSION_QUERY_PROVIDER_ERROR' + | 'SESSION_QUERY_PROVIDER_UNAVAILABLE' + | 'SESSION_QUERY_SESSION_NOT_FOUND' + | 'SESSION_QUERY_SOURCE_CONFLICT' + +/** Typed session-query failure whose `code` is one closed taxonomy member. */ +export class SessionQueryError extends HarnessError { + declare readonly code: SessionQueryErrorCode + + // The base stores the value; this signature narrows its open string code. + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(message: string, code: SessionQueryErrorCode, options?: ErrorOptions) { + super(message, code, options) + } +} diff --git a/packages/session-query/session-query/src/index.ts b/packages/session-query/session-query/src/index.ts index 46e3c15c32..1145332c23 100644 --- a/packages/session-query/session-query/src/index.ts +++ b/packages/session-query/session-query/src/index.ts @@ -40,7 +40,7 @@ import { SessionProviderCoordinator } from './provider.ts' import { eventRecords, traceEventLog, traceLineage } from './tracing.ts' export type * from './types.ts' -export type { Config } from './config.ts' +export type { Config, SessionQueryErrorCode } from './config.ts' export { SESSION_QUERY_DEFAULT_LIMIT, SESSION_QUERY_MAX_LIMIT, diff --git a/packages/session-query/session-query/src/provider.ts b/packages/session-query/session-query/src/provider.ts index 1574bcc0b2..5fd99fde0a 100644 --- a/packages/session-query/session-query/src/provider.ts +++ b/packages/session-query/session-query/src/provider.ts @@ -8,12 +8,14 @@ import type { SessionEventRecord, SessionEventSearchHit, SessionEventSearchRequest, + SessionEventSearchSpec, SessionQueryExecContext, SessionRecord, SessionSearchHit, SessionSearchPage, SessionSearchProvider, SessionSearchRequest, + SessionSearchSpec, } from './types.ts' import type { Config } from './config.ts' import { SessionQueryError } from './config.ts' @@ -28,9 +30,6 @@ interface ProviderState { liveSync: Map> } -type NormalizedSessionSearchRequest = SessionSearchRequest & { limit: number } -type NormalizedEventSearchRequest = SessionEventSearchRequest & { limit: number } - /** Coordinates one selected provider against live and persisted corpus layers. */ export class SessionProviderCoordinator { private readonly _configuredProviderId: string | undefined @@ -257,7 +256,7 @@ export class SessionProviderCoordinator { return single } - private _normalizeSessionSearch(request: SessionSearchRequest): NormalizedSessionSearchRequest { + private _normalizeSessionSearch(request: SessionSearchRequest): SessionSearchSpec { const query = this._queryText(request.query) const limit = this._limitValue(request.limit) filterSessionResults([], request.sessionFilters ?? []) @@ -265,7 +264,7 @@ export class SessionProviderCoordinator { return { ...request, query, limit } } - private _normalizeEventSearch(request: SessionEventSearchRequest): NormalizedEventSearchRequest { + private _normalizeEventSearch(request: SessionEventSearchRequest): SessionEventSearchSpec { const query = this._queryText(request.query) const limit = this._limitValue(request.limit) filterEventResults([], request.filters ?? []) diff --git a/packages/session-query/session-query/src/types.ts b/packages/session-query/session-query/src/types.ts index 43c63a034c..11e6f6d0fc 100644 --- a/packages/session-query/session-query/src/types.ts +++ b/packages/session-query/session-query/src/types.ts @@ -102,6 +102,18 @@ export interface SessionEventSearchRequest extends SessionSearchPageRequest { filters?: readonly SessionEventResultFilter[] } +/** Provider-facing cross-session search spec after service normalization. */ +export interface SessionSearchSpec extends SessionSearchRequest { + /** Required page size validated and defaulted by the query service. */ + limit: number +} + +/** Provider-facing event search spec after service normalization. */ +export interface SessionEventSearchSpec extends SessionEventSearchRequest { + /** Required page size validated and defaulted by the query service. */ + limit: number +} + /** One lightweight event search hit with provider-produced evidence text. */ export interface SessionEventSearchHit extends SessionEventRecord { /** Plain-text excerpt explaining the match. */ @@ -281,12 +293,12 @@ export interface SessionSearchProvider { * @param exec - optional cancellation context. * @returns one ranked session page. */ - searchSessions(request: SessionSearchRequest, exec?: SessionQueryExecContext): Promise> + searchSessions(request: SessionSearchSpec, exec?: SessionQueryExecContext): Promise> /** * Search events within one logical session. * @param request - target session, query, filters, and pagination. * @param exec - optional cancellation context. * @returns one ranked event page. */ - searchEvents(request: SessionEventSearchRequest, exec?: SessionQueryExecContext): Promise> + searchEvents(request: SessionEventSearchSpec, exec?: SessionQueryExecContext): Promise> } diff --git a/packages/session-query/session-query/tests/session-query.spec.ts b/packages/session-query/session-query/tests/session-query.spec.ts index 0da0b4f41d..88d7e906f6 100644 --- a/packages/session-query/session-query/tests/session-query.spec.ts +++ b/packages/session-query/session-query/tests/session-query.spec.ts @@ -12,14 +12,15 @@ import SessionQueryService, { } from '@deepseek-ai/dsh-session-query' import type { SessionEventSearchHit, - SessionEventSearchRequest, + SessionEventSearchSpec, SessionIndexSnapshot, + SessionQueryErrorCode, SessionRecord, SessionSearchHit, SessionSearchPage, SessionSearchProvider, SessionSearchProviderStatus, - SessionSearchRequest, + SessionSearchSpec, } from '@deepseek-ai/dsh-session-query' declare module '@deepseek-ai/dsh-llm' { @@ -98,8 +99,8 @@ class FakeProvider implements SessionSearchProvider { activeHistory: boolean[] = [] removedPersisted: SessionIdType[] = [] removedLive: SessionIdType[] = [] - sessionRequests: SessionSearchRequest[] = [] - eventRequests: SessionEventSearchRequest[] = [] + sessionRequests: SessionSearchSpec[] = [] + eventRequests: SessionEventSearchSpec[] = [] failNextLive = false failNextPersisted = false failNextActive = false @@ -162,12 +163,12 @@ class FakeProvider implements SessionSearchProvider { return Promise.resolve() } - searchSessions(request: SessionSearchRequest): Promise> { + searchSessions(request: SessionSearchSpec): Promise> { this.sessionRequests.push(structuredClone(request)) return Promise.resolve(structuredClone(this.sessionPage)) } - searchEvents(request: SessionEventSearchRequest): Promise> { + searchEvents(request: SessionEventSearchSpec): Promise> { this.eventRequests.push(structuredClone(request)) return Promise.resolve(structuredClone(this.eventPage)) } @@ -180,7 +181,7 @@ async function liveContext(config: ConstructorParameters { TestPersistence.listFailure = undefined TestPersistence.loadFailure = new Error('load unavailable') await expect(ctx.sessionQuery.listEvents(persistedOnly.id)).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED')) - TestPersistence.loadFailure = new SessionQueryError('typed load failure', 'SESSION_QUERY_TEST_FAILURE') - await expect(ctx.sessionQuery.listEvents(persistedOnly.id)).rejects.toThrow(expectCode('SESSION_QUERY_TEST_FAILURE')) + TestPersistence.loadFailure = new SessionQueryError('typed load failure', 'SESSION_QUERY_EVENT_NOT_FOUND') + await expect(ctx.sessionQuery.listEvents(persistedOnly.id)).rejects.toThrow(expectCode('SESSION_QUERY_EVENT_NOT_FOUND')) await persistenceFiber.dispose() TestPersistence.loadFailure = undefined @@ -682,9 +683,11 @@ describe('semantic text extractors', () => { session.append('user/message', { content: [{ type: 'test/text', value: 'block note' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) const provider = new FakeProvider() ctx.sessionQuery.registerSearchProvider(provider) + let disposeEvent!: () => void + let disposeContent!: () => void const extractorFiber = await ctx.plugin(Object.assign((inner: Context) => { - inner.sessionQuery.registerEventTextExtractor('test/note', { version: 'event-v1', extract: event => [event.data.note] }) - inner.sessionQuery.registerContentTextExtractor('test/text', { version: 'block-v1', extract: block => [block.value] }) + disposeEvent = inner.sessionQuery.registerEventTextExtractor('test/note', { version: 'event-v1', extract: event => [event.data.note] }) + disposeContent = inner.sessionQuery.registerContentTextExtractor('test/text', { version: 'block-v1', extract: block => [block.value] }) }, { inject: ['sessionQuery'] })) await ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'x' }) @@ -697,11 +700,13 @@ describe('semantic text extractors', () => { expect(() => ctx.sessionQuery.registerContentTextExtractor('test/text', { version: ' ', extract: () => [] })) .toThrow(expectCode('SESSION_QUERY_INVALID_EXTRACTOR')) - await extractorFiber.dispose() + disposeEvent() + disposeContent() await ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'x' }) const second = provider.live.get(session.id) expect(second?.documents).toEqual([]) expect(second?.fingerprint).not.toBe(first?.fingerprint) + await extractorFiber.dispose() const replacementFiber = await ctx.plugin(Object.assign((inner: Context) => { inner.sessionQuery.registerEventTextExtractor('test/note', { version: 'event-v2', extract: event => [`replacement ${event.data.note}`] }) @@ -712,6 +717,8 @@ describe('semantic text extractors', () => { expect(third?.documents.map(document => document.text)).toEqual(['replacement event note', 'replacement block note']) expect(third?.fingerprint).not.toBe(second?.fingerprint) await replacementFiber.dispose() + await ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'x' }) + expect(provider.live.get(session.id)?.documents).toEqual([]) }) }) @@ -721,8 +728,8 @@ describe('configuration', () => { await ctx.plugin(SessionStore) await expect(ctx.plugin(SessionQueryService, { defaultLimit: 3, maxLimit: 2 })) .rejects.toThrow(expectCode('SESSION_QUERY_INVALID_CONFIG')) - const error = new SessionQueryError('test', 'SESSION_QUERY_TEST') - expect(error).toMatchObject({ name: 'SessionQueryError', code: 'SESSION_QUERY_TEST' }) + const error = new SessionQueryError('test', 'SESSION_QUERY_INVALID_CONFIG') + expect(error).toMatchObject({ name: 'SessionQueryError', code: 'SESSION_QUERY_INVALID_CONFIG' }) }) it('uses constructor defaults and removes the service on plugin disposal', async () => { diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index eada21d0c4..0318e33fea 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -531,7 +531,7 @@ function collectEventRelations(): Map { const visit = (node: ts.Node): void => { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { const method = node.expression.name.text - if (!isCordisContextReceiver(node.expression, sf)) { + if (!isCordisContextReceiver(node.expression)) { ts.forEachChild(node, visit) return } @@ -561,9 +561,12 @@ function collectEventRelations(): Map { return out } -function isCordisContextReceiver(expr: ts.PropertyAccessExpression, sf: ts.SourceFile): boolean { - const target = expr.expression.getText(sf) - return target === 'ctx' || target === 'this.ctx' +function isCordisContextReceiver(expr: ts.PropertyAccessExpression): boolean { + const receiver = expr.expression + if (ts.isIdentifier(receiver)) return receiver.text === 'ctx' || receiver.text === '_ctx' + return ts.isPropertyAccessExpression(receiver) + && receiver.expression.kind === ts.SyntaxKind.ThisKeyword + && (receiver.name.text === 'ctx' || receiver.name.text === '_ctx') } function eventArg(args: ts.NodeArray, method: string): string | undefined { diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 671419ab59..32d8e91f6e 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -49,9 +49,12 @@ { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchPageRequest", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchRequest", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventSearchRequest", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchSpec", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventSearchSpec", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventSearchHit", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchHit", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchPage", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionQueryErrorCode", "source": "packages/session-query/session-query/src/config.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventReadRequest", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventWindow", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionLineageNode", "source": "packages/session-query/session-query/src/types.ts" }, diff --git a/vendor/README.md b/vendor/README.md index dabf1b17af..856b958e8f 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -35,7 +35,7 @@ Keep this log exhaustive — every divergence from upstream must be listed. 3. **All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json`, emit TypeScript intermediates to `lib/types`, and declare project references. 4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`. 5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface. -6. **`cordis/src/events.ts`**: `parallel()` captures each listener invocation in its own promise before awaiting the group. A synchronous throw therefore rejects the dispatch without preventing later parallel listeners from starting. +6. **`cordis/src/events.ts`**: a `FIXME` documents the upstream `parallel()` bug where a synchronous listener throw aborts callback enumeration and starves later listeners; runtime behavior remains upstream-identical pending an upstream fix. ## Sync procedure diff --git a/vendor/cordis/src/events.ts b/vendor/cordis/src/events.ts index 6e5212b734..e483afadf5 100644 --- a/vendor/cordis/src/events.ts +++ b/vendor/cordis/src/events.ts @@ -106,15 +106,9 @@ export class EventsService { /** Run listeners concurrently and wait for all of them. */ async parallel(...args: any[]) { - const callbacks = this.dispatch('emit', args) - const results = callbacks.map((cb) => { - try { - return Promise.resolve(cb(...args)) - } catch (error: unknown) { - return Promise.reject(error) - } - }) - await Promise.all(results) + // FIXME(cordis upstream): A synchronous listener throw aborts callback + // enumeration here and starves later parallel listeners. Fix upstream. + await Promise.all(this.dispatch('emit', args).map(cb => cb(...args))) } /** Run listeners synchronously without waiting for returned promises. */