experimental-a2 0.6.0 → 0.8.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 (148) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +20 -17
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
  27. package/dist/server-B2XNevQA.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
  29. package/dist/server-DjPhHnbI.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
  34. package/dist/store-RJO35BMj.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +80 -78
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.d.ts.map +1 -1
  43. package/dist/store-postgres.js +231 -182
  44. package/dist/store-postgres.js.map +1 -1
  45. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
  46. package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
  47. package/dist/store-redis-http.d.ts +1 -1
  48. package/dist/store-redis-http.js +3 -4
  49. package/dist/store-redis-http.js.map +1 -1
  50. package/dist/store-redis.d.ts +1 -1
  51. package/dist/store-redis.js +4 -5
  52. package/dist/store-redis.js.map +1 -1
  53. package/dist/store-sqlite.d.ts +1 -1
  54. package/dist/store-sqlite.d.ts.map +1 -1
  55. package/dist/store-sqlite.js +104 -91
  56. package/dist/store-sqlite.js.map +1 -1
  57. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  58. package/dist/wire-B6te_wns.js.map +1 -0
  59. package/docs/concepts/02-handlers.mdx +4 -0
  60. package/docs/concepts/04-state.mdx +57 -9
  61. package/docs/guides/03-react.mdx +20 -28
  62. package/docs/guides/05-production.mdx +9 -11
  63. package/docs/guides/06-ai-agents.mdx +10 -15
  64. package/docs/guides/09-presence.mdx +14 -19
  65. package/docs/guides/10-transports.mdx +104 -86
  66. package/docs/reference/01-api.mdx +182 -293
  67. package/docs/reference/02-errors.mdx +5 -7
  68. package/package.json +1 -14
  69. package/src/ai-server.ts +36 -15
  70. package/src/client.ts +2 -2
  71. package/src/errors.ts +1 -0
  72. package/src/internal.ts +3 -62
  73. package/src/push-envelope.ts +24 -21
  74. package/src/scheduler-qstash.ts +3 -3
  75. package/src/scheduler-vercel.ts +2 -2
  76. package/src/server-fetch.ts +344 -0
  77. package/src/server.ts +315 -312
  78. package/src/session-socket.ts +36 -20
  79. package/src/sse.ts +2 -2
  80. package/src/store-memory.ts +138 -101
  81. package/src/store-postgres.ts +355 -238
  82. package/src/store-redis-core.ts +247 -237
  83. package/src/store-redis-http.ts +1 -2
  84. package/src/store-redis.ts +1 -2
  85. package/src/store-sqlite.ts +191 -153
  86. package/src/store.ts +24 -66
  87. package/src/wire.ts +2 -1
  88. package/dist/ai-D_PGS-JR.d.ts +0 -334
  89. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  90. package/dist/cli-B3VuxoDe.js +0 -599
  91. package/dist/cli-B3VuxoDe.js.map +0 -1
  92. package/dist/cli-bin.d.ts +0 -1
  93. package/dist/cli-bin.js +0 -7
  94. package/dist/cli-bin.js.map +0 -1
  95. package/dist/cli.d.ts +0 -20
  96. package/dist/cli.d.ts.map +0 -1
  97. package/dist/cli.js +0 -2
  98. package/dist/client-CdMqi7mC.d.ts +0 -202
  99. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  100. package/dist/client-Dj5d3SP_.js +0 -1026
  101. package/dist/client-Dj5d3SP_.js.map +0 -1
  102. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  103. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  104. package/dist/devtools-kJJaORn-.js +0 -340
  105. package/dist/devtools-kJJaORn-.js.map +0 -1
  106. package/dist/devtools-server.browser.d.ts +0 -1
  107. package/dist/devtools-server.browser.js +0 -6
  108. package/dist/devtools-server.browser.js.map +0 -1
  109. package/dist/devtools-server.d.ts +0 -23
  110. package/dist/devtools-server.d.ts.map +0 -1
  111. package/dist/devtools-server.js +0 -1270
  112. package/dist/devtools-server.js.map +0 -1
  113. package/dist/devtools.d.ts +0 -2
  114. package/dist/devtools.js +0 -2
  115. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  116. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  117. package/dist/http.d.ts +0 -151
  118. package/dist/http.d.ts.map +0 -1
  119. package/dist/http.js +0 -706
  120. package/dist/http.js.map +0 -1
  121. package/dist/inspection-DaxB5jM2.js +0 -13
  122. package/dist/inspection-DaxB5jM2.js.map +0 -1
  123. package/dist/internal-DstsI6Re.js.map +0 -1
  124. package/dist/platform-B4TnJtWu.js +0 -34
  125. package/dist/platform-B4TnJtWu.js.map +0 -1
  126. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  127. package/dist/server-Duw6MVlB.js.map +0 -1
  128. package/dist/store-DysUkTH3.d.ts.map +0 -1
  129. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  130. package/dist/testing.browser.d.ts +0 -1
  131. package/dist/testing.browser.js +0 -6
  132. package/dist/testing.browser.js.map +0 -1
  133. package/dist/testing.d.ts +0 -32
  134. package/dist/testing.d.ts.map +0 -1
  135. package/dist/testing.js +0 -103
  136. package/dist/testing.js.map +0 -1
  137. package/dist/wire-BFQmSJ-9.js.map +0 -1
  138. package/docs/guides/07-devtools.mdx +0 -229
  139. package/src/cli-bin.ts +0 -5
  140. package/src/cli.ts +0 -1046
  141. package/src/devtools-app.ts +0 -989
  142. package/src/devtools-server.browser.ts +0 -5
  143. package/src/devtools-server.ts +0 -604
  144. package/src/devtools.ts +0 -716
  145. package/src/http.ts +0 -394
  146. package/src/inspection.ts +0 -39
  147. package/src/testing.browser.ts +0 -5
  148. package/src/testing.ts +0 -185
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "experimental-a2",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
- "bin": {
8
- "a2": "./dist/cli-bin.js"
9
- },
10
7
  "sideEffects": false,
