Skip to content

fix(observability): make the journal, metrics and one-shot error injection report what happened - #466

Merged
jpr5 merged 5 commits into
mainfrom
fix/s3-observability
Sep 17, 2026
Merged

jpr5 merged 5 commits into
mainfrom
fix/s3-observability

Conversation

@jpr5

@jpr5 jpr5 commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

This PR aligns the request journal and Prometheus metrics with the response actually delivered, and makes one-shot error injection safe across concurrent requests. A handler crash amends its own request entry; a destroyed socket is recorded as interrupted rather than as a delivered error. Compatible requests reserve a queued error before asynchronous chaos processing, and release it if it is never served.

Behavior

Journal and route errors

  • Route catches share error handling for logging, CORS, error envelopes, and journaling. Entries are attributed by server request identity through AsyncLocalStorage and Journal.onAdd, so concurrent requests sharing an x-request-id cannot amend each other's entries.
  • A crash after a completed response preserves the delivered status and adds response.error. A crash during a response marks it interrupted; SSE crashes emit their error frame and destroy the socket. Proxy interruption reasons distinguish a client disconnect from recorder destruction.
  • Files, batches, fine-tuning, and ElevenLabs voice catches retain service attribution. Files and voice journal after successful response writes, avoiding phantom entries when writes throw; files errors include their envelopes, and voice entries carry service: "elevenlabs-voice".
  • Body-size enforcement destroys the request socket. Those requests now log at warn and journal status: 0, interrupted: true, and interruptReason: "request body exceeded size limit", without attempting a response on the dead transport. Metrics count destroyed. Deliverable malformed JSON still receives and journals 400. Client aborts similarly record an interruption with reason client aborted.
  • Files rejection warnings escape control characters and Unicode line separators and cap the complete warning to 1,024 characters before the logger prefix. Request-controlled purpose/cursor text cannot forge another log line or generate request-sized warnings. Response and journal envelopes retain their existing contents and limits.

One-shot errors and chaos

  • LLMock.nextRequestError and POST /__aimock/error share the existing endpoint-compatibility rule and queue. Selection reserves the one-shot before awaiting chaos in chat, embeddings, and all four ElevenLabs voice routes, including mixed chat/embedding concurrency. Exactly one request receives the injected error.
  • Unserved reservations return to the queue after chaos or cancellation. Queue clearing/reset/hot reload advances a generation so an older request cannot restore an error into a reset queue. OpenRouter fallback cannot consume an error while discarding its candidate; queued errors appear as responseKind: "error" in the control API.
  • Chaos source labels use one provider-aware proxy rule, including per-request strict mode. A strict malformed response is applied locally and labeled internal; unrelated upstream configuration cannot label a locally answered request proxy.
  • Chaos journaling follows writes, preserves committed status for disconnects, and records null request bodies for fal gates where appropriate. All five fixture chaos fields and server defaults use the shared validation table, rejecting invalid rates, non-finite values, negative zero, strings, and fractional latency consistently.

Metrics and library semantics

  • Metrics count both completed and destroyed responses once, and retain malformed-Host requests under {unknown}. Labels follow route shape independently of response status, preserving routed fixture-miss 404s while bounding unknown paths.
  • Dynamic Gemini/Vertex actions, fal paths, music paths, and files/batches depths use bounded labels. Control and mount labels take precedence over provider patterns, matching dispatch order, including mounts beneath /fal, /v1/files, and /v1/music. Mounts added after startup are read at record time; overlapping mounts retain registration order. Bare /fal shares the router's path rule.
  • Journal.getAll returns nothing for finite nonpositive limits, floors fractions, and treats non-finite values as unlimited. Journal test-id filtering shares the full-header resolver used for request scoping.
  • clearRequests() preserves fixture match counts; sibling sequencing compares toolResultContains and context. Fixture debug descriptions render predicates and nested objects more readably.

Local red-green proof

The following reproductions exercised real local HTTP/socket surfaces before and after production edits at base 142f7bdb87894714b67541801edf6f209a99b18a. No external LLM APIs were used. Skipped counts below come from explicit test-name filters.

A01 — concurrent one-shot selection

pnpm exec vitest run src/__tests__/one-shot-concurrency.test.ts

Red: 11 failed | 10 passed; concurrent embeddings, four voice routes, and mixed chat/embedding requests returned duplicate [500,500] instead of [200,500]. Green: 21 passed. The same suite covers cancellation, chaos release, and generation-reset controls on the changed HTTP routes.

A02 — oversized request transport and journal agreement

pnpm exec vitest run src/__tests__/server.test.ts -t 'oversized body|deliverable malformed'

