@arnilo/prism 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/migration.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Migration guide
2
2
 
3
+ ## 0.2.2 → 0.2.3 build, coverage, and release evidence integrity (no migration)
4
+
5
+ Release **0.2.3** (plan 023) is a **tooling-and-evidence-only cut**: build serialization (`scripts/with-build-lock.mjs` — one `O_EXCL` lockfile serializing every emit/test leaf so concurrent compilers never expose a partial live `dist/`), corrected workspace coverage denominators (package-local `--test-coverage-include=dist/**`, evidence-based per-package thresholds with `protectedException` durable-leg rows), the machine-auditable release skip manifest (`scripts/release-skip-manifest.mjs` → `scripts/release-evidence.json` with `pass`/`skip`/`blocked`/`protected` states; required surfaces without evidence record `blocked` and fail the release gate), and stabilized quality gates (Biome 2.x `preset` config migration with zero lint diagnostics, deterministic timing-assertion barriers, machine-readable `lint-report.sarif` + `unused-report.json`). **No runtime code path, persisted shape, event schema, default, or exported declaration changed** (the plain compat gate at 0.2.3 shows the version literal only). Store compatibility with 0.2.2: **compatible in both directions** — no migration step; rollback = restore the 0.2.2 manifests/tag (stores never change; rollback reopens only the partial-`dist` race and the polluted coverage denominator, both CI/tooling defects, never data defects).
6
+
7
+ ## 0.2.1 → 0.2.2 concurrent state and durability integrity (plan 022)
8
+
9
+ Release **0.2.2** (plan 022) makes four concurrency/durability boundaries atomic or fail-loud. The API surface is **additive-only** (plain reviewed compat gate at 0.2.2: expected deltas are the version literal, `ModelRouterStateStore.reserveBudget`/`commitBudget`/`releaseBudget` plus `ModelRouterReservation`/`ModelRouterBudgets.reservationTtlMs`/`ModelRouterLimits.maxRateKeys`/`maxBudgetKeys` (memory + Postgres), `SessionRecord.version` with `appendSession` `expectedVersion`, `EventMultiplexerError` with code `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER`, and the `@arnilo/prism/testing/state-concurrency-conformance` subpath; no removal, no `--allow-break`). Three of the four changes tighten behavior where 0.2.1 silently accepted a race — concurrent hosts may now see an explicit conflict where 0.2.1 lost an update or oversubscribed a budget:
10
+
11
+ 1. **Atomic model-budget reservation (`model-router`, `enterprise-postgres`).** Admission is now reserve/commit/release: `reserveBudget` runs at admission and fails the request when `used + reserved + requested` would exceed the window max, returning `{ reservationId, fencingToken, admitted, retryAfterMs? }`; `commitBudget` applies the actual usage delta at the outcome (an expired reservation still charges the reserved amount with `unknownUsage: true` so a late commit can never disappear from accounting); `releaseBudget` frees an uncommitted reservation. `readBudget`-based admission stays for requests with no per-request cap, and the 0.2.1 post-hoc `addUsage` remains as retrospective accounting only — it is no longer admission authority.
12
+
13
+ ```js
14
+ // 0.2.1: readBudget then consumeRate then addUsage — concurrent admissions could collectively oversubscribe
15
+ // 0.2.2: admission reserves the full per-request cap, outcome commits/releases actuals
16
+ const reservation = await store.reserveBudget({
17
+ key: { tenantId, principalId, provider, model },
18
+ tokens: request.maxTokens, costUsd: request.maxCostUsd, // per-request caps, when set
19
+ windowMs: 24 * 60 * 60 * 1000, reservationTtlMs: 60_000,
20
+ });
21
+ if (!reservation.admitted) { /* denied; retry after reservation.retryAfterMs */ }
22
+ // ... run the request ...
23
+ await store.commitBudget({
24
+ key, reservationId: reservation.reservationId,
25
+ fencingToken: reservation.fencingToken, tokens: actualTokens, windowMs: 24 * 60 * 60 * 1000,
26
+ });
27
+ ```
28
+
29
+ Reservations expire after `reservationTtlMs` (default 60,000 ms, bounded to 31 days) even if a host never commits, so a crashed request cannot hold capacity forever. Rate/budget/circuit key maps are now capped (`maxRateKeys`/`maxBudgetKeys`, default 4,096, hard cap 65,536; circuits stay 1,024/16,384) with LRU eviction on insert; a budget row holding an active reservation is never evicted (the eviction candidates exclude held rows, and if nothing is evictable the insert fails with `ERR_PRISM_MODEL_ROUTER_STATE` `capacity-exhausted`). The durable Postgres store keeps reservations in a new `reservations` JSONB column on `prism_model_router_budgets` (migration 003, forward-only, applied automatically by `applyEnterpriseMigrations`; existing rows are untouched and read as no reservations).
30
+
31
+ 2. **Atomic conversation metadata (`session-store-postgres`, `session-store-sqlite`, core `SessionRecord`).** `SessionRecord` gains `version` (fresh rows start at 1; migration 008 backfills legacy 0-version rows to 1) and `appendSession` accepts `expectedVersion`: `0` = create-only, `N > 0` = exact-version CAS update-only, omitted = the 0.2.1 last-write-wins behavior for untyped/legacy callers. A stale write throws `SessionMetadataConflictError` (`metadata_conflict`) carrying only `{ id, expectedVersion, currentVersion }` — never metadata content — and the HTTP server maps it to 409. Concurrent create/branch/archive are now single-statement: the branch `maxActiveBranches` cap is enforced inside the CAS write (a concurrent branch at cap-1 fails its version guard instead of silently dropping the oldest ref), archive wins over a stale concurrent write, and a retention-deleted session is never resurrected (the update arm requires the row to still exist).
32
+
33
+ ```js
34
+ // 0.2.1: create could race to the last metadata write; concurrent branch calls could lose a ref
35
+ // 0.2.2: exactly one concurrent writer wins per version; losers get metadata_conflict
36
+ const { version } = await persistence.appendSession({
37
+ id: sessionId, ...ownership, createdAt, updatedAt, metadata: { state: "active" },
38
+ expectedVersion: 0, // create-only: conflict if the session already exists
39
+ });
40
+ try {
41
+ await persistence.appendSession({ ...record, metadata: { state: "archived" }, expectedVersion: version });
42
+ } catch (error) {
43
+ if (error.code === "metadata_conflict") { /* re-read the winning version and retry */ }
44
+ }
45
+ ```
46
+
47
+ 3. **Single-consumer `EventMultiplexer` (core).** `createEventMultiplexer().subscribe()` now rejects a second concurrent consumer with `EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER` instead of parking both consumers on one queue and silently losing events. The slot frees when the active consumer's iterator completes, is `return()`ed at a yield, or the multiplexer closes. Hosts that previously relied on multiple `subscribe()` calls sharing one multiplexer must either serialize consumption or use the event source's own broadcast `subscribe` (agent-events), which still supports multiple subscribers. `createWorkflowEventBus` and the supervisor (the only in-repo consumers) are unaffected — each already uses a single subscriber.
48
+
49
+ 4. **Restart-stable NATS durable consumer identity (`session-store-nats`).** The durable consumer name is now exactly `prism_<hmac16 of tenantId|sessionId|runId>` — the 0.2.1 random suffix is gone, so a crashed durable subscribe is reused at its last-acked position by a restarting process (cursor resume, at-least-once). Clean stops still delete the durable consumer (resume then relies on the HMAC-signed cursor); only a crash leaves the consumer in place. Pre-0.2.2 consumers minted with the random suffix (`prism_<digest>_<random>`) are orphaned and reclaimed by the existing `deleteConsumer`/consumer-enumeration cleanup path on the next clean stop of a same-subject subscribe.
50
+
51
+ 5. **Bounded, non-durable active-run registries (`workflows`).** The in-process workflow active-run registry is documented as non-durable (no timer, no background service): `registerActiveWorkflowRun` sweeps aborted/leaked entries before every insert and fails closed with `WorkflowRuntimeError` `ERR_PRISM_WORKFLOW_RUN_REGISTRY_OVERFLOW` at the 512 cap instead of evicting a live entry (a live eviction could silently allow a duplicate run). A run whose promise never settles is reclaimed only when it is aborted or the cap forces a sweep — there is no durable recovery of active runs in 0.2.2 (see Further Actions: 0.2.6).
52
+
53
+ **Store compatibility:** 0.2.2 is **not** rollback-compatible with 0.2.1 in the Postgres/SQLite persisted shape: `prism_sessions` gains a `version` column (migration 008) and `prism_model_router_budgets` gains a `reservations` column (enterprise migration 003). Both migrations are forward-only and additive — 0.2.2 code reads 0.2.1 databases correctly after migration (backfill included); a 0.2.1 binary pointed at a 0.2.2 database still works because the new columns are nullable/defaulted, but it will not maintain versions or reservations. The NATS durable-name change touches no persisted data (consumers are runtime state; orphaned 0.2.1 consumers are reclaimed on the next clean stop).
54
+
55
+ **Rollout:** upgrade core and the session stores together (migration 008 runs automatically via the existing checksummed `prism_migrations`; the version column must exist before any host writes CAS updates). Then `enterprise-postgres` (migration 003) and `model-router` (reservation admission can be enabled per-host; hosts that never call `recordUsage` rely on TTL expiry). Then `workflows`/`server` (conversation CAS is transparent to clients except new 409 responses), then `session-store-nats`. Branch/archive callers that intentionally lost races in 0.2.1 must now handle `metadata_conflict` (re-read + retry) where they previously accepted last-write-wins.
56
+
57
+ **Rollback risk:** restoring 0.2.1 against a 0.2.2 database is safe for reads and last-write-wins writes (the new columns are ignored) but silently reopens all four race windows: oversubscription, conversation lost updates, silent multi-subscriber event loss, and non-restart-stable NATS resume. Rollback is therefore only a stopgap, not a mitigation — prefer fixing the failing host on 0.2.2.
58
+
3
59
  ## 0.2.0 → 0.2.1 provider completion and outbound trust boundaries (plan 021)
