stitchkit 0.76.2 → 0.78.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 (74) hide show
  1. package/dist/agent-runtime-coding-tools.js +1 -1
  2. package/dist/agent-runtime-harness.js +6 -6
  3. package/dist/agent-runtime.js +11 -11
  4. package/dist/application/decisions.d.ts +89 -0
  5. package/dist/application/decisions.d.ts.map +1 -0
  6. package/dist/application/kernel.d.ts +35 -1
  7. package/dist/application/kernel.d.ts.map +1 -1
  8. package/dist/application/keyspace.d.ts +11 -1
  9. package/dist/application/keyspace.d.ts.map +1 -1
  10. package/dist/application/schedule.d.ts.map +1 -1
  11. package/dist/application/server-resource.d.ts.map +1 -1
  12. package/dist/application-diagnostic-journal.d.ts +21 -0
  13. package/dist/application-diagnostic-journal.d.ts.map +1 -0
  14. package/dist/application-diagnostic-journal.js +714 -0
  15. package/dist/application-opentelemetry.js +92 -5
  16. package/dist/application-schemas.d.ts +15 -0
  17. package/dist/application-schemas.d.ts.map +1 -0
  18. package/dist/application-schemas.js +26 -0
  19. package/dist/application.d.ts +3 -3
  20. package/dist/application.d.ts.map +1 -1
  21. package/dist/application.js +1832 -844
  22. package/dist/cli.js +7 -7
  23. package/dist/contract/index.js +9 -7
  24. package/dist/files/boundary.d.ts.map +1 -1
  25. package/dist/files.js +1 -1
  26. package/dist/{index-3z73fh2c.js → index-1ckavx4h.js} +49 -1
  27. package/dist/{index-s2rchahr.js → index-31k1ns1s.js} +4 -4
  28. package/dist/{index-m668wzyc.js → index-4ntp63ps.js} +4 -4
  29. package/dist/{index-1rxswfbv.js → index-56fm73re.js} +1 -1
  30. package/dist/{index-ezmn6ac6.js → index-5r0ak8fr.js} +3 -3
  31. package/dist/{index-f6n5n7nz.js → index-7z69kyrh.js} +10 -87
  32. package/dist/{index-k2zczx1g.js → index-93xp5tgg.js} +1 -1
  33. package/dist/{index-bfcpjw20.js → index-actkayzn.js} +7 -3
  34. package/dist/index-anyqnqcv.js +79 -0
  35. package/dist/{index-7qy2ex0m.js → index-cwp38wxn.js} +1 -1
  36. package/dist/{index-35z5h2ty.js → index-dmfn9m4x.js} +125 -4
  37. package/dist/index-h4pj6fta.js +87 -0
  38. package/dist/{index-jqtsc9mj.js → index-j6mfaw26.js} +2 -2
  39. package/dist/{index-58jzmnn4.js → index-k45qqrdh.js} +513 -39
  40. package/dist/{index-8eywc9zv.js → index-kw12fsbc.js} +0 -1
  41. package/dist/index-n1g2xm6a.js +203 -0
  42. package/dist/{index-t8qyvrvg.js → index-n7apa6sy.js} +12 -12
  43. package/dist/{index-s4c8wy8m.js → index-qaajjfp6.js} +8 -8
  44. package/dist/{index-nemjkxjp.js → index-s5tn4kdv.js} +14 -10
  45. package/dist/index-wfq8zerm.js +92 -0
  46. package/dist/index.js +53 -1215
  47. package/dist/internal/deadline.d.ts +17 -0
  48. package/dist/internal/deadline.d.ts.map +1 -0
  49. package/dist/internal/decision.d.ts +31 -0
  50. package/dist/internal/decision.d.ts.map +1 -0
  51. package/dist/live/events.d.ts +8 -13
  52. package/dist/live/events.d.ts.map +1 -1
  53. package/dist/live.d.ts +1 -1
  54. package/dist/live.d.ts.map +1 -1
  55. package/dist/live.js +12 -89
  56. package/dist/node.js +5 -6
  57. package/dist/observability/context.d.ts.map +1 -1
  58. package/dist/observability/index.js +3 -3
  59. package/dist/primitives.js +2 -1
  60. package/dist/remote.js +3 -6
  61. package/dist/server/event-bus.d.ts +3 -2
  62. package/dist/server/event-bus.d.ts.map +1 -1
  63. package/dist/server/index.js +31 -30
  64. package/dist/testing.js +1562 -87
  65. package/dist/tool-invoker.js +6 -6
  66. package/dist/tools.js +66 -23
  67. package/llms-full.txt +276 -6
  68. package/package.json +12 -4
  69. package/dist/index-3cwck0rm.js +0 -897
  70. package/dist/index-7b188kmz.js +0 -49
  71. package/dist/index-a916km3r.js +0 -45
  72. package/dist/index-hvftzz91.js +0 -721
  73. package/dist/{index-da1aqnhb.js → index-aewy3x5j.js} +3 -3
  74. package/dist/{index-nt1mp8km.js → index-efm026pw.js} +3 -3
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  createToolInvoker
3
- } from "./index-jqtsc9mj.js";
4
- import"./index-7qy2ex0m.js";
5
- import"./index-s4c8wy8m.js";
6
- import"./index-nemjkxjp.js";
3
+ } from "./index-j6mfaw26.js";
4
+ import"./index-cwp38wxn.js";
5
+ import"./index-qaajjfp6.js";
7
6
  import"./index-22by16v6.js";