Red, exit 1: 4 failed | 2 passed | 78 skipped (84). Chat, responses, embeddings, and batches sockets returned no HTTP response, while their journals claimed 400. Green, exit 0: 6 passed | 78 skipped (84). Each oversized request has one interrupted status-0 entry and one destroyed metric; both ordinary malformed-JSON controls retain delivered/journaled 400.

A03 — bounded, escaped rejection warnings

The same real HTTP/stderr probe ran before and after, changing only capture filenames:

pnpm exec tsx /Users/jpr5/.local/share/copilotkit/cr/aimock-pr466/fix-A03/probe.mts > /Users/jpr5/.local/share/copilotkit/cr/aimock-pr466/fix-A03/red.stdout 2> /Users/jpr5/.local/share/copilotkit/cr/aimock-pr466/fix-A03/red.stderr
pnpm exec tsx /Users/jpr5/.local/share/copilotkit/cr/aimock-pr466/fix-A03/probe.mts > /Users/jpr5/.local/share/copilotkit/cr/aimock-pr466/fix-A03/green.stdout 2> /Users/jpr5/.local/share/copilotkit/cr/aimock-pr466/fix-A03/green.stderr

Both measurement runs exit 0. Red: 6 physical log lines; maximum warning 200,146 characters. Green: 5 physical log lines; maximum 1,033 characters including the logger's 9-character prefix. Injected newlines become literal escaped text within one rejection line. Response/journal summaries are unchanged, including the existing journal body cap for the large envelope.

Regression assertions before the edit:

pnpm exec vitest run src/__tests__/files-api.test.ts -t 'bounds and escapes'

Red: 4 failed | 179 skipped (183); oversized warnings measured 200,146 and 8,137 characters against a 1,033-character limit. After the edit, the full file ran:

pnpm exec vitest run src/__tests__/files-api.test.ts

Green: 183 passed, including ordinary readable-warning coverage.

A04 — mount labels before provider labels

pnpm exec vitest run src/__tests__/metrics.test.ts -t 'mounted service metric precedence'

Red, exit 1: 4 failed | 2 passed | 86 skipped. Real mounts under /fal/custom, /v1/files/custom, and /v1/music/custom answered 200 but lost their mount labels; for example, /fal/{other} incorrectly counted all four requests. Green, exit 0: 6 passed | 86 skipped. Live scrapes show mount-root count 1, agent-card count 1, bounded other count 2, with control precedence, registration order, late mounts, and provider boundary controls preserved.

Scope and validation

Endpoint compatibility is unchanged: files, batches, and fine-tuning are not newly eligible for injected errors. Existing multipart parser edge cases, malformed control-API input handling, Azure label rules, and the bare control-prefix label discrepancy remain outside this change.

Independent replay at integrated commit 0ebfb27d35360b66b3abb2016e5e9eb089d3a3c9 passed:

pnpm exec vitest run src/__tests__/one-shot-concurrency.test.ts src/__tests__/server.test.ts src/__tests__/metrics.test.ts -t 'one-shot errors across awaited serving selection|oversized body|deliverable malformed|mounted service metric precedence' --reporter=verbose

Result: 3 files passed; 33 passed | 164 skipped (197), exit 0. Both standalone real HTTP probes also exited 0: three concurrent embedding pairs each delivered one 500 and one 200; oversized sockets matched interrupted journals; all three colliding mount namespaces retained bounded mount labels. Independent warning measurements confirmed five warnings, maximum length 1,033, escaped newlines, and no raw control characters or unbounded payload.

Final local pre-push checks passed in order: pnpm run format:check, pnpm run lint, pnpm run typecheck, pnpm run test, and pnpm run build, all exit 0. The full suite passed 6,818 tests across 209 files, with zero skips. The checked source tree is da47a2e702da57a2c74378ab89deb57281c8dbde; the five-commit reorganization at d487e940e1359a6e927b5aa08b3cce8e696fb868 preserves that exact tree. All five rewritten commit messages pass commitlint, and the working tree is clean. All 29 CI checks passed on d487e940e1359a6e927b5aa08b3cce8e696fb868. The Docker check passed on its single retry after the first Linux/arm64 build stalled inside tsdown; the retry used the unchanged commit.

@pkg-pr-new

pkg-pr-new Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@466

commit: d487e94

@jpr5
jpr5 force-pushed the fix/s3-observability branch from 142f7bd to d487e94 Compare September 17, 2026 16:10
@jpr5
jpr5 merged commit b1ba430 into main Sep 17, 2026
29 of 30 checks passed
@jpr5
jpr5 deleted the fix/s3-observability branch September 17, 2026 17:24
tylerslaton added a commit that referenced this pull request Sep 22, 2026
## [1.43.0] - 2026-09-22

