stitchkit 0.81.0 → 0.83.0

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +6613 -0
  2. package/README.md +4 -0
  3. package/dist/agent-runtime/events.d.ts +9 -2
  4. package/dist/agent-runtime/events.d.ts.map +1 -1
  5. package/dist/agent-runtime/observability.d.ts +9 -2
  6. package/dist/agent-runtime/observability.d.ts.map +1 -1
  7. package/dist/agent-runtime-harness.js +2 -2
  8. package/dist/agent-runtime.js +4 -4
  9. package/dist/application/events.d.ts +9 -2
  10. package/dist/application/events.d.ts.map +1 -1
  11. package/dist/application/latest-sink.d.ts +12 -2
  12. package/dist/application/latest-sink.d.ts.map +1 -1
  13. package/dist/application-opentelemetry.js +2 -1
  14. package/dist/application.js +4 -4
  15. package/dist/{index-6s7n2v50.js → index-64ew0vnj.js} +15 -10
  16. package/dist/{index-a59da114.js → index-7z1h3kbr.js} +61 -15
  17. package/dist/{index-1dd82z59.js → index-pq7ak4k8.js} +3 -3
  18. package/dist/internal/observability-sink.d.ts +49 -2
  19. package/dist/internal/observability-sink.d.ts.map +1 -1
  20. package/dist/internal/upgrade-plan.d.ts +18 -0
  21. package/dist/internal/upgrade-plan.d.ts.map +1 -0
  22. package/dist/observability/audit.d.ts +20 -4
  23. package/dist/observability/audit.d.ts.map +1 -1
  24. package/dist/observability/index.d.ts +1 -1
  25. package/dist/observability/index.d.ts.map +1 -1
  26. package/dist/observability/index.js +30 -15
  27. package/dist/observability/status.d.ts +1 -0
  28. package/dist/observability/status.d.ts.map +1 -1
  29. package/dist/release/socket.d.ts +14 -1
  30. package/dist/release/socket.d.ts.map +1 -1
  31. package/dist/tracking/contract.d.ts +11 -0
  32. package/dist/tracking/contract.d.ts.map +1 -1
  33. package/dist/tracking-server.d.ts +1 -0
  34. package/dist/tracking-server.d.ts.map +1 -1
  35. package/dist/tracking.js +1 -0
  36. package/dist/upgrade-cli.d.ts +27 -0
  37. package/dist/upgrade-cli.d.ts.map +1 -0
  38. package/dist/upgrade-cli.js +177 -0
  39. package/llms-full.txt +185 -17
  40. package/package.json +6 -2
package/llms-full.txt CHANGED
@@ -63,11 +63,11 @@ own, recorded as an ADR.
63
63
  | `stitchkit/tracking/server` | server (Bun or Node) | evolving | the decisions a tracking backend makes — dispositions, visit lease over an application-owned store, active intervals, presence; no database |
64
64
  | `stitchkit/release` | browser **and** server | evolving | a page follows the release it was built for — `createReleaseMarker` on the server, `createReleaseWatcher` in the browser, the `X-Build-Id` header and a socket event between them |