8
7
  import"./index-cby4ar3v.js";
9
- import"./index-nt1mp8km.js";
8
+ import"./index-s5tn4kdv.js";
9
+ import"./index-efm026pw.js";
10
10
  import"./index-vj3vvpaa.js";
11
- import"./index-0w9abg87.js";
12
11
  import"./index-qyrqwr4c.js";
12
+ import"./index-0w9abg87.js";
13
13
  import"./index-6k1937bx.js";
14
14
  export {
15
15
  createToolInvoker
package/dist/tools.js CHANGED
@@ -1,18 +1,12 @@
1
- import {
2
- redact
3
- } from "./index-aczggrty.js";
4
1
  import {
5
2
  buildToolManifest,
6
3
  mountAgent
7
- } from "./index-ezmn6ac6.js";
4
+ } from "./index-5r0ak8fr.js";
8
5
  import"./index-3xnq72rz.js";
9
- import {
10
- argumentsDigest
11
- } from "./index-a916km3r.js";
12
6
  import {
13
7
  signJwt,
14
8
  verifyPkce
15
- } from "./index-da1aqnhb.js";
9
+ } from "./index-aewy3x5j.js";
16
10
  import {
17
11
  DEFAULT_CORS_ALLOW_HEADERS,
18
12
  DEFAULT_PROCESS_SIGNALS,
@@ -23,7 +17,7 @@ import {
23
17
  } from "./index-vbf2p6me.js";
24
18
  import {
25
19
  createToolInvoker
26
- } from "./index-jqtsc9mj.js";
20
+ } from "./index-j6mfaw26.js";
27
21
  import {
28
22
  WaitTimeoutError,
29
23
  createCli,
@@ -32,10 +26,10 @@ import {
32
26
  fetchPinnedDocument,
33
27
  readCapped,
34
28
  runWaitOperation
35
- } from "./index-s2rchahr.js";
29
+ } from "./index-31k1ns1s.js";
36
30
  import {
37
31
  collectToolSurface
38
- } from "./index-k2zczx1g.js";
32
+ } from "./index-93xp5tgg.js";
39
33
  import {
40
34
  createRuntimeToolFactory,
41
35
  defineRuntimeTool
@@ -44,22 +38,17 @@ import {
44
38
  collectTools,
45
39
  createToolRunner,
46
40
  formatToolError
47
- } from "./index-7qy2ex0m.js";
41
+ } from "./index-cwp38wxn.js";
48
42
  import {
49
43
  ToolExecutionControlError,
50
44
  coerceJsonArgs,
51
45
  executeToolMethod,
52
46
  isToolExecutionControlError,
53
47
  toolResultFromError
54
- } from "./index-s4c8wy8m.js";
55
- import {
56
- getRequestContext,
57
- getTraceId,
58
- runWithRequestContext
59
- } from "./index-nemjkxjp.js";
48
+ } from "./index-qaajjfp6.js";
60
49
  import {
61
50
  ManagedFileError
62
- } from "./index-bfcpjw20.js";
51
+ } from "./index-actkayzn.js";
63
52
  import"./index-sbdmyz75.js";
64
53
  import {
65
54
  PORTABLE_JSON_SCHEMA_FORMATS,
@@ -73,20 +62,28 @@ import {
73
62
  validateMcpRoundPolicy
74
63
  } from "./index-22by16v6.js";
75
64
  import"./index-cby4ar3v.js";
65
+ import {
66
+ redact
67
+ } from "./index-aczggrty.js";
68
+ import {
69
+ getRequestContext,
70
+ getTraceId,
71
+ runWithRequestContext
72
+ } from "./index-s5tn4kdv.js";
76
73
  import {
77
74
  normalizeError,
78
75
  resolvePropagationContext
79
- } from "./index-nt1mp8km.js";
76
+ } from "./index-efm026pw.js";
80
77
  import {
81
78
  defineContract
82
79
  } from "./index-vj3vvpaa.js";
