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