diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 9611adab6f..6a31eddd70 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -18,6 +18,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t | [llm-streaming.md](llm-streaming.md) | the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam | | [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, the turn-enclosure invariant | | [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` | +| [session-query.md](session-query.md) | the retrieval seam: logical session/event records, filters, traces, search pages, extractors, and provider synchronization types | | [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, the `tools/pre-execute`/`tools/post-execute` pipeline | | [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy | | [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashTask`s | diff --git a/docs/core-data-structures/persistence.md b/docs/core-data-structures/persistence.md index 4dbb8fb2af..012e606f48 100644 --- a/docs/core-data-structures/persistence.md +++ b/docs/core-data-structures/persistence.md @@ -73,6 +73,20 @@ interface CreateSessionOptions { Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`. +## `SessionPersistedChange` — committed-log notification range + +The observe-only `session/persisted` event carries the canonical header and the committed range. A repair can report `toSeq < fromSeq` when it only removes a torn fragment. + +Source: [`packages/session-persistence/session-persistence/src/index.ts`](../../packages/session-persistence/session-persistence/src/index.ts) + +```ts type-equiv +export interface SessionPersistedChange { + kind: 'append' | 'repair' + fromSeq: number + toSeq: number +} +``` + ## The backends Both implement the same abstract `SessionPersistence` (create/append/load/list over `SessionEvent`) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic: diff --git a/docs/core-data-structures/session-query.md b/docs/core-data-structures/session-query.md new file mode 100644 index 0000000000..8ad0f0cbf5 --- /dev/null +++ b/docs/core-data-structures/session-query.md @@ -0,0 +1,221 @@ +# Session Query + +The provider-neutral retrieval seam over live and optionally persisted sessions. The [package contract](../../packages/session-query/session-query) owns resolution, lifecycle, synchronization, and error behavior; this page catalogs the public data exchanged by callers, extractors, and search providers. + +Source: [`packages/session-query/session-query/src/types.ts`](../../packages/session-query/session-query/src/types.ts) + +## Logical records and filters + +`SessionRecord` exposes source availability independently from its live-preferred header. `SessionEventRecord` classifies every raw event against the folded surface. + +```ts type-equiv +export type SessionEventSurface = 'current' | 'shadowed' | 'log-only' +``` + +```ts type-equiv +export interface SessionRecord { + header: SessionHeader + live: boolean + persisted: boolean +} +``` + +```ts type-equiv +export interface SessionEventRecord { + sessionId: SessionId + seq: number + type: SessionEventType + time: number + surface: SessionEventSurface +} +``` + +Filters are serializable discriminated specs. Each spec is one transform in a chain; the literal types below are shared by in-memory filtering and provider pre-ranking requests. + +```ts type-equiv +export interface SessionQueryRange { + from?: number + to?: number +} +``` + +```ts type-equiv +export type SessionResultFilter = + | { kind: 'id'; values: readonly SessionId[] } + | { kind: 'cwd'; values: readonly (string | null)[] } + | { kind: 'created-at'; range: SessionQueryRange } + | { kind: 'parent'; values: readonly (SessionId | null)[] } + | { kind: 'availability'; values: readonly ('live' | 'persisted')[] } +``` + +```ts type-equiv +export type SessionEventResultFilter = + | { kind: 'seq'; range: SessionQueryRange } + | { kind: 'time'; range: SessionQueryRange } + | { kind: 'type'; values: readonly SessionEventType[] } + | { kind: 'surface'; values: readonly SessionEventSurface[] } +``` + +## Search requests and pages + +Both scopes use the same opaque-cursor page envelope. Session hits carry exactly one best event; event hits add only a plain-text snippet to the lightweight record. + +```ts type-equiv +export interface SessionQueryExecContext { + readonly signal?: AbortSignal +} +``` + +```ts type-equiv +export type SessionSearchProviderStatus = + | { readonly available: true } + | { readonly available: false; readonly reason: 'misconfigured' | 'unavailable' } +``` + +```ts type-equiv +export interface SessionSearchPageRequest { + limit?: number + cursor?: string +} +``` + +```ts type-equiv +export interface SessionSearchRequest extends SessionSearchPageRequest { + query: string + sessionFilters?: readonly SessionResultFilter[] + eventFilters?: readonly SessionEventResultFilter[] +} +``` + +```ts type-equiv +export interface SessionEventSearchRequest extends SessionSearchPageRequest { + sessionId: SessionId + query: string + filters?: readonly SessionEventResultFilter[] +} +``` + +```ts type-equiv +export interface SessionEventSearchHit extends SessionEventRecord { + snippet: string +} +``` + +```ts type-equiv +export interface SessionSearchHit extends SessionRecord { + bestMatch: SessionEventSearchHit +} +``` + +```ts type-equiv +export interface SessionSearchPage { + providerId: string + items: readonly T[] + nextCursor?: string +} +``` + +## 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. + +```ts type-equiv +export interface SessionEventReadRequest { + sessionId: SessionId + seq: number + before?: number + after?: number +} +``` + +```ts type-equiv +export interface SessionEventWindow { + session: SessionRecord + target: SessionEvent + events: SessionEvent[] + startSeq: number + endSeq: number +} +``` + +```ts type-equiv +export interface SessionLineageNode { + session: SessionRecord + children: SessionLineageNode[] +} +``` + +```ts type-equiv +export interface SessionLineageTrace { + target: SessionRecord + parents: SessionRecord[] + root?: SessionRecord + unresolvedParentId?: SessionId + children: SessionLineageNode[] +} +``` + +```ts type-equiv +export interface SessionEventTrace { + target: SessionEventRecord + shadowedBy?: number + replacementChain: number[] + shadows: number[] + references: number[] + referencedBy: number[] +} +``` + +## Extraction and provider synchronization + +Custom extractors are keyed by declaration-merged event or content discriminants and carry stable cache-invalidation versions. Providers receive complete event documents grouped into independently replaceable persisted and live snapshots. + +```ts type-equiv +export interface SessionEventTextExtractor { + version: string + extract(event: SessionEvent): readonly string[] +} +``` + +```ts type-equiv +export interface SessionContentTextExtractor { + version: string + extract(block: ContentBlockMap[K]): readonly string[] +} +``` + +```ts type-equiv +export interface SessionIndexDocument extends SessionEventRecord { + text: string +} +``` + +```ts type-equiv +export interface SessionIndexSnapshot { + session: SessionRecord + fingerprint: string + documents: readonly SessionIndexDocument[] +} +``` + +```ts type-equiv +export interface SessionPersistedIndexEntry { + sessionId: SessionId + fingerprint: string +} +``` + +```ts type-equiv +export interface SessionSearchProvider { + readonly id: string + status(): SessionSearchProviderStatus + persistedInventory(): Promise + setPersistedActive(active: boolean): Promise + replacePersisted(snapshot: SessionIndexSnapshot): Promise + removePersisted(sessionId: SessionId): Promise + replaceLive(snapshot: SessionIndexSnapshot): Promise + removeLive(sessionId: SessionId): Promise + searchSessions(request: SessionSearchRequest, exec?: SessionQueryExecContext): Promise> + searchEvents(request: SessionEventSearchRequest, exec?: SessionQueryExecContext): Promise> +} +``` diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index 1b12c3fc48..d8292c49b3 100644 --- a/docs/core-data-structures/session.md +++ b/docs/core-data-structures/session.md @@ -196,6 +196,26 @@ export interface SurfaceNode { } ``` +### `SurfaceFoldReplacement` and `SurfaceFoldResult` — a complete surface replay + +`foldSurface(events)` returns detached current nodes together with the actual node seqs shadowed by each declared replacement range. `SurfaceManager` uses the same transition functions for its incremental cache. + +```ts type-equiv +export interface SurfaceFoldReplacement { + seq: number + start: number + end: number + shadowedSeqs: number[] +} +``` + +```ts type-equiv +export interface SurfaceFoldResult { + nodes: SurfaceNode[] + replacements: SurfaceFoldReplacement[] +} +``` + ## Derived history: `deriveMessages()` and `deriveEventMessage()` `Session.deriveMessages()` projects the event log into the `Message[]` the model sees — cached (each surface node projected once, when first seen; a surface rewrite rebuilds) and frozen (a fresh array per call over shared, deep-frozen messages, so mutating logged history through a projection is unrepresentable). `deriveEventMessage(event)` is the per-node pure function the fold applies — public so external reconstructors and the dev invariant project a log prefix with exactly the same rules and cannot disagree with the cache. The projection rules: diff --git a/packages/core/session/tests/session.spec.ts b/packages/core/session/tests/session.spec.ts index e51ed923df..b32ae07fdd 100644 --- a/packages/core/session/tests/session.spec.ts +++ b/packages/core/session/tests/session.spec.ts @@ -374,9 +374,12 @@ describe('SessionStore', () => { expect(observations).toHaveLength(1) }) - it('contains rejected session/removed listeners during teardown', async () => { + it('contains failing session/removed listeners without starving later observers', 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) @@ -385,6 +388,7 @@ 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 6d95fbb4b2..4cf4e6ff67 100644 --- a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts +++ b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts @@ -128,11 +128,12 @@ 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 ab06959ea5..70b57dad07 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -22,7 +22,7 @@ Session filters cover id, exact cwd, inclusive creation time, parent id/root, an ## Full-text providers -`registerSearchProvider(provider)` is effect-scoped and ids are unique. Without `searchProvider`, exactly one locally available provider must be registered; explicit selection fails loudly when the named provider is missing or unavailable. Search pages default to 20 hits and reject limits above 100. Provider scores never cross the public API: event hits carry a plain snippet, while each session hit carries exactly one best matching event. +`registerSearchProvider(provider)` is effect-scoped and ids are unique. Without `searchProvider`, exactly one locally available provider must be registered; explicit selection fails loudly when the named provider is missing or unavailable. Search pages default to 20 hits and reject limits above 100; a provider returning more hits than the normalized request limit fails with a typed provider error rather than silently dropping cursor-addressable results. Provider scores never cross the public API: event hits carry a plain snippet, while each session hit carries exactly one best matching event. The service feeds providers two independent layers: a durable persisted base (`persistedInventory`, `replacePersisted`, `removePersisted`, `setPersistedActive`) and an ephemeral live override (`replaceLive`, `removeLive`). A search waits for the relevant source state observed before its call: the whole corpus for session search, only the target for a live event search. Failed derived updates do not fail session writes; affected searches receive `SESSION_QUERY_INDEX_FAILED`, and a later search retries the dirty state. `AbortSignal` lets a caller stop waiting and is also passed to provider search. diff --git a/packages/session-query/session-query/src/corpus.ts b/packages/session-query/session-query/src/corpus.ts index 980e11e416..87fb7a5533 100644 --- a/packages/session-query/session-query/src/corpus.ts +++ b/packages/session-query/session-query/src/corpus.ts @@ -12,10 +12,18 @@ interface PersistenceBinding { token: symbol service: SessionPersistence headers: Map + /** Notifications retained until a list that began after them completes. */ + observations: Map + observationGeneration: number error?: unknown refreshing: Promise | undefined } +interface PersistedObservation { + generation: number + header: SessionHeader +} + /** Active persistence view used by provider reconciliation. */ export interface PersistenceView { /** Canonical headers in deterministic creation order. */ @@ -132,6 +140,8 @@ export class SessionCorpus { token: Symbol('session-query-persistence'), service, headers: new Map(), + observations: new Map(), + observationGeneration: 0, refreshing: undefined, } this._persistence = binding @@ -140,7 +150,10 @@ export class SessionCorpus { ctx.on('session/persisted', (header) => { /* v8 ignore next -- a stale notification can race optional-service disposal */ if (this._persistence?.token !== binding.token) return - binding.headers.set(header.id, structuredClone(header)) + const snapshot = structuredClone(header) + const observation = { generation: ++binding.observationGeneration, header: snapshot } + binding.headers.set(header.id, snapshot) + binding.observations.set(header.id, observation) this._onPersistenceChange(true) }) ctx.effect(() => () => { this._detachPersistence(binding) }, 'sessionQuery.persistenceBinding') @@ -155,10 +168,21 @@ export class SessionCorpus { private _refreshPersistence(binding: PersistenceBinding): Promise { if (binding.refreshing !== undefined) return binding.refreshing + const startGeneration = binding.observationGeneration const refresh = binding.service.list().then((headers) => { /* v8 ignore next -- a list completion can race optional-service disposal */ if (this._persistence?.token !== binding.token) return - binding.headers = new Map(headers.map(header => [header.id, structuredClone(header)])) + const nextHeaders = new Map(headers.map(header => [header.id, structuredClone(header)])) + for (const [id, observation] of binding.observations) { + // A notification newer than this list's snapshot is the authoritative + // read-your-writes layer; older ones must already be present in list(). + if (observation.generation > startGeneration) { + nextHeaders.set(id, structuredClone(observation.header)) + } else { + binding.observations.delete(id) + } + } + binding.headers = nextHeaders binding.error = undefined this._onPersistenceChange(true) }).catch((error: unknown) => { diff --git a/packages/session-query/session-query/src/provider.ts b/packages/session-query/session-query/src/provider.ts index 7000c0e6d1..1574bcc0b2 100644 --- a/packages/session-query/session-query/src/provider.ts +++ b/packages/session-query/session-query/src/provider.ts @@ -292,7 +292,10 @@ export class SessionProviderCoordinator { if (page.providerId !== state.provider.id) { throw new SessionQueryError(`session-query provider "${state.provider.id}" returned providerId "${page.providerId}"`, 'SESSION_QUERY_PROVIDER_ERROR') } - return page.items.length <= limit ? page : { ...page, items: page.items.slice(0, limit) } + if (page.items.length > limit) { + throw new SessionQueryError(`session-query provider "${state.provider.id}" returned ${page.items.length} items for limit ${limit}`, 'SESSION_QUERY_PROVIDER_ERROR') + } + return page } } diff --git a/packages/session-query/session-query/src/types.ts b/packages/session-query/session-query/src/types.ts index d3e819cf1a..43c63a034c 100644 --- a/packages/session-query/session-query/src/types.ts +++ b/packages/session-query/session-query/src/types.ts @@ -118,7 +118,7 @@ export interface SessionSearchHit extends SessionRecord { export interface SessionSearchPage { /** Stable id of the provider that produced this page. */ providerId: string - /** Ranked hits in deterministic provider order. */ + /** Ranked hits in deterministic provider order, no longer than the requested limit. */ items: readonly T[] /** Opaque next-page cursor, absent when the result is exhausted. */ nextCursor?: string 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 88fced88d3..0da0b4f41d 100644 --- a/packages/session-query/session-query/tests/session-query.spec.ts +++ b/packages/session-query/session-query/tests/session-query.spec.ts @@ -52,11 +52,15 @@ class TestPersistence extends SessionPersistence { static entries = new Map() static listFailure: unknown static loadFailure: unknown + static listBarrier: Promise | undefined + static onList: (() => void) | undefined static reset(entries: readonly { meta: SessionHeader; events: SessionEvent[] }[] = []): void { this.entries = new Map(entries.map(entry => [entry.meta.id, structuredClone(entry)])) this.listFailure = undefined this.loadFailure = undefined + this.listBarrier = undefined + this.onList = undefined } create(meta: SessionHeader): Promise { @@ -80,7 +84,9 @@ class TestPersistence extends SessionPersistence { list(): Promise { if (TestPersistence.listFailure !== undefined) return Promise.reject(asError(TestPersistence.listFailure)) - return Promise.resolve([...TestPersistence.entries.values()].map(entry => structuredClone(entry.meta))) + const snapshot = [...TestPersistence.entries.values()].map(entry => structuredClone(entry.meta)) + TestPersistence.onList?.() + return (TestPersistence.listBarrier ?? Promise.resolve()).then(() => snapshot) } } @@ -182,6 +188,12 @@ function asError(value: unknown): Error { return value instanceof Error ? value : new Error(String(value)) } +function deferred(): { promise: Promise; resolve: () => void } { + let resolve!: () => void + const promise = new Promise((done) => { resolve = done }) + return { promise, resolve } +} + describe('pure result filters', () => { it('chains session filters as AND while values within one filter are OR', () => { const root: SessionRecord = { header: header('root', 1, { cwd: '/a' }), live: true, persisted: false } @@ -379,8 +391,8 @@ describe('provider selection and synchronization', () => { { ...record, bestMatch }, { ...record, bestMatch }, { ...record, bestMatch }, ], nextCursor: 'next' } - const page = await ctx.sessionQuery.searchSessions({ query: ' hello ', sessionFilters: [{ kind: 'availability', values: ['live'] }] }) - expect(page.items).toHaveLength(2) + await expect(ctx.sessionQuery.searchSessions({ query: ' hello ', sessionFilters: [{ kind: 'availability', values: ['live'] }] })) + .rejects.toThrow(expectCode('SESSION_QUERY_PROVIDER_ERROR')) expect(provider.sessionRequests[0]).toMatchObject({ query: 'hello', limit: 2 }) expect(provider.live.get(session.id)?.documents[0]?.text).toBe('hello') await expect(ctx.sessionQuery.searchSessions({ query: ' ' })).rejects.toThrow(expectCode('SESSION_QUERY_INVALID_QUERY')) @@ -534,6 +546,30 @@ describe('provider selection and synchronization', () => { expect(provider.persisted.has(persisted.id)).toBe(true) }) + it('preserves persisted observations that race an older inventory listing', async () => { + TestPersistence.reset() + const listStarted = deferred() + const releaseList = deferred() + TestPersistence.onList = listStarted.resolve + TestPersistence.listBarrier = releaseList.promise + const ctx = await liveContext() + const provider = new FakeProvider() + ctx.sessionQuery.registerSearchProvider(provider) + await ctx.plugin(TestPersistence) + await listStarted.promise + + const announced = header('racing-announcement', 3) + TestPersistence.entries.set(announced.id, { meta: announced, events: eventLog('after durable notification') }) + await ctx.parallel('session/persisted', announced, { kind: 'append', fromSeq: 0, toSeq: 0 }) + const search = ctx.sessionQuery.searchSessions({ query: 'notification' }) + releaseList.resolve() + + await expect(search).resolves.toMatchObject({ providerId: provider.id }) + expect(provider.persisted.get(announced.id)?.documents[0]?.text).toBe('after durable notification') + TestPersistence.listBarrier = undefined + TestPersistence.onList = undefined + }) + it('synchronizes only a live target for event search and retries dirty failures', async () => { const ctx = await liveContext() const session = ctx.sessions.create(SessionId('target')) @@ -646,11 +682,9 @@ 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) => { - 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] }) + 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] }) }, { inject: ['sessionQuery'] })) await ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'x' }) @@ -663,13 +697,21 @@ describe('semantic text extractors', () => { expect(() => ctx.sessionQuery.registerContentTextExtractor('test/text', { version: ' ', extract: () => [] })) .toThrow(expectCode('SESSION_QUERY_INVALID_EXTRACTOR')) - disposeEvent() - disposeContent() + await extractorFiber.dispose() 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}`] }) + inner.sessionQuery.registerContentTextExtractor('test/text', { version: 'block-v2', extract: block => [`replacement ${block.value}`] }) + }, { inject: ['sessionQuery'] })) + await ctx.sessionQuery.searchEvents({ sessionId: session.id, query: 'x' }) + const third = provider.live.get(session.id) + expect(third?.documents.map(document => document.text)).toEqual(['replacement event note', 'replacement block note']) + expect(third?.fingerprint).not.toBe(second?.fingerprint) + await replacementFiber.dispose() }) }) diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 881ce06578..671419ab59 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -31,9 +31,38 @@ { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceOp", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceIntent", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceNode", "source": "packages/core/session/src/surface.ts" }, + { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldReplacement", "source": "packages/core/session/src/surface.ts" }, + { "doc": "docs/core-data-structures/session.md", "symbol": "SurfaceFoldResult", "source": "packages/core/session/src/surface.ts" }, { "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" }, + { "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionPersistedChange", "source": "packages/session-persistence/session-persistence/src/index.ts" }, + + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventSurface", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionRecord", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventRecord", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionQueryRange", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionResultFilter", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventResultFilter", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionQueryExecContext", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchProviderStatus", "source": "packages/session-query/session-query/src/types.ts" }, + { "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": "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": "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" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionLineageTrace", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventTrace", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionEventTextExtractor", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionContentTextExtractor", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionIndexDocument", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionIndexSnapshot", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionPersistedIndexEntry", "source": "packages/session-query/session-query/src/types.ts" }, + { "doc": "docs/core-data-structures/session-query.md", "symbol": "SessionSearchProvider", "source": "packages/session-query/session-query/src/types.ts" }, { "doc": "docs/core-data-structures/tools.md", "symbol": "ToolDefinition", "source": "packages/core/tools/src/index.ts" }, { "doc": "docs/core-data-structures/tools.md", "symbol": "SchemaProp", "source": "packages/core/tools/src/schema.ts" }, diff --git a/vendor/README.md b/vendor/README.md index bf0f0b5a8c..dabf1b17af 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -35,6 +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. ## Sync procedure diff --git a/vendor/cordis/src/events.ts b/vendor/cordis/src/events.ts index 4461816537..6e5212b734 100644 --- a/vendor/cordis/src/events.ts +++ b/vendor/cordis/src/events.ts @@ -106,7 +106,15 @@ export class EventsService { /** Run listeners concurrently and wait for all of them. */ async parallel(...args: any[]) { - await Promise.all(this.dispatch('emit', args).map(cb => cb(...args))) + 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) } /** Run listeners synchronously without waiting for returned promises. */