@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.
@@ -7,16 +7,18 @@ import { homedir } from "os";
7
7
  import { join } from "path";
8
8
  import { randomUUID as randomUUID3 } from "crypto";
9
9
  import { setTimeout as sleep2 } from "timers/promises";
10
+ import { z as z4 } from "zod";
11
+ import { z } from "zod";
10
12
  import { z as z3 } from "zod";
11
- import { z as z2 } from "zod";
12
13
  import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind3 } from "zod/v3";
13
14
  import { ZodFirstPartyTypeKind } from "zod/v3";
14
15
  import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind2 } from "zod/v3";
15
- import { z } from "zod";
16
+ import { z as z2 } from "zod";
16
17
  import { randomUUID as randomUUID2 } from "crypto";
17
18
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
18
19
  import { homedir as homedir2 } from "os";
19
20
  import { join as join2 } from "path";
21
+ import { execFileSync as execFileSync2 } from "child_process";
20
22
  import { randomUUID as randomUUID4 } from "crypto";
21
23
  import { WebSocket } from "undici";
22
24
  var require2 = __sdkCreateRequire(import.meta.url);
@@ -1301,6 +1303,22 @@ var zodToJsonSchema = (schema, options) => {
1301
1303
  };
1302
1304
  var OPTIONS_MIN = 2;
1303
1305
  var OPTIONS_MAX = 6;
1306
+ var OptionSchema = z.object({
1307
+ id: z.string(),
1308
+ label: z.string(),
1309
+ 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(),
1310
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
1311
+ // the constraints below are what an agent reads when deciding to use these.
1312
+ html: z.string().max(16384).describe(
1313
+ "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."
1314
+ ).optional(),
1315
+ image: z.string().url().describe(
1316
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
1317
+ ).optional()
1318
+ });
1319
+ var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
1320
+ label: z.string().trim().min(1).max(1e3)
1321
+ }).strict();
1304
1322
  var NIGHT = { from: 23, to: 7 };