80
+ import {
81
+ isRecord
82
+ } from "./index-qyrqwr4c.js";
83
83
  import {
84
84
  AppError,
85
85
  STITCH_ERROR_STATUS
86
86
  } from "./index-0w9abg87.js";
87
- import {
88
- isRecord
89
- } from "./index-qyrqwr4c.js";
90
87
  import {
91
88
  ManagedFilePathSchema,
92
89
  ManagedFileRefSchema
@@ -1070,6 +1067,52 @@ import {
1070
1067
  inputResponse,
1071
1068
  PROTOCOL_VERSION_META_KEY
1072
1069
  } from "@modelcontextprotocol/server";
1070
+
1071
+ // src/internal/stable-digest.ts
1072
+ function stableValue(value) {
1073
+ if (Array.isArray(value))
1074
+ return value.map(stableValue);
1075
+ if (value === null || typeof value !== "object")
1076
+ return value;
1077
+ const result = {};
1078
+ for (const key of Object.keys(value).sort()) {
1079
+ result[key] = stableValue(Reflect.get(value, key));
1080
+ }
1081
+ return result;
1082
+ }
1083
+ function mix128(input) {
1084
+ let a = 2654435769;
1085
+ let b = 2246822507;
1086
+ let c = 3266489909;
1087
+ let d = 668265263;
1088
+ for (let index = 0;index < input.length; index += 1) {
1089
+ const code = input.charCodeAt(index);
1090
+ a = Math.imul(a ^ code, 2246822519);
1091
+ b = Math.imul(b ^ code, 3266489917);
1092
+ c = Math.imul(c ^ code, 668265263);
1093
+ d = Math.imul(d ^ code, 374761393);
1094
+ a = a << 13 | a >>> 19;
1095
+ b = b << 17 | b >>> 15;
1096
+ c = c << 7 | c >>> 25;
1097
+ d = d << 11 | d >>> 21;
1098
+ }
1099
+ a ^= b >>> 15;
1100
+ b ^= c >>> 13;
1101
+ c ^= d >>> 11;
1102
+ d ^= a >>> 9;
1103
+ return [
1104
+ Math.imul(a ^ a >>> 16, 575075507) >>> 0,
1105
+ Math.imul(b ^ b >>> 13, 2654435761) >>> 0,
1106
+ Math.imul(c ^ c >>> 16, 2246822507) >>> 0,
1107
+ Math.imul(d ^ d >>> 15, 3266489909) >>> 0
1108
+ ];
1109
+ }
1110
+ function argumentsDigest(args) {
1111
+ const lanes = mix128(JSON.stringify(stableValue(args)) ?? "undefined");
1112
+ return lanes.map((lane) => lane.toString(16).padStart(8, "0")).join("");
1113
+ }
1114
+
1115
+ // src/tools/mcp-round.ts
1073
1116
  function transportContext(context, toolName, outcome, round) {
1074
1117
  const protocolVersionValue = isRecord(context.mcpReq.envelope) ? Reflect.get(context.mcpReq.envelope, PROTOCOL_VERSION_META_KEY) : undefined;
1075
1118
  const protocolVersion = typeof protocolVersionValue === "string" ? protocolVersionValue : undefined;
package/llms-full.txt CHANGED
@@ -62,7 +62,7 @@ own, recorded as an ADR.
62
62
  | `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 |
63
63
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
64
64
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
65
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 21 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
65
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 23 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
66
66
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
67
67
  | `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 |
68
68
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -70,9 +70,11 @@ own, recorded as an ADR.
70
70
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
71
71
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
72
72
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
73
- | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 21 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
73
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 5 of the 23 minors since 0.56.2, most recently 0.78.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
74
74
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
75
75
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
76
+ | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
77
+ | `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
76
78
 
77
79
  Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
78
80
  code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
@@ -5478,6 +5480,133 @@ Every instrument uses unit `1` and reports an absolute current/lifetime value:
5478
5480
  | `stitchkit.application.schedule.accepting`, `.active`, `.queued`, `.runs_started`, `.runs_completed`, `.runs_failed`, `.ticks_skipped` | current schedule state and absolute run/tick counts |
5479
5481
  | `stitchkit.application.activity.active`, `.queued`, `.completed`, `.failed` | absolute stage projections for declared activity sources |
5480
5482
 
5483
+ ## Replacing part of the graph without stopping the process
5484
+
5485
+ `restart` takes down one resource **and everything that depends on it**, then
5486
+ brings that subtree back. Everything else keeps running, and the process epoch
5487
+ does not move.
5488
+
5489
+ ```ts
5490
+ const result = await app.restart({ resourceId: 'database' })
5491
+
5492
+ result.outcome // 'restarted' | 'failed' | 'refused'
5493
+ result.affected // ['database', 'repository', 'api'] — in start order
5494
+ ```
5495
+
5496
+ The dependants come down with it because they are holding what the resource
5497
+ published. A repository that kept running across a database replacement holds a
5498
+ pool that has been closed: it still typechecks, still has methods, and fails at
5499
+ whatever moment the first call happens. There is no version of this where a
5500
+ dependant keeps a live handle, so the subtree — not the resource — is the unit.
5501
+
5502
+ A leaf restart affects one resource, and an independent neighbour is never
5503
+ touched:
5504
+
5505
+ ```ts
5506
+ await app.restart({ resourceId: 'cache' })
5507
+ // mailer and database were not stopped, not started, not activated
5508
+ ```
5509
+
5510
+ **Refused is not failed.** An unknown id, a restart during shutdown, and a
5511
+ restart before the application is ready all return `refused` with a reason and
5512
+ touch nothing. `failed` means the subtree came down and the new generation did
5513
+ not come up — and the result agrees with the snapshot: if any affected resource
5514
+ ends the restart in `failed`, so does the restart, including an optional one
5515
+ whose failure the start loop does not re-throw.
5516
+
5517
+ **The close phase is bounded**, by the application's shutdown budget or by one
5518
+ this call names:
5519
+
5520
+ ```ts
5521
+ await app.restart({ resourceId: 'database', gracePeriodMs: 5_000, forceTimeoutMs: 2_000 })
5522
+ ```
5523
+
5524
+ **A resource has to be able to start twice.** The kernel calls `start` again, so
5525
+ a resource holding state across its own lifetime rebuilds it there. The ones this
5526
+ framework ships do — a schedule re-arms, a keyspace opens a new generation and
5527
+ re-loads, a managed server built from a factory gets a fresh server. A managed
5528
+ server given a server *instance* cannot, and says so by name rather than
5529
+ republishing something it has shut down. Give a keyspace a
5530
+ `backend: () => …` factory when its backend cannot be re-opened after `close()`.
5531
+
5532
+ → [ADR 0157](../decisions/0157-a-restartable-resource-begins-a-generation.md).
5533
+
5534
+ Restarts of overlapping subtrees queue behind each other rather than being
5535
+ refused. Two callers asking at once is ordinary; two generations of one resource
5536
+ alive at once is the thing this must make impossible.
5537
+
5538
+ ```ts
5539
+ // Both succeed, one complete pass after the other.
5540
+ await Promise.all([
5541
+ app.restart({ resourceId: 'database' }),
5542
+ app.restart({ resourceId: 'database' }),
5543
+ ])
5544
+ ```
5545
+
5546
+ What a restart is **not** is a process restart: it replaces resources, and does
5547
+ not re-read configuration the kernel captured when it was constructed.
5548
+
5549
+ → [ADR 0154](../decisions/0154-the-unit-of-a-restart-is-the-subtree.md).
5550
+
5551
+ ## Decisions a policy set makes together
5552
+
5553
+ `createDecisionPipeline` runs an ordered list of policies that each vote
5554
+ `allow`, `deny` (with a reason) or `defer`. The first terminal verdict wins and
5555
+ the rest do not run.
5556
+
5557
+ ```ts
5558
+ import { createDecisionPipeline } from 'stitchkit/application'
5559
+
5560
+ const pipeline = createDecisionPipeline<{ userId: string; scope: string }>([
5561
+ { id: 'banned', decide: (r) => (isBanned(r.userId)
5562
+ ? { outcome: 'deny', reason: 'account suspended' }
5563
+ : { outcome: 'defer' }) },
5564
+ { id: 'scope', decide: (r) => (r.scope === 'admin'
5565
+ ? { outcome: 'allow' }
5566
+ : { outcome: 'defer' }) },
5567
+ { id: 'default', decide: () => ({ outcome: 'deny', reason: 'no policy allowed this' }) },
5568
+ ], { policyTimeoutMs: 2_000 })
5569
+
5570
+ const result = await pipeline.decide(request)
5571
+ result.outcome // 'allow' | 'deny' — never 'defer'
5572
+ result.trace // the policies that actually ran, in order
5573
+ ```
5574
+
5575
+ Three things are deliberate.
5576
+
5577
+ **A deny carries a reason, by schema.** `{ outcome: 'deny' }` alone does not
5578
+ typecheck. A refusal whose cause exists only in the log of whoever refused is a
5579
+ support ticket.
5580
+
5581
+ **The trace is what ran, not what was declared.** It stops at the terminal
5582
+ verdict. When something was denied the question is which policy denied it and
5583
+ what the ones before it said, and a trace listing policies that never ran would
5584
+ answer that wrongly while looking complete.
5585
+
5586
+ **Every policy deferring raises.** `DecisionUndecidedError`, not a default —
5587
+ defaulting to `allow` turns an incomplete policy set into an open door, and
5588
+ defaulting to `deny` turns it into an outage whose cause reads as a legitimate
5589
+ refusal.
5590
+
5591
+ **A policy that does not answer raises too.** A non-decision, a throw, and running
5592
+ past `policyTimeoutMs` are one error — `DecisionPolicyError`, naming the policy
5593
+ and carrying the trace so far — because they are indistinguishable downstream and
5594
+ handling one but not the others is how a broken policy becomes a skipped one. The
5595
+ chain stops there rather than falling through to whatever the next policy would
5596
+ have said.
5597
+
5598
+ `policyTimeoutMs` is required and has no default. Pick a number your slowest
5599
+ policy comfortably beats. The framework will not choose it for you: a policy that
5600
+ never settles hangs every caller of the operation it guards, and a default here
5601
+ is a number nobody chose applied to code the framework has never seen.
5602
+
5603
+ The same `allow`/`deny`/`defer` type is what an event topic declared
5604
+ `mode: 'decision'` uses in `stitchkit/live` — one vocabulary, because a listener
5605
+ voting on an event and a policy voting on a request are answering the same
5606
+ question.
5607
+
5608
+ → [ADR 0155](../decisions/0155-one-decision-vocabulary-and-an-unanswered-question-is-an-error.md).
5609
+
5481
5610
  ## Admission and graceful shutdown
5482
5611
 
5483
5612
  Use the application operation lease for work that is not already counted by a
@@ -5571,8 +5700,14 @@ changing its revision or status contract. → ADR 0119.
5571
5700
  Use `createDiagnosticJournal` when a process needs finite, ordered local metadata evidence and the
5572
5701
  deployment log pipeline is not the right boundary:
5573
5702
 
5703
+ It lives at `stitchkit/application/diagnostic-journal`, not in the main barrel:
5704
+ it is the one part of the kernel that spawns, locks and writes files, and while it
5705
+ was exported from `stitchkit/application` that single line made the whole
5706
+ entrypoint unusable in a browser bundle. Its schemas stay in
5707
+ `stitchkit/application`.
5708
+
5574
5709
  ```ts
5575
- import { createDiagnosticJournal } from 'stitchkit/application'
5710
+ import { createDiagnosticJournal } from 'stitchkit/application/diagnostic-journal'
5576
5711
  import { z } from 'zod'
5577
5712
 
5578
5713
  const journal = await createDiagnosticJournal({
@@ -10001,6 +10136,73 @@ makes one thing your job rather than the resolver's:
10001
10136
  The mechanical part is identical either way. Only the *noticing* differs, and an
10002
10137
  exact pin moves it onto you.
10003
10138
 
10139
+ ## Released migration: 0.78.0
10140
+
10141
+ Two mechanical edits. Find them both:
10142
+
10143
+ ```bash
10144
+ rg -n "createDecisionPipeline|createDiagnosticJournal" --glob '*.ts' --glob '*.tsx'
10145
+ ```
10146
+
10147
+ **1. `createDecisionPipeline` takes a deadline.**
10148
+
10149
+ ```ts
10150
+ // before
10151
+ createDecisionPipeline([policyA, policyB])
10152
+ // after
10153
+ createDecisionPipeline([policyA, policyB], { policyTimeoutMs: 2_000 })
10154
+ ```
10155
+
10156
+ Pick a number your slowest policy comfortably beats. There is no default on
10157
+ purpose: a policy that never settles hangs every caller of the operation it
10158
+ guards, and the framework has no basis for choosing that number for you.
10159
+
10160
+ Past the deadline — and on a throw, and on a non-decision — the pipeline raises
10161
+ `DecisionPolicyError` naming the policy, with the trace so far. If you already
10162
+ catch that for a bad return value, you now catch two more cases with it.
10163
+
10164
+ **2. `createDiagnosticJournal` moved.**
10165
+
10166
+ ```ts
10167
+ // before
10168
+ import { createDiagnosticJournal } from 'stitchkit/application'
10169
+ // after
10170
+ import { createDiagnosticJournal } from 'stitchkit/application/diagnostic-journal'
10171
+ ```
10172
+
10173
+ Only the factory moved. `DiagnosticJournalConfig`, every
10174
+ `DiagnosticJournal*Schema` and `readDiagnosticJournalLockDiagnosis` stay where
10175
+ they were.
10176
+
10177
+ **If you restart resources**, nothing to change — but three things that did not
10178
+ work now do: a managed schedule can be restarted, a restarted keyspace accepts
10179
+ writes again, and a restarted managed server is actually shut down at exit. If
10180
+ you pass `managedServerResource` a server *instance*, a restart is now refused
10181
+ by name; pass a factory (`server: (context) => …`) to restart it.
10182
+
10183
+ ## Released migration: 0.77.0
10184
+
10185
+ Two type renames, and only if you named them. Nothing runtime moved.
10186
+
10187
+ ```bash
10188
+ rg -n "EventDecision|EventUndecided" --glob '*.ts' --glob '*.tsx'
10189
+ ```
10190
+
10191
+ ```ts
10192
+ // before
10193
+ import type { EventDecision, EventUndecided } from 'stitchkit/live'
10194
+ // after
10195
+ import type { PolicyDecision, UndecidedOutcome } from 'stitchkit/live'
10196
+ ```
10197
+
10198
+ The shapes are identical — `{ outcome: 'allow' } | { outcome: 'deny', reason } | { outcome: 'defer' }`
10199
+ and `'allow' | 'deny'`. If you only ever *return* decisions from listeners and never named the
10200
+ type, the search above finds nothing and there is nothing to do.
10201
+
10202
+ They were renamed because `createDecisionPipeline` (new in this release) votes with exactly the
10203
+ same three outcomes, and two identical types under two names is the thing that makes a search for
10204
+ either one return half the truth.
10205
+
10004
10206
  ## Released migration: 0.76.0
10005
10207
 
10006
10208
  One change, and only if you hand `createWatchClient` a transport you wrote yourself.
@@ -13150,7 +13352,8 @@ realtime contract from `stitchkit`, and the server halves live in `stitchkit/app
13150
13352
  |--------|------|---------|
13151
13353
  | `defineEvents` | function | declare topics: a wire name, one payload schema and how the topic is delivered to in-process listeners |
13152
13354
  | `toRealtimeContract` | function | project a declaration onto `RealtimeContract`, so the existing validated socket carries it |
13153
- | `EventDeliveryMode` / `EventDecision` / `EventUndecided` | _types_ | `emit` / `serial` / `decision`, one listener's vote, and the outcome when every listener defers |
13355
+ | `EventDeliveryMode` | _type_ | `emit` / `serial` / `decision` how a topic reaches its in-process listeners |
13356
+ | `PolicyDecision` / `UndecidedOutcome` | _types_ | one voter's `allow` / `deny` / `defer`, and what a run settles on when every voter defers — the same two types the decision pipeline in `stitchkit/application` uses, because a listener voting on an event and a policy voting on a request are answering the same question |
13154
13357
  | `EventTopicDeclaration` / `EventTopicRegistry` / `EventsConfig` / `EventsDeclaration` | _types_ | one topic's schema, mode, `whenAllDefer` and `listenerTimeoutMs`, and the declaration they compose into |
13155
13358
  | `EventPayloads` / `EventTopicsOfMode` / `WireTopic` | _types_ | payload map keyed by wire topic, the topics of one mode, and the `prefix.name` a topic is addressed by |
13156
13359
  | `createWatchClient` | function | contract-shaped watch client: `watch.action(args)` returns a ref-counted handle sharing one subscription |
@@ -13444,7 +13647,11 @@ cutovers are covered by the executable
13444
13647
 
13445
13648
  | Export | Kind | Summary |
13446
13649
  |--------|------|---------|
13447
- | `createApplication` | function | compose a validated resource DAG into one non-restartable startup, readiness, admission and shutdown state machine |
13650
+ | `createApplication` | function | compose a validated resource DAG into one startup, readiness, admission and shutdown state machine |
13651
+ | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
13652
+ | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
13653
+ | `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |
13654
+ | `ApplicationRestartOutcomeSchema` / `ApplicationRestartOutcome` | schema / _type_ | `restarted`, `failed` or `refused` — a refusal (unknown id, shutting down, not yet ready) is not a failure and touches nothing |
13448
13655
  | `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
13449
13656
  | `ApplicationResourcePhase` | _type_ | the phase a managed resource failed in — the vocabulary of `ApplicationResourceShutdown.failures` |
13450
13657
  | `ApplicationShutdownOptionsSchema` / `ApplicationShutdownOptions` | schema / _type_ | the two shutdown budgets and an abort signal — without the HTTP-only `retryAfterSeconds` |
@@ -13568,6 +13775,10 @@ and `ApplicationEventSinkConfig`.
13568
13775
 
13569
13776
  ### Canonical application records
13570
13777
 
13778
+ These are also published on their own as
13779
+ [`stitchkit/application/schemas`](#stitchkitapplicationschemas), which carries
13780
+ no server runtime and can therefore be imported from a browser bundle.
13781
+
13571
13782
  The entrypoint exports each Zod schema beside its inferred type:
13572
13783
  `ApplicationIdSchema` / `ApplicationId`, `ApplicationLifecycleSchema` /
13573
13784
  `ApplicationLifecycle`, `ApplicationHealthSchema` / `ApplicationHealth`,
@@ -13581,13 +13792,28 @@ the function that derives it from a snapshot,
13581
13792
  `ApplicationResourceShutdownSchema` / `ApplicationResourceShutdown`, and
13582
13793
  `ApplicationShutdownResultSchema` / `ApplicationShutdownResult`.
13583
13794
 
13795
+ ### Decisions
13796
+
13797
+ A pipeline of policies that each vote `allow`, `deny` or `defer`, sharing its
13798
+ vocabulary with an event topic declared `mode: 'decision'`.
13799
+
13800
+ | Export | Kind | Summary |
13801
+ |--------|------|---------|
13802
+ | `createDecisionPipeline` | function | run policies in declaration order; the first terminal verdict wins and the rest do not run |
13803
+ | `DecisionPipelineConfig` | _type_ | `policyTimeoutMs` — how long one policy has to answer. Required, no default: a policy that never settles hangs every caller of the operation it guards |
13804
+ | `PolicyDecisionSchema` / `PolicyDecision` | schema / _type_ | `allow`, `deny` with a reason, or `defer` — a deny cannot be silent |
13805
+ | `DecisionPolicy` / `DecisionPipeline` | _types_ | one named voter over the caller's subject, and the pipeline it composes into |
13806
+ | `DecisionResult` / `DecisionTraceEntry` | _types_ | the verdict, and the trace of what actually ran — not what was declared |
13807
+ | `DecisionUndecidedError` | class | every policy deferred and no undecided outcome was declared: an unanswered question, raised rather than guessed |
13808
+ | `DecisionPolicyError` | class | a policy returned a non-decision, threw, or ran past `policyTimeoutMs` — one error for all three, carrying the policy id and the trace so far |
13809
+
13584
13810
  ### Keyspace and watched reads
13585
13811
 
13586
13812
  | Export | Kind | Summary |
13587
13813
  |--------|------|---------|
13588
13814
  | `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
13589
13815
  | `openKeyspace` / `OpenedKeyspace` | function / type | the same keyspace opened directly, for an application that owns its own lifecycle rather than declaring resources to a kernel |
13590
- | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle |
13816
+ | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle. `backend` takes a value or a **factory** — a factory is called once per generation, so a restarted keyspace gets a backend that was never closed |
13591
13817
  | `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
13592
13818
  | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
13593
13819
  | `createWatchHub` / `watchKey` | function | one read per question however many are asking: single-flight per key, re-read on a declared topic, publish only what changed |
@@ -13624,6 +13850,50 @@ snapshots; the adapter owns no SDK lifecycle, polling or delta state.
13624
13850
  | `ApplicationTelemetryMeter` | _type_ | minimal structural `Meter.createObservableGauge` boundary compatible with `@opentelemetry/api` |
13625
13851
  | `ApplicationOpenTelemetryCollectionError` | _type_ | isolated instrument-name/error diagnostic without product/provider attributes |
13626
13852
 
13853
+ ## `stitchkit/application/diagnostic-journal`
13854
+
13855
+ | Export | Kind | Summary |
13856
+ |--------|------|---------|
13857
+ | `createDiagnosticJournal` | function | the bounded local journal: a file lock, a spawn to diagnose a stale one, and framed writes |
13858
+
13859
+ Its own entrypoint because of what it reaches — `node:child_process`,
13860
+ `node:fs`, `node:os`, `node:util` — with `promisify(execFile)` evaluated while
13861
+ the module initialises. Exported from `stitchkit/application`, that one line made
13862
+ the entire barrel unusable in a browser bundle: not by failing at the call, but
13863
+ by throwing during module initialisation, on every route.
13864
+
13865
+ Its **contract** stays in `stitchkit/application` — every
13866
+ `DiagnosticJournal*Schema`, the states and the refusal reasons touch nothing but
13867
+ Zod, and a client reading a journal's status has as much right to them as the
13868
+ server writing it.
13869
+
13870
+ ## `stitchkit/application/schemas`
13871
+
13872
+ The canonical application records — and nothing else. Every export here is also
13873
+ reachable from `stitchkit/application`; the difference is what comes with it.
13874
+
13875
+ `stitchkit/application` is the server runtime: it reaches `node:child_process`,
13876
+ `node:fs` and `node:crypto`. A browser bundler does not omit those, it
13877
+ substitutes stubs — so a module that merely *names* an application schema fails
13878
+ while it is initialising, and the page never mounts, on every route rather than
13879
+ the one that wanted the schema. A contract whose `output` is an application
13880
+ snapshot could be declared and never consumed, which is the one thing a contract
13881
+ is for.
13882
+
13883
+ So the same schemas ship a second way, from a module with nothing behind them:
13884
+
13885
+ ```ts
13886
+ // browser and server alike
13887
+ import { ApplicationSnapshotSchema } from 'stitchkit/application/schemas';
13888
+ ```
13889
+
13890
+ The exports are exactly those listed under
13891
+ [canonical application records](#canonical-application-records) above.
13892
+ `packages/core/scripts/check-browser-clean.mjs` holds the promise against the
13893
+ built artifact: it walks the browser lane's real `dist` output and refuses a
13894
+ `node:` import reachable from any of it, and refuses an entry built for the
13895
+ browser that no `exports` path leads to — which is how this one was missing.
13896
+
13627
13897
  ---
13628
13898
 
13629
13899
  ## `stitchkit/agent-runtime`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.76.2",
3
+ "version": "0.78.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",
@@ -129,6 +129,14 @@
129
129
  "types": "./dist/application-opentelemetry.d.ts",
130
130
  "import": "./dist/application-opentelemetry.js"
131
131
  },
132
+ "./application/diagnostic-journal": {
133
+ "types": "./dist/application-diagnostic-journal.d.ts",
134
+ "import": "./dist/application-diagnostic-journal.js"
135
+ },
136
+ "./application/schemas": {
137
+ "types": "./dist/application-schemas.d.ts",
138
+ "import": "./dist/application-schemas.js"
139
+ },
132
140
  "./testing": {
133
141
  "types": "./dist/testing.d.ts",
134
142
  "import": "./dist/testing.js"
@@ -156,11 +164,11 @@
156
164
  "scripts": {
157
165
  "build:native-contained-files": "node scripts/build-contained-files-native.mjs",
158
166
  "check": "bun x tsc --noEmit",
159
- "build:browser": "bun build src/index.ts src/live.ts src/react.ts src/contract/index.ts src/primitives.ts src/declaration.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
160
- "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/tool-invoker.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-harness.ts src/agent-runtime-coding-tools.ts src/agent-runtime-openrouter.ts src/agent-runtime-sqlite-bun.ts src/agent-runtime-sqlite-node.ts src/application.ts src/application-grammy.ts src/application-opentelemetry.ts src/telegram.ts --outdir dist --target node --packages external --splitting --root src",
167
+ "build:browser": "bun build src/index.ts src/live.ts src/react.ts src/contract/index.ts src/primitives.ts src/declaration.ts src/application-schemas.ts src/application.ts src/remote.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
168
+ "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/tool-invoker.ts src/cli.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-harness.ts src/agent-runtime-coding-tools.ts src/agent-runtime-openrouter.ts src/agent-runtime-sqlite-bun.ts src/agent-runtime-sqlite-node.ts src/application-diagnostic-journal.ts src/application-grammy.ts src/application-opentelemetry.ts src/telegram.ts --outdir dist --target node --packages external --splitting --root src",
161
169
  "build:js": "bun run build:browser && bun run build:server && bun scripts/preserve-webpack-ignore.mjs",
162
170
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/rewrite-declaration-specifiers.mjs",
163
- "build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs",
171
+ "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",
164
172
  "dev": "bun run build:js -- --watch",
165
173
  "prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
166
174
  "test": "bun test",