11
8
  "engines": {
12
9
  "node": ">=22.13"
@@ -39,7 +36,6 @@
39
36
  "browser": "./dist/ai-server.browser.js",
40
37
  "default": "./dist/ai-server.js"
41
38
  },
42
- "./http": "./dist/http.js",
43
39
  "./store-memory": "./dist/store-memory.js",
44
40
  "./store-sqlite": "./dist/store-sqlite.js",
45
41
  "./store-postgres": "./dist/store-postgres.js",
@@ -49,15 +45,6 @@
49
45
  "./scheduler-vercel": "./dist/scheduler-vercel.js",
50
46
  "./cache-indexeddb": "./dist/cache-indexeddb.js",
51
47
  "./otel": "./dist/otel.js",
52
- "./devtools": "./dist/devtools.js",
53
- "./devtools/server": {
54
- "browser": "./dist/devtools-server.browser.js",
55
- "default": "./dist/devtools-server.js"
56
- },
57
- "./testing": {
58
- "browser": "./dist/testing.browser.js",
59
- "default": "./dist/testing.js"
60
- },
61
48
  "./package.json": "./package.json"
62
49
  },
63
50
  "peerDependencies": {
package/src/ai-server.ts CHANGED
@@ -57,11 +57,14 @@ import {
57
57
  createServer,
58
58
  type A2Server,
59
59
  type HandlerContext,
60
- type PushValidationContext,
61
60
  type ServerOptions,
62
61
  } from './server.ts'
62
+ import {
63
+ setServerFetchHooks,
64
+ type ServerIngressContext,
65
+ } from './server-fetch.ts'
63
66
 
64
- export function validateAgentPush(context: PushValidationContext): void {
67
+ function validateAgentIngress(context: ServerIngressContext): void {
65
68
  const rejected = context.events.find((event) => {
66
69
  if (event.type !== 'ai.message.created') {
67
70
  return !(
@@ -203,7 +206,7 @@ export type CreateAgentServerOptions<
203
206
  D extends AIEventDefs<M> & EventDefs,
204
207
  T extends ToolSet = ToolSet,
205
208
  > = CreateHandlersOptions<M, D, T> &
206
- Omit<ServerOptions<D>, 'contract' | 'handlers' | 'validatePush'> & {
209
+ Omit<ServerOptions<D>, 'contract' | 'handlers'> & {
207
210
  handlers?: ServerOptions<D>['handlers']
208
211
  }
209
212
 
@@ -839,13 +842,16 @@ export function createHandlers<
839
842
  const scheduleNext = async (
840
843
  ctx: Pick<HandlerContext<D>, 'session'>,
841
844
  ): Promise<AppendInput<D> | void> =>
842
- requestForNextMessage((await ctx.session.state(coordinator)).state)
845
+ requestForNextMessage(
846
+ (await ctx.session.state(coordinator, { through: 'latest' })).state,
847
+ )
843
848
 
844
849
  const continueIfReady = async (
845
850
  ctx: Pick<HandlerContext<D>, 'session'>,
846
851
  generationId: string,
847
852
  ): Promise<void> => {
848
- const state = (await ctx.session.state(coordinator)).state
853
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
854
+ .state
849
855
  const response = state.response
850
856
  if (response?.generation?.generationId !== generationId) {
851
857
  return
@@ -1089,6 +1095,7 @@ export function createHandlers<
1089
1095
  call,
1090
1096
  ctx.session.history,
1091
1097
  )
1098
+ if (ctx.signal.aborted) return
1092
1099
  const scope: AmbientToolScope = {
1093
1100
  contract: options.agent.contract,
1094
1101
  context: ctx,
@@ -1103,7 +1110,8 @@ export function createHandlers<
1103
1110
  const handleToolCall = async (
1104
1111
  ctx: HandlerContext<D, 'ai.tool.called'>,
1105
1112
  ): Promise<AppendInput<D> | void> => {
1106
- const state = (await ctx.session.state(coordinator)).state
1113
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1114
+ .state
1107
1115
  const response = state.response
1108
1116
  const current = response?.calls.find(
1109
1117
  (candidate) => candidate.index === ctx.event.index,
@@ -1127,7 +1135,8 @@ export function createHandlers<
1127
1135
  const handleApproval = async (
1128
1136
  ctx: HandlerContext<D, 'ai.approval.responded'>,
1129
1137
  ): Promise<AppendInput<D> | void> => {
1130
- const state = (await ctx.session.state(coordinator)).state
1138
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1139
+ .state
1131
1140
  const response = state.response
1132
1141
  const current = response?.calls.find(
1133
1142
  (candidate) =>
@@ -1160,7 +1169,9 @@ export function createHandlers<
1160
1169
  const history = await ctx.session.history()
1161
1170
  const requestId = ctx.event.id
1162
1171
  const request = ctx.event.payload
1163
- const coordinatorState = (await ctx.session.state(coordinator)).state
1172
+ const coordinatorState = (
1173
+ await ctx.session.state(coordinator, { through: 'latest' })
1174
+ ).state
1164
1175
  if (
1165
1176
  coordinatorState.closed ||
1166
1177
  coordinatorState.response?.activeRequestId !== requestId
@@ -1314,7 +1325,9 @@ export function createHandlers<
1314
1325
  } as AppendInput<D>)
1315
1326
  await ctx.session.append('generation-start', ...startEvents)
1316
1327
 
1317
- const startedCoordinatorState = (await ctx.session.state(coordinator)).state
1328
+ const startedCoordinatorState = (
1329
+ await ctx.session.state(coordinator, { through: 'latest' })
1330
+ ).state
1318
1331
  if (
1319
1332
  startedCoordinatorState.response?.activeRequestId !== requestId ||
1320
1333
  startedCoordinatorState.response.generation?.generationId !== generationId
@@ -1361,8 +1374,12 @@ export function createHandlers<
1361
1374
  }
1362
1375
  }
1363
1376
 
1364
- const currentState = (await ctx.session.state(options.agent.reducer)).state
1365
- const currentCoordinatorState = (await ctx.session.state(coordinator)).state
1377
+ const currentState = (
1378
+ await ctx.session.state(options.agent.reducer, { through: 'latest' })
1379
+ ).state
1380
+ const currentCoordinatorState = (
1381
+ await ctx.session.state(coordinator, { through: 'latest' })
1382
+ ).state
1366
1383
  const generationMessages = activeContextMessages(
1367
1384
  currentState,
1368
1385
  currentCoordinatorState,
@@ -1378,6 +1395,7 @@ export function createHandlers<
1378
1395
  options.instructions === undefined
1379
1396
  ? undefined
1380
1397
  : await resolve(options.instructions, resolverContext)
1398
+ if (ctx.signal.aborted) return
1381
1399
  const generateContext: AgentGenerateContext<M, D, T> = {
1382
1400
  request: ctx.event,
1383
1401
  requestId,
@@ -1538,7 +1556,8 @@ export function createHandlers<
1538
1556
  const handleRetry = async (
1539
1557
  ctx: HandlerContext<D, 'ai.retry.requested'>,
1540
1558
  ): Promise<AppendInput<D> | void> => {
1541
- const state = (await ctx.session.state(coordinator)).state
1559
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1560
+ .state
1542
1561
  const response = state.response
1543
1562
  if (
1544
1563
  response?.status !== 'failed' ||
@@ -1561,7 +1580,8 @@ export function createHandlers<
1561
1580
  const handleInputResponse = async (
1562
1581
  ctx: HandlerContext<D, 'ai.input.responded'>,
1563
1582
  ): Promise<void> => {
1564
- const state = (await ctx.session.state(coordinator)).state
1583
+ const state = (await ctx.session.state(coordinator, { through: 'latest' }))
1584
+ .state
1565
1585
  const response = state.response
1566
1586
  if (
1567
1587
  response?.responseMessageId !== ctx.event.payload.messageId ||
@@ -1708,12 +1728,13 @@ export function createAgentServer<
1708
1728
  ...(compaction === undefined ? {} : { compaction }),
1709
1729
  ...(progress === undefined ? {} : { progress }),
1710
1730
  })
1711
- return createServer({
1731
+ const server = createServer({
1712
1732
  ...serverOptions,
1713
1733
  contract: definition.contract,
1714
1734
  handlers: { ...builtIns, ...handlers },
1715
- validatePush: validateAgentPush,
1716
1735
  })
1736
+ setServerFetchHooks(server, { validateIngress: validateAgentIngress })
1737
+ return server
1717
1738
  }
1718
1739
 
1719
1740
  export type { Instructions, LanguageModel, ToolSet }
package/src/client.ts CHANGED
@@ -808,7 +808,7 @@ const wsTransport = (
808
808
  settleChannel(
809
809
  channel,
810
810
  new Error(
811
- 'subscribe not confirmed: is the ws route serving the multiplexed socket (handle with options.upgrade)?',
811
+ 'subscribe not confirmed: is the ws route calling server.fetch with upgradeWebSocket?',
812
812
  ),
813
813
  )
814
814
  }, STREAM_TIMINGS.stallTimeoutMs)
@@ -1562,7 +1562,7 @@ export function createClient<
1562
1562
  const readHistory = transport.history
1563
1563
  if (readHistory === undefined) {
1564
1564
  throw new TypeError(
1565
- 'loadHistory requires an http api — the ws transport has no history lane; serve the session over handle() and pass its route url as the string or { type: "http" } api',
1565
+ 'loadHistory requires an http api — the ws transport has no history lane; serve the session with server.fetch and pass its route url as the string or { type: "http" } api',
1566
1566
  )
1567
1567
  }
1568
1568
  const before = loadOptions?.before
package/src/errors.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  export type A2ErrorCode =
10
10
  | 'INVALID_PAYLOAD' // schema validation failed — thrown before anything is written
11
+ | 'FORBIDDEN' // external ingress was denied by server.fetch authorization
11
12
  | 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map
12
13
  | 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events
13
14
  | 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim replaced
package/src/internal.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ScheduledEvent } from './scheduler-task.ts'
7
- import type { A2Scheduler, DrainableServer } from './server.ts'
7
+ import type { DrainableServer } from './server.ts'
8
8
 
9
9
  export type DrainOutcome = 'busy' | 'stalled' | 'settled'
10
10
 
@@ -67,14 +67,6 @@ export type ServerInternals = {
67
67
 
68
68
  export const serverInternals: WeakMap<object, ServerInternals> = new WeakMap()
69
69
 
70
- type ServerSchedulerBinding = {
71
- scheduler: A2Scheduler | undefined
72
- }
73
-
74
- /** Scheduler configuration captured only for servers made by `createServer`. */
75
- export const serverSchedulerBindings: WeakMap<object, ServerSchedulerBinding> =
76
- new WeakMap()
77
-
78
70
  const objectLike = (value: unknown): value is object =>
79
71
  (typeof value === 'object' && value !== null) || typeof value === 'function'
80
72
 
@@ -232,13 +224,11 @@ const isDrainableServer = (value: unknown): value is DrainableServer => {
232
224
 
233
225
  /**
234
226
  * Validate the server list captured by a scheduler route at construction time.
235
- * Built-in adapters keep their low-level target seam structural for custom
236
- * integrations. The application-facing `schedulerHandler` uses strict mode
237
- * so only servers made by this A2 copy reach a configured adapter.
227
+ * The seam stays structural so custom scheduler integrations can supply their
228
+ * own drainable targets.
238
229
  */
239
230
  export function assertSchedulerTargets(
240
231
  servers: readonly unknown[],
241
- options: { allowStructural: boolean },
242
232
  ): asserts servers is readonly DrainableServer[] {
243
233
  const contracts = new Set<string>()
244
234
  for (const target of servers) {
@@ -252,56 +242,7 @@ export function assertSchedulerTargets(
252
242
  )
253
243
  }
254
244
  contracts.add(name)
255
-
256
- const binding = serverSchedulerBindings.get(target)
257
- if (!binding) {
258
- if (!options.allowStructural) {
259
- throw new TypeError(
260
- `a2 scheduler: server for contract '${name}' was not created by createServer()`,
261
- )
262
- }
263
- continue
264
- }
265
- }
266
- }
267
-
268
- /** Require every real server to share one exact configured scheduler. */
269
- export function assertServerSchedulerBindings(
270
- scheduler: A2Scheduler,
271
- servers: readonly DrainableServer[],
272
- ): void {
273
- for (const server of servers) {
274
- const binding = serverSchedulerBindings.get(server)
275
- if (!binding?.scheduler) {
276
- throw new TypeError(
277
- `a2 scheduler: server for contract '${server.contract.name}' has no scheduler configured`,
278
- )
279
- }
280
- if (binding.scheduler !== scheduler) {
281
- throw new TypeError(
282
- `a2 scheduler: server for contract '${server.contract.name}' is configured with a different scheduler instance`,
283
- )
284
- }
285
- }
286
- }
287
-
288
- /** Resolve the scheduler privately bound to a real A2 server. */
289
- export function schedulerForServer(server: unknown): A2Scheduler {
290
- if (!isDrainableServer(server)) {
291
- throw new TypeError('a2 scheduler: expected a server from createServer()')
292
- }
293
- const binding = serverSchedulerBindings.get(server)
294
- if (!binding) {
295
- throw new TypeError(
296
- `a2 scheduler: server for contract '${server.contract.name}' was not created by createServer()`,
297
- )
298
- }
299
- if (!binding.scheduler) {
300
- throw new TypeError(
301
- `a2 scheduler: server for contract '${server.contract.name}' has no scheduler configured`,
302
- )
303
245
  }
304
- return binding.scheduler
305
246
  }
306
247
 
307
248
  /**
@@ -1,14 +1,25 @@
1
1
  /**
2
- * The push envelope's trust boundary — parsing and provenance-branding
3
- * for `{ sessionId, events, presence? }`, shared verbatim by the HTTP
2
+ * The push envelope's trust boundary for `{ sessionId, events, presence? }`,
3
+ * shared verbatim by the HTTP
4
4
  * POST lane and the socket's push/presence frames so INVALID_PAYLOAD
5
- * shapes are one implementation. Internal module — `handle` (and the
6
- * socket handler) compose it; it is not part of the public surface.
5
+ * shapes are one implementation. Internal module, not public API.
7
6
  */
8
7
 
9
8
  import { A2Error } from './errors.ts'
10
9
  import { MAX_DATE_MS, RESERVED_PARTICIPANT_IDS } from './internal.ts'
11
- import type { PushedEvent, PushedPresence } from './server.ts'
10
+
11
+ export type ParsedPushEvent = {
12
+ type: string
13
+ payload: unknown
14
+ id?: string
15
+ }
16
+
17
+ export type ParsedPushPresence = {
18
+ participant: string
19
+ values: Record<string, unknown>
20
+ seen?: number
21
+ at?: number
22
+ }
12
23
 
13
24
  export const invalidPushBody = (message: string): A2Error =>
14
25
  new A2Error('INVALID_PAYLOAD', `malformed push body: ${message}`)
@@ -17,18 +28,13 @@ const invalid = invalidPushBody
17
28
 
18
29
  export type PushBody = {
19
30
  sessionId: string
20
- /** Branded: `session.append` accepts these directly (the push route
21
- * path); schema validation still happens inside `append`. Empty only
22
- * for a presence-only push. */
23
- events: PushedEvent[]
24
- /** Branded like `events`: `session.setPresence` accepts it whole;
25
- * field validation still happens inside `setPresence`. */
26
- presence?: PushedPresence
31
+ events: ParsedPushEvent[]
32
+ presence?: ParsedPushPresence
27
33
  }
28
34
 
29
35
  export function parsePresenceSibling(
30
36
  value: unknown,
31
- ): PushedPresence | undefined {
37
+ ): ParsedPushPresence | undefined {
32
38
  if (value === undefined) return undefined
33
39
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
34
40
  throw invalid('presence must be an object when present')
@@ -68,14 +74,12 @@ export function parsePresenceSibling(
68
74
  } = { participant, values: values as Record<string, unknown> }
69
75
  if (seen !== undefined) out.seen = seen
70
76
  if (at !== undefined) out.at = at
71
- Object.defineProperty(out, '~a2.pushed', { value: true })
72
- return out as PushedPresence
77
+ return out
73
78
  }
74
79
 
75
- /** The events half of the envelope — shared verbatim by the POST body
76
- * and the socket's `push` frames, so the provenance brand and the
77
- * INVALID_PAYLOAD shapes are one implementation. */
78
- export function parsePushEvents(events: unknown): PushedEvent[] {
80
+ /** The events half of the envelope, shared by the POST body and socket
81
+ * `push` frames so INVALID_PAYLOAD shapes have one implementation. */
82
+ export function parsePushEvents(events: unknown): ParsedPushEvent[] {
79
83
  if (!Array.isArray(events) || events.length === 0) {
80
84
  throw invalid('events must be a non-empty array')
81
85
  }
@@ -95,8 +99,7 @@ export function parsePushEvents(events: unknown): PushedEvent[] {
95
99
  payload,
96
100
  }
97
101
  if (id !== undefined) out.id = id
98
- Object.defineProperty(out, '~a2.pushed', { value: true })
99
- return out as PushedEvent
102
+ return out
100
103
  })
101
104
  }
102
105
 
@@ -94,7 +94,7 @@ export type QStashTransport = {
94
94
  }
95
95
 
96
96
  export type QStashSchedulerOptions = {
97
- /** Exact URL mounted with `schedulerHandler(...)`. Inferred in local dev and on Vercel. */
97
+ /** Exact URL mounted with `scheduler.handler(...)`. Inferred in local dev and on Vercel. */
98
98
  url?: string
99
99
  /** QStash API token. Default: `QSTASH_TOKEN`. */
100
100
  token?: string
@@ -819,7 +819,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
819
819
  },
820
820
 
821
821
  handler(...servers: DrainableServer[]) {
822
- assertSchedulerTargets(servers, { allowStructural: true })
822
+ assertSchedulerTargets(servers)
823
823
  const byContract = new Map<string, DrainableServer>()
824
824
  for (const server of servers) {
825
825
  byContract.set(server.contract.name, server)
@@ -851,7 +851,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
851
851
  const server = byContract.get(task.contract)
852
852
  if (!server) {
853
853
  throw new Error(
854
- `a2 scheduler: no server for contract '${task.contract}' — pass it to schedulerHandler(...)`,
854
+ `a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,
855
855
  )
856
856
  }
857
857
  const internals = serverInternals.get(server)
@@ -368,7 +368,7 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
368
368
  },
369
369
 
370
370
  handler(...servers: DrainableServer[]) {
371
- assertSchedulerTargets(servers, { allowStructural: true })
371
+ assertSchedulerTargets(servers)
372
372
  const byContract = new Map<string, DrainableServer>()
373
373
  for (const server of servers) {
374
374
  byContract.set(server.contract.name, server)
@@ -386,7 +386,7 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
386
386
  // wiring bug — throw so it redelivers and stays visible in
387
387
  // queue observability instead of vanishing on an ack.
388
388
  throw new Error(
389
- `a2 scheduler: no server for contract '${task.contract}' — pass it to schedulerHandler(...)`,
389
+ `a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,
390
390
  )
391
391
  }
392
392
  const internals = serverInternals.get(server)