@paigy/mcp 0.40.11 → 0.40.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,27 @@
1
1
  // ../../packages/schema/dist/index.js
2
+ import { z as z4 } from "zod";
3
+ import { z } from "zod";
2
4
  import { z as z3 } from "zod";
3
- import { z as z2 } from "zod";
4
5
  import { zodToJsonSchema } from "zod-to-json-schema";
5
- import { z } from "zod";
6
+ import { z as z2 } from "zod";
6
7
  var OPTIONS_MIN = 2;
7
8
  var OPTIONS_MAX = 6;
9
+ var OptionSchema = z.object({
10
+ id: z.string(),
11
+ label: z.string(),
12
+ hint: z.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
13
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
14
+ // the constraints below are what an agent reads when deciding to use these.
15
+ html: z.string().max(16384).describe(
16
+ "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. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
17
+ ).optional(),
18
+ image: z.string().url().describe(
19
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
20
+ ).optional()
21
+ });
22
+ var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
23
+ label: z.string().trim().min(1).max(1e3)
24
+ }).strict();
8
25
  var NIGHT = { from: 23, to: 7 };
9
26
  function draft2020(node) {
10
27
  if (Array.isArray(node)) return node.map(draft2020);
@@ -35,39 +52,30 @@ function mcpInputSchema(s) {
35
52
  delete schema.$schema;
36
53
  return draft2020(schema);
37
54
  }
38
- var AskInputSchema = z.object({
39
- id: z.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
40
- parentId: z.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
41
- repo: z.string().optional().describe("Optional repository context."),
42
- ask: z.string().trim().min(1).max(1e4).describe(
55
+ var AskInputSchema = z2.object({
56
+ id: z2.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
57
+ parentId: z2.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
58
+ repo: z2.string().optional().describe("Optional repository context."),
59
+ ask: z2.string().trim().min(1).max(1e4).describe(
43
60
  "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
44
61
  ),
45
- options: z.array(z.object({
46
- label: z.string().trim().min(1).max(1e3),
47
- hint: z.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
48
- image: z.string().url().optional(),
49
- html: z.string().max(16384).describe("Optional sandboxed HTML/CSS preview. No JavaScript or network; inline CSS and data: URIs only. It renders edge-to-edge in a responsive card 200pt tall (about 320pt wide on a phone, with the next option peeking beside it), so fit the HTML to that viewport.").optional()
50
- }).strict()).min(2).max(6).optional()
62
+ options: z2.array(OptionInputSchema).min(2).max(6).optional()
51
63
  }).strict();
52
- var StartContactSchema = z.object({
53
- asks: z.array(AskInputSchema).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
54
- waiting: z.enum(["none", "hard"]).default("none"),
55
- channel: z.enum(["notification", "call"]).default("notification")
64
+ var StartContactSchema = z2.object({
65
+ asks: z2.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
66
+ waiting: z2.enum(["none", "hard"]).default("none"),
67
+ channel: z2.enum(["notification", "call"]).default("notification")
56
68
  }).strict();
57
- var ContactSchema = z.union([StartContactSchema, z.object({ deliveryId: z.string().uuid() }).strict()]);
69
+ var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
58
70
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
59
71
  var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
60
- var CreateGoalSchema = z2.object({
61
- outcome: z2.string().trim().min(1).max(1e4),
72
+ var CreateGoalSchema = z3.object({
73
+ outcome: z3.string().trim().min(1).max(1e4),
62
74
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
63
75
  * rings"). Omit it and the brain writes one at admission from the outcome. */
64
- title: z2.string().trim().min(1).max(80).optional(),
65
- ownerParticipant: z2.string().trim().min(1).optional(),
66
- idempotencyKey: z2.string().trim().min(1).max(200),
67
- /** A past conversation this Goal should be read against — History's "new session from this"
68
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
69
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
70
- * the writer refuses a thread belonging to another account. */
76
+ title: z3.string().trim().min(1).max(80).optional(),
77
+ ownerParticipant: z3.string().trim().min(1).optional(),
78
+ idempotencyKey: z3.string().trim().min(1).max(200),
71
79
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
72
80
  * didn't get created as a child goal of the voice UI goal, which is how it should've
73
81
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -75,55 +83,45 @@ var CreateGoalSchema = z2.object({
75
83
  * took the short one. The hierarchy has been modelled since Goals existed and had been used
76
84
  * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
77
85
  * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
78
- parentGoalId: z2.string().uuid().optional(),
86
+ parentGoalId: z3.string().uuid().optional(),
79
87
  /** The repository or project identifier this Goal belongs to (#2280) — e.g. "owner/repo" or
80
88
  * repo name. Delegated work inherits this from its parent Goal when omitted. */
81
- repo: z2.string().trim().min(1).max(200).optional()
89
+ repo: z3.string().trim().min(1).max(200).optional()
82
90
  });
83
91
  var CreateGoalToolSchema = CreateGoalSchema.extend({
84
92
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
85
93
  }).strict();
86
94
  var CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
87
- var UpdateGoalSchema = z2.object({
88
- revision: z2.number().int().positive(),
89
- changes: z2.object({
90
- outcome: z2.string().trim().min(1).max(1e4).optional(),
95
+ var UpdateGoalSchema = z3.object({
96
+ revision: z3.number().int().positive(),
97
+ changes: z3.object({
98
+ outcome: z3.string().trim().min(1).max(1e4).optional(),
91
99
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
92
100
  * brain writes one at admission; this is the owner saying it better. Null clears it. */
93
- title: z2.string().trim().min(1).max(80).nullable().optional(),
94
- ownerParticipant: z2.string().trim().min(1).optional(),
95
- parentGoalId: z2.string().uuid().nullable().optional(),
96
- dependencies: z2.array(z2.object({ goalId: z2.string().uuid(), gate: z2.enum(["start", "finish"]) }).strict()).optional(),
97
- children: z2.array(z2.object({ outcome: z2.string().trim().min(1).max(1e4), ownerParticipant: z2.string().trim().min(1), gate: z2.enum(["start", "finish"]).optional() }).strict()).optional(),
98
- state: z2.enum(["active", "done", "cancelled"]).optional(),
99
- progress: z2.string().trim().min(1).max(1e4).optional(),
100
- reviewed: z2.literal(true).optional(),
101
- dueAt: z2.string().datetime({ offset: true }).nullable().optional()
101
+ title: z3.string().trim().min(1).max(80).nullable().optional(),
102
+ ownerParticipant: z3.string().trim().min(1).optional(),
103
+ parentGoalId: z3.string().uuid().nullable().optional(),
104
+ dependencies: z3.array(z3.object({ goalId: z3.string().uuid(), gate: z3.enum(["start", "finish"]) }).strict()).optional(),
105
+ children: z3.array(z3.object({ outcome: z3.string().trim().min(1).max(1e4), ownerParticipant: z3.string().trim().min(1), gate: z3.enum(["start", "finish"]).optional() }).strict()).optional(),
106
+ state: z3.enum(["active", "done", "cancelled"]).optional(),
107
+ progress: z3.string().trim().min(1).max(1e4).optional(),
108
+ reviewed: z3.literal(true).optional(),
109
+ dueAt: z3.string().datetime({ offset: true }).nullable().optional()
102
110
  }).strict().refine((v) => Object.keys(v).length > 0),
103
- reason: z2.string().trim().min(1).max(2e3),
104
- operationId: z2.string().uuid().optional()
111
+ reason: z3.string().trim().min(1).max(2e3),
112
+ operationId: z3.string().uuid().optional()
105
113
  }).strict();
106
- var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z2.string().uuid() }).strict();
107
- var ClaimGoalSchema = z2.object({ goalId: z2.string().uuid().optional() }).strict();
108
- var GetGoalSchema = z2.object({ goalId: z2.string().uuid() }).strict();
114
+ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z3.string().uuid() }).strict();
115
+ var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
116
+ var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
109
117
  var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
110
118
  var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
111
119
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
112
120
  var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
113
- var CheckRepliesSchema = z2.object({}).strict();
114
- var GetThreadSchema = z2.object({
115
- parentId: z2.string().describe("The Thread to read \u2014 the parentId of a search_threads hit.")
116
- }).strict();
117
- var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume and get_goal for the conversation on a Goal; use this only to rehydrate a Thread that a search hit named.";
118
- var SearchThreadsSchema = z2.object({
119
- q: z2.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
120
- }).strict();
121
- 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.`;
121
+ var CheckRepliesSchema = z3.object({}).strict();
122
122
  var AGENT_TOOLS = [
123
123
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
124
124
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
125
- { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
126
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
127
125
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
128
126
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
129
127
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
@@ -156,17 +154,17 @@ function entryWords(entry) {
156
154
  return entry.sources.map((source) => source.text).join("\n");
157
155
  }
158
156
  var LIVE_MS = 3 * 6e4;
159
- var ContextSchema = z3.object({
160
- title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
161
- description: z3.array(z3.string().min(1)).describe(
157
+ var ContextSchema = z4.object({
158
+ title: z4.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
159
+ description: z4.array(z4.string().min(1)).describe(
162
160
  "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."
163
161
  )
164
162
  });
165
- var ParticipantSchema = z3.object({
166
- kind: z3.enum(["human", "agent"]),
167
- id: z3.string()
163
+ var ParticipantSchema = z4.object({
164
+ kind: z4.enum(["human", "agent"]),
165
+ id: z4.string()
168
166
  });
169
- var TransformSchema = z3.enum([
167
+ var TransformSchema = z4.enum([
170
168
  "structure",
171
169
  // shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
172
170
  "request_more",
@@ -182,26 +180,13 @@ var TransformSchema = z3.enum([
182
180
  "summarize"
183
181
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
184
182
  ]);
185
- var OptionSchema = z3.object({
186
- id: z3.string(),
187
- label: z3.string(),
188
- hint: z3.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
189
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
190
- // the constraints below are what an agent reads when deciding to use these.
191
- html: z3.string().max(16384).describe(
192
- "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. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
193
- ).optional(),
194
- image: z3.string().url().describe(
195
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
196
- ).optional()
197
- });
198
- var VisualSchema = z3.object({
199
- url: z3.string().url(),
200
- label: z3.string().optional()
183
+ var VisualSchema = z4.object({
184
+ url: z4.string().url(),
185
+ label: z4.string().optional()
201
186
  });
202
- var NotifyLevelSchema = z3.enum(["inbox", "push", "banner", "call"]);
203
- var SelectShapeSchema = z3.enum(["one", "many", "rank", "confirm", "text"]);
204
- var ReceiptEventSchema = z3.enum([
187
+ var NotifyLevelSchema = z4.enum(["inbox", "push", "banner", "call"]);
188
+ var SelectShapeSchema = z4.enum(["one", "many", "rank", "confirm", "text"]);
189
+ var ReceiptEventSchema = z4.enum([
205
190
  "delivered",
206
191
  // the bundle reached the recipient at some level
207
192
  "seen",
@@ -231,47 +216,47 @@ var ReceiptEventSchema = z3.enum([
231
216
  // be rewound by a writer that forgot to advance it.
232
217
  "restarted"
233
218
  ]);
234
- var AttentionSchema = z3.object({
219
+ var AttentionSchema = z4.object({
235
220
  urgency: NotifyLevelSchema,
236
221
  /** The required answer shape, or null for a plain notify that asks nothing back. */
237
222
  select: SelectShapeSchema.nullable(),
238
223
  /** Coverage contract (#396) — points the answer must address; null = none declared. */
239
- points: z3.array(z3.string()).nullable(),
224
+ points: z4.array(z4.string()).nullable(),
240
225
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
241
- blocking: z3.boolean(),
226
+ blocking: z4.boolean(),
242
227
  /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
243
228
  * slice wires it; optional so today's rows/callers project cleanly. */
244
- deadline: z3.string().datetime().nullable().optional()
229
+ deadline: z4.string().datetime().nullable().optional()
245
230
  });
246
- var NotifyRequestFields = z3.object({
231
+ var NotifyRequestFields = z4.object({
247
232
  /** Plaintext message content. Present on the plaintext path (today's shape);
248
233
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
249
234
  * superRefine at the bottom enforces exactly one of the two. */
250
235
  context: ContextSchema.optional(),
251
- options: z3.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
236
+ options: z4.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
252
237
  "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)."
253
238
  ),
254
- points: z3.array(z3.string().min(1)).optional().describe(
239
+ points: z4.array(z4.string().min(1)).optional().describe(
255
240
  "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."
256
241
  ),
257
- visuals: z3.array(VisualSchema).optional().describe(
242
+ visuals: z4.array(VisualSchema).optional().describe(
258
243
  "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."
259
244
  ),
260
245
  /** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
261
- repo: z3.string().optional(),
246
+ repo: z4.string().optional(),
262
247
  /** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
263
- branch: z3.string().optional(),
248
+ branch: z4.string().optional(),
264
249
  /** Continue an existing conversation — the id of any notification in it (its root
265
250
  * is the conversation's identity). Omitted = start a new conversation. Renamed
266
251
  * from `parentId` (2026-08-03): one linkage system, the parent; the API edge
267
252
  * still accepts the old name from older clients. */
268
- parentId: z3.string().uuid().optional(),
253
+ parentId: z4.string().uuid().optional(),
269
254
  /** The durable outcome this contact advances. Optional during the notification-to-Work
270
255
  * migration; when present, a blocking ask creates a DecisionNeed for this Work. */
271
- workId: z3.string().uuid().optional(),
256
+ workId: z4.string().uuid().optional(),
272
257
  /** Target Goal scope. During staged migration this is accepted by the shared contract but
273
258
  * target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
274
- goalId: z3.string().uuid().optional(),
259
+ goalId: z4.string().uuid().optional(),
275
260
  urgency: NotifyLevelSchema.default("inbox").describe(
276
261
  "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."
277
262
  ),
@@ -279,7 +264,7 @@ var NotifyRequestFields = z3.object({
279
264
  * visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
280
265
  * when `parentId` became the conversation handle: `parentId` says WHERE, this
281
266
  * says HOW. */
282
- clarifies: z3.string().optional(),
267
+ clarifies: z4.string().optional(),
283
268
  select: SelectShapeSchema.optional().describe(
284
269
  "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."
285
270
  ),
@@ -293,20 +278,20 @@ var NotifyRequestFields = z3.object({
293
278
  // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
294
279
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
295
280
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
296
- ask: z3.string().min(1).max(1e4).optional().describe(
281
+ ask: z4.string().min(1).max(1e4).optional().describe(
297
282
  '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.'
298
283
  ),
299
- needs: z3.array(z3.string().min(1)).optional().describe(
284
+ needs: z4.array(z4.string().min(1)).optional().describe(
300
285
  "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."
301
286
  ),
302
- urgencyHint: z3.enum(["whenever", "soon", "now"]).optional().describe(
287
+ urgencyHint: z4.enum(["whenever", "soon", "now"]).optional().describe(
303
288
  "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."
304
289
  ),
305
290
  /** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
306
291
  * to the agent's work while it waits. Normalized server-side into those two
307
292
  * fields (normalizeWaiting) so everything downstream is untouched; explicit
308
293
  * urgencyHint/blocking win when both are sent. */
309
- waiting: z3.enum(["none", "soft", "hard"]).optional().describe(
294
+ waiting: z4.enum(["none", "soft", "hard"]).optional().describe(
310
295
  "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."
311
296
  ),
312
297
  /** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
@@ -314,98 +299,98 @@ var NotifyRequestFields = z3.object({
314
299
  * holding by default would charge every quiet claim that minute before any agent could
315
300
  * correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
316
301
  * and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
317
- confirm: z3.boolean().optional().describe(
302
+ confirm: z4.boolean().optional().describe(
318
303
  "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'."
319
304
  ),
320
305
  /** #575: a RELAY of the user's explicitly stated preference, never the agent's
321
306
  * choice. Outranks waiting in both directions: 'call' rings even for a
322
307
  * waiting:'none' "call me when it's done"; 'message' never rings even for
323
308
  * waiting:'hard'. */
324
- channel: z3.enum(["call", "message"]).optional().describe(
309
+ channel: z4.enum(["call", "message"]).optional().describe(
325
310
  "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."
326
311
  ),
327
- confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
312
+ confirmStyle: z4.enum(["yesno", "approve"]).default("yesno").describe(
328
313
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
329
314
  ),
330
- blocking: z3.boolean().default(false).describe(
315
+ blocking: z4.boolean().default(false).describe(
331
316
  "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."
332
317
  )
333
318
  });
334
319
  var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
335
- if (r.workId && r.goalId) ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
320
+ if (r.workId && r.goalId) ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
336
321
  if (r.ask !== void 0) {
337
322
  for (const f of ["context", "select", "points"]) {
338
323
  if (r[f] !== void 0)
339
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
324
+ ctx.addIssue({ code: z4.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.` });
340
325
  }
341
326
  return;
342
327
  }
343
328
  if (r.needs !== void 0 || r.urgencyHint !== void 0)
344
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
329
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
345
330
  if (!r.context)
346
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
331
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
347
332
  if (!r.select)
348
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
333
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
349
334
  const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
350
335
  if (needsOptions && !r.options?.length)
351
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
336
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
352
337
  if (!needsOptions && r.options?.length)
353
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
338
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
354
339
  });
355
- var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
356
- var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
357
- var TurnSchema = z3.object({
358
- prompt: z3.string(),
359
- reply: z3.string()
340
+ var NotifyStatusSchema = z4.enum(["pending", "answered", "ignored"]);
341
+ var AgentStateSchema = z4.enum(["idle", "in_progress", "completed", "needs_input"]);
342
+ var TurnSchema = z4.object({
343
+ prompt: z4.string(),
344
+ reply: z4.string()
360
345
  });
361
- var UserAnswerSchema = z3.discriminatedUnion("kind", [
362
- z3.object({ kind: z3.literal("option"), optionId: z3.string(), label: z3.string().optional() }),
363
- z3.object({ kind: z3.literal("text"), text: z3.string() }),
364
- z3.object({ kind: z3.literal("ignored") }),
365
- z3.object({ kind: z3.literal("multi"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
366
- z3.object({ kind: z3.literal("ranked"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
367
- z3.object({ kind: z3.literal("clarify"), chunks: z3.array(z3.string()).min(1) }),
368
- z3.object({ kind: z3.literal("confirm"), approved: z3.boolean() }),
369
- z3.object({ kind: z3.literal("turns"), turns: z3.array(TurnSchema).min(1) }),
346
+ var UserAnswerSchema = z4.discriminatedUnion("kind", [
347
+ z4.object({ kind: z4.literal("option"), optionId: z4.string(), label: z4.string().optional() }),
348
+ z4.object({ kind: z4.literal("text"), text: z4.string() }),
349
+ z4.object({ kind: z4.literal("ignored") }),
350
+ z4.object({ kind: z4.literal("multi"), optionIds: z4.array(z4.string()), labels: z4.array(z4.string()).optional() }),
351
+ z4.object({ kind: z4.literal("ranked"), optionIds: z4.array(z4.string()), labels: z4.array(z4.string()).optional() }),
352
+ z4.object({ kind: z4.literal("clarify"), chunks: z4.array(z4.string()).min(1) }),
353
+ z4.object({ kind: z4.literal("confirm"), approved: z4.boolean() }),
354
+ z4.object({ kind: z4.literal("turns"), turns: z4.array(TurnSchema).min(1) }),
370
355
  /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
371
356
  * delivered through the same settle/await path as a human answer, carrying the judge's
372
357
  * derivation and the precedent ids it grew from. Always paired with a visible trail
373
358
  * card the user can reply to — the broker never overrides the user. */
374
- z3.object({ kind: z3.literal("precedent"), answer: z3.string(), derivation: z3.string(), sources: z3.array(z3.string()).min(1) })
359
+ z4.object({ kind: z4.literal("precedent"), answer: z4.string(), derivation: z4.string(), sources: z4.array(z4.string()).min(1) })
375
360
  ]);
376
- var IntentSchema = z3.object({
361
+ var IntentSchema = z4.object({
377
362
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
378
363
  // it by two ("detail", "feedback"), and because the settle handler parsed the array
379
364
  // all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
380
365
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
381
- kind: z3.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
382
- detail: z3.string(),
366
+ kind: z4.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
367
+ detail: z4.string(),
383
368
  /** Defer only: seconds until the callback the caller asked for, when something upstream
384
369
  * already read the time. Nothing sets it today (#397 documented an MCP parser that was
385
370
  * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
386
371
  * (`parseDelay`, #1292), and a value here simply wins over that reading. */
387
- dueInSeconds: z3.number().int().positive().optional(),
372
+ dueInSeconds: z4.number().int().positive().optional(),
388
373
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
389
374
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
390
375
  * 'other' on every row. A table that records that something was wrong and nothing
391
376
  * about what cannot answer "is the bot looping less this week?". */
392
- fault: z3.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
377
+ fault: z4.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
393
378
  });
394
- var RideAlongSchema = z3.object({
379
+ var RideAlongSchema = z4.object({
395
380
  /** The note this came from — assign/clarify/close it through /api/notes/:id. */
396
- noteId: z3.string(),
381
+ noteId: z4.string(),
397
382
  /** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
398
- text: z3.string(),
383
+ text: z4.string(),
399
384
  /** The thread to report back on, when the note was dispatched over the request rail. */
400
- parentId: z3.string().nullable()
385
+ parentId: z4.string().nullable()
401
386
  });
402
- var AwaitItemSchema = z3.discriminatedUnion("type", [
403
- z3.object({
404
- type: z3.literal("reply"),
405
- parentId: z3.string(),
406
- notificationId: z3.string(),
407
- workId: z3.string().uuid().optional(),
408
- decisionId: z3.string().uuid().optional(),
387
+ var AwaitItemSchema = z4.discriminatedUnion("type", [
388
+ z4.object({
389
+ type: z4.literal("reply"),
390
+ parentId: z4.string(),
391
+ notificationId: z4.string(),
392
+ workId: z4.string().uuid().optional(),
393
+ decisionId: z4.string().uuid().optional(),
409
394
  answer: UserAnswerSchema,
410
395
  /** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
411
396
  * #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
@@ -415,118 +400,118 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
415
400
  * "call me back after you merge" in their own words decides for itself what to do,
416
401
  * and now knows exactly which call to make. Absent when either half is missing —
417
402
  * a sentence with a hole in it is worse than no sentence. */
418
- note: z3.string().optional(),
403
+ note: z4.string().optional(),
419
404
  /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
420
405
  * shaped answer was mapped from, filtered to its own claims. There is no second list
421
406
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
422
407
  * 2026-09-04): the agent reads the sentence and decides. */
423
- transcript: z3.string().optional(),
408
+ transcript: z4.string().optional(),
424
409
  /** Coverage report (#396), when the ask declared `points`: which of them this
425
410
  * answer addressed. Missing points = re-ask or proceed knowingly partial. */
426
- covered: z3.array(z3.string()).optional(),
411
+ covered: z4.array(z4.string()).optional(),
427
412
  /** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
428
413
  * became free. Only `reply` and `idle` carry it: those are the two outcomes that
429
414
  * END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
430
415
  * agent a side-quest while it is still holding the line is how the main thing gets
431
416
  * dropped. Absent/empty = nothing owed. */
432
- also: z3.array(RideAlongSchema).optional()
417
+ also: z4.array(RideAlongSchema).optional()
433
418
  }),
434
- z3.object({
435
- type: z3.literal("remind"),
436
- parentId: z3.string(),
437
- notificationId: z3.string(),
438
- remindAt: z3.string().datetime({ offset: true }),
419
+ z4.object({
420
+ type: z4.literal("remind"),
421
+ parentId: z4.string(),
422
+ notificationId: z4.string(),
423
+ remindAt: z4.string().datetime({ offset: true }),
439
424
  /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
440
- remindInSeconds: z3.number()
425
+ remindInSeconds: z4.number()
441
426
  }),
442
427
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
443
428
  * post-feedback revision, #633) — the user will never answer this id. Stop
444
429
  * awaiting it; the live ask is the thread's newest turn (await that one, or
445
- * re-orient via get_thread / check_replies). */
446
- z3.object({
447
- type: z3.literal("superseded"),
448
- parentId: z3.string(),
449
- notificationId: z3.string()
430
+ * re-orient via check_replies). */
431
+ z4.object({
432
+ type: z4.literal("superseded"),
433
+ parentId: z4.string(),
434
+ notificationId: z4.string()
450
435
  }),
451
436
  /** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
452
437
  * revise any of these until the final reply arrives — partial = intelligence,
453
438
  * settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
454
439
  * irreversibly. If `acts` carries a question aimed at you and you know the answer,
455
440
  * contact on the same thread right away — the caller hears it on the same call. */
456
- z3.object({
457
- type: z3.literal("partial"),
458
- notificationId: z3.string(),
459
- inFlight: z3.literal(true),
460
- turn: z3.object({
461
- idx: z3.number(),
462
- prompt: z3.string(),
463
- reply: z3.string(),
464
- acts: z3.array(IntentSchema).nullable().optional()
441
+ z4.object({
442
+ type: z4.literal("partial"),
443
+ notificationId: z4.string(),
444
+ inFlight: z4.literal(true),
445
+ turn: z4.object({
446
+ idx: z4.number(),
447
+ prompt: z4.string(),
448
+ reply: z4.string(),
449
+ acts: z4.array(IntentSchema).nullable().optional()
465
450
  })
466
451
  }),
467
- z3.object({
468
- type: z3.literal("idle"),
469
- also: z3.array(RideAlongSchema).optional(),
452
+ z4.object({
453
+ type: z4.literal("idle"),
454
+ also: z4.array(RideAlongSchema).optional(),
470
455
  /** Is a call live for this agent's user right now? The SDK polls the partial stream
471
456
  * (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
472
457
  * during a live call, and polling for one on a banner/message was a wasted HTTP call +
473
458
  * 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
474
459
  * Absent = an older API → the SDK keeps polling, exactly as before. */
475
- inFlight: z3.boolean().optional()
460
+ inFlight: z4.boolean().optional()
476
461
  })
477
462
  ]);
478
- var VoiceKeySchema = z3.enum(["rachel", "george", "jessica", "brian", "lily"]);
479
- var AgendaTurnSchema = z3.object({
463
+ var VoiceKeySchema = z4.enum(["rachel", "george", "jessica", "brian", "lily"]);
464
+ var AgendaTurnSchema = z4.object({
480
465
  /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
481
466
  * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
482
467
  * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
483
468
  * completion of a streamed re-plan carries the published sentence again, and the walk
484
469
  * drops what it already said by identity, never by the API's guess of what was polled.
485
470
  * Absent on plans nothing streams (a ring plan, a floor). */
486
- id: z3.string().optional(),
471
+ id: z4.string().optional(),
487
472
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
488
473
  * the planner declares duplicates instead of asking them twice. */
489
- coveredIds: z3.array(z3.string()).optional(),
474
+ coveredIds: z4.array(z4.string()).optional(),
490
475
  /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
491
476
  * line went out on 2026-07-28 and the caller could not answer it at all. */
492
- info: z3.array(z3.string().min(1)).max(3).default([]),
493
- question: z3.string().min(1).nullable(),
477
+ info: z4.array(z4.string().min(1)).max(3).default([]),
478
+ question: z4.string().min(1).nullable(),
494
479
  /** True on the one turn carrying the agent's own declared question. */
495
- asks: z3.boolean().optional(),
480
+ asks: z4.boolean().optional(),
496
481
  /** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
497
482
  * Absent on a single-claim plan (the session's own claim) and on shared context turns,
498
483
  * which route nothing. */
499
- claimId: z3.string().optional(),
484
+ claimId: z4.string().optional(),
500
485
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
501
- voice: z3.string().optional(),
486
+ voice: z4.string().optional(),
502
487
  /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
503
488
  * whose request this is: an item that folded in from another agent arrived as a bare
504
489
  * non-sequitur ("First real production sign-in is yours to make whenever you want.")
505
490
  * and the owner answered "What?". The bot names the agent before its first turn. */
506
- agent: z3.string().optional(),
491
+ agent: z4.string().optional(),
507
492
  /** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
508
493
  * same id a face is minted from. A name is not an identity: two pairings may be called
509
494
  * "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
510
495
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
511
496
  * speaks for several agents — the turn is the only place that knows which. */
512
- agentId: z3.string().optional(),
497
+ agentId: z4.string().optional(),
513
498
  select: SelectShapeSchema.optional(),
514
- options: z3.array(OptionSchema.omit({ id: true })).optional(),
499
+ options: z4.array(OptionSchema.omit({ id: true })).optional(),
515
500
  /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
516
501
  * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
517
502
  * (the beat for context, the answer window for asks). Clamped bot-side. */
518
- pace: z3.number().positive().optional(),
503
+ pace: z4.number().positive().optional(),
519
504
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
520
505
  * (a question blocks, context flows). blocking:false on a question = ask and move
521
506
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
522
- blocking: z3.boolean().optional()
507
+ blocking: z4.boolean().optional()
523
508
  });
524
509
  var CLAIM_STALE_MS = 30 * 6e4;
525
- var InboxItemSchema = z3.object({
526
- id: z3.string(),
510
+ var InboxItemSchema = z4.object({
511
+ id: z4.string(),
527
512
  /** The conversation thread + connection this item lives on. Present on the replied
528
513
  * detail — they power History's "Continue" / "New session from this" (#57/#251). */
529
- parentId: z3.string().optional(),
514
+ parentId: z4.string().optional(),
530
515
  /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
531
516
  * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
532
517
  * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
@@ -534,13 +519,13 @@ var InboxItemSchema = z3.object({
534
519
  * unrelated updates as a single "12-part request". Absent on rows written before the
535
520
  * `asks` table, and on anything that never went through `notify` — both fall back to the
536
521
  * thread, which is what the client did for all rows until now. */
537
- askId: z3.string().optional(),
522
+ askId: z4.string().optional(),
538
523
  /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
539
524
  * shares one `created_at` to the microsecond, so without it the author's order is
540
525
  * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
541
526
  * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
542
527
  * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
543
- seq: z3.number().int().optional(),
528
+ seq: z4.number().int().optional(),
544
529
  /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
545
530
  * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
546
531
  * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
@@ -549,27 +534,23 @@ var InboxItemSchema = z3.object({
549
534
  * server that can still see every row states it. Absent on any row with no `askId`: a
550
535
  * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
551
536
  * the one part of its own arrival a single row cannot answer. */
552
- units: z3.number().int().positive().optional(),
553
- tokenId: z3.string().optional(),
537
+ units: z4.number().int().positive().optional(),
538
+ tokenId: z4.string().optional(),
554
539
  status: NotifyStatusSchema,
555
540
  context: ContextSchema,
556
- options: z3.array(OptionSchema).optional(),
541
+ options: z4.array(OptionSchema).optional(),
557
542
  /** The ask's declared coverage points (#396), when the agent sent them. */
558
- points: z3.array(z3.string()).optional(),
559
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
560
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
561
- * between them could not express a call as a sequence. `question: null` is a real turn —
562
- * a status update stays a statement instead of being shaped into a yes/no. */
543
+ points: z4.array(z4.string()).optional(),
563
544
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
564
545
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
565
546
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
566
547
  * and neither of those is "got it" (owner, 2026-08-10). */
567
- asks: z3.boolean().optional(),
548
+ asks: z4.boolean().optional(),
568
549
  /** When a live process last pulsed for this row's agent — the liveness input for
569
550
  * "working requires a pulse" (#928): the list said "Working…" from agent_state alone
570
551
  * while the party called the same dead claim stalled. Absent = no token/no data,
571
552
  * which must never CLAIM stalled. */
572
- lastSeenAt: z3.string().optional(),
553
+ lastSeenAt: z4.string().optional(),
573
554
  /** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
574
555
  * the `notification_events` ledger (trigger-written since 20260621010000, so every row a
575
556
  * user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
@@ -579,53 +560,81 @@ var InboxItemSchema = z3.object({
579
560
  * work. Reading the row's birth as the claim's age brands that "No update in 8h" the
580
561
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
581
562
  * `createdAt`. */
582
- agentStateAt: z3.string().datetime().optional(),
583
- agenda: z3.array(AgendaTurnSchema).optional(),
584
- visuals: z3.array(VisualSchema).optional(),
563
+ agentStateAt: z4.string().datetime().optional(),
564
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
565
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
566
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
567
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
568
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
569
+ *
570
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
571
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
572
+ *
573
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
574
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
575
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
576
+ * REPLACED `agenda` (turns), which nothing ever filled. */
577
+ questions: z4.array(z4.object({
578
+ id: z4.string(),
579
+ entryId: z4.string(),
580
+ title: z4.string(),
581
+ state: z4.enum(["open", "answered"]),
582
+ answer: z4.string().nullable()
583
+ })).optional(),
584
+ visuals: z4.array(VisualSchema).optional(),
585
585
  /** The connected agent's name (the single pairing name — user-typed, or the
586
586
  * agent's suggestion, or a default silly name). */
587
- name: z3.string(),
587
+ name: z4.string(),
588
588
  /** The pairing's assigned voice (#462); absent = the default voice. */
589
589
  voice: VoiceKeySchema.optional(),
590
- repo: z3.string().optional(),
591
- branch: z3.string().optional(),
592
- createdAt: z3.string().datetime(),
593
- snoozedUntil: z3.string().datetime().optional(),
590
+ repo: z4.string().optional(),
591
+ branch: z4.string().optional(),
592
+ createdAt: z4.string().datetime(),
593
+ snoozedUntil: z4.string().datetime().optional(),
594
594
  agentState: AgentStateSchema.default("idle"),
595
595
  /** Whose action the item is waiting on: "you" = an agent asked you (the default,
596
596
  * every agent→user notification); "agent" = you sent a request and it's awaiting the
597
597
  * agent (held in the inbox until the agent replies on the thread). */
598
- turn: z3.enum(["you", "agent"]).default("you"),
598
+ turn: z4.enum(["you", "agent"]).default("you"),
599
599
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
600
600
  * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
601
601
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
602
- error: z3.string().optional(),
603
- clarifies: z3.string().optional(),
604
- /** The ring ladder ran out while this was still pending — we tried to reach you and
605
- * STOPPED trying (`arbitration/arbitrate.ts` `ringAt` → null). Distinct from an
606
- * agent with nothing to say, which the roster drew identically until now: "nothing to
607
- * say" and "gave up saying it" are opposite situations wearing the same face
608
- * (navigation-design.md, gap 1). False for anything that never rang. */
609
- gaveUp: z3.boolean().default(false),
602
+ error: z4.string().optional(),
603
+ clarifies: z4.string().optional(),
604
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
605
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
606
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
607
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
608
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
609
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
610
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
611
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
612
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
613
+ ring: z4.object({
614
+ declinedAt: z4.string().datetime(),
615
+ anchorAt: z4.string().datetime(),
616
+ nextRingAt: z4.string().datetime().nullable(),
617
+ step: z4.number().int()
618
+ }).optional(),
610
619
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
611
620
  * Absent for anything never delivered through a push, and for older rows written before
612
621
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
613
622
  * 2026-08-07) — its real job is to give "this didn't need a call" something to be
614
623
  * feedback ABOUT. */
615
- why: z3.object({
624
+ why: z4.object({
616
625
  asked: NotifyLevelSchema,
617
626
  got: NotifyLevelSchema,
618
- because: z3.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
619
- line: z3.string()
627
+ because: z4.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
628
+ line: z4.string()
620
629
  }).optional(),
621
- select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
622
- confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
630
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
631
+ confirmStyle: z4.enum(["yesno", "approve"]).default("yesno").describe(
623
632
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
624
633
  ),
625
634
  /** Real downstream work is stuck behind this one — set by the agent, independent of
626
635
  * urgency (see the main README's "premier use case" + notify/states.md). Drives the
627
636
  * inbox's blocking badge and the extra confirm step before dismissing it. */
628
- blocking: z3.boolean().default(false),
637
+ blocking: z4.boolean().default(false),
629
638
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
630
639
  answer: UserAnswerSchema.optional(),
631
640
  /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
@@ -633,35 +642,35 @@ var InboxItemSchema = z3.object({
633
642
  * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
634
643
  * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
635
644
  * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
636
- communication: z3.object({
637
- deliveryId: z3.string(),
638
- kind: z3.enum(["notification", "call"]),
639
- entryId: z3.string(),
640
- goalIds: z3.array(z3.string()),
641
- decisionNeedId: z3.string().optional(),
642
- sealed: z3.boolean(),
643
- goalState: z3.string().optional()
645
+ communication: z4.object({
646
+ deliveryId: z4.string(),
647
+ kind: z4.enum(["notification", "call"]),
648
+ entryId: z4.string(),
649
+ goalIds: z4.array(z4.string()),
650
+ decisionNeedId: z4.string().optional(),
651
+ sealed: z4.boolean(),
652
+ goalState: z4.string().optional()
644
653
  }).optional()
645
654
  });
646
655
  var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
647
- var PushTokenSchema = z3.object({
648
- voipToken: z3.string().min(1).optional(),
649
- alertToken: z3.string().min(1).optional(),
650
- fcmToken: z3.string().min(1).optional(),
651
- platform: z3.enum(["ios", "android"])
656
+ var PushTokenSchema = z4.object({
657
+ voipToken: z4.string().min(1).optional(),
658
+ alertToken: z4.string().min(1).optional(),
659
+ fcmToken: z4.string().min(1).optional(),
660
+ platform: z4.enum(["ios", "android"])
652
661
  }).superRefine((v, ctx) => {
653
662
  if (v.platform !== "ios") return;
654
663
  for (const field of ["voipToken", "alertToken"]) {
655
664
  const token = v[field];
656
665
  if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
657
666
  ctx.addIssue({
658
- code: z3.ZodIssueCode.custom,
667
+ code: z4.ZodIssueCode.custom,
659
668
  path: [field],
660
669
  message: `not an APNs device token (want 64 hex chars, got ${token.length})`
661
670
  });
662
671
  }
663
672
  });
664
- var MissedCallSchema = z3.enum([
673
+ var MissedCallSchema = z4.enum([
665
674
  "retry_10m",
666
675
  "retry_30m",
667
676
  "retry_60m",
@@ -673,32 +682,32 @@ var MissedCallSchema = z3.enum([
673
682
  ]);
674
683
  var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
675
684
  var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
676
- var BrokerTuningSchema = z3.object({
685
+ var BrokerTuningSchema = z4.object({
677
686
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
678
- ackVerbosity: z3.enum(["normal", "none"]).optional(),
687
+ ackVerbosity: z4.enum(["normal", "none"]).optional(),
679
688
  /** How readily the mapper asks its one clarification: 'low' = only when truly
680
689
  * uninterpretable, 'high' = whenever not fully certain. */
681
- clarifyEagerness: z3.enum(["low", "normal", "high"]).optional(),
690
+ clarifyEagerness: z4.enum(["low", "normal", "high"]).optional(),
682
691
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
683
- phrasebook: z3.array(z3.object({ say: z3.string().min(1).max(60), mean: z3.string().min(1).max(120) })).max(24).optional(),
692
+ phrasebook: z4.array(z4.object({ say: z4.string().min(1).max(60), mean: z4.string().min(1).max(120) })).max(24).optional(),
684
693
  /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
685
694
  * which is every account today — means the agent's own words decide, per ask: a call
686
695
  * about an English ask opens in English. This is the only thing that overrides that,
687
696
  * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
688
697
  * Set per user (no UI yet), like `voiceTuning`. */
689
- language: z3.enum(["en", "es"]).optional()
698
+ language: z4.enum(["en", "es"]).optional()
690
699
  });
691
- var UserSettingsSchema = z3.object({
692
- permissions: z3.object({
693
- call: z3.boolean(),
694
- banner: z3.boolean(),
695
- push: z3.boolean()
700
+ var UserSettingsSchema = z4.object({
701
+ permissions: z4.object({
702
+ call: z4.boolean(),
703
+ banner: z4.boolean(),
704
+ push: z4.boolean()
696
705
  }),
697
- sessionMode: z3.enum(["default", "all_calls", "silent"]),
698
- silentPush: z3.boolean(),
699
- autoCallback: z3.boolean(),
706
+ sessionMode: z4.enum(["default", "all_calls", "silent"]),
707
+ silentPush: z4.boolean(),
708
+ autoCallback: z4.boolean(),
700
709
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
701
- improveConsent: z3.boolean(),
710
+ improveConsent: z4.boolean(),
702
711
  missedCall: MissedCallSchema.default("backoff_standard"),
703
712
  /** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
704
713
  * (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
@@ -706,7 +715,13 @@ var UserSettingsSchema = z3.object({
706
715
  * Optional, NOT defaulted: a stale client PATCHing the full settings object
707
716
  * must not silently reset this privacy choice. Absent = leave unchanged on
708
717
  * write, 'hosted' on read (see store.ts). */
709
- voiceMode: z3.enum(["hosted", "on_device"]).optional(),
718
+ voiceMode: z4.enum(["hosted", "on_device"]).optional(),
719
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
720
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
721
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
722
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
723
+ * read (see store.ts). */
724
+ talk: z4.boolean().optional(),
710
725
  /** Per-user ring budget (#603): calls per rolling day before further calls
711
726
  * degrade to banner. Absent = the global default (25). A number, never a
712
727
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -714,10 +729,10 @@ var UserSettingsSchema = z3.object({
714
729
  * payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
715
730
  * slower speaker). No API-side semantics; the bot resolves each key with its
716
731
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
717
- voiceTuning: z3.record(z3.string(), z3.union([z3.number(), z3.string()])).optional(),
732
+ voiceTuning: z4.record(z4.string(), z4.union([z4.number(), z4.string()])).optional(),
718
733
  /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
719
734
  * defaulted — an older client PATCHing the full object must not clobber it. */
720
- pstnCalls: z3.boolean().optional(),
735
+ pstnCalls: z4.boolean().optional(),
721
736
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
722
737
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
723
738
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -726,52 +741,52 @@ var UserSettingsSchema = z3.object({
726
741
  * that failure reads as the reminder rail being unreliable rather than as a missing
727
742
  * setting. Absent = a spoken time can't be landed, so the reminder rides the next
728
743
  * call — honest about what we know. */
729
- timezone: z3.string().min(1).max(64).optional(),
744
+ timezone: z4.string().min(1).max(64).optional(),
730
745
  /** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
731
746
  * clobber guard as voiceMode: absent = leave unchanged on write. */
732
747
  broker: BrokerTuningSchema.optional()
733
748
  });
734
- var HistoryItemSchema = z3.object({
735
- id: z3.string(),
736
- parentId: z3.string(),
749
+ var HistoryItemSchema = z4.object({
750
+ id: z4.string(),
751
+ parentId: z4.string(),
737
752
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
738
- initiator: z3.enum(["user", "agent"]),
739
- title: z3.string(),
753
+ initiator: z4.enum(["user", "agent"]),
754
+ title: z4.string(),
740
755
  /** The agent on the other end (its name). */
741
- name: z3.string(),
742
- createdAt: z3.string(),
756
+ name: z4.string(),
757
+ createdAt: z4.string(),
743
758
  /** When the agent fetched your request (user→agent only). */
744
- agentAckedAt: z3.string().nullable(),
759
+ agentAckedAt: z4.string().nullable(),
745
760
  /** When you answered the agent's notification (agent→user only). */
746
- humanAckedAt: z3.string().nullable()
761
+ humanAckedAt: z4.string().nullable()
747
762
  });
748
763
  var ACTIVITY_LINES = 2;
749
764
  var ACTIVITY_LINE_MAX = 80;
750
- var AgentActivitySchema = z3.object({
765
+ var AgentActivitySchema = z4.object({
751
766
  /** Oldest first, so the newest line is last — the one that replaces in place. */
752
- lines: z3.array(z3.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
767
+ lines: z4.array(z4.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
753
768
  /** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
754
769
  * that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
755
- at: z3.string().datetime()
770
+ at: z4.string().datetime()
756
771
  });
757
- var ConnectionSummarySchema = z3.object({
772
+ var ConnectionSummarySchema = z4.object({
758
773
  /** The connection = the agent's token id (used to address a request). */
759
- id: z3.string(),
774
+ id: z4.string(),
760
775
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
761
776
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
762
777
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
763
778
  * apps/api/src/tokens/devices-vs-agents-design.md. */
764
- kind: z3.enum(["device", "agent"]).optional(),
779
+ kind: z4.enum(["device", "agent"]).optional(),
765
780
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
766
781
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
767
782
  * agents (phone-launched, provider-managed, or minted before the link existed). */
768
- mintedByDevice: z3.string().nullable().optional(),
769
- device: z3.string().nullable(),
783
+ mintedByDevice: z4.string().nullable().optional(),
784
+ device: z4.string().nullable(),
770
785
  /** The agent's display name (the single pairing name). */
771
- name: z3.string(),
786
+ name: z4.string(),
772
787
  /** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
773
788
  * label; null for a local connection. Sourced from the token's provider, not the name. */
774
- provider: z3.string().nullable(),
789
+ provider: z4.string().nullable(),
775
790
  /** The pairing's assigned voice (#462); null = the default voice. */
776
791
  voice: VoiceKeySchema.nullable(),
777
792
  /** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
@@ -782,22 +797,22 @@ var ConnectionSummarySchema = z3.object({
782
797
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
783
798
  * set once must not overrule a rule they set about one agent. */
784
799
  reach: NotifyLevelSchema.nullable().optional(),
785
- createdAt: z3.string().datetime(),
800
+ createdAt: z4.string().datetime(),
786
801
  /** Most recent notification on this connection, either direction. Null = no contact yet.
787
802
  * Drives the agents-page recency grouping (Today / This week / …). */
788
- lastContactAt: z3.string().datetime().nullable(),
803
+ lastContactAt: z4.string().datetime().nullable(),
789
804
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
790
805
  * desktop app while open. Null = never seen; stale = offline. */
791
- lastSeenAt: z3.string().datetime().nullable().optional(),
806
+ lastSeenAt: z4.string().datetime().nullable().optional(),
792
807
  /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
793
808
  * harness availabilities + granted workspaces — the option set the phone's
794
809
  * "new session" sheet offers. Absent for ordinary MCP agents. */
795
- runtime: z3.object({
810
+ runtime: z4.object({
796
811
  /** The @paigy/harness this host is running — a machine the self-update has not reached
797
812
  * shows its age here (`apps/desktop/src/update.ts`). */
798
- version: z3.string().optional(),
799
- harnesses: z3.array(z3.object({ name: z3.string(), label: z3.string(), status: z3.string() })).optional(),
800
- workspaces: z3.array(z3.string()).optional()
813
+ version: z4.string().optional(),
814
+ harnesses: z4.array(z4.object({ name: z4.string(), label: z4.string(), status: z4.string() })).optional(),
815
+ workspaces: z4.array(z4.string()).optional()
801
816
  }).optional(),
802
817
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
803
818
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -806,148 +821,156 @@ var ConnectionSummarySchema = z3.object({
806
821
  activity: AgentActivitySchema.optional(),
807
822
  /** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
808
823
  * false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
809
- managed: z3.boolean()
824
+ managed: z4.boolean()
810
825
  });
811
- var LedgerItemSchema = z3.object({ id: z3.string(), parentId: z3.string(), title: z3.string(), createdAt: z3.string() });
812
- var AgentLedgerSchema = z3.object({
813
- agent: z3.object({ id: z3.string(), name: z3.string(), revokedAt: z3.string().nullable() }),
826
+ var LedgerItemSchema = z4.object({ id: z4.string(), parentId: z4.string(), title: z4.string(), createdAt: z4.string() });
827
+ var AgentLedgerSchema = z4.object({
828
+ agent: z4.object({ id: z4.string(), name: z4.string(), revokedAt: z4.string().nullable() }),
814
829
  /** Its own questions you have not answered. */
815
- asks: z3.array(LedgerItemSchema),
830
+ asks: z4.array(LedgerItemSchema),
816
831
  /** Its questions you answered that nobody acted on — still owed to somebody. */
817
- answered: z3.array(LedgerItemSchema),
832
+ answered: z4.array(LedgerItemSchema),
818
833
  /** Requests you sent it that it never took. */
819
- requests: z3.array(LedgerItemSchema),
820
- goals: z3.array(z3.object({ id: z3.string(), outcome: z3.string(), state: z3.string() })),
821
- callbacks: z3.array(z3.object({ id: z3.string(), parentId: z3.string(), trigger: z3.string(), note: z3.string(), dueAt: z3.string().nullable() }))
834
+ requests: z4.array(LedgerItemSchema),
835
+ goals: z4.array(z4.object({ id: z4.string(), outcome: z4.string(), state: z4.string() })),
836
+ callbacks: z4.array(z4.object({ id: z4.string(), parentId: z4.string(), trigger: z4.string(), note: z4.string(), dueAt: z4.string().nullable() }))
822
837
  });
823
- var ReassignResultSchema = z3.object({
824
- moved: z3.object({ asks: z3.number(), answered: z3.number(), requests: z3.number(), goals: z3.number(), callbacks: z3.number() }),
825
- parentId: z3.string().nullable()
838
+ var ReassignResultSchema = z4.object({
839
+ moved: z4.object({ asks: z4.number(), answered: z4.number(), requests: z4.number(), goals: z4.number(), callbacks: z4.number() }),
840
+ parentId: z4.string().nullable()
826
841
  });
827
- var MoveRingSchema = z3.enum(["home", "travels", "retired", "quarantined"]);
828
- var MoveSchema = z3.object({
829
- id: z3.string(),
842
+ var MoveRingSchema = z4.enum(["home", "travels", "retired", "quarantined"]);
843
+ var MoveSchema = z4.object({
844
+ id: z4.string(),
830
845
  /** The reusable question, as distill normalized it. */
831
- question: z3.string(),
846
+ question: z4.string(),
832
847
  /** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
833
- answer: z3.string(),
848
+ answer: z4.string(),
834
849
  /** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
835
850
  * told so, and the ruling only derives essentially the same question in the same scope. */
836
- rationale: z3.string().nullable(),
851
+ rationale: z4.string().nullable(),
837
852
  /** Where the ruling lives: a repo/workspace, or 'global'. */
838
- scope: z3.string(),
853
+ scope: z4.string(),
839
854
  ring: MoveRingSchema,
840
855
  /** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
841
- pinned: z3.boolean(),
856
+ pinned: z4.boolean(),
842
857
  /** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
843
858
  * break is visible instead of a pin silently disappearing. */
844
- pinBroken: z3.boolean(),
859
+ pinBroken: z4.boolean(),
845
860
  /** When the ruling was distilled. */
846
- learnedAt: z3.string(),
861
+ learnedAt: z4.string(),
847
862
  /** Last time it answered an ask. Null = never fired. */
848
- lastUsedAt: z3.string().nullable(),
863
+ lastUsedAt: z4.string().nullable(),
849
864
  /** How many asks it has answered. Instrumentation — deliberately NOT an input to the
850
865
  * evidence curve: firing says the question keeps arising, not that the ruling is right. */
851
- usedCount: z3.number(),
866
+ usedCount: z4.number(),
852
867
  /** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
853
- confirms: z3.number(),
868
+ confirms: z4.number(),
854
869
  /** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
855
870
  * Linear and priced above the entire confirmation budget, so any full counter wins. */
856
- counters: z3.number(),
871
+ counters: z4.number(),
857
872
  /** The agent that asked the question this move came from, when known. Null for a move
858
873
  * distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
859
- learnedFrom: z3.object({ id: z3.string(), name: z3.string() }).nullable()
874
+ learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
860
875
  });
861
- var QueueQuestionSchema = z3.object({
876
+ var QueueQuestionSchema = z4.object({
862
877
  /** The decision need's id — what an answer is accepted against. */
863
- id: z3.string(),
878
+ id: z4.string(),
864
879
  /** The words that were asked, from the request Entry that asked them. */
865
- question: z3.string(),
880
+ question: z4.string(),
866
881
  /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
867
882
  * for a need whose request Entry is carried by no interactive Delivery, which nothing
868
883
  * can answer. */
869
- deliveryId: z3.string().nullable().default(null),
884
+ deliveryId: z4.string().nullable().default(null),
870
885
  /** The Entry the ruling is about. */
871
- aboutId: z3.string().nullable().default(null),
886
+ aboutId: z4.string().nullable().default(null),
872
887
  /** Empty for a free-text question. */
873
- options: z3.array(OptionSchema).default([]),
874
- select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
875
- askedAt: z3.string(),
888
+ options: z4.array(OptionSchema).default([]),
889
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
890
+ askedAt: z4.string(),
876
891
  /** Null while the question is open — which is how the page tells the two apart. */
877
- answeredAt: z3.string().nullable().default(null),
892
+ answeredAt: z4.string().nullable().default(null),
878
893
  /** The ruling in the person's own words, from the contribution that replied — not the
879
894
  * option id, which is not something anyone reads back. Null while it is open, and null
880
895
  * for a settled question whose reply carried nothing readable. */
881
- answer: z3.string().nullable().default(null),
896
+ answer: z4.string().nullable().default(null),
882
897
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
883
898
  * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
884
899
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
885
900
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
886
901
  * this is the field they agree through rather than each re-deriving it from the row it
887
902
  * arrived under. Required because the API projects it on every need it sends. */
888
- goalId: z3.string(),
903
+ goalId: z4.string(),
889
904
  /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
890
905
  * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
891
906
  * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
892
907
  * gate at all. */
893
- blocked: z3.boolean().default(false)
908
+ blocked: z4.boolean().default(false)
894
909
  });
895
- var QueueItemSchema = z3.object({
896
- id: z3.string(),
910
+ var QueueItemSchema = z4.object({
911
+ id: z4.string(),
897
912
  /** One-line headline — the first sentence of the outcome. */
898
- title: z3.string(),
913
+ title: z4.string(),
899
914
  /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
900
- intent: z3.string(),
915
+ intent: z4.string(),
901
916
  /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
902
- state: z3.string(),
917
+ state: z4.string(),
903
918
  /** Who holds it (a participant ref); null when nobody does yet. */
904
- assignee: z3.string().nullable().default(null),
919
+ assignee: z4.string().nullable().default(null),
905
920
  /** What the agent last said it was doing; null if it has said nothing. */
906
- progress: z3.string().nullable().default(null),
907
- reviewPending: z3.boolean().default(false),
908
- dueAt: z3.string().nullable().default(null),
921
+ progress: z4.string().nullable().default(null),
922
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
923
+ * served only while it was written for the current note. Null means show the Goal's name. */
924
+ progressLine: z4.string().nullable().optional(),
925
+ reviewPending: z4.boolean().default(false),
926
+ dueAt: z4.string().nullable().default(null),
909
927
  /** The Goal this one was opened under; null at the root. */
910
- parentGoalId: z3.string().nullable().default(null),
928
+ parentGoalId: z4.string().nullable().default(null),
911
929
  /** Goals opened under this one — only those the same list holds. */
912
- childGoalIds: z3.array(z3.string()).default([]),
930
+ childGoalIds: z4.array(z4.string()).default([]),
913
931
  /** Goals this one waits on (start or finish gates). */
914
- dependencyGoalIds: z3.array(z3.string()).default([]),
932
+ dependencyGoalIds: z4.array(z4.string()).default([]),
915
933
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
916
- blocked: z3.boolean().default(false),
934
+ blocked: z4.boolean().default(false),
917
935
  /** Every decision need on it, open or settled — the page decides which to show. */
918
- questions: z3.array(QueueQuestionSchema).default([]),
936
+ questions: z4.array(QueueQuestionSchema).default([]),
919
937
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
920
- repo: z3.string().nullable().optional(),
921
- createdAt: z3.string(),
922
- updatedAt: z3.string().nullable().default(null)
938
+ repo: z4.string().nullable().optional(),
939
+ createdAt: z4.string(),
940
+ updatedAt: z4.string().nullable().default(null),
941
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
942
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
943
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
944
+ lastProgressAt: z4.string().nullable().optional()
923
945
  });
924
- var NoteSourceSchema = z3.enum(["app", "call"]);
925
- var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
926
- var NoteRepeatSchema = z3.enum(["once", "until_done"]);
927
- var DecisionSchema = z3.object({
928
- id: z3.string(),
946
+ var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
947
+ var NoteSourceSchema = z4.enum(["app", "call"]);
948
+ var NoteStatusSchema = z4.enum(["open", "assigned", "in_progress", "done"]);
949
+ var NoteRepeatSchema = z4.enum(["once", "until_done"]);
950
+ var DecisionSchema = z4.object({
951
+ id: z4.string(),
929
952
  /** The note this decision refines; null = recorded on a bare thread (the
930
953
  * extensibility seam — any conversation can accrue decisions). */
931
- noteId: z3.string().nullable(),
954
+ noteId: z4.string().nullable(),
932
955
  /** What was ambiguous — the broker's (or the user's own) question. */
933
- question: z3.string(),
956
+ question: z4.string(),
934
957
  /** The user's ruling; null while the question is open. */
935
- answer: z3.string().nullable(),
936
- decidedAt: z3.string().nullable(),
937
- createdAt: z3.string()
958
+ answer: z4.string().nullable(),
959
+ decidedAt: z4.string().nullable(),
960
+ createdAt: z4.string()
938
961
  });
939
- var NoteSchema = z3.object({
940
- id: z3.string(),
962
+ var NoteSchema = z4.object({
963
+ id: z4.string(),
941
964
  /** One-line headline (broker-titled; deterministic floor). */
942
- title: z3.string(),
965
+ title: z4.string(),
943
966
  /** The original intent, verbatim — assignees always see the user's own words. */
944
- intent: z3.string(),
967
+ intent: z4.string(),
945
968
  source: NoteSourceSchema,
946
969
  status: NoteStatusSchema,
947
970
  /** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
948
- assignee: z3.string().nullable(),
971
+ assignee: z4.string().nullable(),
949
972
  /** The request thread minted at assignment; null until assigned. */
950
- parentId: z3.string().nullable(),
973
+ parentId: z4.string().nullable(),
951
974
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
952
975
  * call — never a deadline. It only ever comes from the user's own words, so when it
953
976
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
@@ -956,131 +979,131 @@ var NoteSchema = z3.object({
956
979
  // Defaulted, not required: a Note from an API deploy older than the reminders
957
980
  // migration has none of these, and the defaults ARE what it means — no not-before,
958
981
  // one ride, never ridden. Parsing must not fail across a rolling deploy.
959
- dueAt: z3.string().nullable().default(null),
982
+ dueAt: z4.string().nullable().default(null),
960
983
  repeat: NoteRepeatSchema.default("once"),
961
984
  /** How many calls have already carried it — the fatigue cap counts rides, not days. */
962
- rides: z3.number().int().default(0),
963
- lastRideAt: z3.string().nullable().default(null),
964
- createdAt: z3.string()
985
+ rides: z4.number().int().default(0),
986
+ lastRideAt: z4.string().nullable().default(null),
987
+ createdAt: z4.string()
965
988
  });
966
- var TriageItemSchema = z3.object({
967
- noteId: z3.string(),
989
+ var TriageItemSchema = z4.object({
990
+ noteId: z4.string(),
968
991
  /** The note's headline at run time. */
969
- title: z3.string(),
992
+ title: z4.string(),
970
993
  /** WHY, in one short human line, evidence first — this is read on a phone underneath
971
994
  * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
972
995
  * Never a model's reasoning transcript, never an id. */
973
- why: z3.string()
996
+ why: z4.string()
974
997
  });
975
- var TriageAssignmentSchema = z3.object({
998
+ var TriageAssignmentSchema = z4.object({
976
999
  /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
977
- agent: z3.string(),
1000
+ agent: z4.string(),
978
1001
  /** Its display name at run time (the name on the hatchling's card). Denormalized for the
979
1002
  * same reason as `title`: the card must render from the proposal alone. */
980
- agentName: z3.string(),
981
- notes: z3.array(TriageItemSchema)
1003
+ agentName: z4.string(),
1004
+ notes: z4.array(TriageItemSchema)
982
1005
  });
983
- var TriageStatusSchema = z3.enum(["open", "superseded", "dismissed"]);
984
- var SubmitTriageSchema = z3.object({
1006
+ var TriageStatusSchema = z4.enum(["open", "superseded", "dismissed"]);
1007
+ var SubmitTriageSchema = z4.object({
985
1008
  /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
986
1009
  * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
987
1010
  * so the phone can say where the content went — an unattributed privacy claim is worth
988
1011
  * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
989
- provider: z3.string().min(1).max(60),
1012
+ provider: z4.string().min(1).max(60),
990
1013
  /** The concrete model when the provider names one (an ollama tag); null otherwise. */
991
- model: z3.string().max(200).nullable().optional(),
1014
+ model: z4.string().max(200).nullable().optional(),
992
1015
  /** How many open notes the run actually looked at — the denominator on the phone
993
1016
  * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
994
- reviewed: z3.number().int().min(0).max(1e4).default(0),
995
- close: z3.array(TriageItemSchema).max(200).default([]),
996
- stale: z3.array(TriageItemSchema).max(200).default([]),
997
- assign: z3.array(TriageAssignmentSchema).max(50).default([])
1017
+ reviewed: z4.number().int().min(0).max(1e4).default(0),
1018
+ close: z4.array(TriageItemSchema).max(200).default([]),
1019
+ stale: z4.array(TriageItemSchema).max(200).default([]),
1020
+ assign: z4.array(TriageAssignmentSchema).max(50).default([])
998
1021
  });
999
1022
  var TriageProposalSchema = SubmitTriageSchema.extend({
1000
- id: z3.string(),
1001
- runAt: z3.string(),
1023
+ id: z4.string(),
1024
+ runAt: z4.string(),
1002
1025
  status: TriageStatusSchema,
1003
- model: z3.string().nullable().default(null)
1026
+ model: z4.string().nullable().default(null)
1004
1027
  });
1005
- var AcceptTriageSchema = z3.discriminatedUnion("group", [
1006
- z3.object({ group: z3.literal("close"), noteIds: z3.array(z3.string()).max(200).optional() }),
1007
- z3.object({ group: z3.literal("stale"), noteIds: z3.array(z3.string()).max(200).optional() }),
1008
- z3.object({
1009
- group: z3.literal("assign"),
1010
- agent: z3.string().min(1),
1011
- noteIds: z3.array(z3.string()).max(200).optional()
1028
+ var AcceptTriageSchema = z4.discriminatedUnion("group", [
1029
+ z4.object({ group: z4.literal("close"), noteIds: z4.array(z4.string()).max(200).optional() }),
1030
+ z4.object({ group: z4.literal("stale"), noteIds: z4.array(z4.string()).max(200).optional() }),
1031
+ z4.object({
1032
+ group: z4.literal("assign"),
1033
+ agent: z4.string().min(1),
1034
+ noteIds: z4.array(z4.string()).max(200).optional()
1012
1035
  })
1013
1036
  ]);
1014
- var AcceptTriageResultSchema = z3.object({
1015
- accepted: z3.array(z3.string()),
1016
- failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
1037
+ var AcceptTriageResultSchema = z4.object({
1038
+ accepted: z4.array(z4.string()),
1039
+ failed: z4.array(z4.object({ noteId: z4.string(), reason: z4.string() }))
1017
1040
  });
1018
- var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
1019
- var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
1020
- var OAuthStartSchema = z3.object({
1021
- provider: z3.enum(["cma"]),
1022
- returnTo: z3.string().min(1)
1041
+ var DeliveryModeSchema = z4.enum(["poll", "self_hosted"]);
1042
+ var RegisterDeliverySchema = z4.object({ mode: DeliveryModeSchema });
1043
+ var OAuthStartSchema = z4.object({
1044
+ provider: z4.enum(["cma"]),
1045
+ returnTo: z4.string().min(1)
1023
1046
  });
1024
- var DeliveryConfigSchema = z3.object({
1025
- tokenId: z3.string(),
1047
+ var DeliveryConfigSchema = z4.object({
1048
+ tokenId: z4.string(),
1026
1049
  mode: DeliveryModeSchema,
1027
1050
  /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
1028
1051
  * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
1029
1052
  * carries credentials; only a `poll` registration can come back with null here. */
1030
- realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
1053
+ realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
1031
1054
  });
1032
- var WakeNudgeSchema = z3.object({
1033
- kind: z3.enum(["reply", "request", "callback"]),
1034
- notificationId: z3.string().optional(),
1035
- parentId: z3.string()
1055
+ var WakeNudgeSchema = z4.object({
1056
+ kind: z4.enum(["reply", "request", "callback"]),
1057
+ notificationId: z4.string().optional(),
1058
+ parentId: z4.string()
1036
1059
  });
1037
- var PairingStatusSchema = z3.enum(["pending", "approved", "denied", "expired"]);
1038
- var DeviceCodeSchema = z3.object({
1039
- device_code: z3.string(),
1040
- user_code: z3.string(),
1041
- verification_uri: z3.string().url(),
1042
- verification_uri_complete: z3.string().url(),
1043
- interval: z3.number(),
1044
- expires_in: z3.number()
1060
+ var PairingStatusSchema = z4.enum(["pending", "approved", "denied", "expired"]);
1061
+ var DeviceCodeSchema = z4.object({
1062
+ device_code: z4.string(),
1063
+ user_code: z4.string(),
1064
+ verification_uri: z4.string().url(),
1065
+ verification_uri_complete: z4.string().url(),
1066
+ interval: z4.number(),
1067
+ expires_in: z4.number()
1045
1068
  });
1046
- var DeviceInfoSchema = z3.object({
1047
- code: z3.string(),
1069
+ var DeviceInfoSchema = z4.object({
1070
+ code: z4.string(),
1048
1071
  /** The agent's suggested name (from /device/code) — shown on the approval screen,
1049
1072
  * pre-filling the name field the human can edit. */
1050
- name: z3.string(),
1073
+ name: z4.string(),
1051
1074
  /** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
1052
1075
  * build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
1053
1076
  * install crashes on the pairing screen on first launch, before the OTA lands
1054
1077
  * (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
1055
- agent: z3.string().optional(),
1056
- device: z3.string().nullable(),
1078
+ agent: z4.string().optional(),
1079
+ device: z4.string().nullable(),
1057
1080
  status: PairingStatusSchema
1058
1081
  });
1059
- var DeviceTokenSchema = z3.object({
1060
- access_token: z3.string(),
1082
+ var DeviceTokenSchema = z4.object({
1083
+ access_token: z4.string(),
1061
1084
  /** The pairing's single name (user-typed at approval, the agent's suggestion, or
1062
1085
  * a default silly name). */
1063
- name: z3.string(),
1064
- device: z3.string().nullable(),
1086
+ name: z4.string(),
1087
+ device: z4.string().nullable(),
1065
1088
  /** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
1066
1089
  * draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
1067
- voice: z3.string().nullable().optional(),
1090
+ voice: z4.string().nullable().optional(),
1068
1091
  /** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
1069
1092
  * that survives a rename. Cached by the host's identity beat. */
1070
- token_id: z3.string().nullable().optional(),
1093
+ token_id: z4.string().nullable().optional(),
1071
1094
  /** WHERE this identity works — the folder a wake should land it in. Written by the host
1072
1095
  * at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
1073
1096
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
1074
1097
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
1075
- workspace: z3.string().nullable().optional(),
1076
- uik_pub: z3.string().nullable().optional()
1098
+ workspace: z4.string().nullable().optional(),
1099
+ uik_pub: z4.string().nullable().optional()
1077
1100
  });
1078
- var SupportRequestSchema = z3.object({
1079
- email: z3.string().email().max(320),
1080
- message: z3.string().trim().min(1).max(5e3),
1081
- name: z3.string().trim().max(120).optional()
1101
+ var SupportRequestSchema = z4.object({
1102
+ email: z4.string().email().max(320),
1103
+ message: z4.string().trim().min(1).max(5e3),
1104
+ name: z4.string().trim().max(120).optional()
1082
1105
  });
1083
- var NotificationFeedbackKindSchema = z3.enum([
1106
+ var NotificationFeedbackKindSchema = z4.enum([
1084
1107
  "break_down",
1085
1108
  // "This should be more than one ask — break it down."
1086
1109
  "regenerate_options",
@@ -1095,9 +1118,117 @@ var NotificationFeedbackKindSchema = z3.enum([
1095
1118
  // anything else — the note carries it.
1096
1119
  ]);
1097
1120
 
1121
+ // src/listening.ts
1122
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
1123
+ import { homedir } from "os";
1124
+ import { dirname, join } from "path";
1125
+ var HARNESS_MARKS = ["PAIGY_HARNESS", "PAIGY_SESSION_ID"];
1126
+ function harnessDriven(env) {
1127
+ return HARNESS_MARKS.some((mark) => !!env[mark]);
1128
+ }
1129
+ function sessionStartHook() {
1130
+ return `#!/bin/sh
1131
+ # Paigy \u2014 written by \`paigy-enable-tools\`. Prints one line into a starting session.
1132
+ # Never edits, never calls the network, and exits 0 whatever happens: a hook that fails
1133
+ # is a session that fails to start.
1134
+ [ -n "${HARNESS_MARKS.map((m) => `\${${m}}`).join("")}" ] && exit 0 # harness-spawned (either mark): its pump already delivers
1135
+ [ -f "\${HOME}/.paigy/token.json" ] || exit 0 # not paired on this machine: nothing to listen as
1136
+ cat <<'PAIGY'
1137
+ Paigy: nothing pushes answers into a session started in a terminal \u2014 an answer waits for your
1138
+ next check_replies. Start your own listener now, as a BACKGROUND task whose output you watch
1139
+ (your harness's monitor/background-task tool), and leave it running for this session:
1140
+
1141
+ npx -y -p @paigy/mcp@latest paigy-listen
1142
+
1143
+ Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
1144
+ the answer needs a decision. Start it once; if it is already running, do nothing.
1145
+ PAIGY
1146
+ exit 0
1147
+ `;
1148
+ }
1149
+ function withSessionStartHook(existing, command) {
1150
+ let config = {};
1151
+ if (existing) {
1152
+ try {
1153
+ config = JSON.parse(existing);
1154
+ } catch {
1155
+ return null;
1156
+ }
1157
+ }
1158
+ const hooks = config.hooks ??= {};
1159
+ const starts = hooks.SessionStart ??= [];
1160
+ const already = starts.some((group) => (group.hooks ?? []).some((h) => h.command === command));
1161
+ if (!already) starts.push({ hooks: [{ type: "command", command }] });
1162
+ return JSON.stringify(config, null, 2);
1163
+ }
1164
+ var word = (s) => `'${s.replace(/'/g, "'\\''")}'`;
1165
+ function decideListen(f) {
1166
+ if (harnessDriven(f.env)) {
1167
+ return {
1168
+ status: "listening",
1169
+ via: "harness",
1170
+ message: "Already listening: this session was started by the Paigy harness, whose pump delivers every answer into it as it lands. Nothing to start."
1171
+ };
1172
+ }
1173
+ if (!f.token) {
1174
+ return {
1175
+ status: "unpaired",
1176
+ message: "Not paired: no token in this session's slot, so there is no identity to listen as \u2014 call onboard first."
1177
+ };
1178
+ }
1179
+ if (f.pidAlive && f.pid !== void 0) {
1180
+ return { status: "listening", via: "daemon", pid: f.pid, message: `Already listening (paigy-listen pid ${f.pid}). Do nothing.` };
1181
+ }
1182
+ return {
1183
+ status: "start",
1184
+ command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${word(f.execPath)} ${word(f.listenJs)} --brief`,
1185
+ how: "Run this command NOW as a watched background task (your harness's background-task/monitor tool), and leave it running for this session. Each line it prints is a Delivery waiting for you: claim_goal the Goal it names, read it, and answer with contact. Start it once."
1186
+ };
1187
+ }
1188
+ function listenMark(slot, home = homedir()) {
1189
+ return join(home, ".paigy", "listen", `${slot.replace(/[^\w.-]/g, "_")}.pid`);
1190
+ }
1191
+ function writeListenMark(slot, pid, home = homedir()) {
1192
+ const path = listenMark(slot, home);
1193
+ mkdirSync(dirname(path), { recursive: true });
1194
+ writeFileSync(path, `${pid}
1195
+ `, { mode: 384 });
1196
+ chmodSync(path, 384);
1197
+ }
1198
+ function removeListenMark(slot, home = homedir()) {
1199
+ rmSync(listenMark(slot, home), { force: true });
1200
+ }
1201
+ function listenerAlive(slot, home = homedir()) {
1202
+ const path = listenMark(slot, home);
1203
+ let pid;
1204
+ try {
1205
+ pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
1206
+ } catch {
1207
+ return null;
1208
+ }
1209
+ if (!Number.isInteger(pid) || pid <= 0) {
1210
+ rmSync(path, { force: true });
1211
+ return null;
1212
+ }
1213
+ try {
1214
+ process.kill(pid, 0);
1215
+ return pid;
1216
+ } catch (e) {
1217
+ if (e.code === "EPERM") return pid;
1218
+ rmSync(path, { force: true });
1219
+ return null;
1220
+ }
1221
+ }
1222
+
1098
1223
  export {
1099
1224
  mcpInputSchema,
1100
1225
  AGENT_TOOLS,
1101
1226
  serverInstructions,
1102
- entryWords
1227
+ entryWords,
1228
+ sessionStartHook,
1229
+ withSessionStartHook,
1230
+ decideListen,
1231
+ writeListenMark,
1232
+ removeListenMark,
1233
+ listenerAlive
1103
1234
  };