@pylonsync/create-pylon 0.20.0 → 0.22.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 (43) hide show
  1. package/package.json +1 -1
  2. package/templates/_root/AGENTS.md +1 -1
  3. package/templates/agency/AGENTS.md +1 -1
  4. package/templates/ai-chat/AGENTS.md +1 -1
  5. package/templates/ai-studio/AGENTS.md +1 -1
  6. package/templates/barebones/AGENTS.md +1 -1
  7. package/templates/chat/AGENTS.md +1 -1
  8. package/templates/chat/app/chat-client.tsx +1 -1
  9. package/templates/consumer/AGENTS.md +1 -1
  10. package/templates/consumer/components/social/post-parts.tsx +1 -1
  11. package/templates/consumer/components/social/post-screen.tsx +1 -1
  12. package/templates/consumer/components/social/use-social.ts +7 -7
  13. package/templates/creator/AGENTS.md +1 -1
  14. package/templates/crm/AGENTS.md +1 -1
  15. package/templates/directory/AGENTS.md +1 -1
  16. package/templates/helpdesk/AGENTS.md +1 -1
  17. package/templates/imessage/.env.example +2 -4
  18. package/templates/imessage/AGENTS.md +5 -4
  19. package/templates/imessage/README.md +14 -14
  20. package/templates/imessage/app/console.tsx +3 -3
  21. package/templates/imessage/components/setup-view.tsx +5 -9
  22. package/templates/imessage/functions/assistant.ts +17 -34
  23. package/templates/imessage/functions/processTurn.ts +14 -13
  24. package/templates/imessage/functions/resolveTurn.ts +10 -15
  25. package/templates/imessage/functions/setupStatus.ts +2 -3
  26. package/templates/imessage/lib/turn.ts +25 -0
  27. package/templates/imessage/lib/urls.ts +20 -0
  28. package/templates/imessage/tests/turn.test.ts +141 -0
  29. package/templates/inventory/AGENTS.md +1 -1
  30. package/templates/invoices/AGENTS.md +1 -1
  31. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +2 -2
  32. package/templates/local-service/AGENTS.md +1 -1
  33. package/templates/marketplace/AGENTS.md +1 -1
  34. package/templates/marketplace/client/WatchButton.tsx +2 -2
  35. package/templates/projects/AGENTS.md +1 -1
  36. package/templates/projects/app/projects/[id]/project-view.tsx +2 -2
  37. package/templates/restaurant/AGENTS.md +1 -1
  38. package/templates/saas/AGENTS.md +1 -1
  39. package/templates/shop/AGENTS.md +1 -1
  40. package/templates/todo/AGENTS.md +1 -1
  41. package/templates/waitlist/AGENTS.md +1 -1
  42. package/templates/imessage/lib/agent-call.ts +0 -251
  43. package/templates/imessage/tests/agent-call.test.ts +0 -112
@@ -1,8 +1,8 @@
1
1
  import { query } from "@pylonsync/functions";
2
- import { selfBaseUrl } from "../lib/agent-call";
3
2
  import { providerStatus } from "../lib/provider";
4
3
  import { findOwner } from "../lib/store";
5
4
  import { isDevMode, isDryRun, selectedTransport, transportChecklist } from "../lib/transport";
5
+ import { appBaseUrl } from "../lib/urls";
6
6
 
7
7
  // What the server is configured with, for the dashboard's setup panel.
8
8
  // Reports which variables are present, never their values. Owner only.