65
65
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
66
- | `stitchkit/observability` | server | stable | request/tool event projections — `createObservability`, trace context, sanitisation |
66
+ | `stitchkit/observability` | server | stable<br>_redefined in 1 of the 28 minors since 0.56.2, most recently 0.83.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
67
67
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
68
68
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
69
69
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
70
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 26 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
70
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 12 of the 28 minors since 0.56.2, most recently 0.83.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
71
71
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
72
72
  | `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
73
73
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -75,7 +75,7 @@ own, recorded as an ADR.
75
75
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
76
76
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
77
77
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
78
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 6 of the 26 minors since 0.56.2, most recently 0.79.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
78
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 7 of the 28 minors since 0.56.2, most recently 0.83.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
79
79
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
80
80
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
81
81
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
@@ -350,13 +350,42 @@ records the verdict and reloads nothing.
350
350
 
351
351
  ## Adopting it
352
352
 
353
- 1. Bake the build id into the bundle: `NEXT_PUBLIC_BUILD_ID=$(git rev-parse
354
- --short HEAD)` at build time, or Next's `generateBuildId`. Without it the
355
- watcher has no `own` and never reloads.
353
+ 1. Bake the build id into the bundle, from **one** source. Without it the
354
+ watcher has no `own` and never reloads and it never says so, which is the
355
+ part worth spending a paragraph on.
356
+
357
+ `NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)` in the build command is
358
+ the short version and it has a failure mode: on an immutable-release layout
359
+ that variable usually lives in a static environment file, so it is easy for
360
+ two releases to ship the same id. Then `own` equals what the server reports
361
+ on every response, the watcher is correct to stay quiet, and nothing
362
+ anywhere is red. A reload that never happens looks exactly like a reload
363
+ that was not needed.
364
+
365
+ So mint it once and let one value reach all three readers. In Next, that is
366
+ `next.config`:
367
+
368
+ ```ts
369
+ const buildId = process.env.BUILD_ID ?? execSync('git rev-parse --short HEAD').toString().trim()
370
+
371
+ export default {
372
+ generateBuildId: () => buildId, // → .next/BUILD_ID, which the server marker reads
373
+ env: { NEXT_PUBLIC_BUILD_ID: buildId }, // → the bundle, which becomes `own`
374
+ }
375
+ ```
376
+
377
+ The bundle's id and the file the server reports now come from the same
378
+ expression evaluated once, so they cannot drift apart per release. Any
379
+ arrangement with that property will do; the one to avoid is two places that
380
+ each decide the id and are expected to agree.
356
381
  2. Point `read` at the id of the **active** release, not the process's cwd.
357
382
  3. Add `release: (data: { buildId: string | null }) => void` to your
358
383
  `ServerToClientEvents` map where you type your socket, so your own `on`
359
- knows the event the binding emits.
384
+ knows the event the binding emits. This is also what makes a **typed**
385
+ `Server<…>` fit `bindReleaseToSocketServer`: its `emit` is narrowed to the
386
+ names in your map, so until `release` is one of them the call fails to
387
+ typecheck — as a long structural mismatch, which reads like a bug in the
388
+ binding and is this line instead.
360
389
  4. Send `SIGUSR2` from the deploy step that activates a frontend without
361
390
  restarting the backend, and name that step in the project's release steps.
362
391
 
@@ -9457,7 +9486,7 @@ export const observability = createObservability({
9457
9486
  })
9458
9487
 
9459
9488
  const status = observability.getStatus()
9460
- const drained = await observability.close()
9489
+ const report = await observability.close({ timeoutMs: 5_000 })
9461
9490
  ```
9462
9491
 
9463
9492
  `maxPending` defaults to `1000` per sink and must be a positive safe integer.
@@ -9475,14 +9504,71 @@ without parsing callback logs.
9475
9504
 
9476
9505
  `flush()` snapshots the current generation and waits only for events admitted
9477
9506
  up to that call. `close()` atomically stops admission, drains every accepted
9478
- generation and returns the final counters plus `durationMs`; repeated calls
9479
- return the same report. Graceful shutdown order is therefore:
9507
+ generation and returns the final counters plus `durationMs`; repeated unbounded
9508
+ calls return the same report. Graceful shutdown order is therefore:
9480
9509
 
9481
9510
  1. stop HTTP/MCP admission;
9482
9511
  2. wait for active requests and tool calls;
9483
- 3. `await observability.close()`;
9512
+ 3. `await observability.close({ timeoutMs })`;
9484
9513
  4. close the database/storage connection used by the sinks.
9485
9514
 