4
60
 
5
61
  Release **0.2.1** (plan 021) tightens the streaming-completion, outbound-fetch, and credential/signing/upload boundaries. The API surface is **additive-only** (plain reviewed compat gate at 0.2.1: the only deltas are the version literal and `@arnilo/prism-mcp` transport helpers `boundResponse`/`defaultResolver`/`isLoopbackAddress`/`isLoopbackHostname`/`normalizeHostname`/`raceAbort`/`requestPinned`/`resolvePinnedAddress` becoming re-exports of the lifted core primitives — same names, same signatures, no removal; no `--allow-break`), with five documented security-motivated behavior tightenings. Untyped/legacy callers may now fail where 0.2.0 silently proceeded:
@@ -17,22 +17,24 @@ Do not put secrets, prompts, or raw OpenRouter keys into diagnostics. Do not hon
17
17
  | `createModelRouter({ resolver, stateStore?, ... })` | Wraps host `ProviderResolver`; omit `stateStore` for in-process memory state or pass durable async state. |
18
18
  | `allowList.providers` / `allowList.models` | Exact provider id / model id or `provider/model` |
19
19
  | `allowedResidencies` | Request residency must match when configured |
20
- | `budgets` / per-call `maxTokens` / `maxCostUsd` | Finite non-negative ceilings; `recordUsage` charges |
20
+ | `budgets` / per-call `maxTokens` / `maxCostUsd` | Finite non-negative ceilings; requests with a per-call cap reserve it atomically at admission; `recordUsage` commits actuals against the reservation |
21
+ | `budgets.reservationTtlMs` | How long an admission reservation pins capacity (default 60s); a run that outlives it reconciles as unknown usage |
21
22
  | `rateLimit` | Per identity+model key window |
22
23
  | `circuit` | Failure threshold + cooldown; keys capped |
23
24
  | `fallbacks` | Ordered candidates after primary; total attempts capped |
24
25
  | `allowOpenRouterRouting` | Default `false`; when false, routing metadata is stripped |
25
26
  | `onDiagnostics` | Optional redacted hook (e.g. policy ledger evidence ref) |
26
- | `router.resolve({ model, identity?, residency?, ... })` | Rich async selection |
27
+ | `router.resolve({ model, identity?, residency?, maxTokens?, ... })` | Rich async selection; returns `budgetReservation` when a per-request cap was reserved |
27
28
  | `router.providerSource` | Sync facade only for memory state; with `stateStore` it throws `ERR_PRISM_MODEL_ROUTER_ASYNC_STATE` rather than bypass durable checks. |
28
29
 
29
- Frozen caps (default / hard): attempts `3 / 8`, circuit keys `1,024 / 16,384`, diagnostics `8 KiB / 64 KiB`.
30
+ Frozen caps (default / hard): attempts `3 / 8`, circuit keys `1,024 / 16,384`, diagnostics `8 KiB / 64 KiB`, rate keys `4,096 / 65,536`, budget keys `4,096 / 65,536`.
30
31
 
31
32
  ## Outputs / response / events
32
33
 
33
- - `ModelRouterResolveResult` — selected `provider` + possibly stripped `model`, `diagnostics`, and `providerRequestPolicy`.
34
- - Deny throws `ModelRouterError` with code + redacted `diagnostics` (allow-list/residency/budget fail closed without calling resolver).
35
- - `await recordOutcome({ identity, success, circuitProbeToken? })` opens/closes circuits; `await recordUsage({ identity, ... })` advances budgets. Pass the probe token returned by `resolve` for a half-open outcome.
34
+ - `ModelRouterResolveResult` — selected `provider` + possibly stripped `model`, `diagnostics`, and `providerRequestPolicy`; `budgetReservation` carries the admission reservation handle when the request had a per-call budget cap.
35
+ - Deny throws `ModelRouterError` with code + redacted `diagnostics` (allow-list/residency/budget fail closed without calling resolver); budget denies carry `details.retryAfterMs`.
36
+ - `await recordOutcome({ identity, success, circuitProbeToken? })` opens/closes circuits; `await recordUsage({ identity, budgetReservation?, ... })` commits the reservation against actual usage (pass the handle returned by `resolve`) or advances budgets directly. Pass the probe token returned by `resolve` for a half-open outcome.
37
+ - A reservation whose TTL elapses before `recordUsage` charges the **reserved** amount and emits one redacted `unknown_usage` diagnostic (deterministic reconciliation, never a silent drop).
36
38
 