@@ -13,7 +13,7 @@ export default query({
13
13
  const transport = selectedTransport(ctx.env);
14
14
  const provider = providerStatus(ctx.env);
15
15
  const owner = await findOwner(ctx);
16
- const base = selfBaseUrl(ctx.env);
16
+ const base = appBaseUrl(ctx.env);
17
17
  const devMode = isDevMode(ctx.env);
18
18
  return {
19
19
  transport,
@@ -22,7 +22,6 @@ export default query({
22
22
  provider,
23
23
  ownerConfigured: owner !== null,
24
24
  adminEmailsSet: (ctx.env.PYLON_ADMIN_EMAILS ?? "").trim() !== "",
25
- adminTokenSet: (ctx.env.PYLON_ADMIN_TOKEN ?? "").trim() !== "",
26
25
  appUrlSet: (ctx.env.APP_URL ?? ctx.env.PYLON_PUBLIC_URL ?? "").trim() !== "",
27
26
  devMode,
28
27
  baseUrl: base,
@@ -0,0 +1,25 @@
1
+ import type { ActionCtx, AgentToolRun } from "@pylonsync/functions";
2
+
3
+ export interface Turn {
4
+ conversationId: string;
5
+ contactId: string;
6
+ }
7
+
8
+ const NOT_ANSWERING = "This tool is only available while answering a text.";
9
+
10
+ /**
11
+ * Which contact a tool call is about. Every assistant tool starts here.
12
+ *
13
+ * processTurn runs the agent with ctx.agents.run and puts the conversation
14
+ * and turn in the run's context; only server code can set it. resolveTurn
15
+ * maps that to exactly one contact, and only while that turn is running.
16
+ * Nothing the model or the texter writes can change the mapping.
17
+ */
18
+ export async function currentTurn(ctx: Pick<ActionCtx, "runQuery">, run: AgentToolRun): Promise<Turn> {
19
+ const conversationId = run.context?.conversationId;
20
+ const turnId = run.context?.turnId;
21
+ if (typeof conversationId !== "string" || typeof turnId !== "string") throw new Error(NOT_ANSWERING);
22
+ const turn = await ctx.runQuery<Turn | null>("resolveTurn", { conversationId, turnId });
23
+ if (!turn) throw new Error(NOT_ANSWERING);
24
+ return turn;
25
+ }
@@ -0,0 +1,20 @@
1
+ type Env = Record<string, string | undefined>;
2
+
3
+ /**
4
+ * This app's origin, for the URLs the Setup page shows (the Sendblue webhook
5
+ * and the relay endpoint): APP_URL or PYLON_PUBLIC_URL when set to an http(s)
6
+ * URL, otherwise the local dev server.
7
+ */
8
+ export function appBaseUrl(env: Env): string {
9
+ const raw = (env.APP_URL || env.PYLON_PUBLIC_URL || "").trim();
10
+ if (raw !== "") {
11
+ try {
12
+ const u = new URL(raw);
13
+ if (u.protocol === "https:" || u.protocol === "http:") return u.origin;
14
+ } catch {
15
+ // Fall through to the local default.
16
+ }
17
+ }
18
+ const port = Number.parseInt(env.PYLON_PORT ?? "", 10);
19
+ return `http://127.0.0.1:${Number.isFinite(port) && port > 0 && port < 65536 ? port : 4321}`;
20
+ }
@@ -0,0 +1,141 @@
1
+ // The agent turn: processTurn runs the assistant as the owner through
2
+ // ctx.agents.run, and the tools find their contact from the run's context.
3
+ import { describe, expect, test } from "bun:test";
4
+ import processTurn from "../functions/processTurn";
5
+ import resolveTurn from "../functions/resolveTurn";
6
+ import { chunkReply } from "../lib/chunk";
7
+ import { currentTurn } from "../lib/turn";
8
+ import { appBaseUrl } from "../lib/urls";
9
+ import { fakeCtx } from "./fake-ctx";
10
+
11
+ type Handler = (ctx: unknown, args: Record<string, unknown>) => Promise<any>;
12
+ const run = (def: unknown, ctx: unknown, args: Record<string, unknown>) =>
13
+ (def as { handler: Handler }).handler(ctx, args);
14
+
15
+ const STARTED = {
16
+ state: "started",
17
+ turnId: "turn_1",
18
+ conversationId: "conv_1",
19
+ runId: null,
20
+ input: "[iMessage from Ana] hi",
21
+ };
22
+
23
+ /** An action ctx for processTurn with the agent call stubbed. */
24
+ function turnCtx(agentsRun: (name: string, options: Record<string, unknown>) => Promise<unknown>) {
25
+ const finished: Record<string, unknown>[] = [];
26
+ const agentCalls: { name: string; options: Record<string, unknown> }[] = [];
27
+ const ctx = {
28
+ env: { ANTHROPIC_API_KEY: "sk-test" },
29
+ async runMutation(name: string, args: Record<string, unknown>) {
30
+ if (name === "beginTurn") return STARTED;
31
+ if (name === "finishTurn") {
32
+ finished.push(args);
33
+ return {};
34
+ }
35
+ throw new Error(`unexpected mutation ${name}`);
36
+ },
37
+ async runQuery(name: string) {
38
+ if (name === "ownerAccount") return { userId: "owner_1" };
39
+ throw new Error(`unexpected query ${name}`);
40
+ },
41
+ agents: {
42
+ run(name: string, options: Record<string, unknown>) {
43
+ agentCalls.push({ name, options });
44
+ return agentsRun(name, options);
45
+ },
46
+ },
47
+ };
48
+ return { ctx, finished, agentCalls };
49
+ }
50
+
51
+ describe("processTurn", () => {
52
+ test("runs the assistant as the owner with the turn as context", async () => {
53
+ const reply = "Hello Ana.\n\nSee you soon.";
54
+ const t = turnCtx(async () => ({ runId: "run_1", text: reply, steps: 1 }));
55
+ const out = await run(processTurn, t.ctx, { conversationId: "conv_1" });
56
+ expect(out).toEqual({ state: "answered", bubbles: chunkReply(reply).length });
57
+ expect(t.agentCalls).toEqual([
58
+ {
59
+ name: "assistant",
60
+ options: {
61
+ input: STARTED.input,
62
+ as: { userId: "owner_1", admin: true },
63
+ context: { conversationId: "conv_1", turnId: "turn_1" },
64
+ },
65
+ },
66
+ ]);
67
+ expect(t.finished).toEqual([
68
+ {
69
+ conversationId: "conv_1",
70
+ turnId: "turn_1",
71
+ outcome: "answered",
72
+ runId: "run_1",
73
+ chunks: chunkReply(reply),
74
+ },
75
+ ]);
76
+ });
77
+
78
+ test("records an agent failure on the turn instead of throwing", async () => {
79
+ const t = turnCtx(async () => {
80
+ throw Object.assign(new Error("Upstream LLM provider returned an error."), { code: "PROVIDER_HTTP_529" });
81
+ });
82
+ const out = await run(processTurn, t.ctx, { conversationId: "conv_1" });
83
+ expect(out).toEqual({ state: "failed" });
84
+ expect(t.finished[0]).toMatchObject({
85
+ outcome: "failed",
86
+ detail: "PROVIDER_HTTP_529: Upstream LLM provider returned an error.",
87
+ });
88
+ });
89
+ });
90
+
91
+ describe("the tool fence", () => {
92
+ test("resolveTurn maps a running turn to its contact and nothing else", async () => {
93
+ const f = fakeCtx();
94
+ const contactId = await f.ctx.db.insert("Contact", { handle: "+15125550148" });
95
+ const conversationId = await f.ctx.db.insert("Conversation", {
96
+ contactId,
97
+ turnState: "running",
98
+ turnId: "turn_1",
99
+ });
100
+ expect(await run(resolveTurn, f.ctx, { conversationId, turnId: "turn_1" })).toEqual({ conversationId, contactId });
101
+ // An older turn of the same conversation.
102
+ expect(await run(resolveTurn, f.ctx, { conversationId, turnId: "turn_0" })).toBeNull();
103
+ // The turn finished.
104
+ await f.ctx.db.update("Conversation", conversationId, { turnState: "idle" });
105
+ expect(await run(resolveTurn, f.ctx, { conversationId, turnId: "turn_1" })).toBeNull();
106
+ expect(await run(resolveTurn, f.ctx, { conversationId: "missing", turnId: "turn_1" })).toBeNull();
107
+ });
108
+
109
+ test("currentTurn needs the server-set context", async () => {
110
+ const queries: unknown[] = [];
111
+ const ctx = {
112
+ async runQuery(name: string, args: unknown) {
113
+ queries.push([name, args]);
114
+ return { conversationId: "conv_1", contactId: "contact_1" };
115
+ },
116
+ } as never;
117
+ const base = { runId: "run_1", agent: "assistant", userId: "owner_1" };
118
+
119
+ expect(
120
+ await currentTurn(ctx, { ...base, context: { conversationId: "conv_1", turnId: "turn_1" } }),
121
+ ).toEqual({ conversationId: "conv_1", contactId: "contact_1" });
122
+ expect(queries).toEqual([["resolveTurn", { conversationId: "conv_1", turnId: "turn_1" }]]);
123
+
124
+ // A run a client started (no context), or a partial one, gets nothing.
125
+ await expect(currentTurn(ctx, { ...base, context: null })).rejects.toThrow("only available while answering");
126
+ await expect(currentTurn(ctx, { ...base, context: { conversationId: "conv_1" } })).rejects.toThrow(
127
+ "only available while answering",
128
+ );
129
+ expect(queries.length).toBe(1);
130
+ });
131
+ });
132
+
133
+ describe("appBaseUrl", () => {
134
+ test("prefers the configured public origin, then the local dev server", () => {
135
+ expect(appBaseUrl({ APP_URL: "https://a.example.com/path", PYLON_PORT: "4541" })).toBe("https://a.example.com");
136
+ expect(appBaseUrl({ PYLON_PUBLIC_URL: "https://b.example.com" })).toBe("https://b.example.com");
137
+ expect(appBaseUrl({ PYLON_PORT: "4541" })).toBe("http://127.0.0.1:4541");
138
+ expect(appBaseUrl({ APP_URL: "javascript:alert(1)" })).toBe("http://127.0.0.1:4321");
139
+ expect(appBaseUrl({})).toBe("http://127.0.0.1:4321");
140
+ });
141
+ });
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -156,9 +156,9 @@ export function InvoiceView({
156
156
  value={invoice.clientId ?? ""}
157
157
  className="h-8 bg-background md:h-7.5"
158
158
  onChange={(event) =>
159
- void db.update("Invoice", invoice.id, {
159
+ db.update("Invoice", invoice.id, {
160
160
  clientId: event.target.value || null,
161
- })
161
+ }).catch(console.warn)
162
162
  }
163
163
  >
164
164
  <option value="">—</option>
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -66,9 +66,9 @@ function Inner({
66
66
  return;
67
67
  }
68
68
  if (mine) {
69
- void db.delete("Watch", mine.id);
69
+ db.delete("Watch", mine.id).catch(console.warn);
70
70
  } else {
71
- void db.insert("Watch", { userId: identity.userId, listingId, listingTitle });
71
+ db.insert("Watch", { userId: identity.userId, listingId, listingTitle }).catch(console.warn);
72
72
  }
73
73
  }
74
74
 
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -93,9 +93,9 @@ export function ProjectView({
93
93
  value={project.status}
94
94
  className="h-8 md:h-7.5"
95
95
  onChange={(event) =>
96
- void db.update("Project", project.id, {
96
+ db.update("Project", project.id, {
97
97
  status: event.target.value,
98
- })
98
+ }).catch(console.warn)
99
99
  }
100
100
  >
101
101
  {PROJECT_STATUSES.map((s) => (
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.