experimental-a2 0.19.0 → 0.20.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.
- package/CHANGELOG.md +16 -0
- package/dist/actor.js +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +34 -4
- package/dist/ai-fx.js.map +1 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +1 -1
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-Clbg5ei7.js} +11 -2
- package/dist/ai-harness-Clbg5ei7.js.map +1 -0
- package/dist/ai-pi.js +2 -2
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +6 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/internal-CscSY7K_.js.map +1 -1
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +5 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +4 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/server-DYAATFuT.d.ts.map +1 -1
- package/dist/{server-D-Y4pqwu.js → server-qyb5aJJG.js} +39 -25
- package/dist/server-qyb5aJJG.js.map +1 -0
- package/dist/server.js +1 -1
- package/dist/session-clear-DGI4jTQ5.js.map +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +38 -136
- package/dist/store-postgres.js.map +1 -1
- package/docs/01-quickstart.mdx +7 -4
- package/docs/actors/01-introduction.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/02-tools.mdx +1 -1
- package/docs/agents/03-conversations.mdx +1 -1
- package/docs/agents/meta.ts +1 -1
- package/docs/client/meta.ts +1 -1
- package/docs/contract/03-sessions.mdx +1 -1
- package/docs/index.mdx +2 -2
- package/docs/reference/01-core.mdx +8 -8
- package/docs/reference/02-server.mdx +3 -3
- package/docs/reference/04-ai.mdx +9 -1
- package/docs/reference/05-telemetry.mdx +132 -0
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-handlers.mdx +29 -2
- package/docs/server/02-timers.mdx +2 -2
- package/docs/server/06-guarantees.mdx +6 -3
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +49 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +121 -0
- package/docs/stores/05-sqlite.mdx +34 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/examples/playground/DEPLOY.md +12 -7
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/package.json +2 -1
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/package.json +3 -1
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +137 -0
- package/src/ai-agent.ts +5 -4
- package/src/ai-fx.ts +37 -3
- package/src/ai-harness.ts +18 -0
- package/src/errors.ts +1 -1
- package/src/internal.ts +2 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +80 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.ts +66 -32
- package/src/store-postgres.ts +5 -123
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/05-production.mdx +0 -323
package/docs/01-quickstart.mdx
CHANGED
|
@@ -60,7 +60,7 @@ export const ordersServer = createServer({
|
|
|
60
60
|
|
|
61
61
|
Development needs no database setup: events go to SQLite at `.a2/dev.db` and
|
|
62
62
|
survive restarts. Tests get an in-memory store. Production needs an explicit
|
|
63
|
-
store; see [
|
|
63
|
+
store; see [Stores](/stores/overview).
|
|
64
64
|
|
|
65
65
|
## 4. Append the first event
|
|
66
66
|
|
|
@@ -169,11 +169,14 @@ You have a validated write path, a handler, a log you can read, and a view
|
|
|
169
169
|
folded from it. The rest of these docs apply the same two moves, append an
|
|
170
170
|
event and react to it, to timers, cancellation, live UIs, and production.
|
|
171
171
|
|
|
172
|
-
<CardGroup cols={
|
|
172
|
+
<CardGroup cols={3}>
|
|
173
173
|
<Card title="Handlers" href="/server/handlers" icon="file">
|
|
174
174
|
Context, chaining, and re-runs.
|
|
175
175
|
</Card>
|
|
176
|
-
<Card title="
|
|
177
|
-
|
|
176
|
+
<Card title="Stores" href="/stores/overview" icon="rocket">
|
|
177
|
+
Where sessions live in production.
|
|
178
|
+
</Card>
|
|
179
|
+
<Card title="Schedulers" href="/schedulers/overview" icon="clock">
|
|
180
|
+
Retries and timers on a queue.
|
|
178
181
|
</Card>
|
|
179
182
|
</CardGroup>
|
|
@@ -57,7 +57,7 @@ handler's first lines and throw `NonRetriableError` on bad shapes.
|
|
|
57
57
|
|
|
58
58
|
`actor` also accepts `createServer`'s `store`, `scheduler`, and
|
|
59
59
|
`telemetry` options, with the same development default store; see
|
|
60
|
-
[
|
|
60
|
+
[Stores](/stores/overview) and [Schedulers](/schedulers/overview). A protocol can also declare
|
|
61
61
|
[`presence`](/actors/routes#presence).
|
|
62
62
|
|
|
63
63
|
## Call it
|
package/docs/actors/meta.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default { order:
|
|
1
|
+
export default { order: 8 }
|
package/docs/agents/02-tools.mdx
CHANGED
|
@@ -81,7 +81,7 @@ for the event payload.
|
|
|
81
81
|
|
|
82
82
|
This tool schedules a reminder as a new user message. `ctx.session` belongs to
|
|
83
83
|
the executing agent, so `schedule()` uses that agent's scheduler: add
|
|
84
|
-
`setReminder` to its `tools` and configure a [scheduler](/
|
|
84
|
+
`setReminder` to its `tools` and configure a [scheduler](/schedulers/overview).
|
|
85
85
|
Naming the timer after `toolCallId` and the message after the triggering
|
|
86
86
|
event's ID makes a retry converge on the same timer and message, even if a
|
|
87
87
|
provider reuses tool-call IDs. The six-day maximum is Vercel Queues'
|
|
@@ -393,7 +393,7 @@ retries, and so does one where A2 itself fails while or just after the tool
|
|
|
393
393
|
runs. Every other tool uses the default, `'rerun'`. See
|
|
394
394
|
[`agent()`](/reference/ai#agentoptions) for the exact cases.
|
|
395
395
|
|
|
396
|
-
Configure a [production scheduler](/
|
|
396
|
+
Configure a [production scheduler](/schedulers/overview) as for any A2 server. It
|
|
397
397
|
recovers an incomplete generation after the original serverless invocation
|
|
398
398
|
disappears.
|
|
399
399
|
|
package/docs/agents/meta.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default { order:
|
|
1
|
+
export default { order: 7 }
|
package/docs/client/meta.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default { order:
|
|
1
|
+
export default { order: 6 }
|
|
@@ -70,7 +70,7 @@ authorize your own route before calling it.
|
|
|
70
70
|
|
|
71
71
|
### Scheduling a clear
|
|
72
72
|
|
|
73
|
-
With a [scheduler](/
|
|
73
|
+
With a [scheduler](/schedulers/overview) configured, a handler can schedule a
|
|
74
74
|
clear. This one clears an idle cart a day after its last activity:
|
|
75
75
|
|
|
76
76
|
```ts server/carts.ts
|
package/docs/index.mdx
CHANGED
|
@@ -156,7 +156,7 @@ from the log.
|
|
|
156
156
|
<Card title="Timers" href="/server/timers" icon="clock">
|
|
157
157
|
Schedule an event for later.
|
|
158
158
|
</Card>
|
|
159
|
-
<Card title="Production" href="/
|
|
159
|
+
<Card title="Production" href="/stores/overview" icon="rocket">
|
|
160
160
|
A store and a scheduler.
|
|
161
161
|
</Card>
|
|
162
162
|
</CardGroup>
|
|
@@ -212,7 +212,7 @@ A durable store backend, such as Postgres or Redis. Add a queue-backed scheduler
|
|
|
212
212
|
when the contract has handlers or calls `session.schedule`. A history-only
|
|
213
213
|
contract with no timers needs no reaction infrastructure. Development needs
|
|
214
214
|
neither: SQLite appears under `.a2/` and state survives restarts. See
|
|
215
|
-
[
|
|
215
|
+
[Stores](/stores/overview) and [Schedulers](/schedulers/overview).
|
|
216
216
|
|
|
217
217
|
</details>
|
|
218
218
|
|
|
@@ -17,15 +17,15 @@ description: "Every entry point, and the isomorphic `experimental-a2` core: cont
|
|
|
17
17
|
| `experimental-a2/ai/pi` | `pi`: the Pi harness for `agent()` | `ai`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai` |
|
|
18
18
|
| `experimental-a2/ai/client` | `createAgentClient`: contract-free agent client | `ai` |
|
|
19
19
|
| `experimental-a2/ai/react` | `useAgent` | `ai`, `react` |
|
|
20
|
-
| [`experimental-a2/store-postgres`](/
|
|
21
|
-
| [`experimental-a2/store-redis`](/
|
|
22
|
-
| [`experimental-a2/store-redis-http`](/
|
|
23
|
-
| `experimental-a2/store-sqlite` | SQLite store backend | none |
|
|
24
|
-
| `experimental-a2/store-memory` | in-memory store backend | none |
|
|
25
|
-
| `experimental-a2/scheduler-vercel` | `vercelQueues`: recovery and timers | `@vercel/queue` |
|
|
26
|
-
| `experimental-a2/scheduler-qstash` | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
20
|
+
| [`experimental-a2/store-postgres`](/stores/postgres) | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
21
|
+
| [`experimental-a2/store-redis`](/stores/redis) | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
22
|
+
| [`experimental-a2/store-redis-http`](/stores/redis-http) | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
23
|
+
| [`experimental-a2/store-sqlite`](/stores/sqlite) | SQLite store backend | none |
|
|
24
|
+
| [`experimental-a2/store-memory`](/stores/memory) | in-memory store backend | none |
|
|
25
|
+
| [`experimental-a2/scheduler-vercel`](/schedulers/vercel-queues) | `vercelQueues`: recovery and timers | `@vercel/queue` |
|
|
26
|
+
| [`experimental-a2/scheduler-qstash`](/schedulers/qstash) | `qstash`: signed recovery and timers | `@upstash/qstash` |
|
|
27
27
|
| `experimental-a2/cache-indexeddb` | `indexedDb` browser cache | none |
|
|
28
|
-
| `experimental-a2/otel` | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
28
|
+
| [`experimental-a2/otel`](/reference/telemetry) | `otel` telemetry adapter | `@opentelemetry/api` |
|
|
29
29
|
|
|
30
30
|
`experimental-a2` imports none of the backends, and a browser-bundle test in CI
|
|
31
31
|
enforces it. Importing a backend's entry point, such as
|
|
@@ -438,7 +438,7 @@ Custom scheduler adapters implement two methods:
|
|
|
438
438
|
|
|
439
439
|
Application routes call `scheduler.handler(...servers)` on the configured
|
|
440
440
|
adapter. See
|
|
441
|
-
[
|
|
441
|
+
[Schedulers](/schedulers/overview).
|
|
442
442
|
|
|
443
443
|
`SchedulerTask` is `SchedulerDrainTask | SchedulerAppendTask |
|
|
444
444
|
SchedulerClearTask`. These task types, plus `ScheduledEvent`, are exported
|
|
@@ -522,7 +522,7 @@ session.append.dispatch(
|
|
|
522
522
|
|
|
523
523
|
Commits the same atomic batch, then sends pending work to the configured
|
|
524
524
|
scheduler instead of starting an inline drain. See
|
|
525
|
-
[Dispatch in a fresh invocation](/
|
|
525
|
+
[Dispatch in a fresh invocation](/schedulers/overview#dispatch-in-a-fresh-invocation).
|
|
526
526
|
|
|
527
527
|
- Awaits acceptance of an immediate scheduler message. A batch that leaves no
|
|
528
528
|
pending handler work sends none.
|
|
@@ -681,7 +681,7 @@ type DeadLetter = Event & {
|
|
|
681
681
|
```
|
|
682
682
|
|
|
683
683
|
The session's dead-lettered events, oldest first, each with why it failed. See
|
|
684
|
-
[
|
|
684
|
+
[Resolve a dead letter](/server/handlers#resolve-a-dead-letter).
|
|
685
685
|
|
|
686
686
|
- `failureCount` counts the attempts that spent its budget: caught failures,
|
|
687
687
|
and attempts that ended before they finished. `attemptCount` counts every
|
package/docs/reference/04-ai.mdx
CHANGED
|
@@ -362,7 +362,7 @@ stub.
|
|
|
362
362
|
| `toolLane` | one lane for every local tool execution, one at a time in admission order; model steps keep their own lane |
|
|
363
363
|
| `toolRecovery` | `'rerun'` (default) or `'report'` per tool, the harness's own tools included |
|
|
364
364
|
| `handlers` | application event handlers; naming an event the agent handles throws a `TypeError` |
|
|
365
|
-
| `store`, `scheduler`, `telemetry` | as for any A2 server; `telemetry` covers Core and AI in one trace ([span catalogue](/reference/
|
|
365
|
+
| `store`, `scheduler`, `telemetry` | as for any A2 server; `telemetry` covers Core and AI in one trace ([span catalogue](/reference/telemetry#the-span-catalogue)) |
|
|
366
366
|
|
|
367
367
|
**Contracts.** A contract not built by `agentContract()` throws a `TypeError`
|
|
368
368
|
and fails to typecheck (serve custom contracts with `createServer()`). Messages
|
|
@@ -601,6 +601,14 @@ call. Replay grows with the response, roughly 0.1 to 0.5 seconds for one of 80
|
|
|
601
601
|
to 200 tool calls. When libfx can't take a checkpoint, A2 replays from an
|
|
602
602
|
earlier one.
|
|
603
603
|
|
|
604
|
+
FX's model calls carry the same [AI Gateway](#agentoptions) defaults as the
|
|
605
|
+
AI SDK harness: `caching: 'auto'`, under any Gateway options FX sets itself,
|
|
606
|
+
and the session's stable `x-session-affinity` header in place of FX's own,
|
|
607
|
+
which changes every time A2 rebuilds the kernel. A2 leaves every other byte of
|
|
608
|
+
the request as FX wrote it, so it adds caching only where FX's own options come
|
|
609
|
+
last in the request, as libfx writes them; otherwise the request goes
|
|
610
|
+
unchanged.
|
|
611
|
+
|
|
604
612
|
Credentials come from `AI_GATEWAY_API_KEY`, or Vercel's OIDC token on Vercel.
|
|
605
613
|
This entry point is server-only.
|
|
606
614
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Telemetry
|
|
3
|
+
description: "Trace A2 with OpenTelemetry, and the spans it reports."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { otel } from 'experimental-a2/otel'
|
|
9
|
+
import { orders } from '@/contracts'
|
|
10
|
+
|
|
11
|
+
export const ordersServer = createServer({
|
|
12
|
+
contract: orders,
|
|
13
|
+
telemetry: otel(),
|
|
14
|
+
// ...store, scheduler, and handlers
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
With any OpenTelemetry setup registered (on Vercel, `@vercel/otel`), one trace
|
|
19
|
+
shows the request, `a2.append`, the inline `a2.drain`, an `a2.event` span per
|
|
20
|
+
handler, and the appends those handlers make. Failures mark their `a2.event`
|
|
21
|
+
span, and `a2.event.outcome = dead_lettered` is the attribute to alert on.
|
|
22
|
+
Agent servers add `a2.ai.*` spans.
|
|
23
|
+
|
|
24
|
+
## `experimental-a2/otel`
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
otel(options?: { tracer?: Tracer; tracerName?: string }): A2Telemetry
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
OpenTelemetry instrumentation for a server (`@opentelemetry/api` is a peer
|
|
31
|
+
dependency). Pass it as `telemetry`. Spans nest through the active context, so
|
|
32
|
+
the happy path lands in one trace. `A2Telemetry` is a one-method interface:
|
|
33
|
+
anything with `span(name, attributes, fn)` works, and `otel()` is the adapter
|
|
34
|
+
A2 ships.
|
|
35
|
+
|
|
36
|
+
## The span catalogue
|
|
37
|
+
|
|
38
|
+
This catalogue is a contract: renames and additions are breaking for
|
|
39
|
+
dashboards and are called out in the package's `CHANGELOG.md`. All spans carry
|
|
40
|
+
`a2.contract` and `a2.session_id`. Core spans describe the durable runtime;
|
|
41
|
+
`a2.ai.*` spans describe the built-in AI handlers.
|
|
42
|
+
|
|
43
|
+
| Layer | Span | Wraps |
|
|
44
|
+
| ----- | ---- | ----- |
|
|
45
|
+
| Core | `a2.append` | validation + the store write (one batch) |
|
|
46
|
+
| Core | `a2.drain` | one drain pass, including concurrent handlers |
|
|
47
|
+
| Core | `a2.event` | one claimed dispatch, including its handler and completion |
|
|
48
|
+
| Core | `a2.claim` | one `claimAvailable` store operation |
|
|
49
|
+
| Core | `a2.commit` | one `completeAttempt` or `failAttempt` store operation |
|
|
50
|
+
| Core | `a2.state` | one `state()` read: snapshot-plus-tail load + reducer fold |
|
|
51
|
+
| Core | `a2.snapshot` | one background batch of checkpoint writes |
|
|
52
|
+
| Core | `a2.redrive` | one `session.redrive()`: the store operation and starting the session's wakeup |
|
|
53
|
+
| AI | `a2.ai.control` | applying an input/work report/session-close decision and preparing its returned events |
|
|
54
|
+
| AI | `a2.ai.generate` | one generation handler, including Core calls and model/SDK work |
|
|
55
|
+
| AI | `a2.ai.context` | constructing model messages, including tool prompt reconstruction when uncached |
|
|
56
|
+
| AI | `a2.ai.compaction` | automatic summarization or the custom compaction callback |
|
|
57
|
+
| AI | `a2.ai.tool` | one durable tool handler, including application execution |
|
|
58
|
+
| AI | `a2.ai.ack` | waiting for a persisted work report's canonical events and control receipt |
|
|
59
|
+
|
|
60
|
+
Durations include nested work: `a2.event` contains its AI handler and later
|
|
61
|
+
commit, AI handlers call Core, and `a2.state` includes reducer code.
|
|
62
|
+
Acknowledgements overlap control processing, claims can cover several events,
|
|
63
|
+
and snapshots run in the background, so summing spans, or subtracting summed
|
|
64
|
+
children, does not yield exclusive Core or AI overhead. Model and application
|
|
65
|
+
I/O is included; its child spans have the detail.
|
|
66
|
+
|
|
67
|
+
Attributes arrive in two waves: **at start** (passed to `span()`) and
|
|
68
|
+
**mid-span** (via `handle.setAttribute`, for outcomes and counts known only
|
|
69
|
+
after the work ran). A recorder must merge both; the ones you alert on are all
|
|
70
|
+
mid-span.
|
|
71
|
+
|
|
72
|
+
| Attribute | Span | When | Values |
|
|
73
|
+
| --------------------- | ----------- | ----- | ----------------------------------------------------- |
|
|
74
|
+
| `a2.append.source` | `a2.append` | start | `external` \| `handler` |
|
|
75
|
+
| `a2.append.mode` | `a2.append` | start | `inline` \| `dispatch` |
|
|
76
|
+
| `a2.append.types` | `a2.append` | start | comma-joined event types |
|
|
77
|
+
| `a2.append.count` | `a2.append` | start | batch size |
|
|
78
|
+
| `a2.append.armed` | `a2.append` | mid | `false` when the scheduler arm failed and this append degraded to append-driven healing |
|
|
79
|
+
| `a2.drain.outcome` | `a2.drain` | mid | `settled` \| `busy` \| `stalled` \| `failed` |
|
|
80
|
+
| `a2.drain.processed` | `a2.drain` | mid | events processed this pass |
|
|
81
|
+
| `a2.claim.outcome` | `a2.claim` | mid | `claimed` \| `busy` \| `settled` |
|
|
82
|
+
| `a2.claim.count` | `a2.claim` | mid | number of claimed events, or zero |
|
|
83
|
+
| `a2.commit.operation` | `a2.commit` | start | `complete` \| `fail` |
|
|
84
|
+
| `a2.commit.index` | `a2.commit` | start | event log position |
|
|
85
|
+
| `a2.commit.attempt` | `a2.commit` | start | durable 1-based attempt ordinal |
|
|
86
|
+
| `a2.commit.outcome` | `a2.commit` | mid | `completed` \| `failed` \| `dead_lettered` \| `superseded` |
|
|
87
|
+
| `a2.event.type` | `a2.event` | start | the event's type |
|
|
88
|
+
| `a2.event.index` | `a2.event` | start | log position |
|
|
89
|
+
| `a2.event.id` | `a2.event` | start | event id |
|
|
90
|
+
| `a2.event.attempt` | `a2.event` | start | same durable 1-based ordinal as `ctx.attempt` |
|
|
91
|
+
| `a2.event.lane` | `a2.event` | start | stored lane value; absent for concurrent unlaned work |
|
|
92
|
+
| `a2.event.handled` | `a2.event` | start | normally `true`; `false` when a custom-adapter row has no handler |
|
|
93
|
+
| `a2.event.outcome` | `a2.event` | mid | `processed` \| `failed` \| `dead_lettered` \| `superseded` \| `surrendered` |
|
|
94
|
+
| `a2.event.aborted` | `a2.event` | mid | `true` when `abortOn` fired during the run |
|
|
95
|
+
| `a2.state.reducer` | `a2.state` | start | the reducer's name |
|
|
96
|
+
| `a2.state.snapshot` | `a2.state` | mid | `hit` \| `miss` \| `rejected` (schema guard discarded it) \| `memory` (served from the server's in-memory state) |
|
|
97
|
+
| `a2.state.folded` | `a2.state` | mid | events folded past the snapshot |
|
|
98
|
+
| `a2.state.index` | `a2.state` | mid | the frontier the returned state reflects |
|
|
99
|
+
| `a2.snapshot.reducer` | `a2.snapshot` | start | the reducer's name |
|
|
100
|
+
| `a2.snapshot.index` | `a2.snapshot` | start | greatest checkpoint index in the write batch |
|
|
101
|
+
| `a2.snapshot.count` | `a2.snapshot` | start | number of checkpoints in the write batch |
|
|
102
|
+
| `a2.redrive.index` | `a2.redrive` | start | log position of the event being redriven |
|
|
103
|
+
| `a2.redrive.action` | `a2.redrive` | start | `retry` \| `skip` |
|
|
104
|
+
| `a2.redrive.redriven` | `a2.redrive` | mid | `true` when the event was dead-lettered and is now resolved |
|
|
105
|
+
| `a2.redrive.armed` | `a2.redrive` | mid | `false` when the scheduler arm failed and the wakeup degraded to append-driven healing |
|
|
106
|
+
| `a2.ai.control.type` | `a2.ai.control` | start | `ai.control.requested` \| `ai.work.reported` \| `ai.session.closed` |
|
|
107
|
+
| `a2.ai.control.id` | `a2.ai.control` | start | triggering event id |
|
|
108
|
+
| `a2.ai.control.attempt` | `a2.ai.control` | start | durable 1-based attempt ordinal |
|
|
109
|
+
| `a2.ai.generate.request_id` | `a2.ai.generate` | start | generation request event id |
|
|
110
|
+
| `a2.ai.generate.attempt` | `a2.ai.generate` | start | durable 1-based attempt ordinal |
|
|
111
|
+
| `a2.ai.context.request_id` | `a2.ai.context` | start | generation request event id |
|
|
112
|
+
| `a2.ai.context.phase` | `a2.ai.context` | start | `generation` \| `refresh` \| `tool` |
|
|
113
|
+
| `a2.ai.compaction.request_id` | `a2.ai.compaction` | start | generation request event id |
|
|
114
|
+
| `a2.ai.compaction.generation_id` | `a2.ai.compaction` | start | generation attempt id |
|
|
115
|
+
| `a2.ai.compaction.policy` | `a2.ai.compaction` | start | `automatic` \| `custom` |
|
|
116
|
+
| `a2.ai.tool.request_id` | `a2.ai.tool` | start | originating generation request event id |
|
|
117
|
+
| `a2.ai.tool.call_id` | `a2.ai.tool` | start | tool call id |
|
|
118
|
+
| `a2.ai.tool.name` | `a2.ai.tool` | start | tool name |
|
|
119
|
+
| `a2.ai.tool.attempt` | `a2.ai.tool` | start | durable 1-based attempt ordinal |
|
|
120
|
+
| `a2.ai.ack.request_id` | `a2.ai.ack` | start | reporting worker event id |
|
|
121
|
+
| `a2.ai.ack.report_id` | `a2.ai.ack` | start | work report event id, matching the control span's id |
|
|
122
|
+
|
|
123
|
+
- Drain outcomes: `settled` means nothing actionable is left; `busy` means live
|
|
124
|
+
per-event claims or a failed event's retry hold remain; `stalled` means a
|
|
125
|
+
caught failure in this drain needs a later retry. `failed` appears only on
|
|
126
|
+
scheduler deliveries: the drain threw after arming its successor watchdog,
|
|
127
|
+
the span records the error, and that successor retries the session.
|
|
128
|
+
- A failing handler marks its `a2.event` span with the exception and error
|
|
129
|
+
status. Alert on `a2.event.outcome = dead_lettered`. A dead-lettered event
|
|
130
|
+
blocks only later events in its lane, until `session.redrive()` resolves it;
|
|
131
|
+
other lanes stay eligible.
|
|
132
|
+
- `superseded` means a stale completion or failure did not change the log.
|
|
@@ -44,7 +44,7 @@ export async function POST(req: Request) {
|
|
|
44
44
|
| `SESSION_CLEARED` | a state read reaches below the latest clear, a handler reads state past a clear of its event, or an actor call is cleared before it receives its answer | no |
|
|
45
45
|
| `CLAIM_EXPIRED` | never thrown; only the `ctx.signal` abort reason when the attempt's lease lapsed without renewal (assumed loss, decided locally, not proof) | no |
|
|
46
46
|
| `STORE_UNAVAILABLE` | the store backend failed; nothing was written. The original error is the `cause` | **yes** |
|
|
47
|
-
| `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use) | no |
|
|
47
|
+
| `STORE_NOT_CONFIGURED` | production boots with no `store` configured (during `next build` page collection, deferred to first use), a store's optional peer dependency is missing, or a Postgres database lacks A2's tables at the version the release needs (or the notification triggers `listenConnectionString` uses). The message names the fix | no |
|
|
48
48
|
|
|
49
49
|
Retrying an *identical* batch is not `PARTIAL_DUPLICATE_BATCH`: it's an
|
|
50
50
|
idempotent success that returns the previously appended events. A failing
|
package/docs/reference/meta.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default { order:
|
|
1
|
+
export default { order: 9 }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: A clock for handler retries and timers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
export const scheduler = vercelQueues()
|
|
13
|
+
|
|
14
|
+
export const ordersServer = createServer({
|
|
15
|
+
contract: orders,
|
|
16
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
17
|
+
scheduler,
|
|
18
|
+
handlers: {
|
|
19
|
+
/* ... */
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts app/api/a2/scheduler/route.ts
|
|
25
|
+
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
26
|
+
|
|
27
|
+
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A scheduler wakes sessions on a clock. It retries a handler that failed or
|
|
31
|
+
whose process died, and it delivers [timers](/server/timers), whose delays must
|
|
32
|
+
fit the [provider's limit](/server/timers#provider-limits). Share one
|
|
33
|
+
scheduler across your servers and mount its route once, with every server it
|
|
34
|
+
delivers to. `scheduler.handler(...)` checks those servers when the module
|
|
35
|
+
loads, so a bad route fails at startup.
|
|
36
|
+
|
|
37
|
+
| Scheduler | Use it for |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| [Vercel Queues](/schedulers/vercel-queues) | Apps on Vercel (recommended) |
|
|
40
|
+
| [QStash](/schedulers/qstash) | A stable public URL, called from separate infrastructure |
|
|
41
|
+
|
|
42
|
+
## Without a scheduler
|
|
43
|
+
|
|
44
|
+
If no event type has a handler and nothing calls `session.schedule`, skip it.
|
|
45
|
+
Events settle as they're appended, with no queue messages. That's a complete
|
|
46
|
+
production setup for history, reducers, and live sync.
|
|
47
|
+
|
|
48
|
+
With handlers but no scheduler, a session only wakes on a top-level append or
|
|
49
|
+
an explicit `server.drain()`. Reads never wake it, so a session nobody writes
|
|
50
|
+
to stays stuck. That's fine for internal tools that can heal on the next
|
|
51
|
+
write. For production, add a scheduler.
|
|
52
|
+
|
|
53
|
+
## More than one scheduler
|
|
54
|
+
|
|
55
|
+
Servers with different scheduler instances need separate routes, even from the
|
|
56
|
+
same adapter. Give each QStash instance its own `url`, and each Vercel Queues
|
|
57
|
+
instance its own `topic` and trigger.
|
|
58
|
+
|
|
59
|
+
## Dispatch in a fresh invocation
|
|
60
|
+
|
|
61
|
+
`append` starts handlers in the same request and uses the scheduler as a
|
|
62
|
+
watchdog. `append.dispatch` stores the event and hands its handlers to the
|
|
63
|
+
queue instead:
|
|
64
|
+
|
|
65
|
+
```ts app/api/imports/route.ts
|
|
66
|
+
import { ordersServer } from '@/server'
|
|
67
|
+
|
|
68
|
+
export async function POST(request: Request) {
|
|
69
|
+
const { orderId, shopId, items } = await request.json()
|
|
70
|
+
await ordersServer.session(orderId).append.dispatch({
|
|
71
|
+
id: `created:${orderId}`,
|
|
72
|
+
type: 'created',
|
|
73
|
+
payload: { shopId, items },
|
|
74
|
+
})
|
|
75
|
+
return Response.json({ accepted: true }, { status: 202 })
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It needs a scheduler and waits for the queue to accept the message. If that
|
|
80
|
+
send fails, the event may already be stored; its explicit `id` makes a retry
|
|
81
|
+
safe.
|
|
82
|
+
|
|
83
|
+
## Deploys and rollbacks
|
|
84
|
+
|
|
85
|
+
Queued work can reach a different deployment than the one that sent it. Keep
|
|
86
|
+
event schemas compatible across deployments. Each adapter's page explains
|
|
87
|
+
where its tasks go: [Vercel Queues](/schedulers/vercel-queues#deployments) and
|
|
88
|
+
[QStash](/schedulers/qstash#deployments).
|
|
89
|
+
|
|
90
|
+
:::warning
|
|
91
|
+
Don't roll back to an A2 version without timer delivery while scheduled tasks
|
|
92
|
+
are outstanding. An old route can acknowledge a scheduled append without
|
|
93
|
+
writing its events. Rolling back past scheduled clears fails too: an old
|
|
94
|
+
Vercel Queues route keeps failing the clear until retention ends, and an old
|
|
95
|
+
QStash route moves it to QStash's dead-letter queue. Roll forward, or wait for
|
|
96
|
+
those tasks to deliver first.
|
|
97
|
+
:::
|
|
98
|
+
|
|
99
|
+
## Send errors
|
|
100
|
+
|
|
101
|
+
When a provider rejects a send, both adapters keep the provider's original
|
|
102
|
+
error.
|
|
103
|
+
|
|
104
|
+
- **Terminal:** a definite 4xx status or error, except 408, 409, 425, and 429.
|
|
105
|
+
The Vercel adapter also treats the SDK's `BadRequestError`,
|
|
106
|
+
`UnauthorizedError`, and `ForbiddenError` as terminal.
|
|
107
|
+
- **Retryable:** 408, 409, 425, 429, 5xx, network failures, and errors with no
|
|
108
|
+
recognized status.
|
|
109
|
+
- Vercel Queues' duplicate-message error counts as success.
|
|
110
|
+
|
|
111
|
+
A2's durable AI tools use this classification. Direct `session.schedule()`
|
|
112
|
+
calls still reject with the original error.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vercel Queues
|
|
3
|
+
description: The recommended scheduler on Vercel.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
9
|
+
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
export const scheduler = vercelQueues()
|
|
13
|
+
|
|
14
|
+
export const ordersServer = createServer({
|
|
15
|
+
contract: orders,
|
|
16
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
17
|
+
scheduler,
|
|
18
|
+
handlers: {
|
|
19
|
+
/* ... */
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts app/api/a2/scheduler/route.ts
|
|
25
|
+
import { ordersServer, scheduler } from '@/server'
|
|
26
|
+
|
|
27
|
+
export const POST = scheduler.handler(ordersServer)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```json vercel.json
|
|
31
|
+
{
|
|
32
|
+
"functions": {
|
|
33
|
+
"app/api/a2/scheduler/route.ts": {
|
|
34
|
+
"experimentalTriggers": [
|
|
35
|
+
{
|
|
36
|
+
"type": "queue/v2beta",
|
|
37
|
+
"topic": "a2"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```package-install
|
|
46
|
+
npm i @vercel/queue
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The trigger is required. It connects the route to Vercel Queues and makes the
|
|
50
|
+
function private, so only the queue can call it. Don't also expose the route
|
|
51
|
+
publicly or wrap it in your own authentication.
|
|
52
|
+
|
|
53
|
+
Messages are kept for seven days (Vercel Queues' default is 24 hours, and
|
|
54
|
+
seven days is its maximum), so timers can be scheduled up to seven days out. A
|
|
55
|
+
longer delay goes out as asked, and Vercel Queues refuses it.
|
|
56
|
+
|
|
57
|
+
A drain delivery is acknowledged once its session settles or a later watchdog
|
|
58
|
+
for it is stored, including when the drain fails after storing that watchdog.
|
|
59
|
+
A drain that fails before then stays unacknowledged, so Vercel Queues
|
|
60
|
+
redelivers it.
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
| Option | Default | |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `topic` | `'a2'` | must match the trigger's `topic`, and `[A-Za-z0-9_-]+` |
|
|
67
|
+
| `deploymentId` | `VERCEL_DEPLOYMENT_ID` | which deployment receives tasks; see below |
|
|
68
|
+
| `transport`, `clock` | the `@vercel/queue` client, the system clock | test seams for custom delivery |
|
|
69
|
+
|
|
70
|
+
An injected `transport` receives the topic, the task, and send options, and
|
|
71
|
+
owns its own provider configuration; `deploymentId` only configures the
|
|
72
|
+
built-in client.
|
|
73
|
+
|
|
74
|
+
## Deployments
|
|
75
|
+
|
|
76
|
+
`deploymentId` applies to every task: retries, timers, and scheduled clears.
|
|
77
|
+
Leave it out to deliver to the deployment that sent the task, pass an ID to
|
|
78
|
+
target one deployment, or pass `null` to let Vercel pick. Every deployment that
|
|
79
|
+
sends or receives must use the same setting.
|
|
80
|
+
|
|
81
|
+
- **Tasks stay with their deployment.** Promoting a new deployment doesn't move
|
|
82
|
+
them, so older deployments keep receiving their own work until it's done or
|
|
83
|
+
expires. Upgrading doesn't fix an older deployment's route.
|
|
84
|
+
- **`null` doesn't mean latest.** Vercel picks any eligible deployment, so every
|
|
85
|
+
one of them must use `deploymentId: null` and handle pending tasks.
|
|
86
|
+
- **Changing the setting doesn't move queued tasks.** Keep the old deployments
|
|
87
|
+
running until those tasks finish.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: QStash
|
|
3
|
+
description: Signed HTTP delivery from Upstash, to a stable public URL.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
```ts server/orders.ts
|
|
7
|
+
import { createServer } from 'experimental-a2/server'
|
|
8
|
+
import { qstash } from 'experimental-a2/scheduler-qstash'
|
|
9
|
+
import { redis } from 'experimental-a2/store-redis'
|
|
10
|
+
import { orders } from '@/contracts'
|
|
11
|
+
|
|
12
|
+
export const scheduler = qstash()
|
|
13
|
+
|
|
14
|
+
export const ordersServer = createServer({
|
|
15
|
+
contract: orders,
|
|
16
|
+
store: redis({ url: process.env.REDIS_URL }),
|
|
17
|
+
scheduler,
|
|
18
|
+
handlers: {
|
|
19
|
+
/* ... */
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts app/api/a2/scheduler/route.ts
|
|
25
|
+
import { ordersServer, scheduler } from '@/server'
|
|
26
|
+
|
|
27
|
+
export const POST = scheduler.handler(ordersServer)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```package-install
|
|
31
|
+
npm i @upstash/qstash
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Set `QSTASH_TOKEN`, `QSTASH_CURRENT_SIGNING_KEY`, and
|
|
35
|
+
`QSTASH_NEXT_SIGNING_KEY`. In development you need none of them: A2 starts
|
|
36
|
+
QStash's local server on the first publish. It needs `@upstash/qstash` 2.11.0
|
|
37
|
+
or newer.
|
|
38
|
+
|
|
39
|
+
The route checks `Upstash-Signature` before it touches the store and answers
|
|
40
|
+
unsigned requests with 401. Delivery is at least once, so handler side effects
|
|
41
|
+
need idempotency keys.
|
|
42
|
+
|
|
43
|
+
## Callback URL
|
|
44
|
+
|
|
45
|
+
QStash calls `/api/a2/scheduler`. An explicit `url` always wins; otherwise A2
|
|
46
|
+
infers it:
|
|
47
|
+
|
|
48
|
+
- **Vercel production:** `https://${VERCEL_PROJECT_PRODUCTION_URL}/api/a2/scheduler`.
|
|
49
|
+
- **Vercel preview:** `https://${VERCEL_BRANCH_URL}/api/a2/scheduler`.
|
|
50
|
+
- **Locally:** `http://localhost:${PORT ?? 3000}/api/a2/scheduler`, against
|
|
51
|
+
QStash's development server. `PORT` must be an integer from 1 through 65535.
|
|
52
|
+
|
|
53
|
+
Pass `url` for anything else: production outside Vercel, a custom path or
|
|
54
|
+
host, Docker networking, or a runtime without `process.env`. With an explicit
|
|
55
|
+
`url` outside production, A2 doesn't start the development server: set
|
|
56
|
+
`QSTASH_DEV`, or `QSTASH_URL` and credentials, yourself. An Edge runtime infers
|
|
57
|
+
the local URL but must also start the development server itself.
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
| Option | Default | |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `url` | inferred, see above | the callback URL |
|
|
64
|
+
| `token` | `QSTASH_TOKEN` | |
|
|
65
|
+
| `currentSigningKey` | `QSTASH_CURRENT_SIGNING_KEY` | |
|
|
66
|
+
| `nextSigningKey` | `QSTASH_NEXT_SIGNING_KEY` | |
|
|
67
|
+
| `headers` | none | static headers on every callback |
|
|
68
|
+
| `client` | a lazy `@upstash/qstash` client | your own configured client |
|
|
69
|
+
| `retries` | QStash's default | retries per delivery; every attempt is billed |
|
|
70
|
+
| `maxBodyBytes` | `1_048_576` (1 MiB) | the largest task A2 sends or accepts |
|
|
71
|
+
| `transport`, `clock` | the `@upstash/qstash` client and receiver, the system clock | test seams for custom delivery |
|
|
72
|
+
|
|
73
|
+
- **`headers`** are checked and copied when `qstash()` runs. A2 asks QStash to
|
|
74
|
+
redact every value from its dashboard and API, but QStash still stores and
|
|
75
|
+
delivers them. Names are case-insensitive. `Upstash-*`, `Content-Type`, HTTP
|
|
76
|
+
framing headers, and A2's Vercel identity headers are reserved.
|
|
77
|
+
- **`client`** is the `publishJSON(...)` part of a configured `Client`, for its
|
|
78
|
+
endpoint, retry policy, or telemetry. It can't be combined with `token` or
|
|
79
|
+
`transport`. It receives callback credentials unredacted and must apply each
|
|
80
|
+
publish's redaction. Put callback credentials in `headers`, not on the
|
|
81
|
+
client.
|
|
82
|
+
- **`maxBodyBytes`**: A2 reserves room for a task's retry metadata before the
|
|
83
|
+
first publish, so an accepted task never outgrows the limit. Raise it for a
|
|
84
|
+
larger paid-plan limit, and keep one value on every deployment that receives
|
|
85
|
+
the URL.
|
|
86
|
+
- **`transport`** implements `publish(request)` and `verify(request)`. It must
|
|
87
|
+
keep the body, deliver every header, and apply every redaction.
|
|
88
|
+
|
|
89
|
+
## How delivery works
|
|
90
|
+
|
|
91
|
+
- The route returns 200 once a task is done, or once its follow-up is queued,
|
|
92
|
+
so each retry gets a fresh message and retry budget. That includes a drain
|
|
93
|
+
that fails after queueing its follow-up.
|
|
94
|
+
- If the store is down (`STORE_UNAVAILABLE`) or not set up yet
|
|
95
|
+
(`STORE_NOT_CONFIGURED`, such as Postgres before its tables exist) before a
|
|
96
|
+
drain queues its follow-up, or during an append or clear task, the task moves
|
|
97
|
+
to a new message: 30 seconds later, four times longer each time, up to one
|
|
98
|
+
day.
|
|
99
|
+
- A signed but malformed task, or one over `maxBodyBytes`, returns 489 with
|
|
100
|
+
`Upstash-NonRetryable-Error: true`. Unknown contracts, missing
|
|
101
|
+
configuration, and other failures return an error, so QStash retries.
|
|
102
|
+
- If QStash can't reach the route at all, nothing queues a follow-up: watch
|
|
103
|
+
QStash's dead-letter queue and redrive from it.
|
|
104
|
+
- Delays are capped by your plan: 7 days on free plans and 1 year on
|
|
105
|
+
usage-based plans, with no fixed cap on fixed plans. Leave a second of
|
|
106
|
+
margin at the cap.
|
|
107
|
+
|
|
108
|
+
## Protected previews
|
|
109
|
+
|
|
110
|
+
For an inferred preview URL, each publish forwards the invocation's Vercel
|
|
111
|
+
OIDC token as `x-vercel-trusted-oidc-idp-token`, redacted, so a Vercel Trusted
|
|
112
|
+
Sources rule can let the first delivery through. This is best effort: a
|
|
113
|
+
missing or expiring token falls back to a plain publish, and retries reuse the
|
|
114
|
+
stored token, which may have expired. When every protected delivery must get
|
|
115
|
+
through, pass an automation bypass in `headers` instead. Signature checks still
|
|
116
|
+
apply after Vercel's.
|
|
117
|
+
|
|
118
|
+
## Deployments
|
|
119
|
+
|
|
120
|
+
Inferred production and preview URLs are moving aliases, so a pending task can
|
|
121
|
+
reach a later deployment. Keep tasks and event schemas compatible across
|
|
122
|
+
deployments, and keep the same `maxBodyBytes` on all of them: a lower value
|
|
123
|
+
rejects tasks that an earlier deployment accepted.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { order: 5 }
|