9515
+ ### Give the drain a bound in a shutdown budget
9516
+
9517
+ Unbounded, `close()` waits for every accepted event however long the sink takes.
9518
+ That is right when the sink is healthy and fatal when it is not: one write that
9519
+ never settles — a database that has stopped answering — holds the drain forever,
9520
+ and a shutdown that gives every other step a deadline then spends its entire
9521
+ budget here and exits by force. A drain that cannot be bounded cannot take part
9522
+ in a shutdown budget: it either fits, or it cancels the budget.
9523
+
9524
+ Both `flush` and `close` take `{ timeoutMs?, signal? }`:
9525
+
9526
+ ```ts
9527
+ const report = await observability.close({ timeoutMs: 5_000 })
9528
+ if (!report.drained) {
9529
+ const t = report.total
9530
+ logger.warn('audit drain incomplete', {
9531
+ stillWriting: t.pending + t.preparing,
9532
+ neverWritten: t.received - t.filtered - t.completed,
9533
+ })
9534
+ }
9535
+ ```
9536
+
9537
+ Those are two different numbers and the difference matters. `pending +
9538
+ preparing` is what the drain was still waiting for when the bound expired —
9539
+ events handed to `write` that may yet succeed. `received - filtered -
9540
+ completed` is every event the sink has not written **for any reason**: those,
9541
+ plus the ones already lost to `failed`, `dropped` and `preparationFailed`. A
9542
+ shutdown log that wants to state how much audit was lost wants the second; one
9543
+ that wants to explain why the drain did not finish wants the first.
9544
+
9545
+ `drained` is read from the counters, not from which side of the race won, so a
9546
+ bound that expires on a sink that has in fact finished — a shutdown signal
9547
+ already aborted by an earlier step — reports `true` rather than a false alarm.
9548
+
9549
+ The bound ends the **waiting**, not the writes: a sink's `write` is handed no
9550
+ cancellation, so an outstanding one keeps running against whatever the consumer
9551
+ closes next. It is a bound on waiting for I/O, not on wall time — no bound can
9552
+ preempt a `write` that occupies the event loop. Racing `close()` against your
9553
+ own timer looks equivalent and is not: it discards the report along with the
9554
+ wait.
9555
+
9556
+ `flush(bound?)` takes the same bound and returns whether the generation it
9557
+ waited on settled. That outcome is returned rather than left to `getStatus()`,
9558
+ which cannot answer it: flush waits on the events admitted before the call while
9559
+ the status counts everything alive right now, so a complete flush and an expired
9560
+ one look identical there.
9561
+
9562
+ Every other sink in the framework takes the same bound —
9563
+ `createApplicationEventSink`, `createApplicationSnapshotSink`,
9564
+ `createAgentRuntimeEventSink`, `createAgentObservability`. The hazard was never
9565
+ specific to the audit sink; it was specific to waiting without a limit.
9566
+
9567
+ The drain itself is started once and shared, so a second `close` with a shorter
9568
+ bound observes the same drain under its own limit rather than starting another.
9569
+ Passing no bound keeps the previous behaviour exactly, including the identical
9570
+ returned promise.
9571
+
9486
9572
  Stitchkit manages only in-process delivery. If process-crash durability matters,
9487
9573
  make `write` enqueue into a consumer-owned durable outbox and let that adapter
9488
9574
  own retry, replay and storage policy:
@@ -10900,10 +10986,19 @@ A release that breaks a public API leads its `CHANGELOG.md` entry with a
10900
10986
  **`### ⚠️ Breaking changes`** section (exact heading), each item carrying a
10901
10987
  **before → after** snippet. A version with **no** such section is **purely
