# Changelog

All notable changes to `@stackbone/sdk` are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`client.connections` surface (gated by the new `connections.actions`
  capability, contract v13).** Agents can list the connectors wired to their
  installation and invoke a connector action through a control-plane proxy —
  credentials never enter the agent container. `client.connections.list()`
  returns the available connectors and their connections;
  `client.connections.invoke(connector, action, args)` runs an action,
  optionally pinned to a specific connection id.

### Removed

- **Breaking:** removed the `client.observability` surface (`spanProcessor`,
  `logger`, `closeRun`, `closeLogger`, `flush`). Observability is no longer a
  creator-facing `client` module: agent code logs through the handler-scoped
  `ctx.logger` (already bound to the run) — plain `console.*` is captured and
  correlated too — and the Studio run timeline is produced by the runtime, not
  by code the agent registers. The platform primitives (`PlatformLogger`,
  `RunStepsSpanProcessor`, `aggregateRunCost`) stay available on the
  `@stackbone/sdk/observability` subpath, consumed by the runtime wrapper and
  the agent emulator.

### Changed

- The runtime now injects the SDK client into every handler context as
  `ctx.client` — one `StackboneClient` per process, shared across invocations.
  Agent code reads it off the invocation context (`async run({ input, client })
{ ... }`) instead of constructing one at module scope, so a module-level
  `const client = createClient()` is no longer required. `createClient()` stays
  exported as an escape hatch for building a client outside a handler (scripts,
  tests, one-off tasks) or with explicit config overrides.

- `client.queues` is now live (was a `notImplemented` stub). `publish`,
  `schedule`, `unschedule` and `listSchedules` make authenticated calls to the
  control-plane BullMQ dispatcher (`/api/v1/agent/queues/*`) using the existing
  `STACKBONE_AGENT_JWT` channel — the agent never touches Redis.
  **Breaking:** `publish` now takes `{ name, payload, retries?, delay?,
deduplicationId? }` and returns `{ messageId }` (was `{ url, body, retries?,
delay?, deduplicationId?, headers? }`). `name` is the opaque job name (e.g.
  `send-email`), `payload` the opaque body, `delay` defers delivery in ms and
  `retries` overrides the platform retry default per job. New public request
  types `ScheduleRequest` / `UnscheduleRequest` are exported from the barrel.
  Errors surface under the new `queues_*` prefix.

- `ResolvedConfig` is now an opaque, typed snapshot — the live `env`
  channel is gone. Every consumer used to read environment variables with
  the pattern `resolved.config.fieldX ?? resolved.env['FIELDX']`, which
  spread two pieces of knowledge across ~20 call sites: the exact env-var
  name and the precedence rule. The resolver now owns both. After this
  change, modules read typed fields like `resolved.stackboneApiUrl`,
  `resolved.s3Bucket`, `resolved.databaseUrl`, `resolved.requireContract`,
  etc. — none of them touch `process.env` directly.

  The resolver is the **single** seam that reads `process.env` in the
  library. New env-driven settings go in two places: a typed field on
  `ResolvedConfig` and the matching `config.xxx ?? env['XXX']` (or parsing
  logic) inside `resolveConfig`. The compiler then flags every consumer
  that needs the new field.

  Runtime knobs previously read inline from `contract/contract-handshake.ts`
  (`STACKBONE_REQUIRE_CONTRACT`, `STACKBONE_CONTRACT_TTL_MS`,
  `STACKBONE_DEBUG`) are now resolved up-front and threaded into the per-
  client `ContractStore`. The database singleton in
  `surfaces/agent-local/database/shared-handle.ts` takes an explicit
  `connectionString` parameter that `DatabaseModule` pulls off
  `resolved.databaseUrl`; the only remaining read of
  `STACKBONE_POSTGRES_URL` is inside `resolveConfig`.

  Booleans (`STACKBONE_DEBUG=1`) and numbers (`STACKBONE_CONTRACT_TTL_MS=30000`)
  get parsed at resolve time, so consumers receive `boolean` / `number`,
  not raw strings — one parse, one place, no drift.

  Behavioural change worth flagging: `ResolvedConfig` is a snapshot, not
  a live view. Tests that mutated `process.env` AFTER `createClient()` to
  exercise late-binding behaviour now need to rebuild the client (or pass
  the new value through the config object). The new `src/config.spec.ts`
  pins both the snapshot semantics and the deletion contract — `.env` is
  no longer a property on `ResolvedConfig`, and reading it is both a type
  error and a runtime `undefined`. Two integration / handshake tests
  (`STACKBONE_REQUIRE_CONTRACT=0`, `STACKBONE_CONTRACT_TTL_MS`,
  `STACKBONE_DEBUG=1`) now construct a fresh `ContractStore` per env-var
  variation to keep the resolve-once contract explicit.