37
39
  ## Request/response example
38
40
 
@@ -139,10 +141,11 @@ await router.recordOutcome({ identity, provider, model, success: true, latencyMs
139
141
  ## Security and performance notes
140
142
 
141
143
  - Allow-list and residency denies never call the underlying resolver.
142
- - Without `stateStore`, budget/rate/circuit state is process-local, memory-capped, and oldest keys evict. It is not a cross-replica production path.
143
- - With `stateStore: createPostgresEnterpriseState(...).modelRouter`, rate/budget updates and circuit probes are atomic across replicas, use database time, and are owner/principal/provider/model scoped. Router calls become asynchronous and require verified identity.
144
- - Diagnostics carry identity refs and attempt outcomes only — no prompts/secrets. Durable state stores at most bounded numeric/timestamp/token material, never prompts or credentials.
145
- - Selection is O(attempts × state operations); no provider network I/O happens inside state updates. Recorded 0.0.23 PostgreSQL p95 point operations stayed under 50 ms and cursor/cleanup pages under 100 ms on the documented fixture.
144
+ - Budget admission is **reservation-based** when the request carries a per-request cap: `resolve` atomically reserves the full cap against remaining capacity (`max − used − reserved`) and returns a `budgetReservation` handle; parallel admissions can never collectively exceed the reserved budget. Commit the handle in `recordUsage` with the actual tokens/cost (a negative remainder is released back); release happens automatically on internal denial (rate limit, circuit open, provider miss), on TTL expiry, or on an explicit late commit (which charges the reserved amount as unknown usage). Requests without a per-request cap keep read-then-compare admission (`used >= cap` denies) and are outside the reservation guarantee.
145
+ - Without `stateStore`, budget/rate/circuit state is process-local, memory-capped (rate/budget/circuit keys), and LRU-evicts on insert; a held reservation's budget row is never evicted. It is not a cross-replica production path.
146
+ - With `stateStore: createPostgresEnterpriseState(...).modelRouter`, rate/budget updates, reservations, and circuit probes are atomic across replicas, use database time, and are owner/principal/provider/model scoped. Router calls become asynchronous and require verified identity.
147
+ - Diagnostics carry identity refs and attempt outcomes only — no prompts/secrets, tokens, or reservation material. Durable state stores at most bounded numeric/timestamp/token material, never prompts or credentials.
148
+ - Selection is O(attempts × state operations); no provider network I/O happens inside state updates. Reservation is one atomic UPSERT (denial adds one retry-after query); commit/release are O(1) row updates. Recorded 0.0.23 PostgreSQL p95 point operations stayed under 50 ms and cursor/cleanup pages under 100 ms on the documented fixture.
146
149
  - Raising hard caps requires a reviewed release update with tests and docs.
147
150
 
148
151
  ## Related APIs
@@ -15,7 +15,7 @@ Current contract groups:
15
15
  - Extensions/middleware: `ExtensionLifecycleEventName`, `ExtensionEvent`, `Extension`, `ExtensionAPI`, `MiddlewareHookName`, `Middleware`, `MiddlewareNext`, `MiddlewareRegistry`
16
16
  - Configuration/manifests: `ConfigProvider`, `ConfigLayer`, `ConfigLoadContext`, `PrismManifest`, `ManifestContributionDeclaration`, `ManifestResourceDeclaration`, `ManifestContributionKind`
17
17
  - Stores/resources/settings/credentials/compaction/retry/cache helpers: `SessionEntry`, `SessionStore`, `StoreFactory`, `Resource`, `ResourceLoader`, `ResourceLoadContext`, `SettingsProvider`, `CredentialRequest`, `Credential`, `CredentialResolver`, `CompactionStrategy`, `CompactionContext`, `CompactionResult`, `CompactionOptions`, `CompactionMiddlewarePayload`, `CompactionEntryData`, `DefaultCompactionStrategyOptions`, `RetryPolicy`, `RetryContext`, `RetryDecision`, `RetryOptions`, `RetryMiddlewarePayload`, `DefaultRetryPolicyOptions`, `CacheUsageReport`, `sanitizeCacheKey`, `mapCacheRetention`, `applyCacheControl`, `cacheHitRate`, `cacheSavings`, `cacheUsageReport`
18
- - Production persistence (adapter-facing): `ProductionPersistenceStore` (optional `lifecycle`), `CheckpointStore`, `CheckpointKey`, `CheckpointSaveInput`, `CheckpointRecord`, `CheckpointQuery`, `LeaseStore`, `LeaseKey`, `LeaseAcquireInput`, `LeaseClaimInput`, `LeaseRecord`, `PersistencePage`, `PersistenceQuery`, `OwnershipScope`, `SessionRecord`, `SessionQuery`, `BranchRecord`, `BranchQuery`, `SessionEntryQuery`, `RunRecord`, `RunQuery`, `RunFeedbackRecord`, `RunFeedbackStore`, `RunFeedbackQuery`, `AgentEventRecord`, `AgentEventQuery`, `ToolCallRecord`, `ToolCallQuery`, `UsageRecord`, `UsageQuery`, `AgentDefinitionRecord`, `AgentDefinitionQuery`, `RetentionPolicy`, `RetentionPolicyQuery`, `MigrationRecord`, `MigrationQuery`, `PersistenceLifecycleStore`, `LegalHoldRecord`, `TenantQuota`
18
+ - Production persistence (adapter-facing): `ProductionPersistenceStore` (optional `lifecycle`), `CheckpointStore`, `CheckpointKey`, `CheckpointSaveInput`, `CheckpointRecord`, `CheckpointQuery`, `LeaseStore`, `LeaseKey`, `LeaseAcquireInput`, `LeaseClaimInput`, `LeaseRecord`, `PersistencePage`, `PersistenceQuery`, `OwnershipScope`, `SessionRecord`, `SessionQuery`, `SessionMetadataConflict`, `SessionMetadataConflictError`, `SESSION_METADATA_CONFLICT_CODE`, `isSessionMetadataConflict`, `BranchRecord`, `BranchQuery`, `SessionEntryQuery`, `RunRecord`, `RunQuery`, `RunFeedbackRecord`, `RunFeedbackStore`, `RunFeedbackQuery`, `AgentEventRecord`, `AgentEventQuery`, `ToolCallRecord`, `ToolCallQuery`, `UsageRecord`, `UsageQuery`, `AgentDefinitionRecord`, `AgentDefinitionQuery`, `RetentionPolicy`, `RetentionPolicyQuery`, `MigrationRecord`, `MigrationQuery`, `PersistenceLifecycleStore`, `LegalHoldRecord`, `TenantQuota`
19
19
  - Identity: `Principal`, `AgentIdentity`, `IdentityVerifier`, `assertIdentityActive`, `narrowIdentity`, `ownershipFromIdentity`, `assertIdentityMatchesOwnership`, `assertIdentityPropagation`, `identityTelemetryAttributes`, `resolveRunIdentity`, `IdentityError`, identity limit constants
20
20
 
21
21
  ## When to use it
@@ -148,11 +148,11 @@ Important request shapes:
148
148
  | `CheckpointStore` | Generic versioned checkpoint capability: save/load/bounded-list/delete by namespace and key, with ownership, exact-version CAS, and lease fencing. `createMemoryCheckpointStore()` is the reference implementation; it is bounded — `maxRecords` (default 10,000, evicts least-recently-saved) and `maxValueBytes` (default 1 MiB per JSON value). |
149
149
  | `LeaseStore` | Atomic acquire/renew/release/get by namespace and key, with opaque claim tokens, expiry, ownership scope, and monotonically increasing takeover fences. `createMemoryLeaseStore()` is the reference implementation. |
150
150
  | `RunFeedbackStore` | Immutable append, bounded owned query, and owned deletion for ratings/comments/tags linked to existing run/trace/evaluation IDs. `createMemoryRunFeedbackStore()` is the reference implementation. |
151
- | `EventMultiplexer<T>` | Generic bounded fan-in from async sources. `createEventMultiplexer()` owns queue limits, overflow policy, abort, source teardown, and close behavior. |
151
+ | `EventMultiplexer<T>` | Generic bounded fan-in from async sources. `createEventMultiplexer()` owns queue limits, overflow policy, abort, source teardown, and close behavior. Single-consumer contract: a second concurrent `subscribe()` throws `EventMultiplexerError` (`ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER`); the slot frees when the active consumer completes/is `return()`ed at a yield or the multiplexer closes. `observe` fan-in is unchanged (broadcast happens at the source). |
152
152
  | `PersistencePage<T>` | Cursor-paginated result page: `items`, optional `nextCursor`, optional `total`. |
153
153
  | `PersistenceQuery` | Common pagination controls: `cursor?`, `limit?`, `order?: "asc" \| "desc"`. |
154
154
  | `OwnershipScope` | Multi-tenant scope: `tenantId?`, `accountId?`, `userId?`. Included in records and queries. |
155
- | `SessionRecord` / `SessionQuery` | Stored session and query filters (parent, agent definition, retention policy, timestamps, ownership). |
155
+ | `SessionRecord` / `SessionQuery` | Stored session and query filters (parent, agent definition, retention policy, timestamps, ownership). `SessionRecord.version` (with `appendSession` `expectedVersion`) is the optimistic metadata CAS: 0 = create-only, N = exact-version update; mismatch throws `SessionMetadataConflictError` (`metadata_conflict`). |
156
156
  | `SessionIndex` / `SessionSearchQuery` / `SessionSearchHit` | Bounded optional session search seam (`search` / `SessionStore.searchSessions?`). Filters: workspace (`metadata.workspaceRoot`), time, provider/model, label/summary, optional FTS `query`, ownership. Hits return `sessionId` + optional `leafId` for resume; never credentials. Caps via `resolveSessionSearchQuery` / `DEFAULT_*` / `HARD_MAX_*` session-search constants. |
157
157
  | `contextBudget` / `getContextBudgetReport` / `ContextBudgetError` | Opt-in assembler budget on `AssembleProviderInputOptions`; deterministic eviction; omission report in `ProviderRequest.metadata` (kinds/ids/sizes only). |
158
158
  | `AgentSession.steer` / `SteerOptions` / pending-steer caps | Mid-run enqueue into active run; optional `softInterrupt`; default 8 msgs / 64 KiB UTF-8. |
@@ -68,6 +68,7 @@ Run `npm run clean` explicitly after deleting source files or switching branches
68
68
  | `@arnilo/prism/providers/media` | `dist/providers/media.{js,d.ts}` |
69
69
  | `@arnilo/prism/testing/provider-conformance` | `dist/testing/provider-conformance.{js,d.ts}` |
70
70
  | `@arnilo/prism/testing/agent-event-source-conformance` | `dist/testing/agent-event-source-conformance.{js,d.ts}` |
71
+ | `@arnilo/prism/testing/state-concurrency-conformance` | `dist/testing/state-concurrency-conformance.{js,d.ts}` |
71
72
  | `@arnilo/prism/testing/session-store-conformance` | `dist/testing/session-store-conformance.{js,d.ts}` |
72
73
  | `@arnilo/prism/testing/compaction-conformance` | `dist/testing/compaction-conformance.{js,d.ts}` |
73
74
  | `@arnilo/prism/testing/tool-conformance` | `dist/testing/tool-conformance.{js,d.ts}` |
@@ -274,6 +275,45 @@ git push origin v0.1.2 # tag push triggers release.yml publish job (prove
274
275
 
275
276
  **Rollback notes.** `release:publish --version 0.1.2 --resume --report release-artifacts/publish-report.json` resumes an interrupted publication and skips only registry versions whose internal dependency fingerprint matches the local manifest. A failed package aborts the run with its status written to the report; re-run after fixing the cause. npm cannot unpublish the `0.1.2` line after 72 hours — a post-publication defect ships as a `0.1.x` patch (additive-only compat promise, `release:gate` enforced), or as a documented break in the next line with a `docs/migration.md` entry. `0.1.2` is store-compatible with `0.1.1` in **both directions** (no migration ran — same checksum-protected contract), so an operator may defer or roll back the patch without a database rollback.
276
277
 
278
+ ### 0.2.3 publish handoff (plan 023 Task 6)
279
+
280
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.2.3** (plan 023) is the build-coverage-and-release-evidence-integrity cut on the 0.2.x review-remediation line. API surface **additive-only** (plain reviewed compat gate at 0.2.3: delta is the version literal only — no export changes; baselines regenerated with `--update-baseline`, no `--allow-break`; freeze manifest `scripts/phase23-freeze-manifest.json`). Four tooling/evidence fixes, **no runtime contract change and no migration**: (1) **build serialization** — dependency-free `scripts/with-build-lock.mjs` serializes every emit/test leaf with one `O_EXCL` lockfile at `node_modules/.prism-build.lock` (pid + startedAt, read-back verified, stale-PID reclaim, `PRISM_BUILD_LOCK_TIMEOUT_MS` env override, fail-closed exit 1), so concurrent compilers can never expose a partial live `dist/`; the lock is never held by orchestrator scripts and `PRISM_BUILD_LOCK_HELD=1` prevents accidental nesting. **Caveat:** the lock only guards the wrapped leaves — a direct `tsc` invoked outside the wrapper can still race an importer, exactly like any external writer. (2) **corrected workspace coverage denominators** — workspace coverage runs use package-local `--test-coverage-include=dist/**` (imported core `dist` no longer pollutes package rows), the 60/70/75 core gate is unchanged, per-package line thresholds in `scripts/coverage-thresholds.json` are evidence-based (freeze-run minus 3 pp), env-gated durable-leg packages (`session-store-postgres`, `enterprise-postgres`, `memory`, `session-store-nats`) are `protectedException` rows shown separately, and `scripts/coverage-summary.json` is the machine-readable artifact the release gate reads. (3) **release skip manifest** — `scripts/release-skip-manifest.mjs` records every surface (`pass`/`skip`/`blocked`/`protected`, reason, required env names only) into `scripts/release-evidence.json`; a required surface with absent evidence records `blocked` and `release.mjs gate` fails closed — missing credentials/services can never convert into a green release. (4) **stabilized quality gates** — Biome 2.x `preset` config migration with zero lint diagnostics, the racy 150 ms MCP bridge timing assert replaced by a deterministic barrier, load-sensitive guards carry documented `ponytail:` ceilings, and `lint-report.sarif` + `unused-report.json` are machine-readable and CI-retained. Regression surface: `phase23-build-race` (8), `phase23-coverage` (4), `phase23-skip-manifest` (6), `phase23-quality-gates` (5), `phase23-security` (3, matrix items 4 and 12 by name) + packed plain-JS `security23.mjs` consumer. Exit gate green: npm test core + workspace + script gates, `sdk:ready` exit 0, audit 0 moderate, secret scans 0 findings, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.3, protected Postgres durable conformance evidence, release-evidence manifest with zero blocked surfaces; evidence in `scripts/phase23-baseline.json` `exitGate`. **Rollback notes.** Rollback = restore the 0.2.2 manifests/tag — but that reopens the partial-`dist` race window and the polluted coverage denominator, so prefer fixing the failing host on 0.2.3. Nothing persisted changes shape, so downgrade is store-safe. (CI remediation 2026-08-14: `coding-security` joined the `protectedException` rows — its native-sandbox legs probe `unshare --net` NETNS at load and skip on GitHub Actions runners, so the host-captured freeze threshold can never be met in CI; measured 72.80 lines in CI vs 80.18 on a NETNS-capable host.)
281
+
282
+ ```bash
283
+ # Operator prerequisites recorded: clean tree at the v0.2.3 tag candidate, GPG key, npm OIDC publisher.
284
+ node scripts/release.mjs bump --from 0.2.2 --to 0.2.3 # already applied by Task 6; idempotent
285
+ npm test # core + workspace suites + all script gates (incl. phase23 suites)
286
+ npm run security:threat-suites # phase8-11 + phase20 + phase21 + phase22 + phase23 public-entry conformance
287
+ PRISM_TEST_POSTGRES_URL=postgres://postgres:prism@127.0.0.1:54329/prism_test npm run sdk:ready
288
+ node scripts/release.mjs gate --version 0.2.3 # plain reviewed gate at 0.2.3: version literal only, 0 breaking deltas
289
+ npm run pack:dry-run # twice; diff reports — deterministic
290
+ npm audit --audit-level=moderate
291
+ npm run release:check -- --version 0.2.3 --report /tmp/prism-0.2.3-preflight.json
292
+ npm run release:publish -- --version 0.2.3 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.2.3-dry-run.json
293
+ # run the dry-run twice and diff the reports: deterministic, byte-identical
294
+ ```
295
+
296
+ Protected evidence (never a passing skip): the durable state-concurrency legs (Postgres `prism_phase23_*` schemas for sessions/checkpoints/events + enterprise router reservations/idempotency — `npm run test:postgres` under `PRISM_TEST_POSTGRES_URL`), the phase23 public-entry build-race + coverage-denominator conformance, and the live canaries (provider OIDC/OPA, MCP, A2A, Brave — always `protected` rows in the manifest, never `pass`). The release skip manifest names every skip class with its required env; missing protected evidence records 0.2.3 as **blocked**, never a passing skip.
297
+
298
+ ### 0.2.2 publish handoff (plan 022 Task 6)
299
+
300
+ **Decision: GO when the operator prerequisites below are recorded.** Release **0.2.2** (plan 022) is the concurrent-state-and-durability-integrity cut on the 0.2.x review-remediation line. API surface **additive-only** vs 0.2.1 (plain reviewed compat gate at 0.2.2: deltas are the version literal plus `ModelRouterStateStore.reserveBudget`/`commitBudget`/`releaseBudget`, `ModelRouterReservation`, `ModelRouterBudgets.reservationTtlMs`, `ModelRouterLimits.maxRateKeys`/`maxBudgetKeys`, `SessionRecord.version` with `appendSession` `expectedVersion`, `EventMultiplexerError`, and the `@arnilo/prism/testing/state-concurrency-conformance` subpath — no removal; baselines regenerated with `--update-baseline`, no `--allow-break`; freeze manifest `scripts/phase22-freeze-manifest.json` records per-task evidence tokens). Four behavior tightenings documented in `docs/migration.md` `0.2.1 → 0.2.2`: (1) **atomic model-budget reservation** — `reserveBudget` at admission (used + reserved + requested <= window max, `{reservationId, fencingToken, admitted, retryAfterMs?}`), `commitBudget`/`releaseBudget` at outcome, TTL expiry (default 60 s) with late commits reconciled as `unknownUsage: true`; rate/budget key maps capped (4,096 default / 65,536 hard) with LRU eviction that never drops a held-reservation row; durable reservations live in a new `reservations` JSONB column (enterprise migration 003). (2) **atomic conversation metadata** — `SessionRecord.version` + `appendSession` `expectedVersion` (`0` create-only, `N>0` exact-CAS update-only, omitted = legacy last-write-wins); stale writes throw `SessionMetadataConflictError` `metadata_conflict` (versions only, HTTP 409); concurrent create/branch/archive single-statement with branch caps inside the CAS, archive wins, deleted rows never resurrect (migration 008). (3) **single-consumer EventMultiplexer** — second concurrent `subscribe()` throws `EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER`. (4) **restart-stable NATS durable identity + bounded non-durable active-run registries** — durable name exactly `prism_<hmac16>`, crash-resume continues from the last ack, orphaned 0.2.1 random-suffixed consumers reclaimed on clean stop; workflow active-run registry sweeps aborted entries and fails closed at the 512 cap. New regression surface: `scripts/phase22-security.test.mjs` (4 blockers + gate accounting over built public entrypoints, wired into `security:threat-suites`), packed plain-JS `security22.mjs` consumer in install-smoke, the `@arnilo/prism/testing/state-concurrency-conformance` harness (7 probes; memory leg in npm test, durable legs in `test:postgres` and the NATS seam; zero timing-only sleeps), and the `scripts/phase22-conformance.test.mjs` gate in the `test:postgres` chain. Store compatibility with 0.2.1: **forward-only migrations** (008 + 003), see `docs/migration.md` for rollback risk. Exit gate green: npm test core + workspace + script gates (incl. phase21-freeze done-phase + phase22 conformance), `sdk:ready` exit 0, audit 0 moderate, secret scans 0 findings, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.2, protected OIDC/OPA evidence + durable state-concurrency evidence; evidence in `scripts/phase22-baseline.json` `exitGate`. Rollback = restore the 0.2.1 manifests/tag — but that reopens all four race windows, so prefer fixing the failing host on 0.2.2.
301
+
302
+ ```bash
303
+ # Operator prerequisites recorded: clean tree at the v0.2.2 tag candidate, GPG key, npm OIDC publisher.
304
+ npm test # core + workspace suites + all script gates (incl. phase22 conformance)
305
+ npm run security:threat-suites # phase8-11 + phase20 + phase21 + phase22 public-entry conformance
306
+ npm run sdk:ready # typecheck, lint, format, test, coverage, pack, release:gate
307
+ node scripts/release.mjs gate --version 0.2.2 # plain reviewed additive gate, 0 breaking deltas
308
+ npm run pack:dry-run # twice; diff reports — deterministic
309
+ npm audit --audit-level=moderate
310
+ npm run release:check -- --version 0.2.2 --report /tmp/prism-0.2.2-preflight.json
311
+ npm run release:publish -- --version 0.2.2 --dry-run --allow-dirty --allow-untagged --report /tmp/prism-0.2.2-dry-run.json
312
+ # run the dry-run twice and diff the reports: deterministic, byte-identical
313
+ ```
314
+
315
+ Protected evidence (never a passing skip): live OIDC JWKS through the default pinned path (`createOidcIdentityVerifier` against a real public IdP — real DNS/TLS/JWKS document, e.g. `https://login.microsoftonline.com/common/discovery/v2.0/keys`, success proven by a key-lookup miss after a 200 fetch), live OPA (dockerized `openpolicyagent/opa`, default pinned path fails closed `ssrf_denied`), and the durable state-concurrency legs (Postgres `prism_phase22_*` schemas for sessions/checkpoints/events + enterprise router reservations/idempotency, NATS restart-durable resume against the seam). Missing protected evidence records 0.2.2 as **blocked**, never a passing skip.
316
+
277
317
  ### 0.2.1 publish handoff (plan 021 Task 8)
278
318
 
279
319
  **Decision: GO when the operator prerequisites below are recorded.** Release **0.2.1** (plan 021) is the provider-completion and outbound-trust-boundaries cut on the 0.2.x review-remediation line. API surface **additive-only** vs 0.2.0 (plain reviewed compat gate at 0.2.1: the only deltas are the version literal and `@arnilo/prism-mcp` transport helpers `boundResponse`/`defaultResolver`/`isLoopbackAddress`/`isLoopbackHostname`/`normalizeHostname`/`raceAbort`/`requestPinned`/`resolvePinnedAddress` becoming re-exports of the lifted core primitives — same names/signatures, no removal; baselines regenerated with `--update-baseline`, no `--allow-break`; freeze manifest `scripts/phase21-freeze-manifest.json` machine-checks each task's diff and the preserved surface). Five documented security-motivated behavior tightenings in `docs/migration.md` `0.2.0 → 0.2.1`: (1) **strict stream completion is the shared default** (`strictCompletion: true` in `createOpenAICompatibleProvider`; explicit `false` stays the documented opt-out; truncated streams fail `ProviderTransportError` `incomplete_delta` instead of a successful `providerDone`; applies to Azure/Bedrock/Vertex/OpenRouter/ZAI/NeuralWatt); (2) **bounded success bodies** — additive `readBoundedResponseJson` (65,536-byte ceiling, depth 32, properties 4096, shape gate, abort, redacted errors, `response_body_shape` code) replaces unbounded `response.json()` on all ten model-discovery sites plus NeuralWatt quota, Alibaba embeddings, OpenAI uploads, and both OAuth success paths; (3) **DNS-pinned OIDC JWKS/OPA/content fetch, redirects rejected** — core `pinnedFetch` (one resolve, 1–32 bound, per-candidate SSRF validation, pinned-lookup socket) serves the default JWKS, OPA decision, and content/media paths; 3xx fails `MediaContentError` `redirect`; private/metadata/loopback fails `ssrf_denied`; MCP re-exports the lifted helpers byte-identically; (4) **shared bounded OAuth device/token polling** — core `pollDeviceCodeToken` serves provider-openai and credentials-node with equivalent cadence/backoff/redaction; (5) **edge fixes** — Azure/Vertex credential-once, Bedrock duplicate-case/repeated-query SigV4 canonicalization, OpenAI upload failed-DELETE retention, cache `__overflow__` tokens-only. New regression surface: `scripts/phase21-security.test.mjs` (10 conformance tests over built public entrypoints, wired into `security:threat-suites`) and a packed plain-JS `security21.mjs` consumer in install-smoke. Store compatibility with 0.2.0: **compatible, no migration**. Exit gate green: npm test core + script gates (incl. phase21-freeze done-phase), `sdk:ready` exit 0, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.1, live OIDC JWKS + live OPA protected evidence; evidence in `scripts/phase21-baseline.json` `exitGate`. Rollback = restore the 0.2.0 manifests/tag — but rollback restores the five boundary gaps, so hosts should disable truncated-stream acceptance, unbounded-body endpoints, redirect-following fetches, rotating-credential reuse, and upload cleanup at their own boundary if rollback is unavoidable.
@@ -727,10 +767,63 @@ Prism uses one tool for formatting and linting — [Biome](https://biomejs.dev)
727
767
  | `npm run lint` | `biome lint .` — fails on any lint error (warnings are non-fatal). |
728
768
  | `npm run format:check` | `biome format .` — fails if any file is unformatted. |
729
769
  | `npm run format` | `biome format --write .` — normalizes formatting in place. |
730
- | `npm run test:coverage` | `node --test --experimental-test-coverage` over the core suite with enforced minimums: **lines 60%**, **functions 70%**, **branches 75%** (current baseline ≈ 64 / 72 / 79). Excludes `__tests__/`, `node_modules/`, and `scripts/` from the report. |
770
+ | `npm run test:coverage` | `node --test --experimental-test-coverage` over the core suite with enforced minimums: **lines 60%**, **functions 70%**, **branches 75%** (current baseline ≈ 90.5 / 84.2 / 90.6), then `scripts/coverage-summary.mjs` + the `phase23-coverage` gate. Excludes `__tests__/`, `node_modules/`, `scripts/`, and `packages/` from the core report. |
731
771
 
732
772
  All four gates run inside `npm run sdk:ready` (after `typecheck`, before `pack:dry-run`). A few rules are disabled in `biome.json` because they are false positives for this codebase: `noControlCharactersInRegex` and `noAssignInExpressions` (security/redaction code intentionally matches control characters and uses `while ((m = re.exec(…)))` loops), `noShadowRestrictedNames`, `noThenProperty` (the workflow DSL has a legitimate `then` branch field), `noExplicitAny`, `noVoidTypeReturn`, and `useYield`. Raise the coverage thresholds in `package.json` `test:coverage` as the baseline climbs.
733
773
 
774
+ ### Coverage denominators and per-package thresholds
775
+
776
+ Workspace coverage rows used to include the symlinked root core `dist/` (workspace tests `import … from "@arnilo/prism"`, which resolves via `node_modules/@arnilo/prism -> ../..`), diluting every package denominator. Each workspace coverage run now passes `--test-coverage-include=dist/**`, so only `packages/<name>/dist/**` counts (the core run and its 60/70/75 gate are unchanged).
777
+
778
+ | Fact | Value |
779
+ | --- | --- |
780
+ | Workspace include filter | `--test-coverage-include=dist/**` per package (package-local denominator) |
781
+ | Per-package gate | `lines >= threshold` from `scripts/coverage-thresholds.json` (frozen 2026-08-14 = recompute − 3pp, two runs were byte-identical); branches/functions recorded, not gated |
782
+ | Protected exceptions | `@arnilo/prism-session-store-postgres`, `@arnilo/prism-enterprise-postgres`, `@arnilo/prism-memory`, `@arnilo/prism-session-store-nats` — durable legs need `PRISM_TEST_POSTGRES_URL` or a real NATS server; plus `@arnilo/prism-coding-security` — native-sandbox legs probe `unshare --net` (NETNS) and skip on CI runners (host runs exercise them); exempt from the gate, reported separately with the reason |
783
+ | Artifact | `scripts/coverage-summary.json` (gitignored, CI-retained): per-package `lines`/`branches`/`functions`/`denominatorFiles`/`threshold`/`pass`/`protectedException` + `belowThreshold` |
784
+ | Fail-closed | a non-protected package below its threshold, a suite failure, or a run producing no coverage data exits non-zero; a missing threshold entry is a config error |
785
+ | Overrides | `PRISM_COVERAGE_THRESHOLDS`, `PRISM_COVERAGE_ARTIFACT` (used by the gate regression) |
786
+
787
+ A new workspace package must add an evidence-based threshold entry (or a `protectedException` reason) to `scripts/coverage-thresholds.json` before `test:coverage` passes.
788
+
789
+ ### Release evidence and protected skips
790
+
791
+ `npm run release:evidence` (run automatically at the start of `npm run release:gate`, and therefore at the end of `npm run sdk:ready`) aggregates every test surface into `scripts/release-evidence.json` — the machine-auditable release skip manifest. It records env var **names only, never values** (the manifest is retained and uploaded by CI).
792
+
793
+ | State | Meaning | Gate effect |
794
+ | --- | --- | --- |
795
+ | `pass` | the surface ran and its recorded evidence is green | pass |
796
+ | `skip` | a documented partial-skip surface (must carry `reason` + `requiredEnv`) | pass (defensive: an unexplained `skip` fails) |
797
+ | `protected` | a documented, permitted gap with a reason (+ required env where applicable) | pass, always visible |
798
+ | `blocked` | a required release surface cannot be attested (required env absent, or evidence missing) | **fail closed** — `release.mjs gate` refuses to release |
799
+
800
+ Surfaces: core `npm test` (counts and the skip total come from the latest `phase*-baseline.json` `exitGate.counts` — currently 33 protected/live skips, a frozen floor), `security:threat-suites`, every workspace suite (evidence from `scripts/coverage-summary.json` of the same run; `protectedException` packages are named with their reason), `test:postgres` durable conformance (**required**: `PRISM_TEST_POSTGRES_URL` must be set when `release:gate` runs — the release workflow's verify job declares it on the `release:gate` phase only, so the env never leaks into the env-gated docs demo / durable integration suites of `npm test`, and the `postgres-integration` job runs the suite against a real server; a local release must set it too, exactly like the phase-22 release profile), `test:nats` real JetStream legs (protected, 0.3.0), the `PRISM_LIVE_PROVIDER_TESTS` provider legs (protected, per package), and the four live canaries from `scripts/live-canary.mjs` (provider/MCP/A2A/web; run by the scheduled `live-canaries` workflow with real credentials — recorded `protected`, never `pass`). The manifest cross-references the latest baseline's `exitGate`/`protectedEvidence` so per-phase records stay the source of truth.
801
+
802
+ Override `PRISM_RELEASE_EVIDENCE` to redirect the manifest (used by the gate regression). The manifest is gitignored and CI-retained (`release-evidence` artifact). A release cannot ship with a required env absent and unexplained — the operator sees every blocked surface in the retained manifest.
803
+
804
+ ### Quality-gate reports and the Biome baseline
805
+
806
+ `npm run lint` runs Biome 2.x with the canonical config (`linter.rules.preset: "recommended"` — the deprecated `recommended: true` key is gone; `npx biome migrate --write` performs the rewrite) and writes a machine-readable SARIF report to `scripts/lint-report.sarif` in the same run (stable `--reporter=sarif`; the experimental `--reporter=json` schema is not used). The repo target is **zero** lint diagnostics; every remaining intentional diagnostic carries a justified `biome-ignore lint/<rule>: reason` comment (shell-interpolated strings, verbatim upstream fixtures, and literal grep targets in tests). Unused-code diagnostics are auto-fixed by `biome lint --write --unsafe .`; public-but-unused exports are never removed by Biome — they route to the unused-code sweep.
807
+
808
+ `npm run sweep:unused` (also part of `npm test`) runs the tsc `--noUnusedLocals/--noUnusedParameters` sweep across the core and every workspace tsconfig and writes both `scripts/unused-sweep-report.txt` (human) and `scripts/unused-report.json` (`--json`, machine-readable: per-tsconfig counts + the dead-export scan). It stays non-blocking by design. Both reports are gitignored and CI-retained (`quality-gate-reports` artifact, 30 days).
809
+
810
+ Timing assertions in tests follow a deterministic-barrier policy: racy wall-clock deltas are replaced by awaiting the conflicting operation and asserting its observable outcome (plus a test-level `timeout` as the anti-hang guard). The few remaining wall-clock guards are generous anti-hang/proof-of-promptness bounds, each marked with a `ponytail:` comment naming its ceiling; the document-reader budget test uses the recorded `scripts/budgets.json` ceilings.
811
+
812
+ ## Build serialization
813
+
814
+ `dist/` is compiled by `tsc` in many small writes, so a concurrent build and test in the same working tree could race — a test importing `@arnilo/prism` mid-emit could observe a partially-written module (reproduced during the 0.2.3 review). Emit-producing leaves (`tsc` builds) and dist-consuming test leaves (`node --test dist/__tests__/*.test.js`) are therefore serialized through a dependency-free lock: `node scripts/with-build-lock.mjs <command>` acquires an `O_EXCL` lockfile at `node_modules/.prism-build.lock` (contents: holder `pid` + timestamp, no secrets), waits with a 100ms backoff, and fails closed — never proceeding without the lock. A stale lock whose holder PID is dead is reclaimed; a live lock is never stolen. Acquisition is leaf-only (never the `npm test`/`sdk:ready` orchestrators), so nested `npm run build` children cannot deadlock.
815
+
816
+ | Fact | Value |
817
+ | --- | --- |
818
+ | Lock path | `node_modules/.prism-build.lock` (repo-root-relative; workspaces share the same lock) |
819
+ | Timeout | 120s default; override with `PRISM_BUILD_LOCK_TIMEOUT_MS` |
820
+ | Retry | 100ms backoff; stale-PID reclaim via `process.kill(pid, 0)` |
821
+ | Fail-closed | acquisition error or timeout exits non-zero, nothing runs |
822
+ | Wrapped | `build:core`, every workspace `build`, the `node --test` runs in `test`/`test:coverage`/workspace tests, `coverage-summary.mjs`, the script-gate `node --test` run (the `phase*-conformance`/`phase*-security` gates import `@arnilo/prism` from `dist`) |
823
+ | Not wrapped | `npm run clean` (standalone), `tsc -p examples --noEmit` and workspace `typecheck` (read `dist` `.d.ts`; within any single script the build completes before reads, so only a concurrent external emitter can cause a spurious typecheck error), `scripts/phase23-build-race.test.mjs` (the lock's own regression — it runs unwrapped so its children acquire the real lock) |
824
+
825
+ Directly invoking `tsc` instead of `npm run build` bypasses the lock — use the npm scripts when another build/test could be running in the same tree (CI runs them sequentially).
826
+
734
827
  ## Dependency major-upgrade isolation
735
828
 
736
829
  Major dependency upgrades are **isolated, compatibility-tested changes — never bundled into a feature release.** A major bump (TypeScript, `@types/node`, `diff`, or any third-party runtime dependency and its successors) ships as its own commit/PR that runs `npm run sdk:ready` plus packed-install evidence, and is reviewed separately from feature work. Release commits contain no unreviewed major bumps.
package/docs/workflows.md CHANGED
@@ -262,7 +262,8 @@ runRpcServer({
262
262
 
263
263
  - Workflow semantics stay in this optional package; generic checkpoint persistence and bounded event fan-in live in core.
264
264
  - `ProductionPersistenceStore.checkpoints` and `.leases` are optional generic capabilities. First-party SQLite/PostgreSQL adapters own `prism_checkpoints` / `prism_leases`; workflows only adapt them.
265
- - `createWorkflowEventBus()` delegates queueing, source fan-in, overflow, abort, and close behavior to core `createEventMultiplexer()`.
265
+ - `createWorkflowEventBus()` delegates queueing, source fan-in, overflow, abort, and close behavior to core `createEventMultiplexer()`, including its single-consumer contract: a second concurrent `subscribe()` is rejected with `EventMultiplexerError` (`ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER`) instead of silently splitting the stream.
266
+ - The in-process active-run registry (`registerActiveWorkflowRun` / `getActiveWorkflowRun` / `abortActiveWorkflowRun`) is **non-durable, in-process only — it does not survive restart**; durable active-run recovery is a later milestone. It is bounded: every register sweeps aborted/leaked entries (runs whose promise never settled) and the registry fails closed at `MAX_ACTIVE_WORKFLOW_RUNS` (512) rather than evicting a live run; `sweepActiveWorkflowRuns()` is available for hosts. Cross-tenant lookups stay ownership-isolated.
266
267
  - `createWorkflowCommands()` is optional; hosts can drive `workflow.start` / `enqueue` / `replay` / `status` / `list` / `cancel` / `resume`. The six `schedule.*` commands appear only when a scoped `schedules` service is supplied.
267
268
  - Hosts may bridge `WorkflowEvent` into OpenTelemetry or custom sinks; there is no built-in TUI.
268
269
  - Agent exclusivity is per session: one active `run()` at a time, same as core.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/testing/agent-event-source-conformance.d.ts",
35
35
  "default": "./dist/testing/agent-event-source-conformance.js"
36
36
  },
37
+ "./testing/state-concurrency-conformance": {
38
+ "types": "./dist/testing/state-concurrency-conformance.d.ts",
39
+ "default": "./dist/testing/state-concurrency-conformance.js"
40
+ },
37
41
  "./testing/session-store-conformance": {
38
42
  "types": "./dist/testing/session-store-conformance.d.ts",
39
43
  "default": "./dist/testing/session-store-conformance.js"
@@ -138,25 +142,26 @@
138
142
  "packages/prism-*"
139
143
  ],
140
144
  "scripts": {
141
- "build:core": "tsc",
145
+ "build:core": "node scripts/with-build-lock.mjs tsc",
142
146
  "clean": "rm -rf dist packages/*/dist",
143
147
  "build": "npm run build:core && npm run build --workspaces --if-present",
144
148
  "typecheck": "npm run build && npm run typecheck --workspaces --if-present && tsc -p examples --noEmit",
145
- "sweep:unused": "node scripts/sweep-unused.mjs",
146
- "test": "npm run build && node --test dist/__tests__/*.test.js && node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/phase19-freeze.test.mjs scripts/phase20-freeze.test.mjs scripts/phase21-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs && npm run test --workspaces --if-present",
147
- "test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=70 --test-coverage-branches=75 --test-coverage-exclude='**/__tests__/**' --test-coverage-exclude='**/node_modules/**' --test-coverage-exclude='**/scripts/**' --test-coverage-exclude='**/packages/**' --test-coverage-exclude='**/examples/**' dist/__tests__/*.test.js && node scripts/coverage-summary.mjs",
148
- "coverage:summary": "node scripts/coverage-summary.mjs",
149
- "lint": "biome lint .",
149
+ "sweep:unused": "node scripts/sweep-unused.mjs --json",
150
+ "test": "npm run build && node scripts/with-build-lock.mjs node --test dist/__tests__/*.test.js && node scripts/with-build-lock.mjs node --test scripts/release-gate.test.mjs scripts/tooling-gate.test.mjs scripts/budget-gate.test.mjs scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase11-freeze.test.mjs scripts/phase12-freeze.test.mjs scripts/phase13-freeze.test.mjs scripts/phase14-freeze.test.mjs scripts/phase15-freeze.test.mjs scripts/phase16-freeze.test.mjs scripts/phase17-freeze.test.mjs scripts/phase18-freeze.test.mjs scripts/phase19-freeze.test.mjs scripts/phase20-freeze.test.mjs scripts/phase21-freeze.test.mjs scripts/benchmark-0.1.0.test.mjs scripts/sweep-unused.test.mjs scripts/e2e-enterprise-journey.test.mjs scripts/e2e-coding-journey.test.mjs scripts/phase23-quality-gates.test.mjs && node --test scripts/phase23-build-race.test.mjs && npm run test --workspaces --if-present",
151
+ "test:coverage": "node scripts/with-build-lock.mjs node --test --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=70 --test-coverage-branches=75 --test-coverage-exclude='**/__tests__/**' --test-coverage-exclude='**/node_modules/**' --test-coverage-exclude='**/scripts/**' --test-coverage-exclude='**/packages/**' --test-coverage-exclude='**/examples/**' dist/__tests__/*.test.js && node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs && node --test scripts/phase23-coverage.test.mjs && node --test scripts/phase23-skip-manifest.test.mjs",
152
+ "coverage:summary": "node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs",
153
+ "lint": "biome lint . --reporter=sarif --reporter-file=scripts/lint-report.sarif",
150
154
  "format": "biome format --write .",
151
155
  "format:check": "biome format .",
152
156
  "pack:dry-run": "npm pack --dry-run && npm run pack:dry-run --workspaces --if-present",
153
- "test:postgres": "node scripts/require-postgres-url.mjs && npm run test:postgres --workspace @arnilo/prism-session-store-postgres && npm run test:postgres --workspace @arnilo/prism-memory && npm run test:postgres --workspace @arnilo/prism-enterprise-postgres && node --test scripts/phase7-conformance.test.mjs scripts/phase12-restart-recovery.test.mjs",
157
+ "test:postgres": "node scripts/require-postgres-url.mjs && npm run test:postgres --workspace @arnilo/prism-session-store-postgres && npm run test:postgres --workspace @arnilo/prism-memory && npm run test:postgres --workspace @arnilo/prism-enterprise-postgres && node --test scripts/phase7-conformance.test.mjs scripts/phase12-restart-recovery.test.mjs scripts/phase22-conformance.test.mjs",
154
158
  "release:dry-run": "npm run sdk:ready",
155
159
  "release:check": "node scripts/release.mjs check",
156
160
  "release:publish": "node scripts/release.mjs publish",
161
+ "release:evidence": "node scripts/release-skip-manifest.mjs",
157
162
  "sdk:ready": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:coverage && npm run pack:dry-run && npm run release:gate",
158
- "release:gate": "node scripts/release.mjs gate",
159
- "security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase20-security.test.mjs scripts/phase21-security.test.mjs"
163
+ "release:gate": "node scripts/release-skip-manifest.mjs && node scripts/release.mjs gate",
164
+ "security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase20-security.test.mjs scripts/phase21-security.test.mjs scripts/phase22-security.test.mjs scripts/phase23-security.test.mjs"
160
165
  },
161
166
  "devDependencies": {
162
167
  "@biomejs/biome": "^2.5.5",