10902
10988
  additive** — adopting it changes nothing in your code. (See
10903
- [`AGENTS.md` → Breaking changes](../../AGENTS.md).) `bun scripts/upgrade-plan.ts
10904
- <installed> <target>` prints every breaking section your range crosses, oldest
10905
- first, each with its **Who must act** line — the list this guide's migrations
10906
- expand on.
10989
+ [`AGENTS.md` → Breaking changes](../../AGENTS.md).) So the list is recoverable
10990
+ mechanically, and the package recovers it for you:
10991
+
10992
+ ```bash
10993
+ bunx stitchkit@latest upgrade # or: npx stitchkit@latest upgrade
10994
+ ```
10995
+
10996
+ Run it inside the project that depends on stitchkit. It reads your installed
10997
+ version out of `node_modules`, reads the changelog **shipped inside the
10998
+ package**, and prints every breaking section your range crosses, oldest first,
10999
+ each with its **Who must act** line — the list this guide's migrations expand
11000
+ on. Nothing to clone, nothing to be told: `--from` / `--to` override either end
11001
+ of the range if you want a different one.
10907
11002
 
10908
11003
  So upgrading is: read the `### ⚠️ Breaking changes` of every version *above* your
10909
11004
  current one *up to* your target, and apply each snippet.
@@ -10932,6 +11027,78 @@ makes one thing your job rather than the resolver's:
10932
11027
  The mechanical part is identical either way. Only the *noticing* differs, and an
10933
11028
  exact pin moves it onto you.
10934
11029
 
11030
+ ## Released migration: 0.83.0
11031
+
11032
+ Two things, both mechanical, and only if you touch an observability sink.
11033
+
11034
+ ```bash
11035
+ rg -n "createObservability|createApplicationEventSink|createApplicationSnapshotSink|createAgentRuntimeEventSink|createAgentObservability"
11036
+ ```
11037
+
11038
+ **1. `flush()` returns `Promise<boolean>`.** `await sink.flush()` needs no
11039
+ change. Only a declared type does:
11040
+
11041
+ ```ts
11042
+ // before
11043
+ const flushAudit: () => Promise<void> = observability.flush
11044
+ // after
11045
+ const flushAudit: () => Promise<boolean> = observability.flush
11046
+ ```
11047
+
11048
+ **2. `ObservabilityDrainReport` carries `drained`.** The framework builds it, so
11049
+ reading code is unchanged. A hand-built report — a test double, or one persisted
11050
+ and re-parsed through `ObservabilityDrainReportSchema` — needs the field:
11051
+
11052
+ ```ts
11053
+ // before
11054
+ { request, tools, total, durationMs }
11055
+ // after
11056
+ { request, tools, total, durationMs, drained: true }
11057
+ ```
11058
+
11059
+ Then take the thing this release exists for. Every drain now accepts
11060
+ `{ timeoutMs?, signal? }`, so a shutdown can give the audit drain the same
11061
+ deadline it gives every other step:
11062
+
11063
+ ```ts
11064
+ const report = await observability.close({ timeoutMs: 5_000 })
11065
+ if (!report.drained) {
11066
+ logger.warn('audit drain incomplete', {
11067
+ unwritten: report.total.received - report.total.filtered - report.total.completed,
11068
+ })
11069
+ }
11070
+ ```
11071
+
11072
+ The bound ends the **waiting**, not the writes: a sink's `write` is handed no
11073
+ cancellation, so an outstanding one keeps running against whatever you close
11074
+ next. Racing `close()` against your own timer looks equivalent and is not — it
11075
+ discards the report along with the wait.
11076
+
11077
+ ## Released migration: 0.82.0
11078
+
11079
+ One thing, and only if you mount MCP or agent tools from a `createTrackingContract` contract.
11080
+
11081
+ ```bash
11082
+ rg -n "createTrackingContract"
11083
+ ```
11084
+
11085
+ The event ingest (`track`, `POST /events`) is now `expose: ['HTTP']`, which is
11086
+ what its sibling `bootstrap` always declared. Before, it declared nothing — and
11087
+ an endpoint with no `expose` is a tool on MCP and AGENT by default, so the
11088
+ mounted surface carried a `track_<prefix>` tool. Since this contract is built
11089
+ inside the framework, an application that had made every tool opt-in for the
11090
+ endpoints *it* authors could not have opted this one out.
11091
+
11092
+ **If you pin a tool-surface digest or count**, it moves by one; re-record it.
11093
+ **If you assert on tool names**, `track_<prefix>` is gone. **If nothing in your
11094
+ project mounts tools from this contract**, there is nothing to do — the HTTP
11095
+ routes, the client and the schemas are unchanged.
11096
+
11097
+ Un-declaring the tool was never the intent, so there is no option to keep it. If
11098
+ an agent in your system genuinely needs to record something, give it your own
11099
+ endpoint with your own schema: the browser ingest trusts a client-minted event
11100
+ id and a visit lease, and an agent holds neither.
11101
+
10935
11102
  ## Released migration: 0.81.0