- `SdkError.code` is now a typed catalog (`SdkErrorCode`) declared in one
  place — `src/errors/codes.ts`. Every literal `code` value that ships on a
  `Result<T>` envelope (and the `errorMapping.prefix` slot on the transport)
  is now compiler-checked against the catalog, so a future PR that adds
  `secrets_not_foud` (typo) or `ai_provdier_down` (typo) fails to build
  instead of silently shipping past the README contract. The catalog also
  ties `HttpClient`'s `<prefix>_<reason>` projection to `SdkErrorPrefix`, so
  a surface that wires `errorMapping: { prefix: '<unknown>' }` no longer
  compiles. The wire-level shape of every code is unchanged: this is a
  type-level constraint, not a rename.

  Adding a new code is a single-file edit in `src/errors/codes.ts` (either
  a new suffix under an existing prefix, or a freestanding code under
  `SDK_ERROR_CODES_STANDALONE`). The compiler then flags every call site
  that needs the new literal — and the new exhaustiveness test in
  `src/lib/http-client.spec.ts` asserts every `<prefix>_<reason>` the
  transport can emit is still a member of the catalog so README/code drift
  fails loudly.

  Two new symbols on the public barrel (`@stackbone/sdk`):
  - `SdkErrorCode` — the union projection of the catalog. Use it as the
    return type when you write a helper that produces an `SdkError`, or
    as a discriminant in a `switch` over `result.error.code`.
  - `isSdkErrorCode(raw: string)` — runtime narrowing for wire strings
    that come from a decoded body. Returns `true` only when `raw` is a
    catalog member; lets the SDK widen upstream errors safely.

  RAG's `toRagError(cause, message, fallbackCode)` helper now constrains
  `fallbackCode` to `SdkErrorCode` and checks `cause.code` against the
  catalog before reusing it. The catalog and the helper are not folded
  into one file — the catalog is the inventory, `toRagError` stays in the
  RAG folder as a Postgres-tagged-error classifier.

- Contract gating membership is now an explicit rule, and the handshake
  cache no longer lives in a process-wide static. Two things changed:
  1. **Membership rule (Option B from the audit)** — a surface gates iff
     `MODULE_CAPABILITIES` (in `@stackbone/validators`) advertises a row
     for its module id. `MODULE_CAPABILITIES` is now the single source of
     truth: adding a surface to gating means adding a `Capability` and a
     `<module>: '<capability>'` row first, then wiring
     `createModuleGate('<module>', resolved, store)` in the surface
     constructor. The new docstring at the top of
     `contract/capability-registry.ts` spells out the rule and the steps.
     Today's gated set stays the same (`database`, `rag`, `queues`,
     `events`, `secrets`, `config`, `approval`, `storage`, `ai`). The
     four ungated surfaces (`memory`, `connections`, `prompts`,
     `observability`) now carry a one-paragraph comment on their class
     header documenting **why** they are not gated — either the runtime
     does not yet exist with a defined capability (`memory`,
     `connections`, `prompts`) or the surface stays local / talks to
     OpenTelemetry, not the Stackbone Agent Protocol (`observability`).

  2. **Per-client handshake store** — the `ContractHandshake` static
     namespace is gone. In its place: `createContractStore()` returns a
     `ContractStore` with three methods (`get`, `peek`, `gatingEnabled`)
     that owns its own cache, single-flight map, TTL eviction and debug
     bookkeeping. `StackboneClient` instantiates exactly one store per
     instance and threads it through every gated surface via
     `createModuleGate(module, resolved, store)`. Two clients
     constructed in the same process no longer share handshake cache or
     suppression-warning state — including `client.contract`, which now
     reads from the per-client store's `peek()`.

  Removed test globals (the visible sign that the cache had escaped its
  owner): `ContractHandshake.__resetForTests` and
  `__resetModuleGateWarningsForTests`. Specs that previously called them
  in `beforeEach`/`afterEach` now simply construct a fresh
  `createContractStore()` (or, for client-level coverage, a fresh
  `createClient(...)`); state isolation is intrinsic to construction.
  `module-gate.spec.ts` gained two coverage tests for the new shape:
  "two gates pinned to the same store share suppression-warning state"
  and "two gates pinned to different stores warn independently". The E2E
  suite (`integration/handshake.spec.ts`) loses its global-reset
  bookkeeping entirely.

  Wire protocol is unchanged: `GET /api/contract` returns the same JSON
  shape, `client.contract` exposes the same read-only `ContractResponse |
null` getter, gate-blocked calls still return the same `Result`
  envelope with `capability_unavailable` / `contract_version_unsupported`
  / `contract_unreachable` codes, and the `STACKBONE_REQUIRE_CONTRACT=0`
  / `STACKBONE_CONTRACT_TTL_MS` / `STACKBONE_DEBUG=1` escape hatches
  behave identically. Surface constructors keep the same `gate?:
ModuleGate` test seam — only the internal default (now backed by a
  per-client store instead of a static singleton) changed.

- The cross-surface shared-handle seam is now an explicit, documented
  pattern on the public surfaces. Two surfaces produce a reusable
  resource and expose it through a canonical accessor:
  - `client.database.shared(): DrizzleClient` — process-wide Drizzle
    handle bound to `STACKBONE_POSTGRES_URL`. Same input → same
    instance. `client.database.raw()` stays as the creator-facing
    escape hatch and is now documented as an alias of `shared()`.
  - `client.ai.shared(): Result<OpenAI>` — process-wide OpenRouter
    `OpenAI` client used by every namespace under `client.ai`. Same
    input → same instance; reflects the `clientOverride` injected at
    construction time. The previous private `client()` accessor folded
    into this public name.

  These accessors are the **only** way cross-surface SDK consumers
  should reach the shared pool / OpenAI client. RAG already consumed
  the database pool via a module-private file (`database/internal.ts`)
  — RAG now reaches it through `client.database.shared().$client`
  instead. The renamed `database/shared-handle.ts` keeps the singleton
  implementation but has zero sibling-relative importers outside its
  own folder and its own spec; cross-surface friction (memory and
  queues will need the same handles per the consolidation ADRs) now
  has one named extension point instead of a per-consumer escape
  hatch.

  Public API of `client.database`, `client.ai`, and `client.rag` is
  unchanged — method signatures and `Result` envelopes match the
  previous release. `RagModule` constructor adds a
  `() => DatabaseModule` accessor alongside the existing
  `() => AiModule`, mirroring the wiring `client.ts` produces; tests
  pass both verbatim.

