@paigy/mcp 0.38.0 → 0.40.1

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.
@@ -1,23 +1,21 @@
1
1
  // ../../packages/schema/dist/index.js
2
+ import { z as z3 } from "zod";
2
3
  import { z as z2 } from "zod";
3
- import { z } from "zod";
4
4
  import { zodToJsonSchema } from "zod-to-json-schema";
5
+ import { z } from "zod";
5
6
  var OPTIONS_MIN = 2;
6
7
  var OPTIONS_MAX = 6;
7
- var MISSED_CALL_PLAN = {
8
- retry_10m: { kind: "every", minutes: 10 },
9
- retry_30m: { kind: "every", minutes: 30 },
10
- retry_60m: { kind: "every", minutes: 60 },
11
- backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
12
- backoff_standard: { kind: "at", minutes: [10, 30, 120] },
13
- backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
14
- inbox: { kind: "once" },
15
- dismiss: { kind: "grace", minutes: 2 }
16
- };
17
8
  function draft2020(node) {
18
9
  if (Array.isArray(node)) return node.map(draft2020);
19
10
  if (node && typeof node === "object") {
20
11
  const o = node;
12
+ if (Array.isArray(o.items)) {
13
+ o.prefixItems = o.items;
14
+ if ("additionalItems" in o) {
15
+ o.items = o.additionalItems;
16
+ delete o.additionalItems;
17
+ } else delete o.items;
18
+ }
21
19
  for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
22
20
  if (typeof o[excl] === "boolean") {
23
21
  if (o[excl] === true && typeof o[lim] === "number") {
@@ -36,88 +34,82 @@ function mcpInputSchema(s) {
36
34
  delete schema.$schema;
37
35
  return draft2020(schema);
38
36
  }
39
- var CreateGoalSchema = z.object({
40
- outcome: z.string().trim().min(1).max(1e4),
41
- ownerParticipant: z.string().trim().min(1).optional(),
42
- idempotencyKey: z.string().trim().min(1).max(200)
43
- });
44
- var CREATE_GOAL_DESCRIPTION = "Create a durable root Goal for an outcome. Admission only: claim it before doing work, then update its state as it advances. Use a stable idempotencyKey so retries do not create duplicates. Returns goalId, state, revision, ownerParticipant, and next claim_goal.";
45
- var UpdateGoalSchema = z.object({
46
- revision: z.number().int().positive(),
47
- changes: z.object({
48
- outcome: z.string().trim().min(1).max(1e4).optional(),
49
- state: z.enum(["active", "waiting", "done", "cancelled"]).optional(),
50
- progress: z.string().trim().min(1).max(1e4).optional(),
51
- reviewed: z.literal(true).optional()
52
- }).refine((v) => Object.keys(v).length > 0),
53
- reason: z.string().trim().min(1).max(2e3)
37
+ var StartContactSchema = z.object({
38
+ goalIds: z.tuple([z.string().uuid()]),
39
+ ask: z.string().trim().min(1).max(1e4),
40
+ waiting: z.enum(["none", "hard"]).default("none"),
41
+ channel: z.enum(["notification", "call"]).default("notification"),
42
+ options: z.array(z.object({ label: z.string().trim().min(1).max(1e3), image: z.string().url().optional(), html: z.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
43
+ threadId: z.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
44
+ }).strict();
45
+ var ContactSchema = z.union([StartContactSchema, z.object({ deliveryId: z.string().uuid() }).strict()]);
46
+ var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
47
+ var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
48
+ var CreateGoalSchema = z2.object({
49
+ outcome: z2.string().trim().min(1).max(1e4),
50
+ ownerParticipant: z2.string().trim().min(1).optional(),
51
+ idempotencyKey: z2.string().trim().min(1).max(200)
54
52
  });
55
- var fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
56
- var STANDARD_MEANS = (() => {
57
- const plan = MISSED_CALL_PLAN.backoff_standard;
58
- const mins = plan.kind === "at" ? plan.minutes : [];
59
- const parts = mins.map(fmtMin);
60
- const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
61
- return `Rings again ${list} after the missed call, then leaves it in your inbox`;
62
- })();
63
- function contactSchemaFrom(fields) {
64
- const surface = z.object({
65
- ask: fields.ask.describe(
66
- `What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). DONE IS SAID ONCE: "all set", "nothing open on my end", "that thread is complete" are the same report in new words, and each one reaches them separately (live 2026-08-12: three of them in three minutes). After the first, you are finished speaking; if they acknowledge it, stop rather than confirming the acknowledgement. Progress is never a contact: set_work_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it.`
67
- ),
68
- waiting: fields.waiting.describe(
69
- "What happens to your work while you wait. 'none': you're just informing them. 'soft': you'd like an answer but can keep working. 'hard': you are STOPPED until they answer \u2014 reaches them urgently and escalates to a real phone call if unanswered."
70
- ),
71
- options: fields.options.describe(
72
- `The choices the user picks from, when you have them \u2014 ${OPTIONS_MIN} to ${OPTIONS_MAX}, drawn from your own sentence.`
73
- ),
74
- channel: fields.channel.describe(
75
- "Relay how the user explicitly said to reach them ('call me' \u2192 'call', 'just message/text me' \u2192 'message'), or use 'call' when promoting the same quiet ask after it becomes a substantial blocker. Omit otherwise; Paigy picks."
76
- ),
77
- parentId: fields.parentId.describe(
78
- "To continue an earlier conversation, pass the parentId a previous contact or reply returned. Omit to start a new one."
79
- ),
80
- workId: fields.workId.describe(
81
- "The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
82
- )
83
- });
84
- const out = mcpInputSchema(surface);
85
- out.required = ["ask"];
86
- return out;
87
- }
88
- var CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, decisionId? } \u2014 pass notificationId to the reply path named by the returned \`message\`, and parentId to a later contact to continue the conversation. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim.`;
89
- var CHECK_REPLIES_DESCRIPTION = "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), `work` \u2014 durable outcomes currently owned by you \u2014 plus your still-pending notifications and `requests`, requests the user started toward you (each { notificationId, parentId, workId?, text }); replies tied to Work carry workId and decisionId too. Pass workId back to contact when a new decision blocks that outcome. A REQUEST keeps reappearing until you engage with it \u2014 call set_work_state with its workId, or notificationId once when workId is absent, to claim it \u2014 because human-initiated work must never be silently dropped just because you read the list. A REPLY is different: it is acknowledged by being delivered to you, and needs no set_work_state to stop resurfacing \u2014 report state on it only when it starts real follow-up work. Also returns `threads` \u2014 the SAME replies + requests grouped by conversation, oldest thread first, each with a `busy` flag and its `items` in arrival order. WORK ONE THREAD AT A TIME: take the oldest thread whose `busy` is false, handle ALL of its items together in a single turn (one set_work_state), then go to the next \u2014 don't interleave threads item-by-item. A `busy` thread already has a turn in progress; leave it and let its new items ride the next turn. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. To wait on an answer to a contact call you just made, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with contact on its parentId. EVERY Paigy reply \u2014 this one, await_reply's, and contact's \u2014 may carry `also`: work assigned to you that no wake could reach, handed to you because you happened to be here. It is NOT what you asked about and it is never urgent: FINISH what you came for first, then take it up. Each entry has a `noteId` and the owner's own words; report on its `parentId` thread when it has one, and call set_work_state with its noteId as the bootstrap notificationId. Ignoring it costs nothing \u2014 it rides your next reply too. Also returns `stalled`: work (either direction) you reported in_progress via set_work_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled. Also returns `you` \u2014 WHICH IDENTITY you are speaking as ({ name, device, tokenId }), the same name and device the user sees on their Agents screen. This is the only safe way to find out (calling `pair` can MINT a new identity instead of telling you about the current one). Use it when the user asks who you are, and to tell whether work addressed to a name is addressed to you. A request may also carry `stranded`: it was addressed to ANOTHER agent on this account (that name) which has not been seen since it landed, so nobody came for it and it is handed to you because you are the session that is here. Take it exactly like your own \u2014 set_work_state claims it, reply with contact on its parentId \u2014 and say whose it was, because the user picked that agent on purpose. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as await_reply's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.";
90
- var SCHEDULE_CALLBACK_DESCRIPTION = "Promise the user a follow-up you'll keep even if you go idle. Use it when they ask you to report back: trigger 'on_done' (when you finish \u2014 fires when you call set_work_state completed), 'on_blocked' (if you hit a blocker \u2014 fires on set_work_state needs_input), or 'scheduled' with dueInSeconds (e.g. 'remind me in 10 min'). Pass the parentId of the conversation and a short note. Fulfill it by calling contact on that parentId; check_replies re-lists due callbacks until you do.";
53
+ var CreateGoalToolSchema = CreateGoalSchema.extend({
54
+ idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
55
+ }).strict();
56
+ var CREATE_GOAL_DESCRIPTION = "Create a durable root Goal for an outcome. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
57
+ var UpdateGoalSchema = z2.object({
58
+ revision: z2.number().int().positive(),
59
+ changes: z2.object({
60
+ outcome: z2.string().trim().min(1).max(1e4).optional(),
61
+ ownerParticipant: z2.string().trim().min(1).optional(),
62
+ parentGoalId: z2.string().uuid().nullable().optional(),
63
+ dependencies: z2.array(z2.object({ goalId: z2.string().uuid(), gate: z2.enum(["start", "finish"]) }).strict()).optional(),
64
+ children: z2.array(z2.object({ outcome: z2.string().trim().min(1).max(1e4), ownerParticipant: z2.string().trim().min(1), gate: z2.enum(["start", "finish"]).optional() }).strict()).optional(),
65
+ state: z2.enum(["active", "done", "cancelled"]).optional(),
66
+ progress: z2.string().trim().min(1).max(1e4).optional(),
67
+ reviewed: z2.literal(true).optional()
68
+ }).strict().refine((v) => Object.keys(v).length > 0),
69
+ reason: z2.string().trim().min(1).max(2e3),
70
+ operationId: z2.string().uuid().optional()
71
+ }).strict();
72
+ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z2.string().uuid() }).strict();
73
+ var ClaimGoalSchema = z2.object({ goalId: z2.string().uuid().optional() }).strict();
74
+ var GetGoalSchema = z2.object({ goalId: z2.string().uuid() }).strict();
75
+ var GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
76
+ var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
77
+ var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
78
+ var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
79
+ var CheckRepliesSchema = z2.object({}).strict();
80
+ var GetThreadSchema = z2.object({
81
+ parentId: z2.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
82
+ }).strict();
83
+ var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
84
+ var SearchThreadsSchema = z2.object({
85
+ q: z2.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
86
+ }).strict();
91
87
  var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
92
- var SET_WORK_STATE_DESCRIPTION = "Report progress on durable Work. Use workId from check_replies, contact, or a reply. For a new assigned request that has only notificationId, pass that once; the result returns its workId and every later report uses workId. States: in_progress (you started or resumed it), completed (done), or needs_input (you need a human decision \u2014 follow with contact carrying the returned workId). This changes Work progress only; reply delivery and acknowledgement stay on await_reply/check_replies.";
93
- function serverInstructions(opts) {
94
- const waits = opts.waits;
95
- return "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose parentId you don't recognize, or one carrying a contextParentId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. " + (waits ? "Follow contact's returned message. An inbox delivery is asynchronous: keep working and collect the answer later through check_replies. A call is live: call await_reply with that notificationId; it is scoped, so replies never cross. If a quiet ask later blocks substantial work, resend contact with the same parentId and channel:'call'. " : "You are wake-driven: you cannot wait for an answer in-context. Inbox and call answers return through check_replies. When a reply is handed to you it is LEASED \u2014 act on it, then call ack_reply with its leaseId so it is not handed to you again; a reply you never acknowledge comes back on your next wake. ") + // WHO IS ASKING, AND WHERE ARE THEY (field report, 2026-08-28 — eleven days of total
96
- // silence on a live pairing). "Never end a turn that still needs the user without
97
- // contact" is correct for an unattended agent and says NOTHING about the case that
98
- // actually dominates: the user sitting at the terminal, where ending the turn with the
99
- // question ALREADY reaches them. So the rule collapsed into two readings — fire on every
100
- // clarifying question (phone spam, they unpair) or never fire at all — and every session
101
- // after install day independently chose the second. Sessions start cold, so the same
102
- // default got re-derived daily with no memory that yesterday did the same, and no session
103
- // ever thought to ask the user which they wanted. Zero notifications for eleven days,
104
- // with no error anywhere: indistinguishable from a healthy account having a quiet week.
105
- //
106
- // The carve-out is stated rather than left to per-session judgment, because judgment
107
- // exercised cold, once per session, is not judgment — it is a coin landing the same way.
108
- "Never end a turn that still needs the user without contact + await_reply \u2014 WHEN THEY ARE NOT THERE TO ASK. In an interactive session with the user at the terminal, the prompt IS the channel: ending your turn with the question already reaches them, faster and richer than a push, so do NOT send one for an ordinary blocked-or-done moment. Reach for Paigy exactly when the terminal is not enough \u2014 work that will run more than a few minutes unattended (a long build, a deploy, a background job, a cloud session), anything you finish or get stuck on while they are away, or when they have said they are stepping out. If you cannot tell which situation you are in, ask them once, in passing, how they want to be reached, and follow that for the rest of the session. When you need a decision or input, WRITE THE QUESTION and Paigy derives the answer shape from it \u2014 there is no shape parameter to set, and passing one is an error. Ask a yes/no question and they get yes/no; ask them to approve an action and they get approve/deny; ask them to pick, and to pick several, and to rank, and each gets the control it needs. So phrase the ask precisely: 'which of these should I do first' and 'should I do this' are different questions and become different answers. The one thing the prose can't supply is the CHOICES themselves \u2014 when you're asking them to pick between concrete alternatives, pass `options` (${OPTIONS_MIN}-${OPTIONS_MAX} of them, drawn from your own sentence), because Paigy will not invent alternatives it can't know. On a { kind: 'clarify' } reply, see contact's own description for how to respond. When you send waiting:'hard' (or the user asked you to call), remember the ask may be spoken aloud \u2014 write it short and conversational, and name things instead of using IDs (e.g. 'the pull request about the agents page', not 'PR #235'). When the user asks you to follow up later \u2014 when you're done, if you're blocked, or at a set time \u2014 record it with schedule_callback so you don't drop it if you go idle. If you're about to start a genuinely long-running or blocking piece of work \u2014 one where the user would otherwise sit and wait \u2014 mention ONCE, in passing, that you can reach them when it's done or if you hit a blocker, instead of them needing to babysit the terminal. Don't offer this for quick tasks, and don't repeat the offer if they've already said yes or no earlier in the conversation. NEVER go quietly idle while something might still be pending for you: whenever you end a turn with any Paigy notification unanswered (or any chance the user replied through the app while you worked), schedule your own ~2-minute wake-up (harness ScheduleWakeup or equivalent) and call check_replies when it fires; if still nothing, re-schedule and keep looping until resolved or the user says stop. For legibility, always use this exact wording \u2014 reason: 'Paigy idle check \u2014 waiting on <thing>', wake-up prompt: 'Paigy idle check: call check_replies and engage with anything unacknowledged; if idle, re-schedule (~2min).' \u2014 so the user can recognize every idle check at a glance. This self-polling in your own live session (full context intact) is the PRIMARY mechanism; the plugin's Stop hooks are only the dead-session safety net.";
88
+ var AGENT_TOOLS = [
89
+ { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
90
+ { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
91
+ { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
92
+ { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
93
+ { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
94
+ { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
95
+ { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
96
+ { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
97
+ ];
98
+ var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
99
+ function serverInstructions(_opts) {
100
+ return "On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and its durable Entries and accepted decisions. get_goal rereads the same evidence without consuming it. Contact only for an existing Goal. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. On stdio, Calls hold one bounded window; on hosted MCP, Calls return after one read; continue with contact({deliveryId}) to reread that exact Delivery. Evidence can repeat: reads do not acknowledge or hide it. Use update_goal to report progress and explicitly acknowledge review. Never infer ringing from an open Call Delivery. Soft waiting, multipart contact, and re-presentation are unsupported. In an interactive session, ask the user directly; use Paigy when they asked for it or are away.";
109
101
  }
110
- var ContextSchema = z2.object({
111
- title: z2.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
112
- description: z2.array(z2.string().min(1)).describe(
102
+ var ContextSchema = z3.object({
103
+ title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
104
+ description: z3.array(z3.string().min(1)).describe(
113
105
  "Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
114
106
  )
115
107
  });
116
- var ParticipantSchema = z2.object({
117
- kind: z2.enum(["human", "agent"]),
118
- id: z2.string()
108
+ var ParticipantSchema = z3.object({
109
+ kind: z3.enum(["human", "agent"]),
110
+ id: z3.string()
119
111
  });
120
- var TransformSchema = z2.enum([
112
+ var TransformSchema = z3.enum([
121
113
  "structure",
122
114
  // shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
123
115
  "request_more",
@@ -133,25 +125,25 @@ var TransformSchema = z2.enum([
133
125
  "summarize"
134
126
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
135
127
  ]);
136
- var OptionSchema = z2.object({
137
- id: z2.string(),
138
- label: z2.string(),
128
+ var OptionSchema = z3.object({
129
+ id: z3.string(),
130
+ label: z3.string(),
139
131
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
140
132
  // the constraints below are what an agent reads when deciding to use these.
141
- html: z2.string().max(16384).describe(
133
+ html: z3.string().max(16384).describe(
142
134
  "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
143
135
  ).optional(),
144
- image: z2.string().url().describe(
136
+ image: z3.string().url().describe(
145
137
  "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
146
138
  ).optional()
147
139
  });
148
- var VisualSchema = z2.object({
149
- url: z2.string().url(),
150
- label: z2.string().optional()
140
+ var VisualSchema = z3.object({
141
+ url: z3.string().url(),
142
+ label: z3.string().optional()
151
143
  });
152
- var NotifyLevelSchema = z2.enum(["inbox", "push", "banner", "call"]);
153
- var SelectShapeSchema = z2.enum(["one", "many", "rank", "confirm", "text"]);
154
- var ReceiptEventSchema = z2.enum([
144
+ var NotifyLevelSchema = z3.enum(["inbox", "push", "banner", "call"]);
145
+ var SelectShapeSchema = z3.enum(["one", "many", "rank", "confirm", "text"]);
146
+ var ReceiptEventSchema = z3.enum([
155
147
  "delivered",
156
148
  // the bundle reached the recipient at some level
157
149
  "seen",
@@ -181,44 +173,47 @@ var ReceiptEventSchema = z2.enum([
181
173
  // be rewound by a writer that forgot to advance it.
182
174
  "restarted"
183
175
  ]);
184
- var AttentionSchema = z2.object({
176
+ var AttentionSchema = z3.object({
185
177
  urgency: NotifyLevelSchema,
186
178
  /** The required answer shape, or null for a plain notify that asks nothing back. */
187
179
  select: SelectShapeSchema.nullable(),
188
180
  /** Coverage contract (#396) — points the answer must address; null = none declared. */
189
- points: z2.array(z2.string()).nullable(),
181
+ points: z3.array(z3.string()).nullable(),
190
182
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
191
- blocking: z2.boolean(),
183
+ blocking: z3.boolean(),
192
184
  /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
193
185
  * slice wires it; optional so today's rows/callers project cleanly. */
194
- deadline: z2.string().datetime().nullable().optional()
186
+ deadline: z3.string().datetime().nullable().optional()
195
187
  });
196
- var NotifyRequestFields = z2.object({
188
+ var NotifyRequestFields = z3.object({
197
189
  /** Plaintext message content. Present on the plaintext path (today's shape);
198
190
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
199
191
  * superRefine at the bottom enforces exactly one of the two. */
200
192
  context: ContextSchema.optional(),
201
- options: z2.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
193
+ options: z3.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
202
194
  "The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
203
195
  ),
204
- points: z2.array(z2.string().min(1)).optional().describe(
196
+ points: z3.array(z3.string().min(1)).optional().describe(
205
197
  "The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
206
198
  ),
207
- visuals: z2.array(VisualSchema).optional().describe(
199
+ visuals: z3.array(VisualSchema).optional().describe(
208
200
  "Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
209
201
  ),
210
202
  /** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
211
- repo: z2.string().optional(),
203
+ repo: z3.string().optional(),
212
204
  /** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
213
- branch: z2.string().optional(),
205
+ branch: z3.string().optional(),
214
206
  /** Continue an existing conversation — the id of any notification in it (its root
215
207
  * is the conversation's identity). Omitted = start a new conversation. Renamed
216
208
  * from `parentId` (2026-08-03): one linkage system, the parent; the API edge
217
209
  * still accepts the old name from older clients. */
218
- parentId: z2.string().uuid().optional(),
210
+ parentId: z3.string().uuid().optional(),
219
211
  /** The durable outcome this contact advances. Optional during the notification-to-Work
220
212
  * migration; when present, a blocking ask creates a DecisionNeed for this Work. */
221
- workId: z2.string().uuid().optional(),
213
+ workId: z3.string().uuid().optional(),
214
+ /** Target Goal scope. During staged migration this is accepted by the shared contract but
215
+ * target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
216
+ goalId: z3.string().uuid().optional(),
222
217
  urgency: NotifyLevelSchema.default("inbox").describe(
223
218
  "The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
224
219
  ),
@@ -226,7 +221,7 @@ var NotifyRequestFields = z2.object({
226
221
  * visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
227
222
  * when `parentId` became the conversation handle: `parentId` says WHERE, this
228
223
  * says HOW. */
229
- clarifies: z2.string().optional(),
224
+ clarifies: z3.string().optional(),
230
225
  /** E2EE (text lane): when the pairing is E2EE, the sealed replacements for the
231
226
  * plaintext content fields, keyed by field name. FINALIZED wire shape (was
232
227
  * provisional in the storage PR): a per-field map `{ context?, options?,
@@ -239,10 +234,10 @@ var NotifyRequestFields = z2.object({
239
234
  * notifications.envelope and relays it blindly; it never decrypts. Absent =
240
235
  * today's plaintext path (context/options/visuals carry the cleartext).
241
236
  * z.lazy because EnvelopeSchema is declared further down (E2EE section). */
242
- envelope: z2.object({
243
- context: z2.lazy(() => EnvelopeSchema).optional(),
244
- options: z2.lazy(() => EnvelopeSchema).optional(),
245
- visuals: z2.lazy(() => EnvelopeSchema).optional()
237
+ envelope: z3.object({
238
+ context: z3.lazy(() => EnvelopeSchema).optional(),
239
+ options: z3.lazy(() => EnvelopeSchema).optional(),
240
+ visuals: z3.lazy(() => EnvelopeSchema).optional()
246
241
  }).optional(),
247
242
  select: SelectShapeSchema.optional().describe(
248
243
  "How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
@@ -257,20 +252,20 @@ var NotifyRequestFields = z2.object({
257
252
  // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
258
253
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
259
254
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
260
- ask: z2.string().min(1).max(1e4).optional().describe(
255
+ ask: z3.string().min(1).max(1e4).optional().describe(
261
256
  'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
262
257
  ),
263
- needs: z2.array(z2.string().min(1)).optional().describe(
258
+ needs: z3.array(z3.string().min(1)).optional().describe(
264
259
  "With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
265
260
  ),
266
- urgencyHint: z2.enum(["whenever", "soon", "now"]).optional().describe(
261
+ urgencyHint: z3.enum(["whenever", "soon", "now"]).optional().describe(
267
262
  "With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
268
263
  ),
269
264
  /** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
270
265
  * to the agent's work while it waits. Normalized server-side into those two
271
266
  * fields (normalizeWaiting) so everything downstream is untouched; explicit
272
267
  * urgencyHint/blocking win when both are sent. */
273
- waiting: z2.enum(["none", "soft", "hard"]).optional().describe(
268
+ waiting: z3.enum(["none", "soft", "hard"]).optional().describe(
274
269
  "With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
275
270
  ),
276
271
  /** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
@@ -278,123 +273,124 @@ var NotifyRequestFields = z2.object({
278
273
  * holding by default would charge every quiet claim that minute before any agent could
279
274
  * correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
280
275
  * and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
281
- confirm: z2.boolean().optional().describe(
276
+ confirm: z3.boolean().optional().describe(
282
277
  "Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
283
278
  ),
284
279
  /** #575: a RELAY of the user's explicitly stated preference, never the agent's
285
280
  * choice. Outranks waiting in both directions: 'call' rings even for a
286
281
  * waiting:'none' "call me when it's done"; 'message' never rings even for
287
282
  * waiting:'hard'. */
288
- channel: z2.enum(["call", "message"]).optional().describe(
283
+ channel: z3.enum(["call", "message"]).optional().describe(
289
284
  "Only if the user explicitly said how to reach them \u2014 'call me' \u2192 'call', 'just message/text me' \u2192 'message'. Omit otherwise; Paigy picks."
290
285
  ),
291
- confirmStyle: z2.enum(["yesno", "approve"]).default("yesno").describe(
286
+ confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
292
287
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
293
288
  ),
294
- blocking: z2.boolean().default(false).describe(
289
+ blocking: z3.boolean().default(false).describe(
295
290
  "Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
296
291
  )
297
292
  });
298
293
  var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
294
+ if (r.workId && r.goalId) ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
299
295
  const sealed = !!r.envelope;
300
296
  if (sealed) {
301
297
  if (!r.envelope?.context)
302
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
298
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
303
299
  for (const f of ["context", "options", "visuals"]) {
304
300
  if (r[f] !== void 0)
305
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
301
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
306
302
  }
307
303
  if (r.points !== void 0)
308
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
304
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
309
305
  if (r.ask !== void 0)
310
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
306
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
311
307
  if (r.needs !== void 0)
312
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
308
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
313
309
  return;
314
310
  }
315
311
  if (r.ask !== void 0) {
316
312
  for (const f of ["context", "select", "points"]) {
317
313
  if (r[f] !== void 0)
318
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
314
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
319
315
  }
320
316
  return;
321
317
  }
322
318
  if (r.needs !== void 0 || r.urgencyHint !== void 0)
323
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
319
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
324
320
  if (!r.context)
325
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
321
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
326
322
  if (!r.select)
327
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
323
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
328
324
  const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
329
325
  if (needsOptions && !r.options?.length)
330
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
326
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
331
327
  if (!needsOptions && r.options?.length)
332
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
328
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
333
329
  });
334
- var NotifyStatusSchema = z2.enum(["pending", "answered", "ignored"]);
335
- var AgentStateSchema = z2.enum(["idle", "in_progress", "completed", "needs_input"]);
336
- var SetTaskStateSchema = z2.object({
337
- state: z2.enum(["in_progress", "completed", "needs_input"])
330
+ var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
331
+ var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
332
+ var SetTaskStateSchema = z3.object({
333
+ state: z3.enum(["in_progress", "completed", "needs_input"])
338
334
  });
339
- var SetWorkStateSchema = z2.object({
340
- workId: z2.string().uuid().optional(),
341
- notificationId: z2.string().optional(),
335
+ var SetWorkStateSchema = z3.object({
336
+ workId: z3.string().uuid().optional(),
337
+ notificationId: z3.string().optional(),
342
338
  state: SetTaskStateSchema.shape.state
343
339
  }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
344
340
  message: "exactly one of workId or notificationId is required"
345
341
  });
346
- var TurnSchema = z2.object({
347
- prompt: z2.string(),
348
- reply: z2.string()
342
+ var TurnSchema = z3.object({
343
+ prompt: z3.string(),
344
+ reply: z3.string()
349
345
  });
350
- var UserAnswerSchema = z2.discriminatedUnion("kind", [
351
- z2.object({ kind: z2.literal("option"), optionId: z2.string(), label: z2.string().optional() }),
352
- z2.object({ kind: z2.literal("text"), text: z2.string() }),
353
- z2.object({ kind: z2.literal("ignored") }),
354
- z2.object({ kind: z2.literal("multi"), optionIds: z2.array(z2.string()), labels: z2.array(z2.string()).optional() }),
355
- z2.object({ kind: z2.literal("ranked"), optionIds: z2.array(z2.string()), labels: z2.array(z2.string()).optional() }),
356
- z2.object({ kind: z2.literal("clarify"), chunks: z2.array(z2.string()).min(1) }),
357
- z2.object({ kind: z2.literal("confirm"), approved: z2.boolean() }),
358
- z2.object({ kind: z2.literal("turns"), turns: z2.array(TurnSchema).min(1) }),
346
+ var UserAnswerSchema = z3.discriminatedUnion("kind", [
347
+ z3.object({ kind: z3.literal("option"), optionId: z3.string(), label: z3.string().optional() }),
348
+ z3.object({ kind: z3.literal("text"), text: z3.string() }),
349
+ z3.object({ kind: z3.literal("ignored") }),
350
+ z3.object({ kind: z3.literal("multi"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
351
+ z3.object({ kind: z3.literal("ranked"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
352
+ z3.object({ kind: z3.literal("clarify"), chunks: z3.array(z3.string()).min(1) }),
353
+ z3.object({ kind: z3.literal("confirm"), approved: z3.boolean() }),
354
+ z3.object({ kind: z3.literal("turns"), turns: z3.array(TurnSchema).min(1) }),
359
355
  /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
360
356
  * delivered through the same settle/await path as a human answer, carrying the judge's
361
357
  * derivation and the precedent ids it grew from. Always paired with a visible trail
362
358
  * card the user can reply to — the broker never overrides the user. */
363
- z2.object({ kind: z2.literal("precedent"), answer: z2.string(), derivation: z2.string(), sources: z2.array(z2.string()).min(1) })
359
+ z3.object({ kind: z3.literal("precedent"), answer: z3.string(), derivation: z3.string(), sources: z3.array(z3.string()).min(1) })
364
360
  ]);
365
- var IntentSchema = z2.object({
361
+ var IntentSchema = z3.object({
366
362
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
367
363
  // it by two ("detail", "feedback"), and because the settle handler parsed the array
368
364
  // all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
369
365
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
370
- kind: z2.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
371
- detail: z2.string(),
366
+ kind: z3.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
367
+ detail: z3.string(),
372
368
  /** Defer only: seconds until the callback the caller asked for, when something upstream
373
369
  * already read the time. Nothing sets it today (#397 documented an MCP parser that was
374
370
  * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
375
371
  * (`parseDelay`, #1292), and a value here simply wins over that reading. */
376
- dueInSeconds: z2.number().int().positive().optional(),
372
+ dueInSeconds: z3.number().int().positive().optional(),
377
373
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
378
374
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
379
375
  * 'other' on every row. A table that records that something was wrong and nothing
380
376
  * about what cannot answer "is the bot looping less this week?". */
381
- fault: z2.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
377
+ fault: z3.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
382
378
  });
383
- var RideAlongSchema = z2.object({
379
+ var RideAlongSchema = z3.object({
384
380
  /** The note this came from — assign/clarify/close it through /api/notes/:id. */
385
- noteId: z2.string(),
381
+ noteId: z3.string(),
386
382
  /** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
387
- text: z2.string(),
383
+ text: z3.string(),
388
384
  /** The thread to report back on, when the note was dispatched over the request rail. */
389
- parentId: z2.string().nullable()
385
+ parentId: z3.string().nullable()
390
386
  });
391
- var AwaitItemSchema = z2.discriminatedUnion("type", [
392
- z2.object({
393
- type: z2.literal("reply"),
394
- parentId: z2.string(),
395
- notificationId: z2.string(),
396
- workId: z2.string().uuid().optional(),
397
- decisionId: z2.string().uuid().optional(),
387
+ var AwaitItemSchema = z3.discriminatedUnion("type", [
388
+ z3.object({
389
+ type: z3.literal("reply"),
390
+ parentId: z3.string(),
391
+ notificationId: z3.string(),
392
+ workId: z3.string().uuid().optional(),
393
+ decisionId: z3.string().uuid().optional(),
398
394
  answer: UserAnswerSchema,
399
395
  /** E2EE: present when the answer is sealed. The server relays the opaque answer
400
396
  * envelope + the plaintext `ignored` status hint; the receiving agent OPENS it
@@ -402,7 +398,7 @@ var AwaitItemSchema = z2.discriminatedUnion("type", [
402
398
  * against `ignored` as tampering. Absent = today's plaintext answer (in `answer`).
403
399
  * On a sealed reply the plaintext `answer` is a placeholder (kind reflects only
404
400
  * the `ignored` bit) — never the real content, which stays sealed. */
405
- sealed: z2.lazy(() => SealedAnswerSchema).optional(),
401
+ sealed: z3.lazy(() => SealedAnswerSchema).optional(),
406
402
  /** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
407
403
  * #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
408
404
  * the notification they belong to, and the `contact` call that reaches the person
@@ -411,138 +407,147 @@ var AwaitItemSchema = z2.discriminatedUnion("type", [
411
407
  * "call me back after you merge" in their own words decides for itself what to do,
412
408
  * and now knows exactly which call to make. Absent when either half is missing —
413
409
  * a sentence with a hole in it is worse than no sentence. */
414
- note: z2.string().optional(),
410
+ note: z3.string().optional(),
415
411
  /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
416
412
  * shaped answer was mapped from, filtered to its own claims. There is no second list
417
413
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
418
414
  * 2026-09-04): the agent reads the sentence and decides. */
419
- transcript: z2.string().optional(),
415
+ transcript: z3.string().optional(),
420
416
  /** Coverage report (#396), when the ask declared `points`: which of them this
421
417
  * answer addressed. Missing points = re-ask or proceed knowingly partial. */
422
- covered: z2.array(z2.string()).optional(),
418
+ covered: z3.array(z3.string()).optional(),
423
419
  /** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
424
420
  * became free. Only `reply` and `idle` carry it: those are the two outcomes that
425
421
  * END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
426
422
  * agent a side-quest while it is still holding the line is how the main thing gets
427
423
  * dropped. Absent/empty = nothing owed. */
428
- also: z2.array(RideAlongSchema).optional()
424
+ also: z3.array(RideAlongSchema).optional()
429
425
  }),
430
- z2.object({
431
- type: z2.literal("remind"),
432
- parentId: z2.string(),
433
- notificationId: z2.string(),
434
- remindAt: z2.string().datetime({ offset: true }),
426
+ z3.object({
427
+ type: z3.literal("remind"),
428
+ parentId: z3.string(),
429
+ notificationId: z3.string(),
430
+ remindAt: z3.string().datetime({ offset: true }),
435
431
  /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
436
- remindInSeconds: z2.number()
432
+ remindInSeconds: z3.number()
437
433
  }),
438
434
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
439
435
  * post-feedback revision, #633) — the user will never answer this id. Stop
440
436
  * awaiting it; the live ask is the thread's newest turn (await that one, or
441
437
  * re-orient via get_thread / check_replies). */
442
- z2.object({
443
- type: z2.literal("superseded"),
444
- parentId: z2.string(),
445
- notificationId: z2.string()
438
+ z3.object({
439
+ type: z3.literal("superseded"),
440
+ parentId: z3.string(),
441
+ notificationId: z3.string()
446
442
  }),
447
443
  /** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
448
444
  * revise any of these until the final reply arrives — partial = intelligence,
449
445
  * settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
450
446
  * irreversibly. If `acts` carries a question aimed at you and you know the answer,
451
447
  * contact on the same thread right away — the caller hears it on the same call. */
452
- z2.object({
453
- type: z2.literal("partial"),
454
- notificationId: z2.string(),
455
- inFlight: z2.literal(true),
456
- turn: z2.object({
457
- idx: z2.number(),
458
- prompt: z2.string(),
459
- reply: z2.string(),
460
- acts: z2.array(IntentSchema).nullable().optional()
448
+ z3.object({
449
+ type: z3.literal("partial"),
450
+ notificationId: z3.string(),
451
+ inFlight: z3.literal(true),
452
+ turn: z3.object({
453
+ idx: z3.number(),
454
+ prompt: z3.string(),
455
+ reply: z3.string(),
456
+ acts: z3.array(IntentSchema).nullable().optional()
461
457
  })
462
458
  }),
463
- z2.object({ type: z2.literal("idle"), also: z2.array(RideAlongSchema).optional() })
459
+ z3.object({
460
+ type: z3.literal("idle"),
461
+ also: z3.array(RideAlongSchema).optional(),
462
+ /** Is a call live for this agent's user right now? The SDK polls the partial stream
463
+ * (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
464
+ * during a live call, and polling for one on a banner/message was a wasted HTTP call +
465
+ * 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
466
+ * Absent = an older API → the SDK keeps polling, exactly as before. */
467
+ inFlight: z3.boolean().optional()
468
+ })
464
469
  ]);
465
- var CallbackTriggerSchema = z2.enum(["on_done", "on_blocked", "scheduled"]);
466
- var ScheduleCallbackSchema = z2.object({
467
- parentId: z2.string().describe("The thread to call back on (from a prior contact / reply / request)."),
468
- goalId: z2.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
470
+ var CallbackTriggerSchema = z3.enum(["on_done", "on_blocked", "scheduled"]);
471
+ var ScheduleCallbackSchema = z3.object({
472
+ parentId: z3.string().describe("The thread to call back on (from a prior contact / reply / request)."),
473
+ goalId: z3.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
469
474
  trigger: CallbackTriggerSchema,
470
- dueInSeconds: z2.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
471
- note: z2.string().optional().describe("What to tell the user when you follow up.")
475
+ dueInSeconds: z3.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
476
+ note: z3.string().optional().describe("What to tell the user when you follow up.")
472
477
  });
473
- var PendingRepliesSchema = z2.object({
474
- replies: z2.array(
475
- z2.object({
476
- parentId: z2.string(),
477
- notificationId: z2.string(),
478
- workId: z2.string().uuid().optional(),
479
- decisionId: z2.string().uuid().optional(),
478
+ var PendingRepliesSchema = z3.object({
479
+ replies: z3.array(
480
+ z3.object({
481
+ parentId: z3.string(),
482
+ notificationId: z3.string(),
483
+ workId: z3.string().uuid().optional(),
484
+ decisionId: z3.string().uuid().optional(),
480
485
  answer: UserAnswerSchema,
481
486
  /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
482
487
  * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
483
488
  * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
484
- sealed: z2.lazy(() => SealedAnswerSchema).optional(),
489
+ sealed: z3.lazy(() => SealedAnswerSchema).optional(),
485
490
  /** The call record rendered for THIS agent: the raw words the shaped answer was
486
491
  * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
487
- transcript: z2.string().optional(),
492
+ transcript: z3.string().optional(),
488
493
  /** Coverage report (#396): which declared `points` this answer addressed. */
489
- covered: z2.array(z2.string()).optional()
494
+ covered: z3.array(z3.string()).optional()
490
495
  })
491
496
  ),
492
- pending: z2.array(
493
- z2.object({ parentId: z2.string(), notificationId: z2.string(), createdAt: z2.string() })
497
+ pending: z3.array(
498
+ z3.object({ parentId: z3.string(), notificationId: z3.string(), createdAt: z3.string() })
494
499
  ),
495
500
  /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
496
501
  * sees for this session's identity. From inside a session there was no way to find out —
497
502
  * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
498
503
  * agent that cannot tell which agent it is cannot tell whether work addressed to
499
504
  * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
500
- you: z2.object({ name: z2.string(), device: z2.string().nullable(), tokenId: z2.string() }).optional(),
505
+ you: z3.object({ name: z3.string(), device: z3.string().nullable(), tokenId: z3.string() }).optional(),
501
506
  /** User-initiated requests addressed to this agent; act on them and reply via
502
507
  * contact on the same parentId. Keeps reappearing until you call
503
508
  * set_work_state on its workId (or notificationId once while bootstrapping). */
504
- requests: z2.array(
505
- z2.object({
506
- parentId: z2.string(),
507
- notificationId: z2.string(),
508
- workId: z2.string().uuid().optional(),
509
- text: z2.string(),
510
- createdAt: z2.string(),
509
+ requests: z3.array(
510
+ z3.object({
511
+ parentId: z3.string(),
512
+ notificationId: z3.string(),
513
+ workId: z3.string().uuid().optional(),
514
+ text: z3.string(),
515
+ createdAt: z3.string(),
511
516
  /** The user seeded this request with a past conversation — call get_thread on it
512
517
  * FIRST and treat the transcript as prior context (#57/#251). */
513
- contextParentId: z2.string().optional(),
518
+ contextParentId: z3.string().optional(),
514
519
  /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
515
520
  * the account — the name here — which has not been seen since it landed, so nobody
516
521
  * came for it. Handed to you because you are the session that is here. Take it like
517
522
  * any request (set_work_state claims it, reply with contact on its parentId), and say
518
523
  * whose it was, because the user chose that agent on purpose. */
519
- stranded: z2.string().optional()
524
+ stranded: z3.string().optional()
520
525
  })
521
526
  ),
522
527
  /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
523
528
  * notification lists remain during migration so old clients keep their existing view. */
524
- work: z2.array(z2.object({
525
- workId: z2.string().uuid(),
526
- parentId: z2.string().optional(),
527
- objective: z2.string().nullable(),
528
- state: z2.enum(["active", "blocked", "waiting_external"]),
529
- blockedOn: z2.array(z2.string().uuid()),
530
- updatedAt: z2.string()
529
+ work: z3.array(z3.object({
530
+ workId: z3.string().uuid(),
531
+ parentId: z3.string().optional(),
532
+ objective: z3.string().nullable(),
533
+ state: z3.enum(["active", "blocked", "waiting_external"]),
534
+ blockedOn: z3.array(z3.string().uuid()),
535
+ updatedAt: z3.string()
531
536
  })).optional(),
532
537
  /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
533
538
  * if blocked, or at a time that has passed). Re-surfaced every sweep until you
534
539
  * fulfill one by calling contact on its parentId. */
535
540
  /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
536
541
  * reach. Same array the contact/await replies carry — one queue, every carrier. */
537
- also: z2.array(RideAlongSchema).optional(),
538
- owedCallbacks: z2.array(
539
- z2.object({ parentId: z2.string(), trigger: CallbackTriggerSchema, note: z2.string() })
542
+ also: z3.array(RideAlongSchema).optional(),
543
+ owedCallbacks: z3.array(
544
+ z3.object({ parentId: z3.string(), trigger: CallbackTriggerSchema, note: z3.string() })
540
545
  ),
541
546
  /** Work (either direction) you reported in_progress a while ago and never reported
542
547
  * completed — likely left half-done by this session or a prior one that crashed or
543
548
  * went idle. Report a real state (set_work_state) or continue the work. */
544
- stalled: z2.array(
545
- z2.object({ parentId: z2.string(), notificationId: z2.string(), title: z2.string().nullable(), startedAt: z2.string() })
549
+ stalled: z3.array(
550
+ z3.object({ parentId: z3.string(), notificationId: z3.string(), title: z3.string().nullable(), startedAt: z3.string() })
546
551
  ),
547
552
  /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
548
553
  * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
@@ -551,125 +556,132 @@ var PendingRepliesSchema = z2.object({
551
556
  * ride the next turn. `items` are that thread's replies/requests in arrival order; the
552
557
  * full payload for each is in the flat `replies`/`requests` arrays (matched by
553
558
  * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
554
- threads: z2.array(
555
- z2.object({
556
- parentId: z2.string(),
557
- busy: z2.boolean(),
558
- items: z2.array(
559
- z2.object({
560
- kind: z2.enum(["reply", "request"]),
561
- notificationId: z2.string(),
562
- at: z2.string()
559
+ threads: z3.array(
560
+ z3.object({
561
+ parentId: z3.string(),
562
+ busy: z3.boolean(),
563
+ items: z3.array(
564
+ z3.object({
565
+ kind: z3.enum(["reply", "request"]),
566
+ notificationId: z3.string(),
567
+ at: z3.string()
563
568
  })
564
569
  )
565
570
  })
566
571
  )
567
572
  });
568
- var NotifyResponseSchema = z2.object({
569
- notificationId: z2.string(),
570
- workId: z2.string().uuid().optional(),
571
- decisionId: z2.string().uuid().optional(),
573
+ var NotifyResponseSchema = z3.object({
574
+ notificationId: z3.string(),
575
+ workId: z3.string().uuid().optional(),
576
+ decisionId: z3.string().uuid().optional(),
572
577
  status: NotifyStatusSchema,
573
- createdAt: z2.string().datetime(),
578
+ createdAt: z3.string().datetime(),
574
579
  answer: UserAnswerSchema.optional(),
575
- answeredAt: z2.string().datetime().optional(),
580
+ answeredAt: z3.string().datetime().optional(),
576
581
  /** Ride-alongs for THIS agent — pending work it should pick up when it's done with
577
582
  * what it came for. Present on any reply, because an unwakeable agent's only
578
583
  * reliable moment is one it initiated. Absent/empty = nothing owed. */
579
- also: z2.array(RideAlongSchema).optional()
584
+ also: z3.array(RideAlongSchema).optional()
580
585
  });
581
- var NotifyPlanUnitSchema = z2.object({
582
- notificationId: z2.string(),
586
+ var NotifyPlanUnitSchema = z3.object({
587
+ notificationId: z3.string(),
583
588
  /** The unit's own heading, so the agent can tell which of its paragraphs this became. */
584
- title: z2.string(),
589
+ title: z3.string(),
585
590
  /** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
586
591
  level: NotifyLevelSchema,
587
592
  /** Answered from something the user already decided: nobody is interrupted, and a trail card
588
593
  * says so. The agent should not wait on this one. */
589
- settled: z2.literal(true).optional(),
594
+ settled: z3.literal(true).optional(),
590
595
  /** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
591
596
  * agent — nothing here changed the ask, and ignoring it costs nothing. */
592
- needs: z2.array(z2.enum(["options", "visuals"])).optional(),
597
+ needs: z3.array(z3.enum(["options", "visuals"])).optional(),
593
598
  /** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
594
599
  * split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
595
600
  * broker that DECIDES that destroys the only fact separating a statement from a real ask
596
601
  * (#731), so it is offered, never applied: the unit is stored `text` until the agent
597
602
  * confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
598
- proposal: z2.object({
603
+ proposal: z3.object({
599
604
  select: SelectShapeSchema,
600
- options: z2.array(z2.object({ label: z2.string().min(1) })).optional()
605
+ options: z3.array(z3.object({ label: z3.string().min(1) })).optional()
601
606
  }).optional(),
602
607
  /** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
603
608
  * between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
604
609
  * needs to know. Reported so the agent can correct a misread the same way it ratifies a
605
610
  * shape — the read RAISES (a decision always asks) and never silences a question the
606
611
  * agent declared (#731, #923). */
607
- wants: z2.enum(["decision", "approval", "knowledge"]).optional()
612
+ wants: z3.enum(["decision", "approval", "knowledge"]).optional()
608
613
  });
609
- var NotifyPlanSchema = z2.object({
610
- units: z2.array(NotifyPlanUnitSchema),
614
+ var NotifyPlanSchema = z3.object({
615
+ units: z3.array(NotifyPlanUnitSchema),
611
616
  /** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
612
617
  * was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
613
- speaks: z2.string().optional()
618
+ speaks: z3.string().optional()
614
619
  });
615
- var UserResponseSchema = z2.object({
616
- requestId: z2.string(),
620
+ var UserResponseSchema = z3.object({
621
+ requestId: z3.string(),
617
622
  answer: UserAnswerSchema,
618
- answeredAt: z2.string().datetime(),
619
- transcript: z2.string().optional(),
623
+ answeredAt: z3.string().datetime(),
624
+ transcript: z3.string().optional(),
620
625
  /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the item was
621
626
  * E2EE-sealed. Present → the server persists it opaquely and branches status on `ignored`;
622
627
  * the plaintext `answer` is a placeholder (`{ kind: "ignored" }`) the server ignores for a
623
628
  * sealed row. Absent = today's plaintext answer, unchanged. */
624
- sealed: z2.lazy(() => SealedAnswerSchema).optional(),
629
+ sealed: z3.lazy(() => SealedAnswerSchema).optional(),
625
630
  /** Coverage report (#396): which of the ask's declared `points` were addressed. */
626
- covered: z2.array(z2.string()).optional()
631
+ covered: z3.array(z3.string()).optional()
627
632
  });
628
- var VoiceKeySchema = z2.enum(["rachel", "george", "jessica", "brian", "lily"]);
629
- var AgendaTurnSchema = z2.object({
633
+ var VoiceKeySchema = z3.enum(["rachel", "george", "jessica", "brian", "lily"]);
634
+ var AgendaTurnSchema = z3.object({
635
+ /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
636
+ * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
637
+ * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
638
+ * completion of a streamed re-plan carries the published sentence again, and the walk
639
+ * drops what it already said by identity, never by the API's guess of what was polled.
640
+ * Absent on plans nothing streams (a ring plan, a floor). */
641
+ id: z3.string().optional(),
630
642
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
631
643
  * the planner declares duplicates instead of asking them twice. */
632
- coveredIds: z2.array(z2.string()).optional(),
644
+ coveredIds: z3.array(z3.string()).optional(),
633
645
  /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
634
646
  * line went out on 2026-07-28 and the caller could not answer it at all. */
635
- info: z2.array(z2.string().min(1)).max(3).default([]),
636
- question: z2.string().min(1).nullable(),
647
+ info: z3.array(z3.string().min(1)).max(3).default([]),
648
+ question: z3.string().min(1).nullable(),
637
649
  /** True on the one turn carrying the agent's own declared question. */
638
- asks: z2.boolean().optional(),
650
+ asks: z3.boolean().optional(),
639
651
  /** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
640
652
  * Absent on a single-claim plan (the session's own claim) and on shared context turns,
641
653
  * which route nothing. */
642
- claimId: z2.string().optional(),
654
+ claimId: z3.string().optional(),
643
655
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
644
- voice: z2.string().optional(),
656
+ voice: z3.string().optional(),
645
657
  /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
646
658
  * whose request this is: an item that folded in from another agent arrived as a bare
647
659
  * non-sequitur ("First real production sign-in is yours to make whenever you want.")
648
660
  * and the owner answered "What?". The bot names the agent before its first turn. */
649
- agent: z2.string().optional(),
661
+ agent: z3.string().optional(),
650
662
  /** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
651
663
  * same id a face is minted from. A name is not an identity: two pairings may be called
652
664
  * "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
653
665
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
654
666
  * speaks for several agents — the turn is the only place that knows which. */
655
- agentId: z2.string().optional(),
667
+ agentId: z3.string().optional(),
656
668
  select: SelectShapeSchema.optional(),
657
- options: z2.array(OptionSchema.omit({ id: true })).optional(),
669
+ options: z3.array(OptionSchema.omit({ id: true })).optional(),
658
670
  /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
659
671
  * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
660
672
  * (the beat for context, the answer window for asks). Clamped bot-side. */
661
- pace: z2.number().positive().optional(),
673
+ pace: z3.number().positive().optional(),
662
674
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
663
675
  * (a question blocks, context flows). blocking:false on a question = ask and move
664
676
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
665
- blocking: z2.boolean().optional()
677
+ blocking: z3.boolean().optional()
666
678
  });
667
679
  var CLAIM_STALE_MS = 30 * 6e4;
668
- var InboxItemSchema = z2.object({
669
- id: z2.string(),
680
+ var InboxItemSchema = z3.object({
681
+ id: z3.string(),
670
682
  /** The conversation thread + connection this item lives on. Present on the replied
671
683
  * detail — they power History's "Continue" / "New session from this" (#57/#251). */
672
- parentId: z2.string().optional(),
684
+ parentId: z3.string().optional(),
673
685
  /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
674
686
  * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
675
687
  * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
@@ -677,13 +689,13 @@ var InboxItemSchema = z2.object({
677
689
  * unrelated updates as a single "12-part request". Absent on rows written before the
678
690
  * `asks` table, and on anything that never went through `notify` — both fall back to the
679
691
  * thread, which is what the client did for all rows until now. */
680
- askId: z2.string().optional(),
692
+ askId: z3.string().optional(),
681
693
  /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
682
694
  * shares one `created_at` to the microsecond, so without it the author's order is
683
695
  * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
684
696
  * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
685
697
  * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
686
- seq: z2.number().int().optional(),
698
+ seq: z3.number().int().optional(),
687
699
  /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
688
700
  * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
689
701
  * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
@@ -692,13 +704,13 @@ var InboxItemSchema = z2.object({
692
704
  * server that can still see every row states it. Absent on any row with no `askId`: a
693
705
  * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
694
706
  * the one part of its own arrival a single row cannot answer. */
695
- units: z2.number().int().positive().optional(),
696
- tokenId: z2.string().optional(),
707
+ units: z3.number().int().positive().optional(),
708
+ tokenId: z3.string().optional(),
697
709
  status: NotifyStatusSchema,
698
710
  context: ContextSchema,
699
- options: z2.array(OptionSchema).optional(),
711
+ options: z3.array(OptionSchema).optional(),
700
712
  /** The ask's declared coverage points (#396), when the agent sent them. */
701
- points: z2.array(z2.string()).optional(),
713
+ points: z3.array(z3.string()).optional(),
702
714
  /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
703
715
  * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
704
716
  * between them could not express a call as a sequence. `question: null` is a real turn —
@@ -707,12 +719,12 @@ var InboxItemSchema = z2.object({
707
719
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
708
720
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
709
721
  * and neither of those is "got it" (owner, 2026-08-10). */
710
- asks: z2.boolean().optional(),
722
+ asks: z3.boolean().optional(),
711
723
  /** When a live process last pulsed for this row's agent — the liveness input for
712
724
  * "working requires a pulse" (#928): the list said "Working…" from agent_state alone
713
725
  * while the party called the same dead claim stalled. Absent = no token/no data,
714
726
  * which must never CLAIM stalled. */
715
- lastSeenAt: z2.string().optional(),
727
+ lastSeenAt: z3.string().optional(),
716
728
  /** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
717
729
  * the `notification_events` ledger (trigger-written since 20260621010000, so every row a
718
730
  * user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
@@ -722,53 +734,53 @@ var InboxItemSchema = z2.object({
722
734
  * work. Reading the row's birth as the claim's age brands that "No update in 8h" the
723
735
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
724
736
  * `createdAt`. */
725
- agentStateAt: z2.string().datetime().optional(),
726
- agenda: z2.array(AgendaTurnSchema).optional(),
727
- visuals: z2.array(VisualSchema).optional(),
737
+ agentStateAt: z3.string().datetime().optional(),
738
+ agenda: z3.array(AgendaTurnSchema).optional(),
739
+ visuals: z3.array(VisualSchema).optional(),
728
740
  /** The connected agent's name (the single pairing name — user-typed, or the
729
741
  * agent's suggestion, or a default silly name). */
730
- name: z2.string(),
742
+ name: z3.string(),
731
743
  /** The pairing's assigned voice (#462); absent = the default voice. */
732
744
  voice: VoiceKeySchema.optional(),
733
- repo: z2.string().optional(),
734
- branch: z2.string().optional(),
735
- createdAt: z2.string().datetime(),
736
- snoozedUntil: z2.string().datetime().optional(),
745
+ repo: z3.string().optional(),
746
+ branch: z3.string().optional(),
747
+ createdAt: z3.string().datetime(),
748
+ snoozedUntil: z3.string().datetime().optional(),
737
749
  agentState: AgentStateSchema.default("idle"),
738
750
  /** Whose action the item is waiting on: "you" = an agent asked you (the default,
739
751
  * every agent→user notification); "agent" = you sent a request and it's awaiting the
740
752
  * agent (held in the inbox until the agent replies on the thread). */
741
- turn: z2.enum(["you", "agent"]).default("you"),
753
+ turn: z3.enum(["you", "agent"]).default("you"),
742
754
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
743
755
  * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
744
756
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
745
- error: z2.string().optional(),
746
- clarifies: z2.string().optional(),
757
+ error: z3.string().optional(),
758
+ clarifies: z3.string().optional(),
747
759
  /** The ring ladder ran out while this was still pending — we tried to reach you and
748
760
  * STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
749
761
  * agent with nothing to say, which the roster drew identically until now: "nothing to
750
762
  * say" and "gave up saying it" are opposite situations wearing the same face
751
763
  * (navigation-design.md, gap 1). False for anything that never rang. */
752
- gaveUp: z2.boolean().default(false),
764
+ gaveUp: z3.boolean().default(false),
753
765
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
754
766
  * Absent for anything never delivered through a push, and for older rows written before
755
767
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
756
768
  * 2026-08-07) — its real job is to give "this didn't need a call" something to be
757
769
  * feedback ABOUT. */
758
- why: z2.object({
770
+ why: z3.object({
759
771
  asked: NotifyLevelSchema,
760
772
  got: NotifyLevelSchema,
761
- because: z2.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
762
- line: z2.string()
773
+ because: z3.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
774
+ line: z3.string()
763
775
  }).optional(),
764
- select: z2.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
765
- confirmStyle: z2.enum(["yesno", "approve"]).default("yesno").describe(
776
+ select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
777
+ confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
766
778
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
767
779
  ),
768
780
  /** Real downstream work is stuck behind this one — set by the agent, independent of
769
781
  * urgency (see the main README's "premier use case" + notify/states.md). Drives the
770
782
  * inbox's blocking badge and the extra confirm step before dismissing it. */
771
- blocking: z2.boolean().default(false),
783
+ blocking: z3.boolean().default(false),
772
784
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
773
785
  answer: UserAnswerSchema.optional(),
774
786
  /** E2EE (text lane): the sealed content this item carries when the pairing is E2EE, in
@@ -777,39 +789,39 @@ var InboxItemSchema = z2.object({
777
789
  * NotifyRequest.envelope. The native app decrypts it locally via openNotification;
778
790
  * web / an un-enrolled device can't and renders a locked placeholder. Absent = today's
779
791
  * plaintext item (context carries the cleartext), so plaintext items are unchanged. */
780
- envelope: z2.object({
781
- context: z2.lazy(() => EnvelopeSchema).optional(),
782
- options: z2.lazy(() => EnvelopeSchema).optional(),
783
- visuals: z2.lazy(() => EnvelopeSchema).optional()
792
+ envelope: z3.object({
793
+ context: z3.lazy(() => EnvelopeSchema).optional(),
794
+ options: z3.lazy(() => EnvelopeSchema).optional(),
795
+ visuals: z3.lazy(() => EnvelopeSchema).optional()
784
796
  }).optional(),
785
797
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
786
798
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
787
799
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
788
- agentX25519: z2.string().optional()
800
+ agentX25519: z3.string().optional()
789
801
  });
790
- var SnoozeRequestSchema = z2.object({
791
- requestId: z2.string(),
792
- until: z2.string().datetime()
802
+ var SnoozeRequestSchema = z3.object({
803
+ requestId: z3.string(),
804
+ until: z3.string().datetime()
793
805
  });
794
806
  var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
795
- var PushTokenSchema = z2.object({
796
- voipToken: z2.string().min(1).optional(),
797
- alertToken: z2.string().min(1).optional(),
798
- fcmToken: z2.string().min(1).optional(),
799
- platform: z2.enum(["ios", "android"])
807
+ var PushTokenSchema = z3.object({
808
+ voipToken: z3.string().min(1).optional(),
809
+ alertToken: z3.string().min(1).optional(),
810
+ fcmToken: z3.string().min(1).optional(),
811
+ platform: z3.enum(["ios", "android"])
800
812
  }).superRefine((v, ctx) => {
801
813
  if (v.platform !== "ios") return;
802
814
  for (const field of ["voipToken", "alertToken"]) {
803
815
  const token = v[field];
804
816
  if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
805
817
  ctx.addIssue({
806
- code: z2.ZodIssueCode.custom,
818
+ code: z3.ZodIssueCode.custom,
807
819
  path: [field],
808
820
  message: `not an APNs device token (want 64 hex chars, got ${token.length})`
809
821
  });
810
822
  }
811
823
  });
812
- var MissedCallSchema = z2.enum([
824
+ var MissedCallSchema = z3.enum([
813
825
  "retry_10m",
814
826
  "retry_30m",
815
827
  "retry_60m",
@@ -819,32 +831,32 @@ var MissedCallSchema = z2.enum([
819
831
  "inbox",
820
832
  "dismiss"
821
833
  ]);
822
- var BrokerTuningSchema = z2.object({
834
+ var BrokerTuningSchema = z3.object({
823
835
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
824
- ackVerbosity: z2.enum(["normal", "none"]).optional(),
836
+ ackVerbosity: z3.enum(["normal", "none"]).optional(),
825
837
  /** How readily the mapper asks its one clarification: 'low' = only when truly
826
838
  * uninterpretable, 'high' = whenever not fully certain. */
827
- clarifyEagerness: z2.enum(["low", "normal", "high"]).optional(),
839
+ clarifyEagerness: z3.enum(["low", "normal", "high"]).optional(),
828
840
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
829
- phrasebook: z2.array(z2.object({ say: z2.string().min(1).max(60), mean: z2.string().min(1).max(120) })).max(24).optional(),
841
+ phrasebook: z3.array(z3.object({ say: z3.string().min(1).max(60), mean: z3.string().min(1).max(120) })).max(24).optional(),
830
842
  /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
831
843
  * which is every account today — means the agent's own words decide, per ask: a call
832
844
  * about an English ask opens in English. This is the only thing that overrides that,
833
845
  * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
834
846
  * Set per user (no UI yet), like `voiceTuning`. */
835
- language: z2.enum(["en", "es"]).optional()
847
+ language: z3.enum(["en", "es"]).optional()
836
848
  });
837
- var UserSettingsSchema = z2.object({
838
- permissions: z2.object({
839
- call: z2.boolean(),
840
- banner: z2.boolean(),
841
- push: z2.boolean()
849
+ var UserSettingsSchema = z3.object({
850
+ permissions: z3.object({
851
+ call: z3.boolean(),
852
+ banner: z3.boolean(),
853
+ push: z3.boolean()
842
854
  }),
843
- sessionMode: z2.enum(["default", "all_calls", "silent"]),
844
- silentPush: z2.boolean(),
845
- autoCallback: z2.boolean(),
855
+ sessionMode: z3.enum(["default", "all_calls", "silent"]),
856
+ silentPush: z3.boolean(),
857
+ autoCallback: z3.boolean(),
846
858
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
847
- improveConsent: z2.boolean(),
859
+ improveConsent: z3.boolean(),
848
860
  missedCall: MissedCallSchema.default("backoff_standard"),
849
861
  /** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
850
862
  * (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
@@ -852,7 +864,7 @@ var UserSettingsSchema = z2.object({
852
864
  * Optional, NOT defaulted: a stale client PATCHing the full settings object
853
865
  * must not silently reset this privacy choice. Absent = leave unchanged on
854
866
  * write, 'hosted' on read (see store.ts). */
855
- voiceMode: z2.enum(["hosted", "on_device"]).optional(),
867
+ voiceMode: z3.enum(["hosted", "on_device"]).optional(),
856
868
  /** Per-user ring budget (#603): calls per rolling day before further calls
857
869
  * degrade to banner. Absent = the global default (25). A number, never a
858
870
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -860,10 +872,10 @@ var UserSettingsSchema = z2.object({
860
872
  * payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
861
873
  * slower speaker). No API-side semantics; the bot resolves each key with its
862
874
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
863
- voiceTuning: z2.record(z2.string(), z2.union([z2.number(), z2.string()])).optional(),
875
+ voiceTuning: z3.record(z3.string(), z3.union([z3.number(), z3.string()])).optional(),
864
876
  /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
865
877
  * defaulted — an older client PATCHing the full object must not clobber it. */
866
- pstnCalls: z2.boolean().optional(),
878
+ pstnCalls: z3.boolean().optional(),
867
879
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
868
880
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
869
881
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -872,59 +884,59 @@ var UserSettingsSchema = z2.object({
872
884
  * that failure reads as the reminder rail being unreliable rather than as a missing
873
885
  * setting. Absent = a spoken time can't be landed, so the reminder rides the next
874
886
  * call — honest about what we know. */
875
- timezone: z2.string().min(1).max(64).optional(),
887
+ timezone: z3.string().min(1).max(64).optional(),
876
888
  /** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
877
889
  * content is sealed end-to-end between the local agent and the phone. Like
878
890
  * voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
879
891
  * settings object without it can't silently flip the account's E2EE state.
880
892
  * Absent = leave unchanged on write, 'off' on read (see store.ts). The demo
881
893
  * account is plaintext by construction and refuses any non-'off' value. */
882
- e2eeMode: z2.enum(["off", "on"]).optional(),
894
+ e2eeMode: z3.enum(["off", "on"]).optional(),
883
895
  /** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
884
896
  * clobber guard as voiceMode: absent = leave unchanged on write. */
885
897
  broker: BrokerTuningSchema.optional()
886
898
  });
887
- var HistoryItemSchema = z2.object({
888
- id: z2.string(),
889
- parentId: z2.string(),
899
+ var HistoryItemSchema = z3.object({
900
+ id: z3.string(),
901
+ parentId: z3.string(),
890
902
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
891
- initiator: z2.enum(["user", "agent"]),
892
- title: z2.string(),
903
+ initiator: z3.enum(["user", "agent"]),
904
+ title: z3.string(),
893
905
  /** The agent on the other end (its name). */
894
- name: z2.string(),
895
- createdAt: z2.string(),
906
+ name: z3.string(),
907
+ createdAt: z3.string(),
896
908
  /** When the agent fetched your request (user→agent only). */
897
- agentAckedAt: z2.string().nullable(),
909
+ agentAckedAt: z3.string().nullable(),
898
910
  /** When you answered the agent's notification (agent→user only). */
899
- humanAckedAt: z2.string().nullable()
911
+ humanAckedAt: z3.string().nullable()
900
912
  });
901
913
  var ACTIVITY_LINES = 2;
902
914
  var ACTIVITY_LINE_MAX = 80;
903
- var AgentActivitySchema = z2.object({
915
+ var AgentActivitySchema = z3.object({
904
916
  /** Oldest first, so the newest line is last — the one that replaces in place. */
905
- lines: z2.array(z2.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
917
+ lines: z3.array(z3.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
906
918
  /** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
907
919
  * that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
908
- at: z2.string().datetime()
920
+ at: z3.string().datetime()
909
921
  });
910
- var ConnectionSummarySchema = z2.object({
922
+ var ConnectionSummarySchema = z3.object({
911
923
  /** The connection = the agent's token id (used to address a request). */
912
- id: z2.string(),
924
+ id: z3.string(),
913
925
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
914
926
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
915
927
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
916
928
  * apps/api/src/tokens/devices-vs-agents-design.md. */
917
- kind: z2.enum(["device", "agent"]).optional(),
929
+ kind: z3.enum(["device", "agent"]).optional(),
918
930
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
919
931
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
920
932
  * agents (phone-launched, provider-managed, or minted before the link existed). */
921
- mintedByDevice: z2.string().nullable().optional(),
922
- device: z2.string().nullable(),
933
+ mintedByDevice: z3.string().nullable().optional(),
934
+ device: z3.string().nullable(),
923
935
  /** The agent's display name (the single pairing name). */
924
- name: z2.string(),
936
+ name: z3.string(),
925
937
  /** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
926
938
  * label; null for a local connection. Sourced from the token's provider, not the name. */
927
- provider: z2.string().nullable(),
939
+ provider: z3.string().nullable(),
928
940
  /** The pairing's assigned voice (#462); null = the default voice. */
929
941
  voice: VoiceKeySchema.nullable(),
930
942
  /** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
@@ -935,19 +947,19 @@ var ConnectionSummarySchema = z2.object({
935
947
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
936
948
  * set once must not overrule a rule they set about one agent. */
937
949
  reach: NotifyLevelSchema.nullable().optional(),
938
- createdAt: z2.string().datetime(),
950
+ createdAt: z3.string().datetime(),
939
951
  /** Most recent notification on this connection, either direction. Null = no contact yet.
940
952
  * Drives the agents-page recency grouping (Today / This week / …). */
941
- lastContactAt: z2.string().datetime().nullable(),
953
+ lastContactAt: z3.string().datetime().nullable(),
942
954
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
943
955
  * desktop app while open. Null = never seen; stale = offline. */
944
- lastSeenAt: z2.string().datetime().nullable().optional(),
956
+ lastSeenAt: z3.string().datetime().nullable().optional(),
945
957
  /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
946
958
  * harness availabilities + granted workspaces — the option set the phone's
947
959
  * "new session" sheet offers. Absent for ordinary MCP agents. */
948
- runtime: z2.object({
949
- harnesses: z2.array(z2.object({ name: z2.string(), label: z2.string(), status: z2.string() })).optional(),
950
- workspaces: z2.array(z2.string()).optional()
960
+ runtime: z3.object({
961
+ harnesses: z3.array(z3.object({ name: z3.string(), label: z3.string(), status: z3.string() })).optional(),
962
+ workspaces: z3.array(z3.string()).optional()
951
963
  }).optional(),
952
964
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
953
965
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -956,103 +968,119 @@ var ConnectionSummarySchema = z2.object({
956
968
  activity: AgentActivitySchema.optional(),
957
969
  /** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
958
970
  * false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
959
- managed: z2.boolean()
971
+ managed: z3.boolean()
972
+ });
973
+ var LedgerItemSchema = z3.object({ id: z3.string(), parentId: z3.string(), title: z3.string(), createdAt: z3.string() });
974
+ var AgentLedgerSchema = z3.object({
975
+ agent: z3.object({ id: z3.string(), name: z3.string(), revokedAt: z3.string().nullable() }),
976
+ /** Its own questions you have not answered. */
977
+ asks: z3.array(LedgerItemSchema),
978
+ /** Its questions you answered that nobody acted on — still owed to somebody. */
979
+ answered: z3.array(LedgerItemSchema),
980
+ /** Requests you sent it that it never took. */
981
+ requests: z3.array(LedgerItemSchema),
982
+ goals: z3.array(z3.object({ id: z3.string(), outcome: z3.string(), state: z3.string() })),
983
+ callbacks: z3.array(z3.object({ id: z3.string(), parentId: z3.string(), trigger: z3.string(), note: z3.string(), dueAt: z3.string().nullable() }))
960
984
  });
961
- var MoveRingSchema = z2.enum(["home", "travels", "retired", "quarantined"]);
962
- var MoveSchema = z2.object({
963
- id: z2.string(),
985
+ var ReassignResultSchema = z3.object({
986
+ moved: z3.object({ asks: z3.number(), answered: z3.number(), requests: z3.number(), goals: z3.number(), callbacks: z3.number() }),
987
+ parentId: z3.string().nullable()
988
+ });
989
+ var MoveRingSchema = z3.enum(["home", "travels", "retired", "quarantined"]);
990
+ var MoveSchema = z3.object({
991
+ id: z3.string(),
964
992
  /** The reusable question, as distill normalized it. */
965
- question: z2.string(),
993
+ question: z3.string(),
966
994
  /** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
967
- answer: z2.string(),
995
+ answer: z3.string(),
968
996
  /** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
969
997
  * told so, and the ruling only derives essentially the same question in the same scope. */
970
- rationale: z2.string().nullable(),
998
+ rationale: z3.string().nullable(),
971
999
  /** Where the ruling lives: a repo/workspace, or 'global'. */
972
- scope: z2.string(),
1000
+ scope: z3.string(),
973
1001
  ring: MoveRingSchema,
974
1002
  /** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
975
- pinned: z2.boolean(),
1003
+ pinned: z3.boolean(),
976
1004
  /** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
977
1005
  * break is visible instead of a pin silently disappearing. */
978
- pinBroken: z2.boolean(),
1006
+ pinBroken: z3.boolean(),
979
1007
  /** When the ruling was distilled. */
980
- learnedAt: z2.string(),
1008
+ learnedAt: z3.string(),
981
1009
  /** Last time it answered an ask. Null = never fired. */
982
- lastUsedAt: z2.string().nullable(),
1010
+ lastUsedAt: z3.string().nullable(),
983
1011
  /** How many asks it has answered. Instrumentation — deliberately NOT an input to the
984
1012
  * evidence curve: firing says the question keeps arising, not that the ruling is right. */
985
- usedCount: z2.number(),
1013
+ usedCount: z3.number(),
986
1014
  /** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
987
- confirms: z2.number(),
1015
+ confirms: z3.number(),
988
1016
  /** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
989
1017
  * Linear and priced above the entire confirmation budget, so any full counter wins. */
990
- counters: z2.number(),
1018
+ counters: z3.number(),
991
1019
  /** The agent that asked the question this move came from, when known. Null for a move
992
1020
  * distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
993
- learnedFrom: z2.object({ id: z2.string(), name: z2.string() }).nullable()
1021
+ learnedFrom: z3.object({ id: z3.string(), name: z3.string() }).nullable()
994
1022
  });
995
- var CreateRequestSchema = z2.object({
1023
+ var CreateRequestSchema = z3.object({
996
1024
  /** The connection (token id) to send to, from GET /api/tokens. */
997
- tokenId: z2.string(),
1025
+ tokenId: z3.string(),
998
1026
  /** The user's message to the agent. */
999
- text: z2.string().min(1),
1027
+ text: z3.string().min(1),
1000
1028
  /** Land the request on an existing conversation thread (History → "Continue")
1001
1029
  * instead of minting a fresh one. Must belong to the requesting user. */
1002
- parentId: z2.string().optional(),
1030
+ parentId: z3.string().optional(),
1003
1031
  /** Point the agent at a past conversation (possibly with a different agent) as
1004
1032
  * starting context (History → "New session from this"). A reference, not a copy —
1005
1033
  * the agent reads it via get_thread. Must belong to the requesting user. */
1006
- contextParentId: z2.string().optional()
1034
+ contextParentId: z3.string().optional()
1007
1035
  });
1008
- var HandoffSchema = z2.object({
1036
+ var HandoffSchema = z3.object({
1009
1037
  /** Move this existing outcome to `target` without reminting it. Requires `target`. */
1010
- workId: z2.string().uuid().optional(),
1038
+ workId: z3.string().uuid().optional(),
1011
1039
  /** Land the note on an existing thread; omitted mints a fresh one. */
1012
- parentId: z2.string().uuid().optional(),
1040
+ parentId: z3.string().uuid().optional(),
1013
1041
  /** One-line headline of the working context handed off. */
1014
- title: z2.string().min(1),
1042
+ title: z3.string().min(1),
1015
1043
  /** The brief — standalone notes the successor reads (what was done, what's left, links). */
1016
- notes: z2.array(z2.string().min(1)).min(1),
1044
+ notes: z3.array(z3.string().min(1)).min(1),
1017
1045
  /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
1018
1046
  * only; omit to leave the thread for the user to hand off in the app. */
1019
- target: z2.string().optional(),
1047
+ target: z3.string().optional(),
1020
1048
  /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
1021
1049
  * thread's earlier turns for rehydration — get_thread returns the latest recap + only
1022
1050
  * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
1023
1051
  * same primitive; a recap is one whose audience includes you. */
1024
- recap: z2.boolean().optional()
1052
+ recap: z3.boolean().optional()
1025
1053
  }).refine((value) => !value.workId || Boolean(value.target), {
1026
1054
  message: "target is required when handing off Work",
1027
1055
  path: ["target"]
1028
1056
  });
1029
- var NoteSourceSchema = z2.enum(["app", "call"]);
1030
- var NoteStatusSchema = z2.enum(["open", "assigned", "in_progress", "done"]);
1031
- var NoteRepeatSchema = z2.enum(["once", "until_done"]);
1032
- var DecisionSchema = z2.object({
1033
- id: z2.string(),
1057
+ var NoteSourceSchema = z3.enum(["app", "call"]);
1058
+ var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
1059
+ var NoteRepeatSchema = z3.enum(["once", "until_done"]);
1060
+ var DecisionSchema = z3.object({
1061
+ id: z3.string(),
1034
1062
  /** The note this decision refines; null = recorded on a bare thread (the
1035
1063
  * extensibility seam — any conversation can accrue decisions). */
1036
- noteId: z2.string().nullable(),
1064
+ noteId: z3.string().nullable(),
1037
1065
  /** What was ambiguous — the broker's (or the user's own) question. */
1038
- question: z2.string(),
1066
+ question: z3.string(),
1039
1067
  /** The user's ruling; null while the question is open. */
1040
- answer: z2.string().nullable(),
1041
- decidedAt: z2.string().nullable(),
1042
- createdAt: z2.string()
1068
+ answer: z3.string().nullable(),
1069
+ decidedAt: z3.string().nullable(),
1070
+ createdAt: z3.string()
1043
1071
  });
1044
- var NoteSchema = z2.object({
1045
- id: z2.string(),
1072
+ var NoteSchema = z3.object({
1073
+ id: z3.string(),
1046
1074
  /** One-line headline (broker-titled; deterministic floor). */
1047
- title: z2.string(),
1075
+ title: z3.string(),
1048
1076
  /** The original intent, verbatim — assignees always see the user's own words. */
1049
- intent: z2.string(),
1077
+ intent: z3.string(),
1050
1078
  source: NoteSourceSchema,
1051
1079
  status: NoteStatusSchema,
1052
1080
  /** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
1053
- assignee: z2.string().nullable(),
1081
+ assignee: z3.string().nullable(),
1054
1082
  /** The request thread minted at assignment; null until assigned. */
1055
- parentId: z2.string().nullable(),
1083
+ parentId: z3.string().nullable(),
1056
1084
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
1057
1085
  * call — never a deadline. It only ever comes from the user's own words, so when it
1058
1086
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
@@ -1061,202 +1089,215 @@ var NoteSchema = z2.object({
1061
1089
  // Defaulted, not required: a Note from an API deploy older than the reminders
1062
1090
  // migration has none of these, and the defaults ARE what it means — no not-before,
1063
1091
  // one ride, never ridden. Parsing must not fail across a rolling deploy.
1064
- dueAt: z2.string().nullable().default(null),
1092
+ dueAt: z3.string().nullable().default(null),
1065
1093
  repeat: NoteRepeatSchema.default("once"),
1066
1094
  /** How many calls have already carried it — the fatigue cap counts rides, not days. */
1067
- rides: z2.number().int().default(0),
1068
- lastRideAt: z2.string().nullable().default(null),
1069
- createdAt: z2.string()
1095
+ rides: z3.number().int().default(0),
1096
+ lastRideAt: z3.string().nullable().default(null),
1097
+ createdAt: z3.string()
1070
1098
  });
1071
- var CreateNoteSchema = z2.object({
1099
+ var CreateNoteSchema = z3.object({
1072
1100
  /** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
1073
- text: z2.string().min(1).max(4e3),
1101
+ text: z3.string().min(1).max(4e3),
1074
1102
  /** Capture it as a REMINDER — a note assigned to the user themselves, which rides
1075
1103
  * their next call instead of being handed to an agent. Everything else about the
1076
1104
  * note is identical; this is the one parameter that separates the two. */
1077
- forMe: z2.boolean().optional(),
1105
+ forMe: z3.boolean().optional(),
1078
1106
  /** The not-before, when the user already said one. Absent = the very next call. */
1079
- dueAt: z2.string().datetime().optional(),
1107
+ dueAt: z3.string().datetime().optional(),
1080
1108
  repeat: NoteRepeatSchema.optional()
1081
1109
  });
1082
- var RecordDecisionSchema = z2.object({
1110
+ var RecordDecisionSchema = z3.object({
1083
1111
  /** An open decision (from /clarify) to answer. */
1084
- decisionId: z2.string().optional(),
1112
+ decisionId: z3.string().optional(),
1085
1113
  /** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
1086
- question: z2.string().min(1).max(500).optional(),
1114
+ question: z3.string().min(1).max(500).optional(),
1087
1115
  /** The ruling. */
1088
- answer: z2.string().min(1).max(2e3)
1116
+ answer: z3.string().min(1).max(2e3)
1089
1117
  }).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
1090
- var AssignNoteSchema = z2.object({
1118
+ var AssignNoteSchema = z3.object({
1091
1119
  /** An EXISTING agent: token id or nickname. Omit when spawning fresh. */
1092
- target: z2.string().min(1).optional(),
1120
+ target: z3.string().min(1).optional(),
1093
1121
  /** Spawn a NEW session for this note (companion.md §2.2): the assignee doesn't
1094
1122
  * exist yet — mint it on a live desktop that advertises the harness+workspace,
1095
1123
  * named after the note. The brief arrives as its opening request. */
1096
- spawn: z2.object({
1097
- hostTokenId: z2.string().uuid(),
1098
- harness: z2.string().min(1),
1099
- workspace: z2.string().min(1)
1124
+ spawn: z3.object({
1125
+ hostTokenId: z3.string().uuid(),
1126
+ harness: z3.string().min(1),
1127
+ workspace: z3.string().min(1)
1100
1128
  }).optional()
1101
1129
  }).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
1102
- var TriageVerdictSchema = z2.enum(["keep", "stale", "close", "assign"]);
1103
- var TriageItemSchema = z2.object({
1104
- noteId: z2.string(),
1130
+ var TriageVerdictSchema = z3.enum(["keep", "stale", "close", "assign"]);
1131
+ var TriageItemSchema = z3.object({
1132
+ noteId: z3.string(),
1105
1133
  /** The note's headline at run time. */
1106
- title: z2.string(),
1134
+ title: z3.string(),
1107
1135
  /** WHY, in one short human line, evidence first — this is read on a phone underneath
1108
1136
  * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
1109
1137
  * Never a model's reasoning transcript, never an id. */
1110
- why: z2.string()
1138
+ why: z3.string()
1111
1139
  });
1112
- var TriageAssignmentSchema = z2.object({
1140
+ var TriageAssignmentSchema = z3.object({
1113
1141
  /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
1114
- agent: z2.string(),
1142
+ agent: z3.string(),
1115
1143
  /** Its display name at run time (the name on the hatchling's card). Denormalized for the
1116
1144
  * same reason as `title`: the card must render from the proposal alone. */
1117
- agentName: z2.string(),
1118
- notes: z2.array(TriageItemSchema)
1145
+ agentName: z3.string(),
1146
+ notes: z3.array(TriageItemSchema)
1119
1147
  });
1120
- var TriageStatusSchema = z2.enum(["open", "superseded", "dismissed"]);
1121
- var SubmitTriageSchema = z2.object({
1148
+ var TriageStatusSchema = z3.enum(["open", "superseded", "dismissed"]);
1149
+ var SubmitTriageSchema = z3.object({
1122
1150
  /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
1123
1151
  * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
1124
1152
  * so the phone can say where the content went — an unattributed privacy claim is worth
1125
1153
  * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
1126
- provider: z2.string().min(1).max(60),
1154
+ provider: z3.string().min(1).max(60),
1127
1155
  /** The concrete model when the provider names one (an ollama tag); null otherwise. */
1128
- model: z2.string().max(200).nullable().optional(),
1156
+ model: z3.string().max(200).nullable().optional(),
1129
1157
  /** How many open notes the run actually looked at — the denominator on the phone
1130
1158
  * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
1131
- reviewed: z2.number().int().min(0).max(1e4).default(0),
1132
- close: z2.array(TriageItemSchema).max(200).default([]),
1133
- stale: z2.array(TriageItemSchema).max(200).default([]),
1134
- assign: z2.array(TriageAssignmentSchema).max(50).default([])
1159
+ reviewed: z3.number().int().min(0).max(1e4).default(0),
1160
+ close: z3.array(TriageItemSchema).max(200).default([]),
1161
+ stale: z3.array(TriageItemSchema).max(200).default([]),
1162
+ assign: z3.array(TriageAssignmentSchema).max(50).default([])
1135
1163
  });
1136
1164
  var TriageProposalSchema = SubmitTriageSchema.extend({
1137
- id: z2.string(),
1138
- runAt: z2.string(),
1165
+ id: z3.string(),
1166
+ runAt: z3.string(),
1139
1167
  status: TriageStatusSchema,
1140
- model: z2.string().nullable().default(null)
1168
+ model: z3.string().nullable().default(null)
1141
1169
  });
1142
- var AcceptTriageSchema = z2.discriminatedUnion("group", [
1143
- z2.object({ group: z2.literal("close"), noteIds: z2.array(z2.string()).max(200).optional() }),
1144
- z2.object({ group: z2.literal("stale"), noteIds: z2.array(z2.string()).max(200).optional() }),
1145
- z2.object({
1146
- group: z2.literal("assign"),
1147
- agent: z2.string().min(1),
1148
- noteIds: z2.array(z2.string()).max(200).optional()
1170
+ var AcceptTriageSchema = z3.discriminatedUnion("group", [
1171
+ z3.object({ group: z3.literal("close"), noteIds: z3.array(z3.string()).max(200).optional() }),
1172
+ z3.object({ group: z3.literal("stale"), noteIds: z3.array(z3.string()).max(200).optional() }),
1173
+ z3.object({
1174
+ group: z3.literal("assign"),
1175
+ agent: z3.string().min(1),
1176
+ noteIds: z3.array(z3.string()).max(200).optional()
1149
1177
  })
1150
1178
  ]);
1151
- var AcceptTriageResultSchema = z2.object({
1152
- accepted: z2.array(z2.string()),
1153
- failed: z2.array(z2.object({ noteId: z2.string(), reason: z2.string() }))
1179
+ var AcceptTriageResultSchema = z3.object({
1180
+ accepted: z3.array(z3.string()),
1181
+ failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
1154
1182
  });
1155
- var DeliveryModeSchema = z2.enum(["poll", "self_hosted"]);
1183
+ var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
1156
1184
  var WAKE_EVENT = "wake";
1157
1185
  var wakeChannel = (tokenId) => `wake:${tokenId}`;
1158
- var RegisterDeliverySchema = z2.object({ mode: DeliveryModeSchema });
1159
- var OAuthStartSchema = z2.object({
1160
- provider: z2.enum(["cma"]),
1161
- returnTo: z2.string().min(1)
1186
+ var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
1187
+ var OAuthStartSchema = z3.object({
1188
+ provider: z3.enum(["cma"]),
1189
+ returnTo: z3.string().min(1)
1162
1190
  });
1163
- var DeliveryConfigSchema = z2.object({
1164
- tokenId: z2.string(),
1191
+ var DeliveryConfigSchema = z3.object({
1192
+ tokenId: z3.string(),
1165
1193
  mode: DeliveryModeSchema,
1166
1194
  /** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
1167
1195
  * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
1168
- realtime: z2.object({ url: z2.string(), anonKey: z2.string() }).nullable()
1196
+ realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
1169
1197
  });
1170
- var StatusSchema = z2.object({
1171
- name: z2.string(),
1172
- sessionMode: z2.enum(["default", "all_calls", "silent"]),
1198
+ var StatusSchema = z3.object({
1199
+ name: z3.string(),
1200
+ sessionMode: z3.enum(["default", "all_calls", "silent"]),
1173
1201
  /** A phone is registered for push/ring (any push token on the account). */
1174
- phone: z2.boolean()
1202
+ phone: z3.boolean(),
1203
+ /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
1204
+ * it never picked up. THE SAME NUMBER the harness's wake gate reads
1205
+ * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
1206
+ * zero while the sweep sees one is two ideas of "waiting".
1207
+ *
1208
+ * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
1209
+ * that nobody spawned, so a terminal session only learns of work by asking. The harness
1210
+ * used to paper over that by spawning a SECOND process on the identity; now it stands
1211
+ * back, correctly, and the person sitting at the terminal is the one who can act. A
1212
+ * coffee-beans request sat unread for three days.
1213
+ *
1214
+ * Optional: an older API sends no field, and the statusline then renders exactly as before. */
1215
+ waiting: z3.number().int().nonnegative().optional()
1175
1216
  });
1176
- var EnvelopeRecipientSchema = z2.object({
1177
- keyId: z2.string(),
1178
- epk: z2.string(),
1179
- wnonce: z2.string(),
1180
- wrap: z2.string()
1217
+ var EnvelopeRecipientSchema = z3.object({
1218
+ keyId: z3.string(),
1219
+ epk: z3.string(),
1220
+ wnonce: z3.string(),
1221
+ wrap: z3.string()
1181
1222
  });
1182
- var EnvelopeHeaderSchema = z2.object({
1183
- field: z2.enum(["context", "options", "visuals", "answer"]),
1184
- kind: z2.string(),
1185
- senderRole: z2.enum(["agent", "user"]),
1186
- recipientKeyIds: z2.array(z2.string()),
1187
- seq: z2.number().int().nonnegative()
1223
+ var EnvelopeHeaderSchema = z3.object({
1224
+ field: z3.enum(["context", "options", "visuals", "answer"]),
1225
+ kind: z3.string(),
1226
+ senderRole: z3.enum(["agent", "user"]),
1227
+ recipientKeyIds: z3.array(z3.string()),
1228
+ seq: z3.number().int().nonnegative()
1188
1229
  });
1189
- var EnvelopeSchema = z2.object({
1190
- v: z2.literal(1),
1191
- alg: z2.literal("x25519-xsalsa20poly1305"),
1192
- msgId: z2.string(),
1230
+ var EnvelopeSchema = z3.object({
1231
+ v: z3.literal(1),
1232
+ alg: z3.literal("x25519-xsalsa20poly1305"),
1233
+ msgId: z3.string(),
1193
1234
  hdr: EnvelopeHeaderSchema,
1194
- recipients: z2.array(EnvelopeRecipientSchema).min(1),
1195
- nonce: z2.string(),
1196
- ct: z2.string()
1235
+ recipients: z3.array(EnvelopeRecipientSchema).min(1),
1236
+ nonce: z3.string(),
1237
+ ct: z3.string()
1197
1238
  });
1198
- var SealedAnswerSchema = z2.object({
1199
- ignored: z2.boolean(),
1239
+ var SealedAnswerSchema = z3.object({
1240
+ ignored: z3.boolean(),
1200
1241
  envelope: EnvelopeSchema
1201
1242
  });
1202
- var DeviceCredentialSchema = z2.object({
1203
- deviceId: z2.string(),
1204
- kind: z2.enum(["phone", "web", "agent"]),
1205
- x25519Pub: z2.string(),
1206
- ed25519Pub: z2.string(),
1207
- sig: z2.string()
1243
+ var DeviceCredentialSchema = z3.object({
1244
+ deviceId: z3.string(),
1245
+ kind: z3.enum(["phone", "web", "agent"]),
1246
+ x25519Pub: z3.string(),
1247
+ ed25519Pub: z3.string(),
1248
+ sig: z3.string()
1208
1249
  });
1209
- var DeviceRosterSchema = z2.object({
1210
- uikPub: z2.string(),
1211
- devices: z2.array(DeviceCredentialSchema)
1250
+ var DeviceRosterSchema = z3.object({
1251
+ uikPub: z3.string(),
1252
+ devices: z3.array(DeviceCredentialSchema)
1212
1253
  });
1213
- var WakeNudgeSchema = z2.object({
1214
- kind: z2.enum(["reply", "request", "callback"]),
1215
- notificationId: z2.string().optional(),
1216
- parentId: z2.string()
1254
+ var WakeNudgeSchema = z3.object({
1255
+ kind: z3.enum(["reply", "request", "callback"]),
1256
+ notificationId: z3.string().optional(),
1257
+ parentId: z3.string()
1217
1258
  });
1218
- var PairingStatusSchema = z2.enum(["pending", "approved", "denied", "expired"]);
1219
- var PairingRevealSchema = z2.object({
1220
- x25519: z2.string(),
1221
- ed25519: z2.string(),
1222
- nonce: z2.string()
1259
+ var PairingStatusSchema = z3.enum(["pending", "approved", "denied", "expired"]);
1260
+ var PairingRevealSchema = z3.object({
1261
+ x25519: z3.string(),
1262
+ ed25519: z3.string(),
1263
+ nonce: z3.string()
1223
1264
  });
1224
- var DeviceCodeRequestSchema = z2.object({
1265
+ var DeviceCodeRequestSchema = z3.object({
1225
1266
  /** The agent's suggested name for the pairing — the human sees it pre-filled at
1226
1267
  * approval and can override. Optional; blank → a default silly name server-side. */
1227
- suggestedName: z2.string().optional(),
1268
+ suggestedName: z3.string().optional(),
1228
1269
  /** Legacy alias for suggestedName (older MCPs sent `agent`). Accepted for
1229
1270
  * back-compat; `suggestedName` wins when both are present. ponytail: drop once no
1230
1271
  * pre-`name` MCP is in the wild. */
1231
- agent: z2.union([z2.string(), z2.object({ name: z2.string().optional() })]).optional(),
1232
- device: z2.string().optional(),
1233
- proto: z2.string().optional(),
1272
+ agent: z3.union([z3.string(), z3.object({ name: z3.string().optional() })]).optional(),
1273
+ device: z3.string().optional(),
1274
+ proto: z3.string().optional(),
1234
1275
  // compiled-in tag, e.g. "paigy-pair-v2|sas=24"
1235
- commitment: z2.string().optional()
1276
+ commitment: z3.string().optional()
1236
1277
  // agent's Ca
1237
1278
  });
1238
- var DeviceCodeSchema = z2.object({
1239
- device_code: z2.string(),
1240
- user_code: z2.string(),
1241
- verification_uri: z2.string().url(),
1242
- verification_uri_complete: z2.string().url(),
1243
- interval: z2.number(),
1244
- expires_in: z2.number()
1279
+ var DeviceCodeSchema = z3.object({
1280
+ device_code: z3.string(),
1281
+ user_code: z3.string(),
1282
+ verification_uri: z3.string().url(),
1283
+ verification_uri_complete: z3.string().url(),
1284
+ interval: z3.number(),
1285
+ expires_in: z3.number()
1245
1286
  });
1246
- var DeviceInfoSchema = z2.object({
1247
- code: z2.string(),
1287
+ var DeviceInfoSchema = z3.object({
1288
+ code: z3.string(),
1248
1289
  /** The agent's suggested name (from /device/code) — shown on the approval screen,
1249
1290
  * pre-filling the name field the human can edit. */
1250
- name: z2.string(),
1291
+ name: z3.string(),
1251
1292
  /** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
1252
1293
  * build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
1253
1294
  * install crashes on the pairing screen on first launch, before the OTA lands
1254
1295
  * (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
1255
- agent: z2.string().optional(),
1256
- device: z2.string().nullable(),
1296
+ agent: z3.string().optional(),
1297
+ device: z3.string().nullable(),
1257
1298
  status: PairingStatusSchema,
1258
- proto: z2.string().nullable().optional(),
1259
- agent_commitment: z2.string().nullable().optional(),
1299
+ proto: z3.string().nullable().optional(),
1300
+ agent_commitment: z3.string().nullable().optional(),
1260
1301
  // Ca
1261
1302
  agent_reveal: PairingRevealSchema.nullable().optional(),
1262
1303
  // present once the agent reveals
@@ -1266,48 +1307,48 @@ var DeviceInfoSchema = z2.object({
1266
1307
  // withheld until e2ee=true) can compute the SAS WITHOUT a token. Public keys — same safety
1267
1308
  // class as agent_reveal above. Present only once the phone reveals; null otherwise.
1268
1309
  phone_reveal: PairingRevealSchema.nullable().optional(),
1269
- uik_pub: z2.string().nullable().optional()
1310
+ uik_pub: z3.string().nullable().optional()
1270
1311
  });
1271
- var DeviceTokenRequestSchema = z2.object({
1272
- device_code: z2.string(),
1312
+ var DeviceTokenRequestSchema = z3.object({
1313
+ device_code: z3.string(),
1273
1314
  reveal: PairingRevealSchema.optional()
1274
1315
  // agent's reveal {x25519, ed25519, nonce}
1275
1316
  });
1276
- var DeviceTokenSchema = z2.object({
1277
- access_token: z2.string(),
1317
+ var DeviceTokenSchema = z3.object({
1318
+ access_token: z3.string(),
1278
1319
  /** The pairing's single name (user-typed at approval, the agent's suggestion, or
1279
1320
  * a default silly name). */
1280
- name: z2.string(),
1281
- device: z2.string().nullable(),
1321
+ name: z3.string(),
1322
+ device: z3.string().nullable(),
1282
1323
  /** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
1283
1324
  * draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
1284
- voice: z2.string().nullable().optional(),
1325
+ voice: z3.string().nullable().optional(),
1285
1326
  /** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
1286
1327
  * that survives a rename. Cached by the host's identity beat. */
1287
- token_id: z2.string().nullable().optional(),
1328
+ token_id: z3.string().nullable().optional(),
1288
1329
  /** WHERE this identity works — the folder a wake should land it in. Written by the host
1289
1330
  * at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
1290
1331
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
1291
1332
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
1292
- workspace: z2.string().nullable().optional(),
1333
+ workspace: z3.string().nullable().optional(),
1293
1334
  phone_reveal: PairingRevealSchema.nullable().optional(),
1294
1335
  // present once the phone reveals
1295
- uik_pub: z2.string().nullable().optional()
1336
+ uik_pub: z3.string().nullable().optional()
1296
1337
  });
1297
- var DeviceCommitRequestSchema = z2.object({
1298
- code: z2.string(),
1299
- commitment: z2.string()
1338
+ var DeviceCommitRequestSchema = z3.object({
1339
+ code: z3.string(),
1340
+ commitment: z3.string()
1300
1341
  // Cb
1301
1342
  });
1302
- var DevicePeerCommitSchema = z2.object({
1303
- commitment: z2.string().nullable()
1343
+ var DevicePeerCommitSchema = z3.object({
1344
+ commitment: z3.string().nullable()
1304
1345
  });
1305
- var SupportRequestSchema = z2.object({
1306
- email: z2.string().email().max(320),
1307
- message: z2.string().trim().min(1).max(5e3),
1308
- name: z2.string().trim().max(120).optional()
1346
+ var SupportRequestSchema = z3.object({
1347
+ email: z3.string().email().max(320),
1348
+ message: z3.string().trim().min(1).max(5e3),
1349
+ name: z3.string().trim().max(120).optional()
1309
1350
  });
1310
- var NotificationFeedbackKindSchema = z2.enum([
1351
+ var NotificationFeedbackKindSchema = z3.enum([
1311
1352
  "break_down",
1312
1353
  // "This should be more than one ask — break it down."
1313
1354
  "regenerate_options",
@@ -1321,46 +1362,27 @@ var NotificationFeedbackKindSchema = z2.enum([
1321
1362
  "other"
1322
1363
  // anything else — the note carries it.
1323
1364
  ]);
1324
- var NotificationFeedbackSchema = z2.object({
1325
- notificationId: z2.string(),
1365
+ var NotificationFeedbackSchema = z3.object({
1366
+ notificationId: z3.string(),
1326
1367
  kind: NotificationFeedbackKindSchema,
1327
1368
  /** Optional free-text elaboration for a preset; required (non-empty) for 'other'. */
1328
- note: z2.string().trim().max(2e3).optional()
1369
+ note: z3.string().trim().max(2e3).optional()
1329
1370
  }).superRefine((r, ctx) => {
1330
1371
  if (r.kind === "other" && !r.note)
1331
- ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["note"], message: "note is required for 'other' feedback" });
1372
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["note"], message: "note is required for 'other' feedback" });
1332
1373
  });
1333
- var FeedbackResolutionSchema = z2.enum(["broker_fixed", "sent_to_agent"]);
1334
- var FeedbackOutcomeSchema = z2.object({
1374
+ var FeedbackResolutionSchema = z3.enum(["broker_fixed", "sent_to_agent"]);
1375
+ var FeedbackOutcomeSchema = z3.object({
1335
1376
  resolution: FeedbackResolutionSchema,
1336
1377
  transform: TransformSchema,
1337
- message: z2.string(),
1338
- childIds: z2.array(z2.string()).optional()
1339
- });
1340
- var CONTACT_SCHEMA = contactSchemaFrom({
1341
- ask: NotifyRequestFields.shape.ask,
1342
- waiting: NotifyRequestFields.shape.waiting,
1343
- options: NotifyRequestFields.shape.options,
1344
- channel: NotifyRequestFields.shape.channel,
1345
- parentId: NotifyRequestFields.shape.parentId,
1346
- workId: NotifyRequestFields.shape.workId
1378
+ message: z3.string(),
1379
+ childIds: z3.array(z3.string()).optional()
1347
1380
  });
1348
1381
 
1349
1382
  export {
1350
1383
  mcpInputSchema,
1351
- CreateGoalSchema,
1352
- CREATE_GOAL_DESCRIPTION,
1353
- CONTACT_DESCRIPTION,
1354
- CHECK_REPLIES_DESCRIPTION,
1355
- SCHEDULE_CALLBACK_DESCRIPTION,
1356
- SEARCH_THREADS_DESCRIPTION,
1357
- SET_WORK_STATE_DESCRIPTION,
1384
+ AGENT_TOOLS,
1358
1385
  serverInstructions,
1359
- SetTaskStateSchema,
1360
- SetWorkStateSchema,
1361
- ScheduleCallbackSchema,
1362
- HandoffSchema,
1363
1386
  WAKE_EVENT,
1364
- wakeChannel,
1365
- CONTACT_SCHEMA
1387
+ wakeChannel
1366
1388
  };