10936
11103
 
10937
11104
  **Path literals own string params.**
@@ -15216,10 +15383,11 @@ audit event. See the [Observability guide](../guide/observability.md).
15216
15383
  | `BoundedLoggerBounds` / `BoundedLoggerOptions` | _type_ | per-value and total record ceilings plus sink/redaction configuration |
15217
15384
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
15218
15385
  | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
15219
- | `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(), close() }` with bounded sink lifecycle |
15386
+ | `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(bound?): Promise<boolean>, close(bound?) }` with bounded sink lifecycle |
15387
+ | `ObservabilityDrainBound` | _type_ | `timeoutMs` and/or `signal` limiting how long `flush`/`close` wait; the wait ends, outstanding writes do not |
15220
15388
  | `ObservabilitySinkStatus` | _type_ | immutable counters for one bounded request/tool sink |
15221
15389
  | `ObservabilityStatus` | _type_ | per-surface plus aggregate operational snapshot |
15222
- | `ObservabilityDrainReport` | _type_ | final closed/drained snapshot plus duration |
15390
+ | `ObservabilityDrainReport` | _type_ | final closed/drained snapshot, `durationMs` (the shared drain's age, not this call's wait), and `drained` — read from the counters, `false` only when `total.pending + total.preparing` is nonzero |
15223
15391
  | `ObservabilitySinkStatusSchema` / `ObservabilityStatusSchema` / `ObservabilityDrainReportSchema` | schema | runtime schemas for status/report integration boundaries |
15224
15392
  | `RequestEventSinkConfig` | _type_ | `write`, filter/sanitisation, `maxPending`, `onSinkError` and `onDrop` |
15225
15393
  | `RequestObservabilityConfig` | _type_ | request sink plus opt-in payload capture and default-off `includeCancelled` rows |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.81.0",
3
+ "version": "0.83.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -34,6 +34,9 @@
34
34
  "bun": ">=1.2.0",
35
35
  "node": ">=22"
36
36
  },
37
+ "bin": {
38
+ "stitchkit": "./dist/upgrade-cli.js"
39
+ },
37
40
  "main": "./dist/index.js",
38
41
  "types": "./dist/index.d.ts",
39
42
  "files": [
@@ -42,6 +45,7 @@
42
45
  "llms.txt",
43
46
  "llms-full.txt",
44
47
  "README.md",
48
+ "CHANGELOG.md",
45
49
  "LICENSE"
46
50
  ],
47
51
  "exports": {
@@ -186,7 +190,7 @@
186
190
  "check": "bun x tsc --noEmit",
187
191
  "build:js": "bun run build:entrypoints && bun scripts/preserve-webpack-ignore.mjs",
188
192
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/rewrite-declaration-specifiers.mjs",
189
- "build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-browser-executes.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs && bun scripts/check-declarations-strict.mjs",
193
+ "build": "rm -rf dist && cp ../../CHANGELOG.md ./CHANGELOG.md && bun run build:js && chmod +x dist/upgrade-cli.js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-browser-executes.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs && bun scripts/check-declarations-strict.mjs",
190
194
  "dev": "bun run build:entrypoints -- --watch",
191
195
  "prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
192
196
  "test": "bun test",