@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.
- package/README.md +22 -10
- package/dist/{chunk-I5QINUN6.js → chunk-NOZRU5CD.js} +558 -427
- package/dist/{chunk-H5GGWCPS.js → chunk-R2GTL7N3.js} +491 -467
- package/dist/{chunk-XIHVHYDU.js → chunk-SKLVQ73Q.js} +1 -1
- package/dist/{chunk-FQVK7SO4.js → chunk-SO5NRTXG.js} +56 -5
- package/dist/enable.js +9 -4
- package/dist/index.js +29 -5
- package/dist/listen.js +43 -12
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -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 =
|
|
1335
|
-
id:
|
|
1336
|
-
parentId:
|
|
1337
|
-
repo:
|
|
1338
|
-
ask:
|
|
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:
|
|
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 =
|
|
1349
|
-
asks:
|
|
1350
|
-
waiting:
|
|
1351
|
-
channel:
|
|
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 =
|
|
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 =
|
|
1357
|
-
outcome:
|
|
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:
|
|
1361
|
-
ownerParticipant:
|
|
1362
|
-
idempotencyKey:
|
|
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:
|
|
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:
|
|
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 =
|
|
1384
|
-
revision:
|
|
1385
|
-
changes:
|
|
1386
|
-
outcome:
|
|
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:
|
|
1390
|
-
ownerParticipant:
|
|
1391
|
-
parentGoalId:
|
|
1392
|
-
dependencies:
|
|
1393
|
-
children:
|
|
1394
|
-
state:
|
|
1395
|
-
progress:
|
|
1396
|
-
reviewed:
|
|
1397
|
-
dueAt:
|
|
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:
|
|
1400
|
-
operationId:
|
|
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:
|
|
1403
|
-
var ClaimGoalSchema =
|
|
1404
|
-
var GetGoalSchema =
|
|
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 =
|
|
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 =
|
|
1446
|
-
title:
|
|
1447
|
-
description:
|
|
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 =
|
|
1452
|
-
kind:
|
|
1453
|
-
id:
|
|
1450
|
+
var ParticipantSchema = z4.object({
|
|
1451
|
+
kind: z4.enum(["human", "agent"]),
|
|
1452
|
+
id: z4.string()
|
|
1454
1453
|
});
|
|
1455
|
-
var TransformSchema =
|
|
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
|
|
1472
|
-
|
|
1473
|
-
label:
|
|
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 =
|
|
1489
|
-
var SelectShapeSchema =
|
|
1490
|
-
var ReceiptEventSchema =
|
|
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 =
|
|
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:
|
|
1511
|
+
points: z4.array(z4.string()).nullable(),
|
|
1526
1512
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
1527
|
-
blocking:
|
|
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:
|
|
1516
|
+
deadline: z4.string().datetime().nullable().optional()
|
|
1531
1517
|
});
|
|
1532
|
-
var NotifyRequestFields =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1618
|
+
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
1633
1619
|
if (!r.select)
|
|
1634
|
-
ctx.addIssue({ code:
|
|
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:
|
|
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:
|
|
1625
|
+
ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
1640
1626
|
});
|
|
1641
|
-
var NotifyStatusSchema =
|
|
1642
|
-
var AgentStateSchema =
|
|
1643
|
-
var TurnSchema =
|
|
1644
|
-
prompt:
|
|
1645
|
-
reply:
|
|
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 =
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
1668
|
-
detail:
|
|
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:
|
|
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:
|
|
1664
|
+
fault: z4.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
1679
1665
|
});
|
|
1680
|
-
var RideAlongSchema =
|
|
1666
|
+
var RideAlongSchema = z4.object({
|
|
1681
1667
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
1682
|
-
noteId:
|
|
1668
|
+
noteId: z4.string(),
|
|
1683
1669
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
1684
|
-
text:
|
|
1670
|
+
text: z4.string(),
|
|
1685
1671
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
1686
|
-
parentId:
|
|
1672
|
+
parentId: z4.string().nullable()
|
|
1687
1673
|
});
|
|
1688
|
-
var AwaitItemSchema =
|
|
1689
|
-
|
|
1690
|
-
type:
|
|
1691
|
-
parentId:
|
|
1692
|
-
notificationId:
|
|
1693
|
-
workId:
|
|
1694
|
-
decisionId:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1704
|
+
also: z4.array(RideAlongSchema).optional()
|
|
1719
1705
|
}),
|
|
1720
|
-
|
|
1721
|
-
type:
|
|
1722
|
-
parentId:
|
|
1723
|
-
notificationId:
|
|
1724
|
-
remindAt:
|
|
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:
|
|
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
|
|
1732
|
-
|
|
1733
|
-
type:
|
|
1734
|
-
parentId:
|
|
1735
|
-
notificationId:
|
|
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
|
-
|
|
1743
|
-
type:
|
|
1744
|
-
notificationId:
|
|
1745
|
-
inFlight:
|
|
1746
|
-
turn:
|
|
1747
|
-
idx:
|
|
1748
|
-
prompt:
|
|
1749
|
-
reply:
|
|
1750
|
-
acts:
|
|
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
|
-
|
|
1754
|
-
type:
|
|
1755
|
-
also:
|
|
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:
|
|
1747
|
+
inFlight: z4.boolean().optional()
|
|
1762
1748
|
})
|
|
1763
1749
|
]);
|
|
1764
|
-
var VoiceKeySchema =
|
|
1765
|
-
var AgendaTurnSchema =
|
|
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:
|
|
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:
|
|
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:
|
|
1779
|
-
question:
|
|
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:
|
|
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:
|
|
1771
|
+
claimId: z4.string().optional(),
|
|
1786
1772
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
1787
|
-
voice:
|
|
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:
|
|
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:
|
|
1784
|
+
agentId: z4.string().optional(),
|
|
1799
1785
|
select: SelectShapeSchema.optional(),
|
|
1800
|
-
options:
|
|
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:
|
|
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:
|
|
1794
|
+
blocking: z4.boolean().optional()
|
|
1809
1795
|
});
|
|
1810
1796
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
1811
|
-
var InboxItemSchema =
|
|
1812
|
-
id:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1839
|
-
tokenId:
|
|
1824
|
+
units: z4.number().int().positive().optional(),
|
|
1825
|
+
tokenId: z4.string().optional(),
|
|
1840
1826
|
status: NotifyStatusSchema,
|
|
1841
1827
|
context: ContextSchema,
|
|
1842
|
-
options:
|
|
1828
|
+
options: z4.array(OptionSchema).optional(),
|
|
1843
1829
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
1844
|
-
points:
|
|
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:
|
|
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:
|
|
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:
|
|
1869
|
-
agenda
|
|
1870
|
-
|
|
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:
|
|
1874
|
+
name: z4.string(),
|
|
1874
1875
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
1875
1876
|
voice: VoiceKeySchema.optional(),
|
|
1876
|
-
repo:
|
|
1877
|
-
branch:
|
|
1878
|
-
createdAt:
|
|
1879
|
-
snoozedUntil:
|
|
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:
|
|
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:
|
|
1889
|
-
clarifies:
|
|
1890
|
-
/**
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
*
|
|
1894
|
-
*
|
|
1895
|
-
|
|
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:
|
|
1911
|
+
why: z4.object({
|
|
1902
1912
|
asked: NotifyLevelSchema,
|
|
1903
1913
|
got: NotifyLevelSchema,
|
|
1904
|
-
because:
|
|
1905
|
-
line:
|
|
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:
|
|
1908
|
-
confirmStyle:
|
|
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:
|
|
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:
|
|
1923
|
-
deliveryId:
|
|
1924
|
-
kind:
|
|
1925
|
-
entryId:
|
|
1926
|
-
goalIds:
|
|
1927
|
-
decisionNeedId:
|
|
1928
|
-
sealed:
|
|
1929
|
-
goalState:
|
|
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 =
|
|
1934
|
-
voipToken:
|
|
1935
|
-
alertToken:
|
|
1936
|
-
fcmToken:
|
|
1937
|
-
platform:
|
|
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:
|
|
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 =
|
|
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 =
|
|
1972
|
+
var BrokerTuningSchema = z4.object({
|
|
1963
1973
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
1964
|
-
ackVerbosity:
|
|
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:
|
|
1977
|
+
clarifyEagerness: z4.enum(["low", "normal", "high"]).optional(),
|
|
1968
1978
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
1969
|
-
phrasebook:
|
|
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:
|
|
1985
|
+
language: z4.enum(["en", "es"]).optional()
|
|
1976
1986
|
});
|
|
1977
|
-
var UserSettingsSchema =
|
|
1978
|
-
permissions:
|
|
1979
|
-
call:
|
|
1980
|
-
banner:
|
|
1981
|
-
push:
|
|
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:
|
|
1984
|
-
silentPush:
|
|
1985
|
-
autoCallback:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
2021
|
-
id:
|
|
2022
|
-
parentId:
|
|
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:
|
|
2025
|
-
title:
|
|
2040
|
+
initiator: z4.enum(["user", "agent"]),
|
|
2041
|
+
title: z4.string(),
|
|
2026
2042
|
/** The agent on the other end (its name). */
|
|
2027
|
-
name:
|
|
2028
|
-
createdAt:
|
|
2043
|
+
name: z4.string(),
|
|
2044
|
+
createdAt: z4.string(),
|
|
2029
2045
|
/** When the agent fetched your request (user→agent only). */
|
|
2030
|
-
agentAckedAt:
|
|
2046
|
+
agentAckedAt: z4.string().nullable(),
|
|
2031
2047
|
/** When you answered the agent's notification (agent→user only). */
|
|
2032
|
-
humanAckedAt:
|
|
2048
|
+
humanAckedAt: z4.string().nullable()
|
|
2033
2049
|
});
|
|
2034
2050
|
var ACTIVITY_LINES = 2;
|
|
2035
2051
|
var ACTIVITY_LINE_MAX = 80;
|
|
2036
|
-
var AgentActivitySchema =
|
|
2052
|
+
var AgentActivitySchema = z4.object({
|
|
2037
2053
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
2038
|
-
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:
|
|
2057
|
+
at: z4.string().datetime()
|
|
2042
2058
|
});
|
|
2043
|
-
var ConnectionSummarySchema =
|
|
2059
|
+
var ConnectionSummarySchema = z4.object({
|
|
2044
2060
|
/** The connection = the agent's token id (used to address a request). */
|
|
2045
|
-
id:
|
|
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:
|
|
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:
|
|
2055
|
-
device:
|
|
2070
|
+
mintedByDevice: z4.string().nullable().optional(),
|
|
2071
|
+
device: z4.string().nullable(),
|
|
2056
2072
|
/** The agent's display name (the single pairing name). */
|
|
2057
|
-
name:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
2085
|
-
harnesses:
|
|
2086
|
-
workspaces:
|
|
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:
|
|
2111
|
+
managed: z4.boolean()
|
|
2096
2112
|
});
|
|
2097
|
-
var LedgerItemSchema =
|
|
2098
|
-
var AgentLedgerSchema =
|
|
2099
|
-
agent:
|
|
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:
|
|
2117
|
+
asks: z4.array(LedgerItemSchema),
|
|
2102
2118
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
2103
|
-
answered:
|
|
2119
|
+
answered: z4.array(LedgerItemSchema),
|
|
2104
2120
|
/** Requests you sent it that it never took. */
|
|
2105
|
-
requests:
|
|
2106
|
-
goals:
|
|
2107
|
-
callbacks:
|
|
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 =
|
|
2110
|
-
moved:
|
|
2111
|
-
parentId:
|
|
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 =
|
|
2114
|
-
var MoveSchema =
|
|
2115
|
-
id:
|
|
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:
|
|
2133
|
+
question: z4.string(),
|
|
2118
2134
|
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
2119
|
-
answer:
|
|
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:
|
|
2138
|
+
rationale: z4.string().nullable(),
|
|
2123
2139
|
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
2124
|
-
scope:
|
|
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:
|
|
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:
|
|
2146
|
+
pinBroken: z4.boolean(),
|
|
2131
2147
|
/** When the ruling was distilled. */
|
|
2132
|
-
learnedAt:
|
|
2148
|
+
learnedAt: z4.string(),
|
|
2133
2149
|
/** Last time it answered an ask. Null = never fired. */
|
|
2134
|
-
lastUsedAt:
|
|
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:
|
|
2153
|
+
usedCount: z4.number(),
|
|
2138
2154
|
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
2139
|
-
confirms:
|
|
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:
|
|
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:
|
|
2161
|
+
learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
|
|
2146
2162
|
});
|
|
2147
|
-
var QueueQuestionSchema =
|
|
2163
|
+
var QueueQuestionSchema = z4.object({
|
|
2148
2164
|
/** The decision need's id — what an answer is accepted against. */
|
|
2149
|
-
id:
|
|
2165
|
+
id: z4.string(),
|
|
2150
2166
|
/** The words that were asked, from the request Entry that asked them. */
|
|
2151
|
-
question:
|
|
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:
|
|
2171
|
+
deliveryId: z4.string().nullable().default(null),
|
|
2156
2172
|
/** The Entry the ruling is about. */
|
|
2157
|
-
aboutId:
|
|
2173
|
+
aboutId: z4.string().nullable().default(null),
|
|
2158
2174
|
/** Empty for a free-text question. */
|
|
2159
|
-
options:
|
|
2160
|
-
select:
|
|
2161
|
-
askedAt:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
2195
|
+
blocked: z4.boolean().default(false)
|
|
2180
2196
|
});
|
|
2181
|
-
var QueueItemSchema =
|
|
2182
|
-
id:
|
|
2197
|
+
var QueueItemSchema = z4.object({
|
|
2198
|
+
id: z4.string(),
|
|
2183
2199
|
/** One-line headline — the first sentence of the outcome. */
|
|
2184
|
-
title:
|
|
2200
|
+
title: z4.string(),
|
|
2185
2201
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
2186
|
-
intent:
|
|
2202
|
+
intent: z4.string(),
|
|
2187
2203
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
2188
|
-
state:
|
|
2204
|
+
state: z4.string(),
|
|
2189
2205
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
2190
|
-
assignee:
|
|
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:
|
|
2193
|
-
|
|
2194
|
-
|
|
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:
|
|
2215
|
+
parentGoalId: z4.string().nullable().default(null),
|
|
2197
2216
|
/** Goals opened under this one — only those the same list holds. */
|
|
2198
|
-
childGoalIds:
|
|
2217
|
+
childGoalIds: z4.array(z4.string()).default([]),
|
|
2199
2218
|
/** Goals this one waits on (start or finish gates). */
|
|
2200
|
-
dependencyGoalIds:
|
|
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:
|
|
2221
|
+
blocked: z4.boolean().default(false),
|
|
2203
2222
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
2204
|
-
questions:
|
|
2223
|
+
questions: z4.array(QueueQuestionSchema).default([]),
|
|
2205
2224
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
2206
|
-
repo:
|
|
2207
|
-
createdAt:
|
|
2208
|
-
updatedAt:
|
|
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
|
|
2211
|
-
var
|
|
2212
|
-
var
|
|
2213
|
-
var
|
|
2214
|
-
|
|
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:
|
|
2241
|
+
noteId: z4.string().nullable(),
|
|
2218
2242
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
2219
|
-
question:
|
|
2243
|
+
question: z4.string(),
|
|
2220
2244
|
/** The user's ruling; null while the question is open. */
|
|
2221
|
-
answer:
|
|
2222
|
-
decidedAt:
|
|
2223
|
-
createdAt:
|
|
2245
|
+
answer: z4.string().nullable(),
|
|
2246
|
+
decidedAt: z4.string().nullable(),
|
|
2247
|
+
createdAt: z4.string()
|
|
2224
2248
|
});
|
|
2225
|
-
var NoteSchema =
|
|
2226
|
-
id:
|
|
2249
|
+
var NoteSchema = z4.object({
|
|
2250
|
+
id: z4.string(),
|
|
2227
2251
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
2228
|
-
title:
|
|
2252
|
+
title: z4.string(),
|
|
2229
2253
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
2230
|
-
intent:
|
|
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:
|
|
2258
|
+
assignee: z4.string().nullable(),
|
|
2235
2259
|
/** The request thread minted at assignment; null until assigned. */
|
|
2236
|
-
parentId:
|
|
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:
|
|
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:
|
|
2249
|
-
lastRideAt:
|
|
2250
|
-
createdAt:
|
|
2272
|
+
rides: z4.number().int().default(0),
|
|
2273
|
+
lastRideAt: z4.string().nullable().default(null),
|
|
2274
|
+
createdAt: z4.string()
|
|
2251
2275
|
});
|
|
2252
|
-
var TriageItemSchema =
|
|
2253
|
-
noteId:
|
|
2276
|
+
var TriageItemSchema = z4.object({
|
|
2277
|
+
noteId: z4.string(),
|
|
2254
2278
|
/** The note's headline at run time. */
|
|
2255
|
-
title:
|
|
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:
|
|
2283
|
+
why: z4.string()
|
|
2260
2284
|
});
|
|
2261
|
-
var TriageAssignmentSchema =
|
|
2285
|
+
var TriageAssignmentSchema = z4.object({
|
|
2262
2286
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
2263
|
-
agent:
|
|
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:
|
|
2267
|
-
notes:
|
|
2290
|
+
agentName: z4.string(),
|
|
2291
|
+
notes: z4.array(TriageItemSchema)
|
|
2268
2292
|
});
|
|
2269
|
-
var TriageStatusSchema =
|
|
2270
|
-
var SubmitTriageSchema =
|
|
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:
|
|
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:
|
|
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:
|
|
2281
|
-
close:
|
|
2282
|
-
stale:
|
|
2283
|
-
assign:
|
|
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:
|
|
2287
|
-
runAt:
|
|
2310
|
+
id: z4.string(),
|
|
2311
|
+
runAt: z4.string(),
|
|
2288
2312
|
status: TriageStatusSchema,
|
|
2289
|
-
model:
|
|
2313
|
+
model: z4.string().nullable().default(null)
|
|
2290
2314
|
});
|
|
2291
|
-
var AcceptTriageSchema =
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
group:
|
|
2296
|
-
agent:
|
|
2297
|
-
noteIds:
|
|
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 =
|
|
2301
|
-
accepted:
|
|
2302
|
-
failed:
|
|
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 =
|
|
2328
|
+
var DeliveryModeSchema = z4.enum(["poll", "self_hosted"]);
|
|
2305
2329
|
var WAKE_EVENT = "wake";
|
|
2306
2330
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
2307
|
-
var RegisterDeliverySchema =
|
|
2308
|
-
var OAuthStartSchema =
|
|
2309
|
-
provider:
|
|
2310
|
-
returnTo:
|
|
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 =
|
|
2313
|
-
tokenId:
|
|
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:
|
|
2342
|
+
realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
|
|
2319
2343
|
});
|
|
2320
|
-
var WakeNudgeSchema =
|
|
2321
|
-
kind:
|
|
2322
|
-
notificationId:
|
|
2323
|
-
parentId:
|
|
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 =
|
|
2326
|
-
var DeviceCodeSchema =
|
|
2327
|
-
device_code:
|
|
2328
|
-
user_code:
|
|
2329
|
-
verification_uri:
|
|
2330
|
-
verification_uri_complete:
|
|
2331
|
-
interval:
|
|
2332
|
-
expires_in:
|
|
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 =
|
|
2335
|
-
code:
|
|
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:
|
|
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:
|
|
2344
|
-
device:
|
|
2367
|
+
agent: z4.string().optional(),
|
|
2368
|
+
device: z4.string().nullable(),
|
|
2345
2369
|
status: PairingStatusSchema
|
|
2346
2370
|
});
|
|
2347
|
-
var DeviceTokenSchema =
|
|
2348
|
-
access_token:
|
|
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:
|
|
2352
|
-
device:
|
|
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:
|
|
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:
|
|
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:
|
|
2364
|
-
uik_pub:
|
|
2387
|
+
workspace: z4.string().nullable().optional(),
|
|
2388
|
+
uik_pub: z4.string().nullable().optional()
|
|
2365
2389
|
});
|
|
2366
|
-
var SupportRequestSchema =
|
|
2367
|
-
email:
|
|
2368
|
-
message:
|
|
2369
|
-
name:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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",
|
|
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,
|