- **BREAKING** — Control-plane facades (`client.secrets`, `client.config`,
  `client.approval`) now surface HTTP failures with surface-specific error
  codes instead of the generic `http_*` family. The transport (`HttpClient`)
  absorbs response validation, status→domain remapping, and querystring
  serialisation, so a facade method now reads as "POST to /path with body X,
  validate against schema Y, error prefix Z" instead of a 30–80 line
  hand-rolled pipeline per surface. Migration is mechanical: pattern-match
  the surface prefix instead of `http_*`.
  - 401 → `<prefix>_unauthorized` (was `http_unauthorized`).
  - 403 → `<prefix>_forbidden` (was `http_forbidden`).
  - 404 → `<prefix>_not_found` (was `http_not_found`).
  - 408 → `<prefix>_timeout` (was `http_timeout`).
  - 429 → `<prefix>_rate_limited` (was `http_rate_limited`).
  - 5xx → `<prefix>_unavailable` (was `http_server_error`).
  - other 4xx → `<prefix>_invalid_request` (was `http_client_error`).
  - schema-drift on a 2xx body → `<prefix>_invalid_response`.

  The raw HTTP status stays accessible at `result.error.meta.status` for
  callers that still want to pattern-match on the wire. Endpoint-specific
  overrides (e.g. a POST whose `409` means "already exists") are wired by
  facades via the new `errorOverrides` option on the transport request.

  Transport-level errors (`http_timeout`, `http_aborted`,
  `http_network_error`, `http_parse_error`, `http_request_failed`,
  `http_invalid_response`, `http_invalid_request`) keep the `http_*`
  prefix the README documents — they are not surface-specific.

  Internal cleanup: `src/lib/facade-helpers.ts` is gone (the two utilities
  it held — `remapHttpNotFound` and `validateStringArray` — were either
  absorbed by the transport or relocated to `lib/http-client.ts` as a
  named export). The per-method bodies on the control-plane facades
  shrank substantially: `secrets.get` and `config.get` lost their
  hand-rolled `value` type-checks and remap blocks, batch endpoints
  pushed array validation + comma-join down to the transport, and the
  approval list endpoint dropped its `pickDefinedAsStrings` helper for
  the transport's built-in scalar coercion. File-level line counts barely
  move because the public type definitions dominate each facade (notably
  on `approval.ts` where 80+ lines are interface exports), but the
  per-method logic now reads like the design target: "POST to /path,
  validate against schema, error prefix is X".

- **BREAKING** — Platform-side observability primitives moved off the main
  `@stackbone/sdk` barrel and into a dedicated subpath entrypoint,
  `@stackbone/sdk/observability`. The main barrel previously exposed both the
  handler-scoped `createStructuredLogger` (the per-invocation `Logger` the
  runtime injects into creator handlers) and the platform/run-scoped
  `createPlatformLogger` factory side-by-side, with no signal about which
  belonged to which audience. Creators only ever consume the run-scoped
  surface through `client.observability.*` (the live accessors stay on
  `StackboneClient` unchanged); only platform/runtime tooling and integration
  tests need to construct the primitives directly. The symbols that moved:
  - Per-run logger — `createPlatformLogger`, `LOG_LEVELS`, `defaultRunsDir`,
    plus the types `LogLevelName`, `LogLevelNumber`, `LogRecord`, `PinoLike`,
    `PlatformLogger`, `PlatformLoggerMode`, `PlatformLoggerOptions`.
  - OTel span processor — `RunStepsSpanProcessor`, `RUN_STEP_TYPES`,
    `RUN_ID_ATTRIBUTE`, `STEP_TYPE_ATTRIBUTE`, plus the types
    `ReadableSpanLike`, `RunStepType`, `RunStepsSpanProcessorOptions`,
    `SpanContextLike`, `PostgresLike`.
  - Run-cost aggregator — `aggregateRunCost`, plus the types
    `AggregateRunCostOptions`, `AggregateRunCostResult`.

  Migration is mechanical:

  ```ts
  // Before
  import { createPlatformLogger, RunStepsSpanProcessor } from '@stackbone/sdk';
  // After
  import { createPlatformLogger, RunStepsSpanProcessor } from '@stackbone/sdk/observability';
  ```

  The handler-scoped `createStructuredLogger`, `Logger`, `LoggerBindings`,
  `LogSink` and `StructuredLoggerOptions` stay on the main `@stackbone/sdk`
  barrel: those are the surface the runtime wrapper hands to creator
  handlers via `InvokeContext.logger`. JSONL output shape, OTLP/HTTP/JSON
  serialisation and `~/.stackbone/dev/runs/<runId>.jsonl` paths are
  unchanged — only the import path moved.