> **BREAKING** — `aimock -h` is `--help`, not `--host`: `aimock -h 0.0.0.0` exits 1 with `Error:
> Unexpected argument '0.0.0.0'. This command does not take positional arguments`. Migration:
> `--host <string>` (long form only). The `llmock` bin (the Docker ENTRYPOINT) keeps `-h, --host`
> (#453).

### Added

- OpenAI GPT-Live mock, record and offline replay on `GET /v1/live/sessions`, with `onLive` fixtures
  (#468)
- Live sessions enforce message/audio/queue/lifetime limits; recordings sanitize credentials (#468)
- Live drift canaries for lifecycle, audio/transcript, delegation and usage, attributed to
  `openai-live` (#468)
- Chaos `rateLimitRate` / `--chaos-ratelimit`: deterministic 429 with `Retry-After` (#449)
- Chaos `latencyMs` / `--chaos-latency` now actually delays responses on every path (#449)
- OpenAI Files API mock — byte-exact uploads, create-purpose enum, CORS on faults (#445)
- OpenAI fine-tuning jobs mock — deterministic lifecycle, events, cursor pages (#447)
- OpenAI Batches API mock — create/list/retrieve/cancel with real output files (#446)
- `X-Request-Id` echoed or minted on every response; `?requestId=` filters the journal (#450)
- `aimock validate` lints fixture files or directories offline, failing on broken files (#453)
- ElevenLabs Voice Design record/replay — design, save-as-voice, and voice slot management (#452)
- ElevenLabs Voice Design provenance block and strict-mode 503 coverage (#454)

### Changed

- **BREAKING:** `aimock -h` is `--help`, matching `aimock convert -h` and `aimock validate -h`; the
  host override is `--host` only. The `llmock` bin (the Docker entrypoint) keeps its own `-h,
  --host` — see the note above (#453)
- `aimock --config ""`, `--port ""` and `--host ""` are usage errors naming the option (#453)
- Realtime `OpenAI-Beta: realtime=v1` now returns the real sunset rejection, not a session (#461)
- `POST /v1/images/variations` now replays the real removal 404; OpenAI deleted it (#462)
- `ChaosAction` gains `"rateLimit"` — an exhaustive switch over it needs a case (#449)
- `applyChaosAsync()` returns `false | "handled" | "unwritable"` instead of a bare `boolean` (#449)
- Journal `headers` now ALWAYS carry `x-request-id` — exact `toEqual` asserts break (#450)

### Deprecated

- Synchronous `applyChaos()` warns once per process; it skips chaos latency. Use `await
  applyChaosAsync(...)` (#449)

### Fixed

- Reasoning-first chat streams with content and tool calls carry the assistant role in the first
  chunk (#470)
- Journals and metrics reflect delivered vs interrupted responses; one-shot errors never
  double-deliver (#466)
- Nonstreaming OpenAI chat returns block text and tools; text-only blocks finish normally on every
  provider (#467)
- AG-UI drift reads generated 1.0 schemas; `AGUIRunStartedEvent` gains optional `protocolVersion`
  (#469)
- Drift reports list `unverifiedSurfaces`; offline Bedrock/Vertex checks no longer skip silently
  (#460)
- Drift docs match actual coverage; ElevenLabs drift separates vendor observations from fixture
  checks (#465)
- Chaos no longer treats committed headers as a dead response; only status-writing actions are
  skipped (#449)
- Chaos skip logs name what happened; `aimock_chaos_triggered_total` counts only written responses
  (#449)
- A request whose chaos latency was cancelled by client hang-up is no longer served or journalled
  (#449)
- Moderations echoes the request's `model`; default is now `omni-moderation-latest` (#459)
- Image endpoints default to `gpt-image-1` — `dall-e-2`/`dall-e-3` were removed (#459)
- AG-UI record/proxy forwards the caller's headers and raw body upstream; `Accept` is forced to SSE
  (#455)
- The AG-UI recorder refuses to write a fixture from a non-stream or empty 2xx upstream reply (#455)
- A MINTED `x-request-id` is no longer forwarded upstream in record/proxy mode (#450)
MikeRyanDev added a commit that referenced this pull request Sep 22, 2026
Cuts the accumulated `[Unreleased]` work. **MINOR (1.43.0)**, kept at a
minor bump despite the
`-h` BREAKING banner (#453): the pre-2.0 precedent (1.14.2 shipped a
BREAKING note as a patch)
applies, and the version was agreed ahead of this PR. Do not re-version
to 2.0.0 at review.

**Prepared, not merged.** `publish-release.yml` fires on push-to-main,
so merging this
publishes to npm, tags `v1.43.0`, force-moves `v1`, cuts the GitHub
Release, dispatches the
Docker build, posts to Slack, and runs the PyPI job gated on
`_version.py`. Merge when you
want it live.

## What ships


> **BREAKING** — `aimock -h` is `--help`, not `--host`: `aimock -h
0.0.0.0` exits 1 with `Error: Unexpected argument '0.0.0.0'. This
command does not take positional arguments`. Migration: `--host
<string>` (long form only). The `llmock` bin (the Docker ENTRYPOINT)
keeps `-h, --host` (#453).

### Added

- OpenAI GPT-Live mock, record and offline replay on `GET
/v1/live/sessions`, with `onLive` fixtures (#468)
- Live sessions enforce message/audio/queue/lifetime limits; recordings
sanitize credentials (#468)
- Live drift canaries for lifecycle, audio/transcript, delegation and
usage, attributed to `openai-live` (#468)
- Chaos `rateLimitRate` / `--chaos-ratelimit`: deterministic 429 with
`Retry-After` (#449)
- Chaos `latencyMs` / `--chaos-latency` now actually delays responses on
every path (#449)
- OpenAI Files API mock — byte-exact uploads, create-purpose enum, CORS
on faults (#445)
- OpenAI fine-tuning jobs mock — deterministic lifecycle, events, cursor
pages (#447)
- OpenAI Batches API mock — create/list/retrieve/cancel with real output
files (#446)
- `X-Request-Id` echoed or minted on every response; `?requestId=`
filters the journal (#450)
- `aimock validate` lints fixture files or directories offline, failing
on broken files (#453)
- ElevenLabs Voice Design record/replay — design, save-as-voice, and
voice slot management (#452)
- ElevenLabs Voice Design provenance block and strict-mode 503 coverage
(#454)

### Changed

- **BREAKING:** `aimock -h` is `--help`, matching `aimock convert -h`
and `aimock validate -h`; the host override is `--host` only. The
`llmock` bin (the Docker entrypoint) keeps its own `-h, --host` — see
the note above (#453)
- `aimock --config ""`, `--port ""` and `--host ""` are usage errors
naming the option (#453)
- Realtime `OpenAI-Beta: realtime=v1` now returns the real sunset
rejection, not a session (#461)
- `POST /v1/images/variations` now replays the real removal 404; OpenAI
deleted it (#462)
- `ChaosAction` gains `"rateLimit"` — an exhaustive switch over it needs
a case (#449)
- `applyChaosAsync()` returns `false | "handled" | "unwritable"` instead
of a bare `boolean` (#449)
- Journal `headers` now ALWAYS carry `x-request-id` — exact `toEqual`
asserts break (#450)

### Deprecated

- Synchronous `applyChaos()` warns once per process; it skips chaos
latency. Use `await applyChaosAsync(...)` (#449)

### Fixed

- Reasoning-first chat streams with content and tool calls carry the
assistant role in the first chunk (#470)
- Journals and metrics reflect delivered vs interrupted responses;
one-shot errors never double-deliver (#466)
- Nonstreaming OpenAI chat returns block text and tools; text-only
blocks finish normally on every provider (#467)
- AG-UI drift reads generated 1.0 schemas; `AGUIRunStartedEvent` gains
optional `protocolVersion` (#469)
- Drift reports list `unverifiedSurfaces`; offline Bedrock/Vertex checks
no longer skip silently (#460)
- Drift docs match actual coverage; ElevenLabs drift separates vendor
observations from fixture checks (#465)
- Chaos no longer treats committed headers as a dead response; only
status-writing actions are skipped (#449)
- Chaos skip logs name what happened; `aimock_chaos_triggered_total`
counts only written responses (#449)
- A request whose chaos latency was cancelled by client hang-up is no
longer served or journalled (#449)
- Moderations echoes the request's `model`; default is now
`omni-moderation-latest` (#459)
- Image endpoints default to `gpt-image-1` — `dall-e-2`/`dall-e-3` were
removed (#459)
- AG-UI record/proxy forwards the caller's headers and raw body
upstream; `Accept` is forced to SSE (#455)
- The AG-UI recorder refuses to write a fixture from a non-stream or
empty 2xx upstream reply (#455)
- A MINTED `x-request-id` is no longer forwarded upstream in
record/proxy mode (#450)

The 35 `[Unreleased]` bullets were condensed to 34 one-liners, most ≤100
chars, because
this text is fed verbatim into the GitHub Release body and summarized
into `#oss-alerts`.
Every `(#N)` reference is preserved; the three un-numbered GPT-Live
bullets now cite #468.
Detail lives in the PRs.

## Behaviour changes to read before you merge

- **#453** — `aimock -h` is now `--help`. `aimock -h 0.0.0.0` exits 1.
Migrate to `--host 0.0.0.0`.
The `llmock` bin (Docker `ENTRYPOINT`) is unchanged and still accepts
`-h, --host`.
`--config ""`, `--port ""`, `--host ""` are now usage errors instead of
"not given".
- **#450** — journal `headers` always carry `x-request-id`; exact
`toEqual` asserts on headers break.
- **#449** — `ChaosAction` gains `"rateLimit"` (exhaustive switches need
a case);
`applyChaosAsync()` returns `false | "handled" | "unwritable"`
(truthiness unchanged, explicit
`boolean` bindings need updating); sync `applyChaos()` warns once per
process.
- **#459 / #461 / #462** — defaults track upstream: images default to
`gpt-image-1`, moderations
to `omni-moderation-latest`; `OpenAI-Beta: realtime=v1` returns the real
sunset rejection;
  `POST /v1/images/variations` returns the real removal 404.
- **#466 / #467** — journal and metrics now count interrupted responses
and text-only block
fixtures finish with a normal terminal reason; suites asserting the old
counts or
`finish_reason: "tool_calls"` on text-only blocks will see different
values.

## Version surfaces

Enumerated with `git grep -F 1.42.0`, not from a list. Seven carriers
bumped:

| Surface | Field |
| --- | --- |
| `package.json` | `version` |
| `charts/aimock/Chart.yaml` | `appVersion` |
| `.claude-plugin/plugin.json` | `version` |
| `.claude-plugin/marketplace.json` | `plugins[0].source.version`
(`^1.43.0`) |
| `docs/index.html` | the `aimock v…` banner |
| `packages/aimock-pytest/src/aimock_pytest/_version.py` |
`AIMOCK_VERSION` |
| `packages/aimock-pytest/README.md` | the `--aimock-version` default |

Deliberately untouched: `packages/aimock-pytest/pyproject.toml` (own
PyPI cadence, stays
`0.5.3`), `charts/aimock/Chart.yaml` `version: 0.1.0` (the chart's own
version), the
historical `1.41.0` mentions in `publish-release.yml` and
`npm-publish-verify-workflow.test.ts`, and CHANGELOG history.

`_version.py` is bumped because #466 and #450 change `/__aimock/journal`
and one-shot
`/__aimock/error` delivery — routes the pytest client calls — same rule
as 1.42.0.

## 1.42.1

npm `latest` is currently **1.42.1**, cut from
`maintenance/v1.42.1-lgts-mcp` (#471) and never
merged to main, so main's CHANGELOG had no `[1.42.1]` entry. Its fix
(#470) is on main and ships
here. This PR adds the one-line `## [1.42.1] - 2026-09-18` history entry
so every published
version appears in the file; every prior patch release (1.37.1–1.37.4)
already did.

## README coverage

The Features list had no mention of the OpenAI Files API (#445),
fine-tuning jobs (#447) or
Batches (#446), all shipped in this release — the same gap #443 closed
for the control API.
Added one **OpenAI platform APIs** bullet under Multimedia APIs linking
the Files and fine-tuning
docs pages; Batches has no docs page yet so it is described inline.
`aimock validate`, `--host`,
chaos latency/rate-limit, `X-Request-Id` and GPT-Live were already
covered. The "11 providers
across 22 API surfaces" headline was left as-is: that line enumerates
providers, and re-counting
it is a docs decision rather than a release step.

## npm description sync

Ran the workflow's inline extractor against this branch's README (after
the edit); output is
byte-identical to `package.json.description`, so the on-merge sync is a
no-op.

## Gates

Exit codes captured by redirect to separate files, never through a pipe.
Fresh worktree,
`pnpm install --frozen-lockfile`.

| Gate | Exit |
| --- | --- |
| `pnpm format:check` | 0 |
| `pnpm lint` | 0 |
| `pnpm typecheck` | 0 |
| `pnpm build` | 0 |
| `pnpm test:exports` | 0 |
| `pnpm test` | 0 — 225 files passed; 7341 tests passed |
| `npx commitlint --from origin/main --to HEAD` | 0 (1 cosmetic
`footer-leading-blank` warning) |

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant