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