- **BREAKING** — SDK source tree reorganised under `src/surfaces/` by **where
  each surface's state lives** instead of "wraps SDK vs makes HTTP fetch". The
  old `src/modules/` and `src/facades/` directories are gone. New layout:
  - `src/surfaces/agent-local/` — state in the agent's own Postgres
    (`database`, `rag`).
  - `src/surfaces/external/` — managed partner SDKs the SDK wraps directly
    (`ai`, `storage`, `observability`).
  - `src/surfaces/control-plane/` — thin HTTP calls to the Stackbone control
    plane (`approval`, `secrets`, `config`).
  - `src/surfaces/pending/` — surface types are exported but the runtime is
    not built yet (`queues`, `memory`, `prompts`, `connections`, `events`).

  Subpath entrypoints (`@stackbone/sdk/db`, `@stackbone/sdk/db/testing`,
  `@stackbone/sdk/queues/types`, `@stackbone/sdk/rag/migrations`,
  `@stackbone/sdk/rag/schema`) keep working — only the underlying file
  paths moved.

- The five pending surfaces (`queues`, `memory`, `prompts`, `connections`,
  `events`) now live under `src/surfaces/pending/` and are exposed as live
  `client.X` accessors that return `not_implemented` on every method. The
  earlier draft of this refactor dropped the accessors entirely, but MVP
  agent code needs to be authored against the eventual contract today and
  switch over the day the backing runtime ships — keeping the getters as
  placeholders makes that one-line change instead of a refactor. Public
  types continue to ship from `@stackbone/sdk` (`PublishRequest`,
  `MemoryItem`, `MemoryHit`, `Prompt`, `CreatePromptRequest`, …) so
  creators can keep modelling integrations. `queues` and `events` go
  through the contract gate (they are listed in `MODULE_CAPABILITIES`);
  `memory`, `prompts` and `connections` are intentionally ungated.

- **BREAKING** — `@stackbone/sdk/db/testing` `createTestDatabase` now spins up
  a real Postgres container via `@testcontainers/postgresql` instead of
  carving a random schema out of a shared dev DB. Callers no longer pass a
  `connectionString`; pass an optional `image` (defaults to
  `pgvector/pgvector:pg17`) if you need extensions beyond pgvector. Tests are
  self-contained: no `docker compose up` ceremony, no `STACKBONE_*_POSTGRES_URL`
  env, no schema bookkeeping. Aligns the SDK helper with the rest of the
  monorepo (see `apps/api/src/db/__tests__/setup-postgres.ts`). Docker must
  be running on the host.

## [0.1.0-alpha.2] - 2026-05-18

### Fixed

- `client.rag.ingest` (batched mode, e.g. when an `onProgress` callback or a
  `jobWriter` is wired): every chunk batch was wrapping its INSERT in a
  transaction that first ran `DELETE FROM rag_chunks WHERE namespace=? AND
doc_id=?`. With N batches per document, only the rows inserted by the LAST
  batch survived — every earlier slice was wiped by the next batch's DELETE.
  Split the DELETE into a single `clearDocument` call before the loop;
  `ingestDocument` now only INSERTs its slice.
- `client.rag` error mapping: `42P01 relation does not exist` was only
  translated to `SdkError('rag_schema_missing')` from the pipeline path.
  `reset()`, the database-handle resolver, and `client.rag.ingestAsync`'s job
  writer used a divergent local copy of `toRagError` that surfaced the raw
  Postgres code instead. Consolidated all three call sites onto a shared
  `modules/rag/errors.ts` so the schema-missing hint is consistent.
- `RunStepsSpanProcessor.stepIdBySpanId`: spans without a `stackbone.run.id`
  attribute leaked their reservation in the map — `buildRow` returned early
  before the cleanup line ran. Cleanup now happens unconditionally on every
  `onEnd`, so dropped spans no longer accumulate.

### Changed

- OTLP log records sent by `createPlatformLogger` now carry the actual
  installed SDK version under `scope.version`. Previously every record was
  tagged with a hardcoded `'0.0.1'` regardless of what was published.
- `client.rag.reset()` invalidates the cached schema fingerprint, so the
  first ingest after a reset re-provisions the schema without serving a
  stale `rag_dim_mismatch`.
- `client.rag.ingest`: `ensureSchema` now memoises the verified dimensions
  per `Sql` instance, dropping the per-call `_rag_meta` SELECT on the hot
  path. The `stackbone_rag_jobs` writer also collapses its
  `ensureCollection` lookup to a single `INSERT … ON CONFLICT … RETURNING`
  (was a `SELECT` followed by an upsert) and trims `isCancelled` to
  `SELECT status … LIMIT 1`.
- `OtlpLogsBody` serialisation no longer iterates `Object.entries(LOG_LEVELS)`
  per record to map level numbers to severity text — precomputed lookup
  table replaces the linear scan.
- `RagModule` test seams (`sqlProvider`, `jobWriterFactory`) now flow
  through a typed 4th constructor parameter (`RagModuleTestDeps`) instead of
  private fields mutated via `as unknown as` casts.

## [0.1.0-alpha.1] - 2026-05-15

### BREAKING

- Runtime env contract collapsed under `STACKBONE_*`. The SDK no longer
  reads `AWS_ACCESS_KEY_ID` / `S3_ACCESS_KEY` / `S3_ENDPOINT` /
  `S3_BUCKET` / `S3_REGION` / `DATABASE_URL`. Creators must export
  `STACKBONE_S3_ACCESS_KEY`, `STACKBONE_S3_SECRET_KEY`,
  `STACKBONE_S3_ENDPOINT`, `STACKBONE_S3_BUCKET`, `STACKBONE_S3_REGION`,
  `STACKBONE_POSTGRES_URL`. `client.storage` derives its multi-tenant
  key prefix from `STACKBONE_AGENT_ID` (injected by `stackbone dev` and
  the cloud runner) so the run-steps exporter and Studio Storage share
  the same prefix.

### Added

- New validator schemas re-exported from the bundled `@stackbone/validators`
  surface for the Studio Playground contract: `schemaResponseSchema`
  (`{ input: JsonSchema | null, output: JsonSchema | null }`) and
  `fixtureIdParamSchema`.

## [0.1.0-alpha.0] - 2026-05-14

### Added

- First public alpha release on npm. `@stackbone/sdk@alpha` is now installable
  by Stackbone agents outside the monorepo. `createClient()` exposes the
  monolithic agent surface (`client.database`, `client.storage`, `client.ai`,
  `client.rag`, `client.queues`, `client.approval`, `client.secrets`,
  `client.config`, etc.) defined by the SDK contract.
- Subpath exports for the curated entrypoints creators need at install time:
  `@stackbone/sdk/db` (re-exports the Drizzle ORM surface bound to the agent's
  Postgres branch), `@stackbone/sdk/db/testing`, `@stackbone/sdk/queues/types`,
  `@stackbone/sdk/rag/migrations`, `@stackbone/sdk/rag/schema`.
- Tag-driven release pipeline mirroring `@stackbone/cli`: `pnpm release:sdk
<version>` promotes this changelog, bumps `libs/sdk/package.json`, commits,
  tags `sdk-v<version>`, pushes, and `.github/workflows/sdk-publish.yml`
  builds, smoke-tests the tarball against the published `createClient` /
  subpath surface, and runs `npm publish --provenance --access public`.

### Changed

- `@stackbone/validators` is bundled into the SDK's published output (was an
  external workspace dependency). Creators consume Stackbone through
  `@stackbone/sdk` alone — they never declare `@stackbone/validators` in their
  agent's `package.json`.