1305
1323
  function draft2020(node) {
1306
1324
  if (Array.isArray(node)) return node.map(draft2020);
@@ -1331,39 +1349,30 @@ function mcpInputSchema(s) {
1331
1349
  delete schema.$schema;
1332
1350
  return draft2020(schema);
1333
1351
  }
1334
- var AskInputSchema = z.object({
1335
- id: z.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
1336
- 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."),
1337
- repo: z.string().optional().describe("Optional repository context."),
1338
- ask: z.string().trim().min(1).max(1e4).describe(
1352
+ var AskInputSchema = z2.object({
1353
+ id: z2.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
1354
+ 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."),
1355
+ repo: z2.string().optional().describe("Optional repository context."),
1356
+ ask: z2.string().trim().min(1).max(1e4).describe(
1339
1357
  "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."
1340
1358
  ),
1341
- options: z.array(z.object({
1342
- label: z.string().trim().min(1).max(1e3),
1343
- hint: z.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
1344
- image: z.string().url().optional(),
1345
- 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()
1346
- }).strict()).min(2).max(6).optional()
1359
+ options: z2.array(OptionInputSchema).min(2).max(6).optional()
1347
1360
  }).strict();
1348
- var StartContactSchema = z.object({
1349
- asks: z.array(AskInputSchema).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
1350
- waiting: z.enum(["none", "hard"]).default("none"),
1351
- channel: z.enum(["notification", "call"]).default("notification")
1361
+ var StartContactSchema = z2.object({
1362
+ 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."),
1363
+ waiting: z2.enum(["none", "hard"]).default("none"),
1364
+ channel: z2.enum(["notification", "call"]).default("notification")
1352
1365
  }).strict();
1353
- var ContactSchema = z.union([StartContactSchema, z.object({ deliveryId: z.string().uuid() }).strict()]);
1366
+ var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
1354
1367
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
1355
1368
  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.";
1356
- var CreateGoalSchema = z2.object({
1357
- outcome: z2.string().trim().min(1).max(1e4),
1369
+ var CreateGoalSchema = z3.object({
1370
+ outcome: z3.string().trim().min(1).max(1e4),
1358
1371
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
1359
1372
  * rings"). Omit it and the brain writes one at admission from the outcome. */
1360
- title: z2.string().trim().min(1).max(80).optional(),
1361
- ownerParticipant: z2.string().trim().min(1).optional(),
1362
- idempotencyKey: z2.string().trim().min(1).max(200),
1363
- /** A past conversation this Goal should be read against — History's "new session from this"
1364
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
1365
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
1366
- * the writer refuses a thread belonging to another account. */
1373
+ title: z3.string().trim().min(1).max(80).optional(),
1374
+ ownerParticipant: z3.string().trim().min(1).optional(),
1375
+ idempotencyKey: z3.string().trim().min(1).max(200),
1367
1376
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
1368
1377
  * didn't get created as a child goal of the voice UI goal, which is how it should've
1369
1378
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -1371,55 +1380,45 @@ var CreateGoalSchema = z2.object({
1371
1380
  * took the short one. The hierarchy has been modelled since Goals existed and had been used
1372
1381
  * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
1373
1382
  * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
1374
- parentGoalId: z2.string().uuid().optional(),
1383
+ parentGoalId: z3.string().uuid().optional(),
1375
1384
  /** The repository or project identifier this Goal belongs to (#2280) — e.g. "owner/repo" or
1376
1385
  * repo name. Delegated work inherits this from its parent Goal when omitted. */
1377
- repo: z2.string().trim().min(1).max(200).optional()
1386
+ repo: z3.string().trim().min(1).max(200).optional()
1378
1387
  });
1379
1388
  var CreateGoalToolSchema = CreateGoalSchema.extend({
1380
1389
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
1381
1390
  }).strict();
1382
1391
  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.';
1383
- var UpdateGoalSchema = z2.object({
1384
- revision: z2.number().int().positive(),
1385
- changes: z2.object({
1386
- outcome: z2.string().trim().min(1).max(1e4).optional(),
1392
+ var UpdateGoalSchema = z3.object({
1393
+ revision: z3.number().int().positive(),
1394
+ changes: z3.object({
1395
+ outcome: z3.string().trim().min(1).max(1e4).optional(),
1387
1396
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
1388
1397
  * brain writes one at admission; this is the owner saying it better. Null clears it. */
1389
- title: z2.string().trim().min(1).max(80).nullable().optional(),
1390
- ownerParticipant: z2.string().trim().min(1).optional(),
1391
- parentGoalId: z2.string().uuid().nullable().optional(),
1392
- dependencies: z2.array(z2.object({ goalId: z2.string().uuid(), gate: z2.enum(["start", "finish"]) }).strict()).optional(),
1393
- 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(),
1394
- state: z2.enum(["active", "done", "cancelled"]).optional(),
1395
- progress: z2.string().trim().min(1).max(1e4).optional(),
1396
- reviewed: z2.literal(true).optional(),
1397
- dueAt: z2.string().datetime({ offset: true }).nullable().optional()
1398
+ title: z3.string().trim().min(1).max(80).nullable().optional(),
1399
+ ownerParticipant: z3.string().trim().min(1).optional(),
1400
+ parentGoalId: z3.string().uuid().nullable().optional(),
1401
+ dependencies: z3.array(z3.object({ goalId: z3.string().uuid(), gate: z3.enum(["start", "finish"]) }).strict()).optional(),
1402
+ 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(),
1403
+ state: z3.enum(["active", "done", "cancelled"]).optional(),
1404
+ progress: z3.string().trim().min(1).max(1e4).optional(),
1405
+ reviewed: z3.literal(true).optional(),
1406
+ dueAt: z3.string().datetime({ offset: true }).nullable().optional()
1398
1407
  }).strict().refine((v) => Object.keys(v).length > 0),
1399
- reason: z2.string().trim().min(1).max(2e3),
1400
- operationId: z2.string().uuid().optional()
1408
+ reason: z3.string().trim().min(1).max(2e3),
1409
+ operationId: z3.string().uuid().optional()
1401
1410
  }).strict();
1402
- var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z2.string().uuid() }).strict();
1403
- var ClaimGoalSchema = z2.object({ goalId: z2.string().uuid().optional() }).strict();
1404
- var GetGoalSchema = z2.object({ goalId: z2.string().uuid() }).strict();
1411
+ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z3.string().uuid() }).strict();
1412
+ var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
1413
+ var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
1405
1414
  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.";
1406
1415
  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.";
1407
1416
  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.";
1408
1417
  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.";
1409
- var CheckRepliesSchema = z2.object({}).strict();
1410
- var GetThreadSchema = z2.object({
1411
- parentId: z2.string().describe("The Thread to read \u2014 the parentId of a search_threads hit.")
1412
- }).strict();
1413
- 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.";
1414
- var SearchThreadsSchema = z2.object({
1415
- q: z2.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
1416
- }).strict();
1417
- 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.`;
1418
+ var CheckRepliesSchema = z3.object({}).strict();
1418
1419
  var AGENT_TOOLS = [
1419
1420
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
1420
1421
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
1421
- { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
1422
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
1423
1422
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
1424
1423
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
1425
1424
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
@@ -1442,17 +1441,17 @@ function entryWords(entry) {
1442
1441
  return entry.sources.map((source) => source.text).join("\n");
1443
1442
  }
1444
1443
  var LIVE_MS = 3 * 6e4;
1445
- var ContextSchema = z3.object({
1446
- title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
1447
- description: z3.array(z3.string().min(1)).describe(
1444
+ var ContextSchema = z4.object({
1445
+ title: z4.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
1446
+ description: z4.array(z4.string().min(1)).describe(
1448
1447
  "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."
1449
1448
  )
1450
1449
  });
1451
- var ParticipantSchema = z3.object({
1452
- kind: z3.enum(["human", "agent"]),
1453
- id: z3.string()
1450
+ var ParticipantSchema = z4.object({
1451
+ kind: z4.enum(["human", "agent"]),
1452
+ id: z4.string()
1454
1453
  });
1455
- var TransformSchema = z3.enum([
1454
+ var TransformSchema = z4.enum([
1456
1455
  "structure",
1457
1456
  // shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
1458
1457
  "request_more",
@@ -1468,26 +1467,13 @@ var TransformSchema = z3.enum([
1468
1467
  "summarize"
1469
1468
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
1470
1469
  ]);
1471
- var OptionSchema = z3.object({
1472
- id: z3.string(),
1473
- label: z3.string(),
1474
- 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(),
1475
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
1476
- // the constraints below are what an agent reads when deciding to use these.
1477
- html: z3.string().max(16384).describe(
1478
- "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."
1479
- ).optional(),
1480
- image: z3.string().url().describe(
1481
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
1482
- ).optional()
1483
- });
1484
- var VisualSchema = z3.object({
1485
- url: z3.string().url(),
1486
- label: z3.string().optional()
1470
+ var VisualSchema = z4.object({
1471
+ url: z4.string().url(),
1472
+ label: z4.string().optional()
1487
1473
  });
1488
- var NotifyLevelSchema = z3.enum(["inbox", "push", "banner", "call"]);
1489
- var SelectShapeSchema = z3.enum(["one", "many", "rank", "confirm", "text"]);
1490
- var ReceiptEventSchema = z3.enum([
1474
+ var NotifyLevelSchema = z4.enum(["inbox", "push", "banner", "call"]);
1475
+ var SelectShapeSchema = z4.enum(["one", "many", "rank", "confirm", "text"]);
1476
+ var ReceiptEventSchema = z4.enum([
1491
1477
  "delivered",
1492
1478
  // the bundle reached the recipient at some level
1493
1479
  "seen",
@@ -1517,47 +1503,47 @@ var ReceiptEventSchema = z3.enum([
1517
1503
  // be rewound by a writer that forgot to advance it.
1518
1504
  "restarted"
1519
1505
  ]);
1520
- var AttentionSchema = z3.object({
1506
+ var AttentionSchema = z4.object({
1521
1507
  urgency: NotifyLevelSchema,
1522
1508
  /** The required answer shape, or null for a plain notify that asks nothing back. */
1523
1509
  select: SelectShapeSchema.nullable(),
1524
1510
  /** Coverage contract (#396) — points the answer must address; null = none declared. */
1525
- points: z3.array(z3.string()).nullable(),
1511
+ points: z4.array(z4.string()).nullable(),
1526
1512
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
1527
- blocking: z3.boolean(),
1513
+ blocking: z4.boolean(),
1528
1514
  /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
1529
1515
  * slice wires it; optional so today's rows/callers project cleanly. */
1530
- deadline: z3.string().datetime().nullable().optional()
1516
+ deadline: z4.string().datetime().nullable().optional()
1531
1517
  });
1532
- var NotifyRequestFields = z3.object({
1518
+ var NotifyRequestFields = z4.object({
1533
1519
  /** Plaintext message content. Present on the plaintext path (today's shape);
1534
1520
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
1535
1521
  * superRefine at the bottom enforces exactly one of the two. */
1536
1522
  context: ContextSchema.optional(),
1537
- options: z3.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
1523
+ options: z4.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
1538
1524
  "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)."
1539
1525
  ),
1540
- points: z3.array(z3.string().min(1)).optional().describe(
1526
+ points: z4.array(z4.string().min(1)).optional().describe(
1541
1527
  "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."
1542
1528
  ),
1543
- visuals: z3.array(VisualSchema).optional().describe(
1529
+ visuals: z4.array(VisualSchema).optional().describe(
1544
1530
  "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."
1545
1531
  ),
1546
1532
  /** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
1547
- repo: z3.string().optional(),
1533
+ repo: z4.string().optional(),
1548
1534
  /** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
1549
- branch: z3.string().optional(),
1535
+ branch: z4.string().optional(),
1550
1536
  /** Continue an existing conversation — the id of any notification in it (its root
1551
1537
  * is the conversation's identity). Omitted = start a new conversation. Renamed
1552
1538
  * from `parentId` (2026-08-03): one linkage system, the parent; the API edge
1553
1539
  * still accepts the old name from older clients. */
1554
- parentId: z3.string().uuid().optional(),
1540
+ parentId: z4.string().uuid().optional(),
1555
1541
  /** The durable outcome this contact advances. Optional during the notification-to-Work
1556
1542
  * migration; when present, a blocking ask creates a DecisionNeed for this Work. */
1557
- workId: z3.string().uuid().optional(),
1543
+ workId: z4.string().uuid().optional(),
1558
1544
  /** Target Goal scope. During staged migration this is accepted by the shared contract but
1559
1545
  * target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
1560
- goalId: z3.string().uuid().optional(),
1546
+ goalId: z4.string().uuid().optional(),
1561
1547
  urgency: NotifyLevelSchema.default("inbox").describe(
1562
1548
  "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."
1563
1549
  ),
@@ -1565,7 +1551,7 @@ var NotifyRequestFields = z3.object({
1565
1551
  * visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
1566
1552
  * when `parentId` became the conversation handle: `parentId` says WHERE, this
1567
1553
  * says HOW. */
1568
- clarifies: z3.string().optional(),
1554
+ clarifies: z4.string().optional(),
1569
1555
  select: SelectShapeSchema.optional().describe(
1570
1556
  "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."
1571
1557
  ),
@@ -1579,20 +1565,20 @@ var NotifyRequestFields = z3.object({
1579
1565
  // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
1580
1566
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
1581
1567
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
1582
- ask: z3.string().min(1).max(1e4).optional().describe(
1568
+ ask: z4.string().min(1).max(1e4).optional().describe(
1583
1569
  '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.'
1584
1570
  ),
1585
- needs: z3.array(z3.string().min(1)).optional().describe(
1571
+ needs: z4.array(z4.string().min(1)).optional().describe(
1586
1572
  "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."
1587
1573
  ),
1588
- urgencyHint: z3.enum(["whenever", "soon", "now"]).optional().describe(
1574
+ urgencyHint: z4.enum(["whenever", "soon", "now"]).optional().describe(
1589
1575
  "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."
1590
1576
  ),
1591
1577
  /** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
1592
1578
  * to the agent's work while it waits. Normalized server-side into those two
1593
1579
  * fields (normalizeWaiting) so everything downstream is untouched; explicit
1594
1580
  * urgencyHint/blocking win when both are sent. */
1595
- waiting: z3.enum(["none", "soft", "hard"]).optional().describe(
1581
+ waiting: z4.enum(["none", "soft", "hard"]).optional().describe(
1596
1582
  "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."
1597
1583
  ),
1598
1584
  /** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
@@ -1600,98 +1586,98 @@ var NotifyRequestFields = z3.object({
1600
1586
  * holding by default would charge every quiet claim that minute before any agent could
1601
1587
  * correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
1602
1588
  * and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
1603
- confirm: z3.boolean().optional().describe(
1589
+ confirm: z4.boolean().optional().describe(
1604
1590
  "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'."
1605
1591
  ),
1606
1592
  /** #575: a RELAY of the user's explicitly stated preference, never the agent's
1607
1593
  * choice. Outranks waiting in both directions: 'call' rings even for a
1608
1594
  * waiting:'none' "call me when it's done"; 'message' never rings even for
1609
1595
  * waiting:'hard'. */
1610
- channel: z3.enum(["call", "message"]).optional().describe(
1596
+ channel: z4.enum(["call", "message"]).optional().describe(
1611
1597
  "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."
1612
1598
  ),
1613
- confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
1599
+ confirmStyle: z4.enum(["yesno", "approve"]).default("yesno").describe(
1614
1600
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
1615
1601
  ),
1616
- blocking: z3.boolean().default(false).describe(
1602
+ blocking: z4.boolean().default(false).describe(
1617
1603
  "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."
1618
1604
  )
1619
1605
  });
1620
1606
  var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
1621
- if (r.workId && r.goalId) ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
1607
+ if (r.workId && r.goalId) ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
1622
1608
  if (r.ask !== void 0) {
1623
1609
  for (const f of ["context", "select", "points"]) {
1624
1610
  if (r[f] !== void 0)
1625
- 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.` });
1611
+ 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.` });
1626
1612
  }
1627
1613
  return;
1628
1614
  }
1629
1615
  if (r.needs !== void 0 || r.urgencyHint !== void 0)
1630
- 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" });
1616
+ 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" });
1631
1617
  if (!r.context)
1632
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
1618
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
1633
1619
  if (!r.select)
1634
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
1620
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
1635
1621
  const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
1636
1622
  if (needsOptions && !r.options?.length)
1637
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
1623
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
1638
1624
  if (!needsOptions && r.options?.length)
1639
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
1625
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
1640
1626
  });
1641
- var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
1642
- var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
1643
- var TurnSchema = z3.object({
1644
- prompt: z3.string(),
1645
- reply: z3.string()
1627
+ var NotifyStatusSchema = z4.enum(["pending", "answered", "ignored"]);
1628
+ var AgentStateSchema = z4.enum(["idle", "in_progress", "completed", "needs_input"]);
1629
+ var TurnSchema = z4.object({
1630
+ prompt: z4.string(),
1631
+ reply: z4.string()
1646
1632
  });
1647
- var UserAnswerSchema = z3.discriminatedUnion("kind", [
1648
- z3.object({ kind: z3.literal("option"), optionId: z3.string(), label: z3.string().optional() }),
1649
- z3.object({ kind: z3.literal("text"), text: z3.string() }),
1650
- z3.object({ kind: z3.literal("ignored") }),
1651
- z3.object({ kind: z3.literal("multi"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
1652
- z3.object({ kind: z3.literal("ranked"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
1653
- z3.object({ kind: z3.literal("clarify"), chunks: z3.array(z3.string()).min(1) }),
1654
- z3.object({ kind: z3.literal("confirm"), approved: z3.boolean() }),
1655
- z3.object({ kind: z3.literal("turns"), turns: z3.array(TurnSchema).min(1) }),
1633
+ var UserAnswerSchema = z4.discriminatedUnion("kind", [
1634
+ z4.object({ kind: z4.literal("option"), optionId: z4.string(), label: z4.string().optional() }),
1635
+ z4.object({ kind: z4.literal("text"), text: z4.string() }),
1636
+ z4.object({ kind: z4.literal("ignored") }),
1637
+ z4.object({ kind: z4.literal("multi"), optionIds: z4.array(z4.string()), labels: z4.array(z4.string()).optional() }),
1638
+ z4.object({ kind: z4.literal("ranked"), optionIds: z4.array(z4.string()), labels: z4.array(z4.string()).optional() }),
1639
+ z4.object({ kind: z4.literal("clarify"), chunks: z4.array(z4.string()).min(1) }),
1640
+ z4.object({ kind: z4.literal("confirm"), approved: z4.boolean() }),
1641
+ z4.object({ kind: z4.literal("turns"), turns: z4.array(TurnSchema).min(1) }),
1656
1642
  /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
1657
1643
  * delivered through the same settle/await path as a human answer, carrying the judge's
1658
1644
  * derivation and the precedent ids it grew from. Always paired with a visible trail
1659
1645
  * card the user can reply to — the broker never overrides the user. */
1660
- z3.object({ kind: z3.literal("precedent"), answer: z3.string(), derivation: z3.string(), sources: z3.array(z3.string()).min(1) })
1646
+ z4.object({ kind: z4.literal("precedent"), answer: z4.string(), derivation: z4.string(), sources: z4.array(z4.string()).min(1) })
1661
1647
  ]);
1662
- var IntentSchema = z3.object({
1648
+ var IntentSchema = z4.object({
1663
1649
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
1664
1650
  // it by two ("detail", "feedback"), and because the settle handler parsed the array
1665
1651
  // all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
1666
1652
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
1667
- kind: z3.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
1668
- detail: z3.string(),
1653
+ kind: z4.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
1654
+ detail: z4.string(),
1669
1655
  /** Defer only: seconds until the callback the caller asked for, when something upstream
1670
1656
  * already read the time. Nothing sets it today (#397 documented an MCP parser that was
1671
1657
  * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
1672
1658
  * (`parseDelay`, #1292), and a value here simply wins over that reading. */
1673
- dueInSeconds: z3.number().int().positive().optional(),
1659
+ dueInSeconds: z4.number().int().positive().optional(),
1674
1660
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
1675
1661
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
1676
1662
  * 'other' on every row. A table that records that something was wrong and nothing
1677
1663
  * about what cannot answer "is the bot looping less this week?". */
1678
- fault: z3.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
1664
+ fault: z4.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
1679
1665
  });
1680
- var RideAlongSchema = z3.object({
1666
+ var RideAlongSchema = z4.object({
1681
1667
  /** The note this came from — assign/clarify/close it through /api/notes/:id. */
1682
- noteId: z3.string(),
1668
+ noteId: z4.string(),
1683
1669
  /** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
1684
- text: z3.string(),
1670
+ text: z4.string(),
1685
1671
  /** The thread to report back on, when the note was dispatched over the request rail. */
1686
- parentId: z3.string().nullable()
1672
+ parentId: z4.string().nullable()
1687
1673
  });
1688
- var AwaitItemSchema = z3.discriminatedUnion("type", [
1689
- z3.object({
1690
- type: z3.literal("reply"),
1691
- parentId: z3.string(),
1692
- notificationId: z3.string(),
1693
- workId: z3.string().uuid().optional(),
1694
- decisionId: z3.string().uuid().optional(),
1674
+ var AwaitItemSchema = z4.discriminatedUnion("type", [
1675
+ z4.object({
1676
+ type: z4.literal("reply"),
1677
+ parentId: z4.string(),
1678
+ notificationId: z4.string(),
1679
+ workId: z4.string().uuid().optional(),
1680
+ decisionId: z4.string().uuid().optional(),
1695
1681
  answer: UserAnswerSchema,
1696
1682
  /** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
1697
1683
  * #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
@@ -1701,118 +1687,118 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
1701
1687
  * "call me back after you merge" in their own words decides for itself what to do,
1702
1688
  * and now knows exactly which call to make. Absent when either half is missing —
1703
1689
  * a sentence with a hole in it is worse than no sentence. */
1704
- note: z3.string().optional(),
1690
+ note: z4.string().optional(),
1705
1691
  /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
1706
1692
  * shaped answer was mapped from, filtered to its own claims. There is no second list
1707
1693
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
1708
1694
  * 2026-09-04): the agent reads the sentence and decides. */
1709
- transcript: z3.string().optional(),
1695
+ transcript: z4.string().optional(),
1710
1696
  /** Coverage report (#396), when the ask declared `points`: which of them this
1711
1697
  * answer addressed. Missing points = re-ask or proceed knowingly partial. */
1712
- covered: z3.array(z3.string()).optional(),
1698
+ covered: z4.array(z4.string()).optional(),
1713
1699
  /** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
1714
1700
  * became free. Only `reply` and `idle` carry it: those are the two outcomes that
1715
1701
  * END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
1716
1702
  * agent a side-quest while it is still holding the line is how the main thing gets
1717
1703
  * dropped. Absent/empty = nothing owed. */
1718
- also: z3.array(RideAlongSchema).optional()
1704
+ also: z4.array(RideAlongSchema).optional()
1719
1705
  }),
1720
- z3.object({
1721
- type: z3.literal("remind"),
1722
- parentId: z3.string(),
1723
- notificationId: z3.string(),
1724
- remindAt: z3.string().datetime({ offset: true }),
1706
+ z4.object({
1707
+ type: z4.literal("remind"),
1708
+ parentId: z4.string(),
1709
+ notificationId: z4.string(),
1710
+ remindAt: z4.string().datetime({ offset: true }),
1725
1711
  /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
1726
- remindInSeconds: z3.number()
1712
+ remindInSeconds: z4.number()
1727
1713
  }),
1728
1714
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
1729
1715
  * post-feedback revision, #633) — the user will never answer this id. Stop
1730
1716
  * awaiting it; the live ask is the thread's newest turn (await that one, or
1731
- * re-orient via get_thread / check_replies). */
1732
- z3.object({
1733
- type: z3.literal("superseded"),
1734
- parentId: z3.string(),
1735
- notificationId: z3.string()
1717
+ * re-orient via check_replies). */
1718
+ z4.object({
1719
+ type: z4.literal("superseded"),
1720
+ parentId: z4.string(),
1721
+ notificationId: z4.string()
1736
1722
  }),
1737
1723
  /** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
1738
1724
  * revise any of these until the final reply arrives — partial = intelligence,
1739
1725
  * settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
1740
1726
  * irreversibly. If `acts` carries a question aimed at you and you know the answer,
1741
1727
  * contact on the same thread right away — the caller hears it on the same call. */
1742
- z3.object({
1743
- type: z3.literal("partial"),
1744
- notificationId: z3.string(),
1745
- inFlight: z3.literal(true),
1746
- turn: z3.object({
1747
- idx: z3.number(),
1748
- prompt: z3.string(),
1749
- reply: z3.string(),
1750
- acts: z3.array(IntentSchema).nullable().optional()
1728
+ z4.object({
1729
+ type: z4.literal("partial"),
1730
+ notificationId: z4.string(),
1731
+ inFlight: z4.literal(true),
1732
+ turn: z4.object({
1733
+ idx: z4.number(),
1734
+ prompt: z4.string(),
1735
+ reply: z4.string(),
1736
+ acts: z4.array(IntentSchema).nullable().optional()
1751
1737
  })
1752
1738
  }),
1753
- z3.object({
1754
- type: z3.literal("idle"),
1755
- also: z3.array(RideAlongSchema).optional(),
1739
+ z4.object({
1740
+ type: z4.literal("idle"),
1741
+ also: z4.array(RideAlongSchema).optional(),
1756
1742
  /** Is a call live for this agent's user right now? The SDK polls the partial stream
1757
1743
  * (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
1758
1744
  * during a live call, and polling for one on a banner/message was a wasted HTTP call +
1759
1745
  * 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
1760
1746
  * Absent = an older API → the SDK keeps polling, exactly as before. */
1761
- inFlight: z3.boolean().optional()
1747
+ inFlight: z4.boolean().optional()
1762
1748
  })
1763
1749
  ]);
1764
- var VoiceKeySchema = z3.enum(["rachel", "george", "jessica", "brian", "lily"]);
1765
- var AgendaTurnSchema = z3.object({
1750
+ var VoiceKeySchema = z4.enum(["rachel", "george", "jessica", "brian", "lily"]);
1751
+ var AgendaTurnSchema = z4.object({
1766
1752
  /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
1767
1753
  * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
1768
1754
  * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
1769
1755
  * completion of a streamed re-plan carries the published sentence again, and the walk
1770
1756
  * drops what it already said by identity, never by the API's guess of what was polled.
1771
1757
  * Absent on plans nothing streams (a ring plan, a floor). */
1772
- id: z3.string().optional(),
1758
+ id: z4.string().optional(),
1773
1759
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
1774
1760
  * the planner declares duplicates instead of asking them twice. */
1775
- coveredIds: z3.array(z3.string()).optional(),
1761
+ coveredIds: z4.array(z4.string()).optional(),
1776
1762
  /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
1777
1763
  * line went out on 2026-07-28 and the caller could not answer it at all. */
1778
- info: z3.array(z3.string().min(1)).max(3).default([]),
1779
- question: z3.string().min(1).nullable(),
1764
+ info: z4.array(z4.string().min(1)).max(3).default([]),
1765
+ question: z4.string().min(1).nullable(),
1780
1766
  /** True on the one turn carrying the agent's own declared question. */
1781
- asks: z3.boolean().optional(),
1767
+ asks: z4.boolean().optional(),
1782
1768
  /** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
1783
1769
  * Absent on a single-claim plan (the session's own claim) and on shared context turns,
1784
1770
  * which route nothing. */
1785
- claimId: z3.string().optional(),
1771
+ claimId: z4.string().optional(),
1786
1772
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
1787
- voice: z3.string().optional(),
1773
+ voice: z4.string().optional(),
1788
1774
  /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
1789
1775
  * whose request this is: an item that folded in from another agent arrived as a bare
1790
1776
  * non-sequitur ("First real production sign-in is yours to make whenever you want.")
1791
1777
  * and the owner answered "What?". The bot names the agent before its first turn. */
1792
- agent: z3.string().optional(),
1778
+ agent: z4.string().optional(),
1793
1779
  /** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
1794
1780
  * same id a face is minted from. A name is not an identity: two pairings may be called
1795
1781
  * "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
1796
1782
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
1797
1783
  * speaks for several agents — the turn is the only place that knows which. */
1798
- agentId: z3.string().optional(),
1784
+ agentId: z4.string().optional(),
1799
1785
  select: SelectShapeSchema.optional(),
1800
- options: z3.array(OptionSchema.omit({ id: true })).optional(),
1786
+ options: z4.array(OptionSchema.omit({ id: true })).optional(),
1801
1787
  /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
1802
1788
  * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
1803
1789
  * (the beat for context, the answer window for asks). Clamped bot-side. */
1804
- pace: z3.number().positive().optional(),
1790
+ pace: z4.number().positive().optional(),
1805
1791
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
1806
1792
  * (a question blocks, context flows). blocking:false on a question = ask and move
1807
1793
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
1808
- blocking: z3.boolean().optional()
1794
+ blocking: z4.boolean().optional()
1809
1795
  });
1810
1796
  var CLAIM_STALE_MS = 30 * 6e4;
1811
- var InboxItemSchema = z3.object({
1812
- id: z3.string(),
1797
+ var InboxItemSchema = z4.object({
1798
+ id: z4.string(),
1813
1799
  /** The conversation thread + connection this item lives on. Present on the replied
1814
1800
  * detail — they power History's "Continue" / "New session from this" (#57/#251). */
1815
- parentId: z3.string().optional(),
1801
+ parentId: z4.string().optional(),
1816
1802
  /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
1817
1803
  * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
1818
1804
  * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
@@ -1820,13 +1806,13 @@ var InboxItemSchema = z3.object({
1820
1806
  * unrelated updates as a single "12-part request". Absent on rows written before the
1821
1807
  * `asks` table, and on anything that never went through `notify` — both fall back to the
1822
1808
  * thread, which is what the client did for all rows until now. */
1823
- askId: z3.string().optional(),
1809
+ askId: z4.string().optional(),
1824
1810
  /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
1825
1811
  * shares one `created_at` to the microsecond, so without it the author's order is
1826
1812
  * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
1827
1813
  * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
1828
1814
  * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
1829
- seq: z3.number().int().optional(),
1815
+ seq: z4.number().int().optional(),
1830
1816
  /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
1831
1817
  * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
1832
1818
  * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
@@ -1835,27 +1821,23 @@ var InboxItemSchema = z3.object({
1835
1821
  * server that can still see every row states it. Absent on any row with no `askId`: a
1836
1822
  * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
1837
1823
  * the one part of its own arrival a single row cannot answer. */
1838
- units: z3.number().int().positive().optional(),
1839
- tokenId: z3.string().optional(),
1824
+ units: z4.number().int().positive().optional(),
1825
+ tokenId: z4.string().optional(),
1840
1826
  status: NotifyStatusSchema,
1841
1827
  context: ContextSchema,
1842
- options: z3.array(OptionSchema).optional(),
1828
+ options: z4.array(OptionSchema).optional(),
1843
1829
  /** The ask's declared coverage points (#396), when the agent sent them. */
1844
- points: z3.array(z3.string()).optional(),
1845
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
1846
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
1847
- * between them could not express a call as a sequence. `question: null` is a real turn —
1848
- * a status update stays a statement instead of being shaped into a yes/no. */
1830
+ points: z4.array(z4.string()).optional(),
1849
1831
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
1850
1832
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
1851
1833
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
1852
1834
  * and neither of those is "got it" (owner, 2026-08-10). */
1853
- asks: z3.boolean().optional(),
1835
+ asks: z4.boolean().optional(),
1854
1836
  /** When a live process last pulsed for this row's agent — the liveness input for
1855
1837
  * "working requires a pulse" (#928): the list said "Working…" from agent_state alone
1856
1838
  * while the party called the same dead claim stalled. Absent = no token/no data,
1857
1839
  * which must never CLAIM stalled. */
1858
- lastSeenAt: z3.string().optional(),
1840
+ lastSeenAt: z4.string().optional(),
1859
1841
  /** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
1860
1842
  * the `notification_events` ledger (trigger-written since 20260621010000, so every row a
1861
1843
  * user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
@@ -1865,53 +1847,81 @@ var InboxItemSchema = z3.object({
1865
1847
  * work. Reading the row's birth as the claim's age brands that "No update in 8h" the
1866
1848
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
1867
1849
  * `createdAt`. */
1868
- agentStateAt: z3.string().datetime().optional(),
1869
- agenda: z3.array(AgendaTurnSchema).optional(),
1870
- visuals: z3.array(VisualSchema).optional(),
1850
+ agentStateAt: z4.string().datetime().optional(),
1851
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
1852
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
1853
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
1854
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
1855
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
1856
+ *
1857
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
1858
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
1859
+ *
1860
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
1861
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
1862
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
1863
+ * REPLACED `agenda` (turns), which nothing ever filled. */
1864
+ questions: z4.array(z4.object({
1865
+ id: z4.string(),
1866
+ entryId: z4.string(),
1867
+ title: z4.string(),
1868
+ state: z4.enum(["open", "answered"]),
1869
+ answer: z4.string().nullable()
1870
+ })).optional(),
1871
+ visuals: z4.array(VisualSchema).optional(),
1871
1872
  /** The connected agent's name (the single pairing name — user-typed, or the
1872
1873
  * agent's suggestion, or a default silly name). */
1873
- name: z3.string(),
1874
+ name: z4.string(),
1874
1875
  /** The pairing's assigned voice (#462); absent = the default voice. */
1875
1876
  voice: VoiceKeySchema.optional(),
1876
- repo: z3.string().optional(),
1877
- branch: z3.string().optional(),
1878
- createdAt: z3.string().datetime(),
1879
- snoozedUntil: z3.string().datetime().optional(),
1877
+ repo: z4.string().optional(),
1878
+ branch: z4.string().optional(),
1879
+ createdAt: z4.string().datetime(),
1880
+ snoozedUntil: z4.string().datetime().optional(),
1880
1881
  agentState: AgentStateSchema.default("idle"),
1881
1882
  /** Whose action the item is waiting on: "you" = an agent asked you (the default,
1882
1883
  * every agent→user notification); "agent" = you sent a request and it's awaiting the
1883
1884
  * agent (held in the inbox until the agent replies on the thread). */
1884
- turn: z3.enum(["you", "agent"]).default("you"),
1885
+ turn: z4.enum(["you", "agent"]).default("you"),
1885
1886
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
1886
1887
  * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
1887
1888
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
1888
- error: z3.string().optional(),
1889
- clarifies: z3.string().optional(),
1890
- /** The ring ladder ran out while this was still pending — we tried to reach you and
1891
- * STOPPED trying (`arbitration/arbitrate.ts` `ringAt` → null). Distinct from an
1892
- * agent with nothing to say, which the roster drew identically until now: "nothing to
1893
- * say" and "gave up saying it" are opposite situations wearing the same face
1894
- * (navigation-design.md, gap 1). False for anything that never rang. */
1895
- gaveUp: z3.boolean().default(false),
1889
+ error: z4.string().optional(),
1890
+ clarifies: z4.string().optional(),
1891
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
1892
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
1893
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
1894
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
1895
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
1896
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
1897
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
1898
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
1899
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
1900
+ ring: z4.object({
1901
+ declinedAt: z4.string().datetime(),
1902
+ anchorAt: z4.string().datetime(),
1903
+ nextRingAt: z4.string().datetime().nullable(),
1904
+ step: z4.number().int()
1905
+ }).optional(),
1896
1906
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
1897
1907
  * Absent for anything never delivered through a push, and for older rows written before
1898
1908
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
1899
1909
  * 2026-08-07) — its real job is to give "this didn't need a call" something to be
1900
1910
  * feedback ABOUT. */
1901
- why: z3.object({
1911
+ why: z4.object({
1902
1912
  asked: NotifyLevelSchema,
1903
1913
  got: NotifyLevelSchema,
1904
- because: z3.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
1905
- line: z3.string()
1914
+ because: z4.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
1915
+ line: z4.string()
1906
1916
  }).optional(),
1907
- select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
1908
- confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
1917
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
1918
+ confirmStyle: z4.enum(["yesno", "approve"]).default("yesno").describe(
1909
1919
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
1910
1920
  ),
1911
1921
  /** Real downstream work is stuck behind this one — set by the agent, independent of
1912
1922
  * urgency (see the main README's "premier use case" + notify/states.md). Drives the
1913
1923
  * inbox's blocking badge and the extra confirm step before dismissing it. */
1914
- blocking: z3.boolean().default(false),
1924
+ blocking: z4.boolean().default(false),
1915
1925
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
1916
1926
  answer: UserAnswerSchema.optional(),
1917
1927
  /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
@@ -1919,35 +1929,35 @@ var InboxItemSchema = z3.object({
1919
1929
  * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
1920
1930
  * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
1921
1931
  * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
1922
- communication: z3.object({
1923
- deliveryId: z3.string(),
1924
- kind: z3.enum(["notification", "call"]),
1925
- entryId: z3.string(),
1926
- goalIds: z3.array(z3.string()),
1927
- decisionNeedId: z3.string().optional(),
1928
- sealed: z3.boolean(),
1929
- goalState: z3.string().optional()
1932
+ communication: z4.object({
1933
+ deliveryId: z4.string(),
1934
+ kind: z4.enum(["notification", "call"]),
1935
+ entryId: z4.string(),
1936
+ goalIds: z4.array(z4.string()),
1937
+ decisionNeedId: z4.string().optional(),
1938
+ sealed: z4.boolean(),
1939
+ goalState: z4.string().optional()
1930
1940
  }).optional()
1931
1941
  });
1932
1942
  var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
1933
- var PushTokenSchema = z3.object({
1934
- voipToken: z3.string().min(1).optional(),
1935
- alertToken: z3.string().min(1).optional(),
1936
- fcmToken: z3.string().min(1).optional(),
1937
- platform: z3.enum(["ios", "android"])
1943
+ var PushTokenSchema = z4.object({
1944
+ voipToken: z4.string().min(1).optional(),
1945
+ alertToken: z4.string().min(1).optional(),
1946
+ fcmToken: z4.string().min(1).optional(),
1947
+ platform: z4.enum(["ios", "android"])
1938
1948
  }).superRefine((v, ctx) => {
1939
1949
  if (v.platform !== "ios") return;
1940
1950
  for (const field of ["voipToken", "alertToken"]) {
1941
1951
  const token = v[field];
1942
1952
  if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
1943
1953
  ctx.addIssue({
1944
- code: z3.ZodIssueCode.custom,
1954
+ code: z4.ZodIssueCode.custom,
1945
1955
  path: [field],
1946
1956
  message: `not an APNs device token (want 64 hex chars, got ${token.length})`
1947
1957
  });
1948
1958
  }
1949
1959
  });
1950
- var MissedCallSchema = z3.enum([
1960
+ var MissedCallSchema = z4.enum([
1951
1961
  "retry_10m",
1952
1962
  "retry_30m",
1953
1963
  "retry_60m",
@@ -1959,32 +1969,32 @@ var MissedCallSchema = z3.enum([
1959
1969
  ]);
1960
1970
  var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
1961
1971
  var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
1962
- var BrokerTuningSchema = z3.object({
1972
+ var BrokerTuningSchema = z4.object({
1963
1973
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
1964
- ackVerbosity: z3.enum(["normal", "none"]).optional(),
1974
+ ackVerbosity: z4.enum(["normal", "none"]).optional(),
1965
1975
  /** How readily the mapper asks its one clarification: 'low' = only when truly
1966
1976
  * uninterpretable, 'high' = whenever not fully certain. */
1967
- clarifyEagerness: z3.enum(["low", "normal", "high"]).optional(),
1977
+ clarifyEagerness: z4.enum(["low", "normal", "high"]).optional(),
1968
1978
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
1969
- phrasebook: z3.array(z3.object({ say: z3.string().min(1).max(60), mean: z3.string().min(1).max(120) })).max(24).optional(),
1979
+ phrasebook: z4.array(z4.object({ say: z4.string().min(1).max(60), mean: z4.string().min(1).max(120) })).max(24).optional(),
1970
1980
  /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
1971
1981
  * which is every account today — means the agent's own words decide, per ask: a call
1972
1982
  * about an English ask opens in English. This is the only thing that overrides that,
1973
1983
  * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
1974
1984
  * Set per user (no UI yet), like `voiceTuning`. */
1975
- language: z3.enum(["en", "es"]).optional()
1985
+ language: z4.enum(["en", "es"]).optional()
1976
1986
  });
1977
- var UserSettingsSchema = z3.object({
1978
- permissions: z3.object({
1979
- call: z3.boolean(),
1980
- banner: z3.boolean(),
1981
- push: z3.boolean()
1987
+ var UserSettingsSchema = z4.object({
1988
+ permissions: z4.object({
1989
+ call: z4.boolean(),
1990
+ banner: z4.boolean(),
1991
+ push: z4.boolean()
1982
1992
  }),
1983
- sessionMode: z3.enum(["default", "all_calls", "silent"]),
1984
- silentPush: z3.boolean(),
1985
- autoCallback: z3.boolean(),
1993
+ sessionMode: z4.enum(["default", "all_calls", "silent"]),
1994
+ silentPush: z4.boolean(),
1995
+ autoCallback: z4.boolean(),
1986
1996
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
1987
- improveConsent: z3.boolean(),
1997
+ improveConsent: z4.boolean(),
1988
1998
  missedCall: MissedCallSchema.default("backoff_standard"),
1989
1999
  /** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
1990
2000
  * (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
@@ -1992,7 +2002,13 @@ var UserSettingsSchema = z3.object({
1992
2002
  * Optional, NOT defaulted: a stale client PATCHing the full settings object
1993
2003
  * must not silently reset this privacy choice. Absent = leave unchanged on
1994
2004
  * write, 'hosted' on read (see store.ts). */
1995
- voiceMode: z3.enum(["hosted", "on_device"]).optional(),
2005
+ voiceMode: z4.enum(["hosted", "on_device"]).optional(),
2006
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
2007
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
2008
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
2009
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
2010
+ * read (see store.ts). */
2011
+ talk: z4.boolean().optional(),
1996
2012
  /** Per-user ring budget (#603): calls per rolling day before further calls
1997
2013
  * degrade to banner. Absent = the global default (25). A number, never a
1998
2014
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -2000,10 +2016,10 @@ var UserSettingsSchema = z3.object({
2000
2016
  * payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
2001
2017
  * slower speaker). No API-side semantics; the bot resolves each key with its
2002
2018
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
2003
- voiceTuning: z3.record(z3.string(), z3.union([z3.number(), z3.string()])).optional(),
2019
+ voiceTuning: z4.record(z4.string(), z4.union([z4.number(), z4.string()])).optional(),
2004
2020
  /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
2005
2021
  * defaulted — an older client PATCHing the full object must not clobber it. */
2006
- pstnCalls: z3.boolean().optional(),
2022
+ pstnCalls: z4.boolean().optional(),
2007
2023
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
2008
2024
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
2009
2025
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -2012,52 +2028,52 @@ var UserSettingsSchema = z3.object({
2012
2028
  * that failure reads as the reminder rail being unreliable rather than as a missing
2013
2029
  * setting. Absent = a spoken time can't be landed, so the reminder rides the next
2014
2030
  * call — honest about what we know. */
2015
- timezone: z3.string().min(1).max(64).optional(),
2031
+ timezone: z4.string().min(1).max(64).optional(),
2016
2032
  /** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
2017
2033
  * clobber guard as voiceMode: absent = leave unchanged on write. */
2018
2034
  broker: BrokerTuningSchema.optional()
2019
2035
  });
2020
- var HistoryItemSchema = z3.object({
2021
- id: z3.string(),
2022
- parentId: z3.string(),
2036
+ var HistoryItemSchema = z4.object({
2037
+ id: z4.string(),
2038
+ parentId: z4.string(),
2023
2039
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
2024
- initiator: z3.enum(["user", "agent"]),
2025
- title: z3.string(),
2040
+ initiator: z4.enum(["user", "agent"]),
2041
+ title: z4.string(),
2026
2042
  /** The agent on the other end (its name). */
2027
- name: z3.string(),
2028
- createdAt: z3.string(),
2043
+ name: z4.string(),
2044
+ createdAt: z4.string(),
2029
2045
  /** When the agent fetched your request (user→agent only). */
2030
- agentAckedAt: z3.string().nullable(),
2046
+ agentAckedAt: z4.string().nullable(),
2031
2047
  /** When you answered the agent's notification (agent→user only). */
2032
- humanAckedAt: z3.string().nullable()
2048
+ humanAckedAt: z4.string().nullable()
2033
2049
  });
2034
2050
  var ACTIVITY_LINES = 2;
2035
2051
  var ACTIVITY_LINE_MAX = 80;
2036
- var AgentActivitySchema = z3.object({
2052
+ var AgentActivitySchema = z4.object({
2037
2053
  /** Oldest first, so the newest line is last — the one that replaces in place. */
2038
- lines: z3.array(z3.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
2054
+ lines: z4.array(z4.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
2039
2055
  /** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
2040
2056
  * that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
2041
- at: z3.string().datetime()
2057
+ at: z4.string().datetime()
2042
2058
  });
2043
- var ConnectionSummarySchema = z3.object({
2059
+ var ConnectionSummarySchema = z4.object({
2044
2060
  /** The connection = the agent's token id (used to address a request). */
2045
- id: z3.string(),
2061
+ id: z4.string(),
2046
2062
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
2047
2063
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
2048
2064
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
2049
2065
  * apps/api/src/tokens/devices-vs-agents-design.md. */
2050
- kind: z3.enum(["device", "agent"]).optional(),
2066
+ kind: z4.enum(["device", "agent"]).optional(),
2051
2067
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
2052
2068
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
2053
2069
  * agents (phone-launched, provider-managed, or minted before the link existed). */
2054
- mintedByDevice: z3.string().nullable().optional(),
2055
- device: z3.string().nullable(),
2070
+ mintedByDevice: z4.string().nullable().optional(),
2071
+ device: z4.string().nullable(),
2056
2072
  /** The agent's display name (the single pairing name). */
2057
- name: z3.string(),
2073
+ name: z4.string(),
2058
2074
  /** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
2059
2075
  * label; null for a local connection. Sourced from the token's provider, not the name. */
2060
- provider: z3.string().nullable(),
2076
+ provider: z4.string().nullable(),
2061
2077
  /** The pairing's assigned voice (#462); null = the default voice. */
2062
2078
  voice: VoiceKeySchema.nullable(),
2063
2079
  /** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
@@ -2068,22 +2084,22 @@ var ConnectionSummarySchema = z3.object({
2068
2084
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
2069
2085
  * set once must not overrule a rule they set about one agent. */
2070
2086
  reach: NotifyLevelSchema.nullable().optional(),
2071
- createdAt: z3.string().datetime(),
2087
+ createdAt: z4.string().datetime(),
2072
2088
  /** Most recent notification on this connection, either direction. Null = no contact yet.
2073
2089
  * Drives the agents-page recency grouping (Today / This week / …). */
2074
- lastContactAt: z3.string().datetime().nullable(),
2090
+ lastContactAt: z4.string().datetime().nullable(),
2075
2091
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
2076
2092
  * desktop app while open. Null = never seen; stale = offline. */
2077
- lastSeenAt: z3.string().datetime().nullable().optional(),
2093
+ lastSeenAt: z4.string().datetime().nullable().optional(),
2078
2094
  /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
2079
2095
  * harness availabilities + granted workspaces — the option set the phone's
2080
2096
  * "new session" sheet offers. Absent for ordinary MCP agents. */
2081
- runtime: z3.object({
2097
+ runtime: z4.object({
2082
2098
  /** The @paigy/harness this host is running — a machine the self-update has not reached
2083
2099
  * shows its age here (`apps/desktop/src/update.ts`). */
2084
- version: z3.string().optional(),
2085
- harnesses: z3.array(z3.object({ name: z3.string(), label: z3.string(), status: z3.string() })).optional(),
2086
- workspaces: z3.array(z3.string()).optional()
2100
+ version: z4.string().optional(),
2101
+ harnesses: z4.array(z4.object({ name: z4.string(), label: z4.string(), status: z4.string() })).optional(),
2102
+ workspaces: z4.array(z4.string()).optional()
2087
2103
  }).optional(),
2088
2104
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
2089
2105
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -2092,148 +2108,156 @@ var ConnectionSummarySchema = z3.object({
2092
2108
  activity: AgentActivitySchema.optional(),
2093
2109
  /** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
2094
2110
  * false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
2095
- managed: z3.boolean()
2111
+ managed: z4.boolean()
2096
2112
  });
2097
- var LedgerItemSchema = z3.object({ id: z3.string(), parentId: z3.string(), title: z3.string(), createdAt: z3.string() });
2098
- var AgentLedgerSchema = z3.object({
2099
- agent: z3.object({ id: z3.string(), name: z3.string(), revokedAt: z3.string().nullable() }),
2113
+ var LedgerItemSchema = z4.object({ id: z4.string(), parentId: z4.string(), title: z4.string(), createdAt: z4.string() });
2114
+ var AgentLedgerSchema = z4.object({
2115
+ agent: z4.object({ id: z4.string(), name: z4.string(), revokedAt: z4.string().nullable() }),
2100
2116
  /** Its own questions you have not answered. */
2101
- asks: z3.array(LedgerItemSchema),
2117
+ asks: z4.array(LedgerItemSchema),
2102
2118
  /** Its questions you answered that nobody acted on — still owed to somebody. */
2103
- answered: z3.array(LedgerItemSchema),
2119
+ answered: z4.array(LedgerItemSchema),
2104
2120
  /** Requests you sent it that it never took. */
2105
- requests: z3.array(LedgerItemSchema),
2106
- goals: z3.array(z3.object({ id: z3.string(), outcome: z3.string(), state: z3.string() })),
2107
- callbacks: z3.array(z3.object({ id: z3.string(), parentId: z3.string(), trigger: z3.string(), note: z3.string(), dueAt: z3.string().nullable() }))
2121
+ requests: z4.array(LedgerItemSchema),
2122
+ goals: z4.array(z4.object({ id: z4.string(), outcome: z4.string(), state: z4.string() })),
2123
+ callbacks: z4.array(z4.object({ id: z4.string(), parentId: z4.string(), trigger: z4.string(), note: z4.string(), dueAt: z4.string().nullable() }))
2108
2124
  });
2109
- var ReassignResultSchema = z3.object({
2110
- moved: z3.object({ asks: z3.number(), answered: z3.number(), requests: z3.number(), goals: z3.number(), callbacks: z3.number() }),
2111
- parentId: z3.string().nullable()
2125
+ var ReassignResultSchema = z4.object({
2126
+ moved: z4.object({ asks: z4.number(), answered: z4.number(), requests: z4.number(), goals: z4.number(), callbacks: z4.number() }),
2127
+ parentId: z4.string().nullable()
2112
2128
  });
2113
- var MoveRingSchema = z3.enum(["home", "travels", "retired", "quarantined"]);
2114
- var MoveSchema = z3.object({
2115
- id: z3.string(),
2129
+ var MoveRingSchema = z4.enum(["home", "travels", "retired", "quarantined"]);
2130
+ var MoveSchema = z4.object({
2131
+ id: z4.string(),
2116
2132
  /** The reusable question, as distill normalized it. */
2117
- question: z3.string(),
2133
+ question: z4.string(),
2118
2134
  /** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
2119
- answer: z3.string(),
2135
+ answer: z4.string(),
2120
2136
  /** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
2121
2137
  * told so, and the ruling only derives essentially the same question in the same scope. */
2122
- rationale: z3.string().nullable(),
2138
+ rationale: z4.string().nullable(),
2123
2139
  /** Where the ruling lives: a repo/workspace, or 'global'. */
2124
- scope: z3.string(),
2140
+ scope: z4.string(),
2125
2141
  ring: MoveRingSchema,
2126
2142
  /** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
2127
- pinned: z3.boolean(),
2143
+ pinned: z4.boolean(),
2128
2144
  /** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
2129
2145
  * break is visible instead of a pin silently disappearing. */
2130
- pinBroken: z3.boolean(),
2146
+ pinBroken: z4.boolean(),
2131
2147
  /** When the ruling was distilled. */
2132
- learnedAt: z3.string(),
2148
+ learnedAt: z4.string(),
2133
2149
  /** Last time it answered an ask. Null = never fired. */
2134
- lastUsedAt: z3.string().nullable(),
2150
+ lastUsedAt: z4.string().nullable(),
2135
2151
  /** How many asks it has answered. Instrumentation — deliberately NOT an input to the
2136
2152
  * evidence curve: firing says the question keeps arising, not that the ruling is right. */
2137
- usedCount: z3.number(),
2153
+ usedCount: z4.number(),
2138
2154
  /** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
2139
- confirms: z3.number(),
2155
+ confirms: z4.number(),
2140
2156
  /** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
2141
2157
  * Linear and priced above the entire confirmation budget, so any full counter wins. */
2142
- counters: z3.number(),
2158
+ counters: z4.number(),
2143
2159
  /** The agent that asked the question this move came from, when known. Null for a move
2144
2160
  * distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
2145
- learnedFrom: z3.object({ id: z3.string(), name: z3.string() }).nullable()
2161
+ learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
2146
2162
  });
2147
- var QueueQuestionSchema = z3.object({
2163
+ var QueueQuestionSchema = z4.object({
2148
2164
  /** The decision need's id — what an answer is accepted against. */
2149
- id: z3.string(),
2165
+ id: z4.string(),
2150
2166
  /** The words that were asked, from the request Entry that asked them. */
2151
- question: z3.string(),
2167
+ question: z4.string(),
2152
2168
  /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
2153
2169
  * for a need whose request Entry is carried by no interactive Delivery, which nothing
2154
2170
  * can answer. */
2155
- deliveryId: z3.string().nullable().default(null),
2171
+ deliveryId: z4.string().nullable().default(null),
2156
2172
  /** The Entry the ruling is about. */
2157
- aboutId: z3.string().nullable().default(null),
2173
+ aboutId: z4.string().nullable().default(null),
2158
2174
  /** Empty for a free-text question. */
2159
- options: z3.array(OptionSchema).default([]),
2160
- select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
2161
- askedAt: z3.string(),
2175
+ options: z4.array(OptionSchema).default([]),
2176
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
2177
+ askedAt: z4.string(),
2162
2178
  /** Null while the question is open — which is how the page tells the two apart. */
2163
- answeredAt: z3.string().nullable().default(null),
2179
+ answeredAt: z4.string().nullable().default(null),
2164
2180
  /** The ruling in the person's own words, from the contribution that replied — not the
2165
2181
  * option id, which is not something anyone reads back. Null while it is open, and null
2166
2182
  * for a settled question whose reply carried nothing readable. */
2167
- answer: z3.string().nullable().default(null),
2183
+ answer: z4.string().nullable().default(null),
2168
2184
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
2169
2185
  * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
2170
2186
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
2171
2187
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
2172
2188
  * this is the field they agree through rather than each re-deriving it from the row it
2173
2189
  * arrived under. Required because the API projects it on every need it sends. */
2174
- goalId: z3.string(),
2190
+ goalId: z4.string(),
2175
2191
  /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
2176
2192
  * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
2177
2193
  * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
2178
2194
  * gate at all. */
2179
- blocked: z3.boolean().default(false)
2195
+ blocked: z4.boolean().default(false)
2180
2196
  });
2181
- var QueueItemSchema = z3.object({
2182
- id: z3.string(),
2197
+ var QueueItemSchema = z4.object({
2198
+ id: z4.string(),
2183
2199
  /** One-line headline — the first sentence of the outcome. */
2184
- title: z3.string(),
2200
+ title: z4.string(),
2185
2201
  /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
2186
- intent: z3.string(),
2202
+ intent: z4.string(),
2187
2203
  /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
2188
- state: z3.string(),
2204
+ state: z4.string(),
2189
2205
  /** Who holds it (a participant ref); null when nobody does yet. */
2190
- assignee: z3.string().nullable().default(null),
2206
+ assignee: z4.string().nullable().default(null),
2191
2207
  /** What the agent last said it was doing; null if it has said nothing. */
2192
- progress: z3.string().nullable().default(null),
2193
- reviewPending: z3.boolean().default(false),
2194
- dueAt: z3.string().nullable().default(null),
2208
+ progress: z4.string().nullable().default(null),
2209
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
2210
+ * served only while it was written for the current note. Null means show the Goal's name. */
2211
+ progressLine: z4.string().nullable().optional(),
2212
+ reviewPending: z4.boolean().default(false),
2213
+ dueAt: z4.string().nullable().default(null),
2195
2214
  /** The Goal this one was opened under; null at the root. */
2196
- parentGoalId: z3.string().nullable().default(null),
2215
+ parentGoalId: z4.string().nullable().default(null),
2197
2216
  /** Goals opened under this one — only those the same list holds. */
2198
- childGoalIds: z3.array(z3.string()).default([]),
2217
+ childGoalIds: z4.array(z4.string()).default([]),
2199
2218
  /** Goals this one waits on (start or finish gates). */
2200
- dependencyGoalIds: z3.array(z3.string()).default([]),
2219
+ dependencyGoalIds: z4.array(z4.string()).default([]),
2201
2220
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
2202
- blocked: z3.boolean().default(false),
2221
+ blocked: z4.boolean().default(false),
2203
2222
  /** Every decision need on it, open or settled — the page decides which to show. */
2204
- questions: z3.array(QueueQuestionSchema).default([]),
2223
+ questions: z4.array(QueueQuestionSchema).default([]),
2205
2224
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
2206
- repo: z3.string().nullable().optional(),
2207
- createdAt: z3.string(),
2208
- updatedAt: z3.string().nullable().default(null)
2225
+ repo: z4.string().nullable().optional(),
2226
+ createdAt: z4.string(),
2227
+ updatedAt: z4.string().nullable().default(null),
2228
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
2229
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
2230
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
2231
+ lastProgressAt: z4.string().nullable().optional()
2209
2232
  });
2210
- var NoteSourceSchema = z3.enum(["app", "call"]);
2211
- var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
2212
- var NoteRepeatSchema = z3.enum(["once", "until_done"]);
2213
- var DecisionSchema = z3.object({
2214
- id: z3.string(),
2233
+ var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
2234
+ var NoteSourceSchema = z4.enum(["app", "call"]);
2235
+ var NoteStatusSchema = z4.enum(["open", "assigned", "in_progress", "done"]);
2236
+ var NoteRepeatSchema = z4.enum(["once", "until_done"]);
2237
+ var DecisionSchema = z4.object({
2238
+ id: z4.string(),
2215
2239
  /** The note this decision refines; null = recorded on a bare thread (the
2216
2240
  * extensibility seam — any conversation can accrue decisions). */
2217
- noteId: z3.string().nullable(),
2241
+ noteId: z4.string().nullable(),
2218
2242
  /** What was ambiguous — the broker's (or the user's own) question. */
2219
- question: z3.string(),
2243
+ question: z4.string(),
2220
2244
  /** The user's ruling; null while the question is open. */
2221
- answer: z3.string().nullable(),
2222
- decidedAt: z3.string().nullable(),
2223
- createdAt: z3.string()
2245
+ answer: z4.string().nullable(),
2246
+ decidedAt: z4.string().nullable(),
2247
+ createdAt: z4.string()
2224
2248
  });
2225
- var NoteSchema = z3.object({
2226
- id: z3.string(),
2249
+ var NoteSchema = z4.object({
2250
+ id: z4.string(),
2227
2251
  /** One-line headline (broker-titled; deterministic floor). */
2228
- title: z3.string(),
2252
+ title: z4.string(),
2229
2253
  /** The original intent, verbatim — assignees always see the user's own words. */
2230
- intent: z3.string(),
2254
+ intent: z4.string(),
2231
2255
  source: NoteSourceSchema,
2232
2256
  status: NoteStatusSchema,
2233
2257
  /** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
2234
- assignee: z3.string().nullable(),
2258
+ assignee: z4.string().nullable(),
2235
2259
  /** The request thread minted at assignment; null until assigned. */
2236
- parentId: z3.string().nullable(),
2260
+ parentId: z4.string().nullable(),
2237
2261
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
2238
2262
  * call — never a deadline. It only ever comes from the user's own words, so when it
2239
2263
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
@@ -2242,133 +2266,133 @@ var NoteSchema = z3.object({
2242
2266
  // Defaulted, not required: a Note from an API deploy older than the reminders
2243
2267
  // migration has none of these, and the defaults ARE what it means — no not-before,
2244
2268
  // one ride, never ridden. Parsing must not fail across a rolling deploy.
2245
- dueAt: z3.string().nullable().default(null),
2269
+ dueAt: z4.string().nullable().default(null),
2246
2270
  repeat: NoteRepeatSchema.default("once"),
2247
2271
  /** How many calls have already carried it — the fatigue cap counts rides, not days. */
2248
- rides: z3.number().int().default(0),
2249
- lastRideAt: z3.string().nullable().default(null),
2250
- createdAt: z3.string()
2272
+ rides: z4.number().int().default(0),
2273
+ lastRideAt: z4.string().nullable().default(null),
2274
+ createdAt: z4.string()
2251
2275
  });
2252
- var TriageItemSchema = z3.object({
2253
- noteId: z3.string(),
2276
+ var TriageItemSchema = z4.object({
2277
+ noteId: z4.string(),
2254
2278
  /** The note's headline at run time. */
2255
- title: z3.string(),
2279
+ title: z4.string(),
2256
2280
  /** WHY, in one short human line, evidence first — this is read on a phone underneath
2257
2281
  * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
2258
2282
  * Never a model's reasoning transcript, never an id. */
2259
- why: z3.string()
2283
+ why: z4.string()
2260
2284
  });
2261
- var TriageAssignmentSchema = z3.object({
2285
+ var TriageAssignmentSchema = z4.object({
2262
2286
  /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
2263
- agent: z3.string(),
2287
+ agent: z4.string(),
2264
2288
  /** Its display name at run time (the name on the hatchling's card). Denormalized for the
2265
2289
  * same reason as `title`: the card must render from the proposal alone. */
2266
- agentName: z3.string(),
2267
- notes: z3.array(TriageItemSchema)
2290
+ agentName: z4.string(),
2291
+ notes: z4.array(TriageItemSchema)
2268
2292
  });
2269
- var TriageStatusSchema = z3.enum(["open", "superseded", "dismissed"]);
2270
- var SubmitTriageSchema = z3.object({
2293
+ var TriageStatusSchema = z4.enum(["open", "superseded", "dismissed"]);
2294
+ var SubmitTriageSchema = z4.object({
2271
2295
  /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
2272
2296
  * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
2273
2297
  * so the phone can say where the content went — an unattributed privacy claim is worth
2274
2298
  * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
2275
- provider: z3.string().min(1).max(60),
2299
+ provider: z4.string().min(1).max(60),
2276
2300
  /** The concrete model when the provider names one (an ollama tag); null otherwise. */
2277
- model: z3.string().max(200).nullable().optional(),
2301
+ model: z4.string().max(200).nullable().optional(),
2278
2302
  /** How many open notes the run actually looked at — the denominator on the phone
2279
2303
  * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
2280
- reviewed: z3.number().int().min(0).max(1e4).default(0),
2281
- close: z3.array(TriageItemSchema).max(200).default([]),
2282
- stale: z3.array(TriageItemSchema).max(200).default([]),
2283
- assign: z3.array(TriageAssignmentSchema).max(50).default([])
2304
+ reviewed: z4.number().int().min(0).max(1e4).default(0),
2305
+ close: z4.array(TriageItemSchema).max(200).default([]),
2306
+ stale: z4.array(TriageItemSchema).max(200).default([]),
2307
+ assign: z4.array(TriageAssignmentSchema).max(50).default([])
2284
2308
  });
2285
2309
  var TriageProposalSchema = SubmitTriageSchema.extend({
2286
- id: z3.string(),
2287
- runAt: z3.string(),
2310
+ id: z4.string(),
2311
+ runAt: z4.string(),
2288
2312
  status: TriageStatusSchema,
2289
- model: z3.string().nullable().default(null)
2313
+ model: z4.string().nullable().default(null)
2290
2314
  });
2291
- var AcceptTriageSchema = z3.discriminatedUnion("group", [
2292
- z3.object({ group: z3.literal("close"), noteIds: z3.array(z3.string()).max(200).optional() }),
2293
- z3.object({ group: z3.literal("stale"), noteIds: z3.array(z3.string()).max(200).optional() }),
2294
- z3.object({
2295
- group: z3.literal("assign"),
2296
- agent: z3.string().min(1),
2297
- noteIds: z3.array(z3.string()).max(200).optional()
2315
+ var AcceptTriageSchema = z4.discriminatedUnion("group", [
2316
+ z4.object({ group: z4.literal("close"), noteIds: z4.array(z4.string()).max(200).optional() }),
2317
+ z4.object({ group: z4.literal("stale"), noteIds: z4.array(z4.string()).max(200).optional() }),
2318
+ z4.object({
2319
+ group: z4.literal("assign"),
2320
+ agent: z4.string().min(1),
2321
+ noteIds: z4.array(z4.string()).max(200).optional()
2298
2322
  })
2299
2323
  ]);
2300
- var AcceptTriageResultSchema = z3.object({
2301
- accepted: z3.array(z3.string()),
2302
- failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
2324
+ var AcceptTriageResultSchema = z4.object({
2325
+ accepted: z4.array(z4.string()),
2326
+ failed: z4.array(z4.object({ noteId: z4.string(), reason: z4.string() }))
2303
2327
  });
2304
- var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
2328
+ var DeliveryModeSchema = z4.enum(["poll", "self_hosted"]);
2305
2329
  var WAKE_EVENT = "wake";
2306
2330
  var wakeChannel = (tokenId) => `wake:${tokenId}`;
2307
- var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
2308
- var OAuthStartSchema = z3.object({
2309
- provider: z3.enum(["cma"]),
2310
- returnTo: z3.string().min(1)
2331
+ var RegisterDeliverySchema = z4.object({ mode: DeliveryModeSchema });
2332
+ var OAuthStartSchema = z4.object({
2333
+ provider: z4.enum(["cma"]),
2334
+ returnTo: z4.string().min(1)
2311
2335
  });
2312
- var DeliveryConfigSchema = z3.object({
2313
- tokenId: z3.string(),
2336
+ var DeliveryConfigSchema = z4.object({
2337
+ tokenId: z4.string(),
2314
2338
  mode: DeliveryModeSchema,
2315
2339
  /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
2316
2340
  * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
2317
2341
  * carries credentials; only a `poll` registration can come back with null here. */
2318
- realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
2342
+ realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
2319
2343
  });
2320
- var WakeNudgeSchema = z3.object({
2321
- kind: z3.enum(["reply", "request", "callback"]),
2322
- notificationId: z3.string().optional(),
2323
- parentId: z3.string()
2344
+ var WakeNudgeSchema = z4.object({
2345
+ kind: z4.enum(["reply", "request", "callback"]),
2346
+ notificationId: z4.string().optional(),
2347
+ parentId: z4.string()
2324
2348
  });
2325
- var PairingStatusSchema = z3.enum(["pending", "approved", "denied", "expired"]);
2326
- var DeviceCodeSchema = z3.object({
2327
- device_code: z3.string(),
2328
- user_code: z3.string(),
2329
- verification_uri: z3.string().url(),
2330
- verification_uri_complete: z3.string().url(),
2331
- interval: z3.number(),
2332
- expires_in: z3.number()
2349
+ var PairingStatusSchema = z4.enum(["pending", "approved", "denied", "expired"]);
2350
+ var DeviceCodeSchema = z4.object({
2351
+ device_code: z4.string(),
2352
+ user_code: z4.string(),
2353
+ verification_uri: z4.string().url(),
2354
+ verification_uri_complete: z4.string().url(),
2355
+ interval: z4.number(),
2356
+ expires_in: z4.number()
2333
2357
  });
2334
- var DeviceInfoSchema = z3.object({
2335
- code: z3.string(),
2358
+ var DeviceInfoSchema = z4.object({
2359
+ code: z4.string(),
2336
2360
  /** The agent's suggested name (from /device/code) — shown on the approval screen,
2337
2361
  * pre-filling the name field the human can edit. */
2338
- name: z3.string(),
2362
+ name: z4.string(),
2339
2363
  /** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
2340
2364
  * build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
2341
2365
  * install crashes on the pairing screen on first launch, before the OTA lands
2342
2366
  * (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
2343
- agent: z3.string().optional(),
2344
- device: z3.string().nullable(),
2367
+ agent: z4.string().optional(),
2368
+ device: z4.string().nullable(),
2345
2369
  status: PairingStatusSchema
2346
2370
  });
2347
- var DeviceTokenSchema = z3.object({
2348
- access_token: z3.string(),
2371
+ var DeviceTokenSchema = z4.object({
2372
+ access_token: z4.string(),
2349
2373
  /** The pairing's single name (user-typed at approval, the agent's suggestion, or
2350
2374
  * a default silly name). */
2351
- name: z3.string(),
2352
- device: z3.string().nullable(),
2375
+ name: z4.string(),
2376
+ device: z4.string().nullable(),
2353
2377
  /** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
2354
2378
  * draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
2355
- voice: z3.string().nullable().optional(),
2379
+ voice: z4.string().nullable().optional(),
2356
2380
  /** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
2357
2381
  * that survives a rename. Cached by the host's identity beat. */
2358
- token_id: z3.string().nullable().optional(),
2382
+ token_id: z4.string().nullable().optional(),
2359
2383
  /** WHERE this identity works — the folder a wake should land it in. Written by the host
2360
2384
  * at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
2361
2385
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
2362
2386
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
2363
- workspace: z3.string().nullable().optional(),
2364
- uik_pub: z3.string().nullable().optional()
2387
+ workspace: z4.string().nullable().optional(),
2388
+ uik_pub: z4.string().nullable().optional()
2365
2389
  });
2366
- var SupportRequestSchema = z3.object({
2367
- email: z3.string().email().max(320),
2368
- message: z3.string().trim().min(1).max(5e3),
2369
- name: z3.string().trim().max(120).optional()
2390
+ var SupportRequestSchema = z4.object({
2391
+ email: z4.string().email().max(320),
2392
+ message: z4.string().trim().min(1).max(5e3),
2393
+ name: z4.string().trim().max(120).optional()
2370
2394
  });
2371
- var NotificationFeedbackKindSchema = z3.enum([
2395
+ var NotificationFeedbackKindSchema = z4.enum([
2372
2396
  "break_down",
2373
2397
  // "This should be more than one ask — break it down."
2374
2398
  "regenerate_options",
@@ -2510,6 +2534,11 @@ var ApiError = class extends Error {
2510
2534
  this.body = body;
2511
2535
  }
2512
2536
  };
2537
+ var tokenOverride = null;
2538
+ function overrideToken(secret) {
2539
+ tokenOverride = secret;
2540
+ }
2541
+ var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
2513
2542
  async function fail(what, res) {
2514
2543
  throw new ApiError(what, res.status, await res.text());
2515
2544
  }
@@ -2546,25 +2575,6 @@ async function updateGoal(goalId, input, opts = {}) {
2546
2575
  return await res.json();
2547
2576
  }
2548
2577
  var AWAIT_WINDOW_MS = 45e3;
2549
- async function getThread(parentId, opts = {}) {
2550
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/thread/${encodeURIComponent(parentId)}`, {
2551
- headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
2552
- }));
2553
- if (!res.ok) await fail("get_thread", res);
2554
- return await res.json();
2555
- }
2556
- async function searchThreads(q, opts = {}) {
2557
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/search?q=${encodeURIComponent(q)}`, {
2558
- headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
2559
- }));
2560
- if (!res.ok) await fail("search_threads", res);
2561
- return await res.json();
2562
- }
2563
- var tokenOverride = null;
2564
- function overrideToken(secret) {
2565
- tokenOverride = secret;
2566
- }
2567
- var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
2568
2578
  async function hatch(name, voice = null) {
2569
2579
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
2570
2580
  method: "POST",
@@ -2614,6 +2624,32 @@ async function registerDelivery(mode, opts = {}) {
2614
2624
  if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
2615
2625
  return await res.json();
2616
2626
  }
2627
+ function repoFromRemote(url) {
2628
+ const said = (url ?? "").trim();
2629
+ if (!said) return null;
2630
+ const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
2631
+ const parts = path.split("/").filter(Boolean);
2632
+ if (parts.length < 2) return null;
2633
+ const [owner, name] = parts.slice(-2);
2634
+ if (!owner || !name) return null;
2635
+ return `${owner}/${name}`.toLowerCase();
2636
+ }
2637
+ var asked;
2638
+ function currentRepo(cwd = process.cwd()) {
2639
+ if (asked !== void 0) return asked;
2640
+ try {
2641
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], {
2642
+ cwd,
2643
+ encoding: "utf8",
2644
+ timeout: 2e3,
2645
+ stdio: ["ignore", "pipe", "ignore"]
2646
+ });
2647
+ asked = repoFromRemote(url);
2648
+ } catch {
2649
+ asked = null;
2650
+ }
2651
+ return asked;
2652
+ }
2617
2653
  async function contact(input, opts = {}) {
2618
2654
  const parsed = ContactSchema.parse(input);
2619
2655
  opts.signal?.throwIfAborted();
@@ -2627,7 +2663,11 @@ async function contact(input, opts = {}) {
2627
2663
  const reqAsks = parsed.asks.map((a) => ({
2628
2664
  id: a.id,
2629
2665
  parentId: a.parentId,
2630
- repo: a.repo,
2666
+ // Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
2667
+ // work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
2668
+ // still wins — normalised the same way — because a caller that names one knows something
2669
+ // this cannot see.
2670
+ repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
2631
2671
  ask: a.ask,
2632
2672
  options: a.options ?? []
2633
2673
  }));
@@ -2755,16 +2795,6 @@ function deliveryView(d) {
2755
2795
  next: d.kind === "notification" ? "Answers land on the Goal: read them with claim_goal or get_goal. Do not poll this notification." : d.message
2756
2796
  });
2757
2797
  }
2758
- function threadView(t) {
2759
- return {
2760
- parentId: t.parentId,
2761
- turns: (t.turns ?? []).map((turn) => {
2762
- const lines2 = turn.role === "user" ? [turn.text ?? ""] : [turn.title ?? "", ...turn.description ?? []];
2763
- const said = lines2.map((l) => l.trim()).filter((l, i, all) => l && all.indexOf(l) === i).join("\n");
2764
- return compact({ from: turn.role === "user" ? "person" : "agent", at: turn.at ? at(turn.at) : void 0, said });
2765
- })
2766
- };
2767
- }
2768
2798
  function repliesView(r) {
2769
2799
  const deliveries = r.deliveries ?? [];
2770
2800
  return {
@@ -2787,14 +2817,6 @@ async function runTool(name, args, opts) {
2787
2817
  case "check_replies":
2788
2818
  CheckRepliesSchema.parse(input);
2789
2819
  return repliesView(await checkReplies(client));
2790
- case "get_thread": {
2791
- const { parentId } = GetThreadSchema.parse(input);
2792
- return threadView(await getThread(parentId, client));
2793
- }
2794
- case "search_threads": {
2795
- const { q } = SearchThreadsSchema.parse(input);
2796
- return searchThreads(q, client);
2797
- }
2798
2820
  case "create_goal": {
2799
2821
  const goal = CreateGoalToolSchema.parse(input);
2800
2822
  return createGoal({ ...goal, idempotencyKey: goal.idempotencyKey ?? randomUUID4() }, client);
@@ -2892,7 +2914,8 @@ function subscribeRealtime(args) {
2892
2914
  };
2893
2915
  }
2894
2916
  async function subscribeWake(onNudge, opts = {}) {
2895
- const cfg = await registerDelivery("self_hosted", opts);
2917
+ const { release = true, ...client } = opts;
2918
+ const cfg = await registerDelivery("self_hosted", client);
2896
2919
  const { url, anonKey } = cfg.realtime;
2897
2920
  const channel = wakeChannel(cfg.tokenId);
2898
2921
  const ch = subscribeRealtime({
@@ -2907,13 +2930,14 @@ async function subscribeWake(onNudge, opts = {}) {
2907
2930
  channel,
2908
2931
  close: async () => {
2909
2932
  ch.close();
2910
- await registerDelivery("poll", opts).catch(() => {
2933
+ if (release) await registerDelivery("poll", client).catch(() => {
2911
2934
  });
2912
2935
  }
2913
2936
  };
2914
2937
  }
2915
2938
 
2916
2939
  export {
2940
+ sessionId,
2917
2941
  isNetworkError,
2918
2942
  sessionSlot,
2919
2943
  agentName,
@@ -2928,8 +2952,8 @@ export {
2928
2952
  requestCode,
2929
2953
  pollToken,
2930
2954
  UnpairedError,
2931
- getGoal,
2932
2955
  overrideToken,
2956
+ getGoal,
2933
2957
  hatch,
2934
2958
  whoAmI,
2935
2959
  setIdentity,