@paigy/mcp 0.40.22 → 0.40.24
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 +29 -16
- package/dist/{chunk-2ED4GL4N.js → chunk-J7T3EB5Y.js} +11 -9
- package/dist/chunk-RK5LT7NH.js +110 -0
- package/dist/{chunk-BFN2GJ42.js → chunk-TP7DM2MP.js} +1 -1
- package/dist/{chunk-WDZ67U4G.js → chunk-VUKR2E24.js} +688 -505
- package/dist/{chunk-KSXJCZ2L.js → chunk-WR2M3RME.js} +762 -535
- package/dist/{dist-YAKVSYVQ.js → dist-EKLTLANV.js} +13 -3
- package/dist/enable.js +3 -3
- package/dist/index.js +18 -7
- package/dist/listen.js +83 -15
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -7,13 +7,14 @@ 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
|
|
10
|
+
import { z as z5 } from "zod";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { z as z3 } from "zod";
|
|
13
13
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind3 } from "zod/v3";
|
|
14
14
|
import { ZodFirstPartyTypeKind } from "zod/v3";
|
|
15
15
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind2 } from "zod/v3";
|
|
16
16
|
import { z as z2 } from "zod";
|
|
17
|
+
import { z as z4 } from "zod";
|
|
17
18
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
18
19
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
19
20
|
import { homedir as homedir2 } from "os";
|
|
@@ -22,6 +23,7 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
22
23
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
23
24
|
import { WebSocket } from "undici";
|
|
24
25
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
26
|
+
var CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
|
|
25
27
|
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
26
28
|
var NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
|
|
27
29
|
var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
@@ -38,8 +40,8 @@ var WORKSPACE_ID = (() => {
|
|
|
38
40
|
return INSTANCE_ID;
|
|
39
41
|
}
|
|
40
42
|
})();
|
|
41
|
-
var envSession = (
|
|
42
|
-
const v = process.env[
|
|
43
|
+
var envSession = (key2) => {
|
|
44
|
+
const v = process.env[key2];
|
|
43
45
|
return v && v.trim() ? v : void 0;
|
|
44
46
|
};
|
|
45
47
|
function parentOf(pid) {
|
|
@@ -79,15 +81,20 @@ function isNetworkError(e) {
|
|
|
79
81
|
async function reach(url, init) {
|
|
80
82
|
return send(url, init, sessionId());
|
|
81
83
|
}
|
|
82
|
-
function reachAs(session) {
|
|
83
|
-
return (url, init) => send(url, init, session);
|
|
84
|
+
function reachAs(session, instance) {
|
|
85
|
+
return (url, init) => send(url, init, session, instance);
|
|
86
|
+
}
|
|
87
|
+
var CLIENT;
|
|
88
|
+
function setClient(name) {
|
|
89
|
+
CLIENT = name;
|
|
84
90
|
}
|
|
85
|
-
async function send(url, init, session) {
|
|
91
|
+
async function send(url, init, session, instance = envSession("PAIGY_INSTANCE_ID") ?? INSTANCE_ID) {
|
|
86
92
|
try {
|
|
87
93
|
const headers = {
|
|
88
94
|
...init?.headers,
|
|
89
|
-
"x-paigy-instance":
|
|
90
|
-
"x-paigy-session": session
|
|
95
|
+
"x-paigy-instance": instance,
|
|
96
|
+
"x-paigy-session": session,
|
|
97
|
+
...CLIENT ? { "x-paigy-client": CLIENT } : {}
|
|
91
98
|
};
|
|
92
99
|
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
93
100
|
} catch (e) {
|
|
@@ -145,19 +152,19 @@ var getRefs = (options) => {
|
|
|
145
152
|
]))
|
|
146
153
|
};
|
|
147
154
|
};
|
|
148
|
-
function addErrorMessage(res,
|
|
155
|
+
function addErrorMessage(res, key2, errorMessage, refs) {
|
|
149
156
|
if (!refs?.errorMessages)
|
|
150
157
|
return;
|
|
151
158
|
if (errorMessage) {
|
|
152
159
|
res.errorMessage = {
|
|
153
160
|
...res.errorMessage,
|
|
154
|
-
[
|
|
161
|
+
[key2]: errorMessage
|
|
155
162
|
};
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
|
-
function setResponseValueAndErrors(res,
|
|
159
|
-
res[
|
|
160
|
-
addErrorMessage(res,
|
|
165
|
+
function setResponseValueAndErrors(res, key2, value, errorMessage, refs) {
|
|
166
|
+
res[key2] = value;
|
|
167
|
+
addErrorMessage(res, key2, errorMessage, refs);
|
|
161
168
|
}
|
|
162
169
|
var getRelativePath = (pathA, pathB) => {
|
|
163
170
|
let i = 0;
|
|
@@ -719,11 +726,11 @@ function parseRecordDef(def, refs) {
|
|
|
719
726
|
return {
|
|
720
727
|
type: "object",
|
|
721
728
|
required: def.keyType._def.values,
|
|
722
|
-
properties: def.keyType._def.values.reduce((acc,
|
|
729
|
+
properties: def.keyType._def.values.reduce((acc, key2) => ({
|
|
723
730
|
...acc,
|
|
724
|
-
[
|
|
731
|
+
[key2]: parseDef(def.valueType._def, {
|
|
725
732
|
...refs,
|
|
726
|
-
currentPath: [...refs.currentPath, "properties",
|
|
733
|
+
currentPath: [...refs.currentPath, "properties", key2]
|
|
727
734
|
}) ?? parseAnyDef(refs)
|
|
728
735
|
}), {}),
|
|
729
736
|
additionalProperties: refs.rejectedAdditionalProperties
|
|
@@ -786,10 +793,10 @@ function parseMapDef(def, refs) {
|
|
|
786
793
|
}
|
|
787
794
|
function parseNativeEnumDef(def) {
|
|
788
795
|
const object = def.values;
|
|
789
|
-
const actualKeys = Object.keys(def.values).filter((
|
|
790
|
-
return typeof object[object[
|
|
796
|
+
const actualKeys = Object.keys(def.values).filter((key2) => {
|
|
797
|
+
return typeof object[object[key2]] !== "number";
|
|
791
798
|
});
|
|
792
|
-
const actualValues = actualKeys.map((
|
|
799
|
+
const actualValues = actualKeys.map((key2) => object[key2]);
|
|
793
800
|
const parsedTypes = Array.from(new Set(actualValues.map((values) => typeof values)));
|
|
794
801
|
return {
|
|
795
802
|
type: parsedTypes.length === 1 ? parsedTypes[0] === "string" ? "string" : "number" : ["string", "number"],
|
|
@@ -1522,34 +1529,31 @@ function mcpInputSchema(s) {
|
|
|
1522
1529
|
}
|
|
1523
1530
|
var AskInputSchema = z2.object({
|
|
1524
1531
|
id: z2.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
|
|
1525
|
-
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
|
|
1532
|
+
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 the question starts a new Goal of yours."),
|
|
1526
1533
|
repo: z2.string().optional().describe("Optional repository context."),
|
|
1527
1534
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
1528
|
-
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy
|
|
1535
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy sends each ask exactly as you wrote it: a bundled ask arrives as one card. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
1529
1536
|
),
|
|
1530
1537
|
options: z2.array(OptionInputSchema).min(1).max(6).optional(),
|
|
1531
|
-
//
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1534
|
-
//
|
|
1535
|
-
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
1536
|
-
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
1537
|
-
// not: whether its own options exclude each other.
|
|
1538
|
+
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
1539
|
+
// brain_prompts.md §1: structured agent questions need no model). Every card was pick-one while no
|
|
1540
|
+
// agent sent this; no read reshapes cards any more, so this is the card's shape, and the
|
|
1541
|
+
// description says so plainly.
|
|
1538
1542
|
select: z2.enum(["one", "many"]).optional().describe(
|
|
1539
|
-
'How the options are answered: "
|
|
1543
|
+
'How the options are answered: "many" makes a checklist (they may want several, e.g. independent fixes), "one" (the default) a pick (choosing one rules out the others). Send "many" whenever more than one option could be wanted at once. The person can always answer in their own words as well. Ignored without options.'
|
|
1540
1544
|
),
|
|
1541
1545
|
answers: z2.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
1542
|
-
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them.
|
|
1546
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. parentId is optional here: the question's own Goal is used. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
1543
1547
|
)
|
|
1544
1548
|
}).strict();
|
|
1545
1549
|
var StartContactSchema = z2.object({
|
|
1546
|
-
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;
|
|
1550
|
+
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; an ask naming no Goal starts a new Goal of yours."),
|
|
1547
1551
|
waiting: z2.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
1548
1552
|
channel: z2.enum(["notification", "call"]).default("notification")
|
|
1549
1553
|
}).strict();
|
|
1550
1554
|
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
|
|
1551
1555
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
1552
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', '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
|
|
1556
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', '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 starts a new Goal of yours. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. Each ask is sent exactly as written, with no reading in between: a bundled ask arrives as one card, so separate questions are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; Where your work stands while under way is progress: report it with update_goal, not contact. Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). select:'many' makes the card a checklist, for independent options they may want several of; select:'one' (the default) a pick, for alternatives. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
1553
1557
|
var CreateGoalSchema = z3.object({
|
|
1554
1558
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1555
1559
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1572,7 +1576,7 @@ var CreateGoalSchema = z3.object({
|
|
|
1572
1576
|
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
1573
1577
|
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
1574
1578
|
}).strict();
|
|
1575
|
-
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:
|
|
1579
|
+
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: 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.';
|
|
1576
1580
|
var UpdateGoalSchema = z3.object({
|
|
1577
1581
|
revision: z3.number().int().positive(),
|
|
1578
1582
|
changes: z3.object({
|
|
@@ -1608,12 +1612,13 @@ var GetGoalSchema = z3.object({
|
|
|
1608
1612
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
1609
1613
|
diagnose: z3.boolean().optional()
|
|
1610
1614
|
}).strict();
|
|
1611
|
-
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), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full.
|
|
1612
|
-
var UPDATE_GOAL_DESCRIPTION = `Update
|
|
1613
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own
|
|
1615
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
1616
|
+
var UPDATE_GOAL_DESCRIPTION = `Update a Goal you are on at an exact revision. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. 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. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
1617
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change its state; use update_goal with ownerParticipant when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
1614
1618
|
var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. 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 a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
1615
1619
|
var CheckRepliesSchema = z3.object({}).strict();
|
|
1616
1620
|
var AGENT_TOOLS = [
|
|
1621
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
1617
1622
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
1618
1623
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
1619
1624
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
@@ -1622,6 +1627,153 @@ var AGENT_TOOLS = [
|
|
|
1622
1627
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
|
|
1623
1628
|
];
|
|
1624
1629
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
1630
|
+
var id = z4.string().uuid();
|
|
1631
|
+
var key = z4.string().min(1).max(40);
|
|
1632
|
+
var participant = z4.string().regex(/^(human|agent):.+$/, "a participant ID such as agent:<uuid>");
|
|
1633
|
+
var SourceSchema = z4.object({ entryId: id }).strict();
|
|
1634
|
+
var ResultRefSchema = z4.union([z4.object({ id }).strict(), z4.object({ local: key }).strict()]);
|
|
1635
|
+
var NodeRefSchema = z4.object({ type: z4.enum(["goal", "question"]), id }).strict();
|
|
1636
|
+
var DependencySchema = z4.object({
|
|
1637
|
+
blocker: NodeRefSchema,
|
|
1638
|
+
blocked: NodeRefSchema,
|
|
1639
|
+
action: z4.enum(["start", "complete", "answer"]),
|
|
1640
|
+
reason: z4.string().max(500).optional()
|
|
1641
|
+
}).strict();
|
|
1642
|
+
var goalState = z4.enum(["open", "completed", "canceled"]);
|
|
1643
|
+
var existingGoalChanges = [
|
|
1644
|
+
z4.object({ kind: z4.literal("edit"), goalId: id, title: z4.string().min(1).max(120).optional(), outcome: z4.string().min(1).optional() }).strict().refine((c) => c.title !== void 0 || c.outcome !== void 0, "an edit changes the title or the outcome"),
|
|
1645
|
+
z4.object({ kind: z4.literal("state"), goalId: id, state: goalState }).strict(),
|
|
1646
|
+
z4.object({ kind: z4.literal("assign"), goalId: id, ownerId: participant }).strict(),
|
|
1647
|
+
z4.object({ kind: z4.literal("defer"), goalId: id, until: z4.string().datetime().nullable() }).strict(),
|
|
1648
|
+
z4.object({ kind: z4.literal("move"), goalId: id, parentGoalId: id.nullable() }).strict(),
|
|
1649
|
+
z4.object({
|
|
1650
|
+
kind: z4.literal("dependency"),
|
|
1651
|
+
change: z4.enum(["add", "remove"]),
|
|
1652
|
+
goalId: id,
|
|
1653
|
+
dependsOnGoalId: id,
|
|
1654
|
+
action: z4.enum(["start", "complete"]),
|
|
1655
|
+
reason: z4.string().max(500).optional()
|
|
1656
|
+
}).strict()
|
|
1657
|
+
];
|
|
1658
|
+
var GoalChangeSchema = z4.union([
|
|
1659
|
+
z4.object({
|
|
1660
|
+
kind: z4.literal("create"),
|
|
1661
|
+
title: z4.string().min(1).max(120),
|
|
1662
|
+
outcome: z4.string().min(1),
|
|
1663
|
+
ownerId: participant,
|
|
1664
|
+
parentGoalId: id.optional(),
|
|
1665
|
+
sourceEntryIds: z4.array(id)
|
|
1666
|
+
}).strict(),
|
|
1667
|
+
...existingGoalChanges
|
|
1668
|
+
]);
|
|
1669
|
+
var BrainGoalChangeSchema = z4.union([
|
|
1670
|
+
// newSession: Paigy starts a session to own it (owner, 2026-10-06); until it has, the person owns it.
|
|
1671
|
+
z4.object({
|
|
1672
|
+
kind: z4.literal("create"),
|
|
1673
|
+
outcome: z4.string().min(1),
|
|
1674
|
+
title: z4.string().min(1).max(120),
|
|
1675
|
+
ownerId: participant,
|
|
1676
|
+
parentGoal: ResultRefSchema.nullable(),
|
|
1677
|
+
newSession: z4.literal(true).optional()
|
|
1678
|
+
}).strict(),
|
|
1679
|
+
...existingGoalChanges
|
|
1680
|
+
]);
|
|
1681
|
+
var option = z4.object({ id: z4.string().min(1).max(40), label: z4.string().min(1), hint: z4.string().optional() }).strict();
|
|
1682
|
+
var QuestionChangeSchema = z4.union([
|
|
1683
|
+
z4.object({
|
|
1684
|
+
kind: z4.literal("create"),
|
|
1685
|
+
text: z4.string().min(1),
|
|
1686
|
+
answererId: participant,
|
|
1687
|
+
goal: ResultRefSchema.optional(),
|
|
1688
|
+
blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
|
|
1689
|
+
options: z4.array(option).min(2).optional(),
|
|
1690
|
+
pickMode: z4.enum(["one", "many", "rank"]).optional()
|
|
1691
|
+
}).strict().refine((c) => c.options === void 0 === (c.pickMode === void 0), "options and pickMode come together"),
|
|
1692
|
+
z4.object({ kind: z4.literal("edit"), questionId: id, text: z4.string().min(1) }).strict(),
|
|
1693
|
+
z4.object({ kind: z4.literal("assign"), questionId: id, answererId: participant }).strict(),
|
|
1694
|
+
z4.object({ kind: z4.literal("withdraw"), questionId: id }).strict(),
|
|
1695
|
+
z4.object({ kind: z4.literal("dependency"), change: z4.enum(["add", "remove"]), edge: DependencySchema }).strict()
|
|
1696
|
+
]);
|
|
1697
|
+
var lessonScope = z4.union([
|
|
1698
|
+
z4.object({ kind: z4.literal("user") }).strict(),
|
|
1699
|
+
z4.object({ kind: z4.literal("goal"), goal: ResultRefSchema }).strict()
|
|
1700
|
+
]);
|
|
1701
|
+
var LessonChangeSchema = z4.union([
|
|
1702
|
+
z4.object({ kind: z4.literal("remember"), text: z4.string().min(1), scope: lessonScope }).strict(),
|
|
1703
|
+
z4.object({ kind: z4.literal("revise"), lessonId: id, text: z4.string().min(1), scope: lessonScope }).strict(),
|
|
1704
|
+
z4.object({ kind: z4.literal("withdraw"), lessonId: id }).strict()
|
|
1705
|
+
]);
|
|
1706
|
+
var BrainNextSchema = z4.object({
|
|
1707
|
+
instructions: z4.array(z4.object({ entryKeys: z4.array(key).min(1) }).strict()),
|
|
1708
|
+
say: z4.array(key),
|
|
1709
|
+
then: z4.enum(["listen", "hold", "end", "none"]),
|
|
1710
|
+
waitFor: z4.array(ResultRefSchema),
|
|
1711
|
+
reason: z4.string()
|
|
1712
|
+
}).strict();
|
|
1713
|
+
var messageBase = {
|
|
1714
|
+
key,
|
|
1715
|
+
text: z4.string().min(1),
|
|
1716
|
+
goals: z4.array(ResultRefSchema),
|
|
1717
|
+
questions: z4.array(ResultRefSchema),
|
|
1718
|
+
entryKeys: z4.array(key),
|
|
1719
|
+
obligationIds: z4.array(id)
|
|
1720
|
+
};
|
|
1721
|
+
var BrainMessageSchema = z4.union([
|
|
1722
|
+
z4.object({ ...messageBase, to: z4.object({ kind: z4.literal("user") }).strict() }).strict(),
|
|
1723
|
+
z4.object({
|
|
1724
|
+
...messageBase,
|
|
1725
|
+
to: z4.object({ kind: z4.literal("agent"), agentId: z4.string().regex(/^agent:(?!paigy$)\S+$/) }).strict(),
|
|
1726
|
+
call: z4.object({ callId: id, listen: z4.literal(true) }).strict().optional()
|
|
1727
|
+
}).strict()
|
|
1728
|
+
]);
|
|
1729
|
+
var BrainSearchSchema = z4.object({
|
|
1730
|
+
query: z4.string().min(1).max(500),
|
|
1731
|
+
within: z4.array(z4.enum(["entries", "goals", "answers"])).min(1),
|
|
1732
|
+
goalId: id.optional()
|
|
1733
|
+
}).strict();
|
|
1734
|
+
var BrainResultSchema = z4.object({
|
|
1735
|
+
messages: z4.array(BrainMessageSchema),
|
|
1736
|
+
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
|
|
1737
|
+
questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
|
|
1738
|
+
answers: z4.array(z4.object({
|
|
1739
|
+
question: ResultRefSchema,
|
|
1740
|
+
entryKeys: z4.array(key).min(1),
|
|
1741
|
+
summary: z4.string().min(1),
|
|
1742
|
+
selectedOptionIds: z4.array(z4.string().min(1)).optional()
|
|
1743
|
+
}).strict()),
|
|
1744
|
+
goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
|
|
1745
|
+
feedback: z4.array(z4.object({ entryKeys: z4.array(key).min(1) }).strict()),
|
|
1746
|
+
lessons: z4.array(z4.object({ entryKeys: z4.array(key).min(1), change: LessonChangeSchema }).strict()),
|
|
1747
|
+
next: BrainNextSchema,
|
|
1748
|
+
search: BrainSearchSchema.optional()
|
|
1749
|
+
}).strict();
|
|
1750
|
+
var str = z4.string();
|
|
1751
|
+
var strs = z4.array(z4.string());
|
|
1752
|
+
var CompactResultSchema = z4.object({
|
|
1753
|
+
next: z4.object({ say: strs, then: z4.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason: str, instructions: strs }).strict(),
|
|
1754
|
+
messages: z4.array(z4.object({ key: str, to: str, text: str, about: strs, entries: strs, owed: strs, inviteToCall: z4.boolean() }).strict()),
|
|
1755
|
+
entries: z4.array(z4.object({ key: str, from: str, on: strs }).strict()),
|
|
1756
|
+
answers: z4.array(z4.object({ question: str, entries: strs, summary: str, picked: strs }).strict()),
|
|
1757
|
+
changes: z4.array(z4.object({
|
|
1758
|
+
key: str,
|
|
1759
|
+
what: z4.enum(["goal", "question", "lesson"]),
|
|
1760
|
+
op: z4.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
|
|
1761
|
+
id: str,
|
|
1762
|
+
text: str,
|
|
1763
|
+
title: str,
|
|
1764
|
+
who: str,
|
|
1765
|
+
under: str,
|
|
1766
|
+
gate: z4.enum(["start", "complete", "answer", "none"]),
|
|
1767
|
+
options: z4.array(z4.object({ id: str, label: str }).strict()),
|
|
1768
|
+
pick: z4.enum(["one", "many", "rank", "words"]),
|
|
1769
|
+
entries: strs
|
|
1770
|
+
}).strict()),
|
|
1771
|
+
feedback: z4.array(strs),
|
|
1772
|
+
search: z4.object({ query: str, within: strs, goal: str }).strict()
|
|
1773
|
+
}).strict();
|
|
1774
|
+
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
1775
|
+
zodToJsonSchema(CompactResultSchema, { $refStrategy: "none" })
|
|
1776
|
+
);
|
|
1625
1777
|
function entryWords(entry) {
|
|
1626
1778
|
const content = entry.content;
|
|
1627
1779
|
if (content && "sealed" in content) return "";
|
|
@@ -1634,22 +1786,24 @@ function entryWords(entry) {
|
|
|
1634
1786
|
const description = plain.description;
|
|
1635
1787
|
const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
|
|
1636
1788
|
if (parts.length) return parts.join("\n\n");
|
|
1789
|
+
const line = plain.line;
|
|
1790
|
+
if (typeof line === "string" && line.trim()) return line;
|
|
1637
1791
|
}
|
|
1638
1792
|
return entry.sources.map((source) => source.text).join("\n");
|
|
1639
1793
|
}
|
|
1640
1794
|
var LIVE_MS = 3 * 6e4;
|
|
1641
|
-
var WORKING_MS =
|
|
1642
|
-
var ContextSchema =
|
|
1643
|
-
title:
|
|
1644
|
-
description:
|
|
1795
|
+
var WORKING_MS = 60 * 6e4;
|
|
1796
|
+
var ContextSchema = z5.object({
|
|
1797
|
+
title: z5.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
1798
|
+
description: z5.array(z5.string().min(1)).describe(
|
|
1645
1799
|
"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."
|
|
1646
1800
|
)
|
|
1647
1801
|
});
|
|
1648
|
-
var ParticipantSchema =
|
|
1649
|
-
kind:
|
|
1650
|
-
id:
|
|
1802
|
+
var ParticipantSchema = z5.object({
|
|
1803
|
+
kind: z5.enum(["human", "agent"]),
|
|
1804
|
+
id: z5.string()
|
|
1651
1805
|
});
|
|
1652
|
-
var TransformSchema =
|
|
1806
|
+
var TransformSchema = z5.enum([
|
|
1653
1807
|
"structure",
|
|
1654
1808
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
1655
1809
|
"request_more",
|
|
@@ -1672,13 +1826,13 @@ var PAIGY_SELF = { kind: "agent", id: "paigy" };
|
|
|
1672
1826
|
function isPaigy(ref) {
|
|
1673
1827
|
return ref === participantRef(PAIGY_SELF);
|
|
1674
1828
|
}
|
|
1675
|
-
var VisualSchema =
|
|
1676
|
-
url:
|
|
1677
|
-
label:
|
|
1829
|
+
var VisualSchema = z5.object({
|
|
1830
|
+
url: z5.string().url(),
|
|
1831
|
+
label: z5.string().optional()
|
|
1678
1832
|
});
|
|
1679
|
-
var NotifyLevelSchema =
|
|
1680
|
-
var SelectShapeSchema =
|
|
1681
|
-
var ReceiptEventSchema =
|
|
1833
|
+
var NotifyLevelSchema = z5.enum(["inbox", "push", "banner", "call"]);
|
|
1834
|
+
var SelectShapeSchema = z5.enum(["one", "many", "rank", "confirm", "text"]);
|
|
1835
|
+
var ReceiptEventSchema = z5.enum([
|
|
1682
1836
|
"delivered",
|
|
1683
1837
|
// the bundle reached the recipient at some level
|
|
1684
1838
|
"seen",
|
|
@@ -1708,47 +1862,47 @@ var ReceiptEventSchema = z4.enum([
|
|
|
1708
1862
|
// be rewound by a writer that forgot to advance it.
|
|
1709
1863
|
"restarted"
|
|
1710
1864
|
]);
|
|
1711
|
-
var AttentionSchema =
|
|
1865
|
+
var AttentionSchema = z5.object({
|
|
1712
1866
|
urgency: NotifyLevelSchema,
|
|
1713
1867
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
1714
1868
|
select: SelectShapeSchema.nullable(),
|
|
1715
1869
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
1716
|
-
points:
|
|
1870
|
+
points: z5.array(z5.string()).nullable(),
|
|
1717
1871
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
1718
|
-
blocking:
|
|
1872
|
+
blocking: z5.boolean(),
|
|
1719
1873
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
1720
1874
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
1721
|
-
deadline:
|
|
1875
|
+
deadline: z5.string().datetime().nullable().optional()
|
|
1722
1876
|
});
|
|
1723
|
-
var NotifyRequestFields =
|
|
1877
|
+
var NotifyRequestFields = z5.object({
|
|
1724
1878
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
1725
1879
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
1726
1880
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
1727
1881
|
context: ContextSchema.optional(),
|
|
1728
|
-
options:
|
|
1882
|
+
options: z5.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
1729
1883
|
"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)."
|
|
1730
1884
|
),
|
|
1731
|
-
points:
|
|
1885
|
+
points: z5.array(z5.string().min(1)).optional().describe(
|
|
1732
1886
|
"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."
|
|
1733
1887
|
),
|
|
1734
|
-
visuals:
|
|
1888
|
+
visuals: z5.array(VisualSchema).optional().describe(
|
|
1735
1889
|
"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."
|
|
1736
1890
|
),
|
|
1737
1891
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
1738
|
-
repo:
|
|
1892
|
+
repo: z5.string().optional(),
|
|
1739
1893
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
1740
|
-
branch:
|
|
1894
|
+
branch: z5.string().optional(),
|
|
1741
1895
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
1742
1896
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
1743
1897
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
1744
1898
|
* still accepts the old name from older clients. */
|
|
1745
|
-
parentId:
|
|
1899
|
+
parentId: z5.string().uuid().optional(),
|
|
1746
1900
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
1747
1901
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
1748
|
-
workId:
|
|
1902
|
+
workId: z5.string().uuid().optional(),
|
|
1749
1903
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
1750
1904
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
1751
|
-
goalId:
|
|
1905
|
+
goalId: z5.string().uuid().optional(),
|
|
1752
1906
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
1753
1907
|
"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."
|
|
1754
1908
|
),
|
|
@@ -1756,7 +1910,7 @@ var NotifyRequestFields = z4.object({
|
|
|
1756
1910
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
1757
1911
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
1758
1912
|
* says HOW. */
|
|
1759
|
-
clarifies:
|
|
1913
|
+
clarifies: z5.string().optional(),
|
|
1760
1914
|
select: SelectShapeSchema.optional().describe(
|
|
1761
1915
|
"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."
|
|
1762
1916
|
),
|
|
@@ -1770,20 +1924,20 @@ var NotifyRequestFields = z4.object({
|
|
|
1770
1924
|
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
1771
1925
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
1772
1926
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
1773
|
-
ask:
|
|
1927
|
+
ask: z5.string().min(1).max(1e4).optional().describe(
|
|
1774
1928
|
'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.'
|
|
1775
1929
|
),
|
|
1776
|
-
needs:
|
|
1930
|
+
needs: z5.array(z5.string().min(1)).optional().describe(
|
|
1777
1931
|
"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."
|
|
1778
1932
|
),
|
|
1779
|
-
urgencyHint:
|
|
1933
|
+
urgencyHint: z5.enum(["whenever", "soon", "now"]).optional().describe(
|
|
1780
1934
|
"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."
|
|
1781
1935
|
),
|
|
1782
1936
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
1783
1937
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
1784
1938
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
1785
1939
|
* urgencyHint/blocking win when both are sent. */
|
|
1786
|
-
waiting:
|
|
1940
|
+
waiting: z5.enum(["none", "soft", "hard"]).optional().describe(
|
|
1787
1941
|
"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."
|
|
1788
1942
|
),
|
|
1789
1943
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -1791,98 +1945,98 @@ var NotifyRequestFields = z4.object({
|
|
|
1791
1945
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
1792
1946
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
1793
1947
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
1794
|
-
confirm:
|
|
1948
|
+
confirm: z5.boolean().optional().describe(
|
|
1795
1949
|
"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'."
|
|
1796
1950
|
),
|
|
1797
1951
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
1798
1952
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
1799
1953
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
1800
1954
|
* waiting:'hard'. */
|
|
1801
|
-
channel:
|
|
1955
|
+
channel: z5.enum(["call", "message"]).optional().describe(
|
|
1802
1956
|
"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."
|
|
1803
1957
|
),
|
|
1804
|
-
confirmStyle:
|
|
1958
|
+
confirmStyle: z5.enum(["yesno", "approve"]).default("yesno").describe(
|
|
1805
1959
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
1806
1960
|
),
|
|
1807
|
-
blocking:
|
|
1961
|
+
blocking: z5.boolean().default(false).describe(
|
|
1808
1962
|
"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."
|
|
1809
1963
|
)
|
|
1810
1964
|
});
|
|
1811
1965
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
1812
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
1966
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
1813
1967
|
if (r.ask !== void 0) {
|
|
1814
1968
|
for (const f of ["context", "select", "points"]) {
|
|
1815
1969
|
if (r[f] !== void 0)
|
|
1816
|
-
ctx.addIssue({ code:
|
|
1970
|
+
ctx.addIssue({ code: z5.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.` });
|
|
1817
1971
|
}
|
|
1818
1972
|
return;
|
|
1819
1973
|
}
|
|
1820
1974
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
1821
|
-
ctx.addIssue({ code:
|
|
1975
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
1822
1976
|
if (!r.context)
|
|
1823
|
-
ctx.addIssue({ code:
|
|
1977
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
1824
1978
|
if (!r.select)
|
|
1825
|
-
ctx.addIssue({ code:
|
|
1979
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
1826
1980
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
1827
1981
|
if (needsOptions && !r.options?.length)
|
|
1828
|
-
ctx.addIssue({ code:
|
|
1982
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
1829
1983
|
if (!needsOptions && r.options?.length)
|
|
1830
|
-
ctx.addIssue({ code:
|
|
1984
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
1831
1985
|
});
|
|
1832
|
-
var NotifyStatusSchema =
|
|
1833
|
-
var AgentStateSchema =
|
|
1834
|
-
var TurnSchema =
|
|
1835
|
-
prompt:
|
|
1836
|
-
reply:
|
|
1986
|
+
var NotifyStatusSchema = z5.enum(["pending", "answered", "ignored"]);
|
|
1987
|
+
var AgentStateSchema = z5.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
1988
|
+
var TurnSchema = z5.object({
|
|
1989
|
+
prompt: z5.string(),
|
|
1990
|
+
reply: z5.string()
|
|
1837
1991
|
});
|
|
1838
|
-
var UserAnswerSchema =
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1992
|
+
var UserAnswerSchema = z5.discriminatedUnion("kind", [
|
|
1993
|
+
z5.object({ kind: z5.literal("option"), optionId: z5.string(), label: z5.string().optional() }),
|
|
1994
|
+
z5.object({ kind: z5.literal("text"), text: z5.string() }),
|
|
1995
|
+
z5.object({ kind: z5.literal("ignored") }),
|
|
1996
|
+
z5.object({ kind: z5.literal("multi"), optionIds: z5.array(z5.string()), labels: z5.array(z5.string()).optional() }),
|
|
1997
|
+
z5.object({ kind: z5.literal("ranked"), optionIds: z5.array(z5.string()), labels: z5.array(z5.string()).optional() }),
|
|
1998
|
+
z5.object({ kind: z5.literal("clarify"), chunks: z5.array(z5.string()).min(1) }),
|
|
1999
|
+
z5.object({ kind: z5.literal("confirm"), approved: z5.boolean() }),
|
|
2000
|
+
z5.object({ kind: z5.literal("turns"), turns: z5.array(TurnSchema).min(1) }),
|
|
1847
2001
|
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
1848
2002
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
1849
2003
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
1850
2004
|
* card the user can reply to — the broker never overrides the user. */
|
|
1851
|
-
|
|
2005
|
+
z5.object({ kind: z5.literal("precedent"), answer: z5.string(), derivation: z5.string(), sources: z5.array(z5.string()).min(1) })
|
|
1852
2006
|
]);
|
|
1853
|
-
var IntentSchema =
|
|
2007
|
+
var IntentSchema = z5.object({
|
|
1854
2008
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
1855
2009
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
1856
2010
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
1857
2011
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
1858
|
-
kind:
|
|
1859
|
-
detail:
|
|
2012
|
+
kind: z5.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
2013
|
+
detail: z5.string(),
|
|
1860
2014
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
1861
2015
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
1862
2016
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
1863
2017
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
1864
|
-
dueInSeconds:
|
|
2018
|
+
dueInSeconds: z5.number().int().positive().optional(),
|
|
1865
2019
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
1866
|
-
* already read the utterance, so `
|
|
2020
|
+
* already read the utterance, so `signals.kind` stops defaulting to
|
|
1867
2021
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
1868
2022
|
* about what cannot answer "is the bot looping less this week?". */
|
|
1869
|
-
fault:
|
|
2023
|
+
fault: z5.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
1870
2024
|
});
|
|
1871
|
-
var RideAlongSchema =
|
|
2025
|
+
var RideAlongSchema = z5.object({
|
|
1872
2026
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
1873
|
-
noteId:
|
|
2027
|
+
noteId: z5.string(),
|
|
1874
2028
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
1875
|
-
text:
|
|
2029
|
+
text: z5.string(),
|
|
1876
2030
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
1877
|
-
parentId:
|
|
2031
|
+
parentId: z5.string().nullable()
|
|
1878
2032
|
});
|
|
1879
|
-
var AwaitItemSchema =
|
|
1880
|
-
|
|
1881
|
-
type:
|
|
1882
|
-
parentId:
|
|
1883
|
-
notificationId:
|
|
1884
|
-
workId:
|
|
1885
|
-
decisionId:
|
|
2033
|
+
var AwaitItemSchema = z5.discriminatedUnion("type", [
|
|
2034
|
+
z5.object({
|
|
2035
|
+
type: z5.literal("reply"),
|
|
2036
|
+
parentId: z5.string(),
|
|
2037
|
+
notificationId: z5.string(),
|
|
2038
|
+
workId: z5.string().uuid().optional(),
|
|
2039
|
+
decisionId: z5.string().uuid().optional(),
|
|
1886
2040
|
answer: UserAnswerSchema,
|
|
1887
2041
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
1888
2042
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -1892,103 +2046,103 @@ var AwaitItemSchema = z4.discriminatedUnion("type", [
|
|
|
1892
2046
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
1893
2047
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
1894
2048
|
* a sentence with a hole in it is worse than no sentence. */
|
|
1895
|
-
note:
|
|
2049
|
+
note: z5.string().optional(),
|
|
1896
2050
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
1897
2051
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
1898
2052
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
1899
2053
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
1900
|
-
transcript:
|
|
2054
|
+
transcript: z5.string().optional(),
|
|
1901
2055
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
1902
2056
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
1903
|
-
covered:
|
|
2057
|
+
covered: z5.array(z5.string()).optional(),
|
|
1904
2058
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
1905
2059
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
1906
2060
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
1907
2061
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
1908
2062
|
* dropped. Absent/empty = nothing owed. */
|
|
1909
|
-
also:
|
|
2063
|
+
also: z5.array(RideAlongSchema).optional()
|
|
1910
2064
|
}),
|
|
1911
|
-
|
|
1912
|
-
type:
|
|
1913
|
-
parentId:
|
|
1914
|
-
notificationId:
|
|
1915
|
-
remindAt:
|
|
2065
|
+
z5.object({
|
|
2066
|
+
type: z5.literal("remind"),
|
|
2067
|
+
parentId: z5.string(),
|
|
2068
|
+
notificationId: z5.string(),
|
|
2069
|
+
remindAt: z5.string().datetime({ offset: true }),
|
|
1916
2070
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
1917
|
-
remindInSeconds:
|
|
2071
|
+
remindInSeconds: z5.number()
|
|
1918
2072
|
}),
|
|
1919
2073
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
1920
2074
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
1921
2075
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
1922
2076
|
* re-orient via check_replies). */
|
|
1923
|
-
|
|
1924
|
-
type:
|
|
1925
|
-
parentId:
|
|
1926
|
-
notificationId:
|
|
2077
|
+
z5.object({
|
|
2078
|
+
type: z5.literal("superseded"),
|
|
2079
|
+
parentId: z5.string(),
|
|
2080
|
+
notificationId: z5.string()
|
|
1927
2081
|
}),
|
|
1928
2082
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
1929
2083
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
1930
2084
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
1931
2085
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
1932
2086
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
1933
|
-
|
|
1934
|
-
type:
|
|
1935
|
-
notificationId:
|
|
1936
|
-
inFlight:
|
|
1937
|
-
turn:
|
|
1938
|
-
idx:
|
|
1939
|
-
prompt:
|
|
1940
|
-
reply:
|
|
1941
|
-
acts:
|
|
2087
|
+
z5.object({
|
|
2088
|
+
type: z5.literal("partial"),
|
|
2089
|
+
notificationId: z5.string(),
|
|
2090
|
+
inFlight: z5.literal(true),
|
|
2091
|
+
turn: z5.object({
|
|
2092
|
+
idx: z5.number(),
|
|
2093
|
+
prompt: z5.string(),
|
|
2094
|
+
reply: z5.string(),
|
|
2095
|
+
acts: z5.array(IntentSchema).nullable().optional()
|
|
1942
2096
|
})
|
|
1943
2097
|
}),
|
|
1944
|
-
|
|
1945
|
-
type:
|
|
1946
|
-
also:
|
|
2098
|
+
z5.object({
|
|
2099
|
+
type: z5.literal("idle"),
|
|
2100
|
+
also: z5.array(RideAlongSchema).optional(),
|
|
1947
2101
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
1948
2102
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
1949
2103
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
1950
2104
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
1951
2105
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
1952
|
-
inFlight:
|
|
2106
|
+
inFlight: z5.boolean().optional()
|
|
1953
2107
|
})
|
|
1954
2108
|
]);
|
|
1955
|
-
var VoiceKeySchema =
|
|
1956
|
-
var AgendaTurnSchema =
|
|
2109
|
+
var VoiceKeySchema = z5.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2110
|
+
var AgendaTurnSchema = z5.object({
|
|
1957
2111
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
1958
2112
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
1959
2113
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
1960
2114
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
1961
2115
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
1962
2116
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
1963
|
-
id:
|
|
2117
|
+
id: z5.string().optional(),
|
|
1964
2118
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
1965
2119
|
* the planner declares duplicates instead of asking them twice. */
|
|
1966
|
-
coveredIds:
|
|
2120
|
+
coveredIds: z5.array(z5.string()).optional(),
|
|
1967
2121
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
1968
2122
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
1969
|
-
info:
|
|
1970
|
-
question:
|
|
2123
|
+
info: z5.array(z5.string().min(1)).default([]),
|
|
2124
|
+
question: z5.string().min(1).nullable(),
|
|
1971
2125
|
/** True on the one turn carrying the agent's own declared question. */
|
|
1972
|
-
asks:
|
|
2126
|
+
asks: z5.boolean().optional(),
|
|
1973
2127
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
1974
2128
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
1975
2129
|
* which route nothing. */
|
|
1976
|
-
claimId:
|
|
2130
|
+
claimId: z5.string().optional(),
|
|
1977
2131
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
1978
|
-
voice:
|
|
2132
|
+
voice: z5.string().optional(),
|
|
1979
2133
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
1980
2134
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
1981
2135
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
1982
2136
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
1983
|
-
agent:
|
|
2137
|
+
agent: z5.string().optional(),
|
|
1984
2138
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
1985
2139
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
1986
2140
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
1987
2141
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
1988
2142
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
1989
|
-
agentId:
|
|
2143
|
+
agentId: z5.string().optional(),
|
|
1990
2144
|
select: SelectShapeSchema.optional(),
|
|
1991
|
-
options:
|
|
2145
|
+
options: z5.array(OptionSchema.omit({ id: true })).optional(),
|
|
1992
2146
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
1993
2147
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
1994
2148
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -1997,27 +2151,33 @@ var AgendaTurnSchema = z4.object({
|
|
|
1997
2151
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
1998
2152
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
1999
2153
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
2000
|
-
blocking:
|
|
2154
|
+
blocking: z5.boolean().optional(),
|
|
2155
|
+
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
2156
|
+
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
2157
|
+
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
2158
|
+
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
2159
|
+
* speak, it is dropped and their words are taken like any reply. */
|
|
2160
|
+
ifSilent: z5.boolean().optional()
|
|
2001
2161
|
});
|
|
2002
2162
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
2003
|
-
var InboxItemSchema =
|
|
2004
|
-
id:
|
|
2005
|
-
tokenId:
|
|
2163
|
+
var InboxItemSchema = z5.object({
|
|
2164
|
+
id: z5.string(),
|
|
2165
|
+
tokenId: z5.string().optional(),
|
|
2006
2166
|
status: NotifyStatusSchema,
|
|
2007
2167
|
context: ContextSchema,
|
|
2008
|
-
options:
|
|
2168
|
+
options: z5.array(OptionSchema).optional(),
|
|
2009
2169
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
2010
|
-
points:
|
|
2170
|
+
points: z5.array(z5.string()).optional(),
|
|
2011
2171
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
2012
2172
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
2013
2173
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
2014
2174
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
2015
|
-
asks:
|
|
2175
|
+
asks: z5.boolean().optional(),
|
|
2016
2176
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
2017
2177
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
2018
2178
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
2019
2179
|
* which must never CLAIM stalled. */
|
|
2020
|
-
lastSeenAt:
|
|
2180
|
+
lastSeenAt: z5.string().optional(),
|
|
2021
2181
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
2022
2182
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
2023
2183
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -2027,7 +2187,7 @@ var InboxItemSchema = z4.object({
|
|
|
2027
2187
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
2028
2188
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
2029
2189
|
* `createdAt`. */
|
|
2030
|
-
agentStateAt:
|
|
2190
|
+
agentStateAt: z5.string().datetime().optional(),
|
|
2031
2191
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
2032
2192
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
2033
2193
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -2041,54 +2201,54 @@ var InboxItemSchema = z4.object({
|
|
|
2041
2201
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
2042
2202
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
2043
2203
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
2044
|
-
questions:
|
|
2045
|
-
id:
|
|
2046
|
-
entryId:
|
|
2047
|
-
title:
|
|
2048
|
-
state:
|
|
2049
|
-
answer:
|
|
2204
|
+
questions: z5.array(z5.object({
|
|
2205
|
+
id: z5.string(),
|
|
2206
|
+
entryId: z5.string(),
|
|
2207
|
+
title: z5.string(),
|
|
2208
|
+
state: z5.enum(["open", "answered"]),
|
|
2209
|
+
answer: z5.string().nullable(),
|
|
2050
2210
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
2051
2211
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
2052
2212
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
2053
2213
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
2054
|
-
agent:
|
|
2214
|
+
agent: z5.object({ tokenId: z5.string(), name: z5.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
2055
2215
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
2056
2216
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
2057
2217
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
2058
2218
|
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
2059
2219
|
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
2060
|
-
options:
|
|
2220
|
+
options: z5.array(OptionSchema).optional()
|
|
2061
2221
|
})).optional(),
|
|
2062
|
-
visuals:
|
|
2222
|
+
visuals: z5.array(VisualSchema).optional(),
|
|
2063
2223
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
2064
2224
|
* agent's suggestion, or a default silly name). */
|
|
2065
|
-
name:
|
|
2225
|
+
name: z5.string(),
|
|
2066
2226
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
2067
2227
|
voice: VoiceKeySchema.optional(),
|
|
2068
|
-
repo:
|
|
2069
|
-
branch:
|
|
2070
|
-
createdAt:
|
|
2071
|
-
snoozedUntil:
|
|
2228
|
+
repo: z5.string().optional(),
|
|
2229
|
+
branch: z5.string().optional(),
|
|
2230
|
+
createdAt: z5.string().datetime(),
|
|
2231
|
+
snoozedUntil: z5.string().datetime().optional(),
|
|
2072
2232
|
agentState: AgentStateSchema.default("idle"),
|
|
2073
2233
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
2074
2234
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
2075
2235
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
2076
|
-
turn:
|
|
2236
|
+
turn: z5.enum(["you", "agent"]).default("you"),
|
|
2077
2237
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
2078
2238
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
2079
2239
|
* error badge + Retry. */
|
|
2080
|
-
error:
|
|
2240
|
+
error: z5.string().optional(),
|
|
2081
2241
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
2082
2242
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
2083
2243
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
2084
2244
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
2085
|
-
cold:
|
|
2086
|
-
clarifies:
|
|
2245
|
+
cold: z5.string().datetime().optional(),
|
|
2246
|
+
clarifies: z5.string().optional(),
|
|
2087
2247
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
2088
2248
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
2089
2249
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
2090
2250
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
2091
|
-
onCall:
|
|
2251
|
+
onCall: z5.literal(true).optional(),
|
|
2092
2252
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
2093
2253
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
2094
2254
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -2098,31 +2258,31 @@ var InboxItemSchema = z4.object({
|
|
|
2098
2258
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
2099
2259
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
2100
2260
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
2101
|
-
ring:
|
|
2102
|
-
declinedAt:
|
|
2103
|
-
anchorAt:
|
|
2104
|
-
nextRingAt:
|
|
2105
|
-
step:
|
|
2261
|
+
ring: z5.object({
|
|
2262
|
+
declinedAt: z5.string().datetime(),
|
|
2263
|
+
anchorAt: z5.string().datetime(),
|
|
2264
|
+
nextRingAt: z5.string().datetime().nullable(),
|
|
2265
|
+
step: z5.number().int()
|
|
2106
2266
|
}).optional(),
|
|
2107
2267
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
2108
2268
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
2109
2269
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
2110
2270
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
2111
2271
|
* feedback ABOUT. */
|
|
2112
|
-
why:
|
|
2272
|
+
why: z5.object({
|
|
2113
2273
|
asked: NotifyLevelSchema,
|
|
2114
2274
|
got: NotifyLevelSchema,
|
|
2115
|
-
because:
|
|
2116
|
-
line:
|
|
2275
|
+
because: z5.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
2276
|
+
line: z5.string()
|
|
2117
2277
|
}).optional(),
|
|
2118
|
-
select:
|
|
2119
|
-
confirmStyle:
|
|
2278
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
2279
|
+
confirmStyle: z5.enum(["yesno", "approve"]).default("yesno").describe(
|
|
2120
2280
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
2121
2281
|
),
|
|
2122
2282
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
2123
2283
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
2124
2284
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
2125
|
-
blocking:
|
|
2285
|
+
blocking: z5.boolean().default(false),
|
|
2126
2286
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
2127
2287
|
answer: UserAnswerSchema.optional(),
|
|
2128
2288
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -2130,38 +2290,49 @@ var InboxItemSchema = z4.object({
|
|
|
2130
2290
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
2131
2291
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
2132
2292
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
2133
|
-
communication:
|
|
2134
|
-
deliveryId:
|
|
2135
|
-
kind:
|
|
2136
|
-
entryId:
|
|
2137
|
-
goalIds:
|
|
2138
|
-
decisionNeedId:
|
|
2139
|
-
sealed:
|
|
2140
|
-
goalState:
|
|
2293
|
+
communication: z5.object({
|
|
2294
|
+
deliveryId: z5.string(),
|
|
2295
|
+
kind: z5.enum(["notification", "call"]),
|
|
2296
|
+
entryId: z5.string(),
|
|
2297
|
+
goalIds: z5.array(z5.string()),
|
|
2298
|
+
decisionNeedId: z5.string().optional(),
|
|
2299
|
+
sealed: z5.boolean(),
|
|
2300
|
+
goalState: z5.string().optional(),
|
|
2141
2301
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
2142
2302
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
2143
|
-
goalTitle:
|
|
2144
|
-
}).optional()
|
|
2303
|
+
goalTitle: z5.string().optional()
|
|
2304
|
+
}).optional(),
|
|
2305
|
+
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
2306
|
+
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
2307
|
+
* how the incremental read knows a card has not moved: the phone echoes back the revs it holds
|
|
2308
|
+
* (`POST /api/inbox/changes`) and is sent only the cards whose rev differs.
|
|
2309
|
+
*
|
|
2310
|
+
* THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK — the same mechanism as `QueueItem.rev`
|
|
2311
|
+
* (#2928) and an entry's (#3018), for the same reason: a card shows facts no `updated_at` of its
|
|
2312
|
+
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
2313
|
+
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
2314
|
+
* with no rev is simply always re-sent. */
|
|
2315
|
+
rev: z5.string().optional()
|
|
2145
2316
|
});
|
|
2146
2317
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
2147
|
-
var PushTokenSchema =
|
|
2148
|
-
voipToken:
|
|
2149
|
-
alertToken:
|
|
2150
|
-
fcmToken:
|
|
2151
|
-
platform:
|
|
2318
|
+
var PushTokenSchema = z5.object({
|
|
2319
|
+
voipToken: z5.string().min(1).optional(),
|
|
2320
|
+
alertToken: z5.string().min(1).optional(),
|
|
2321
|
+
fcmToken: z5.string().min(1).optional(),
|
|
2322
|
+
platform: z5.enum(["ios", "android"])
|
|
2152
2323
|
}).superRefine((v, ctx) => {
|
|
2153
2324
|
if (v.platform !== "ios") return;
|
|
2154
2325
|
for (const field of ["voipToken", "alertToken"]) {
|
|
2155
2326
|
const token = v[field];
|
|
2156
2327
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
2157
2328
|
ctx.addIssue({
|
|
2158
|
-
code:
|
|
2329
|
+
code: z5.ZodIssueCode.custom,
|
|
2159
2330
|
path: [field],
|
|
2160
2331
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
2161
2332
|
});
|
|
2162
2333
|
}
|
|
2163
2334
|
});
|
|
2164
|
-
var MissedCallSchema =
|
|
2335
|
+
var MissedCallSchema = z5.enum([
|
|
2165
2336
|
"retry_10m",
|
|
2166
2337
|
"retry_30m",
|
|
2167
2338
|
"retry_60m",
|
|
@@ -2173,31 +2344,31 @@ var MissedCallSchema = z4.enum([
|
|
|
2173
2344
|
]);
|
|
2174
2345
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
2175
2346
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
2176
|
-
var BrokerTuningSchema =
|
|
2347
|
+
var BrokerTuningSchema = z5.object({
|
|
2177
2348
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
2178
|
-
ackVerbosity:
|
|
2349
|
+
ackVerbosity: z5.enum(["normal", "none"]).optional(),
|
|
2179
2350
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
2180
2351
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
2181
|
-
clarifyEagerness:
|
|
2352
|
+
clarifyEagerness: z5.enum(["low", "normal", "high"]).optional(),
|
|
2182
2353
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
2183
|
-
phrasebook:
|
|
2354
|
+
phrasebook: z5.array(z5.object({ say: z5.string().min(1).max(60), mean: z5.string().min(1).max(120) })).max(24).optional(),
|
|
2184
2355
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
2185
2356
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
2186
2357
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
2187
2358
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
2188
2359
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
2189
|
-
language:
|
|
2360
|
+
language: z5.enum(["en", "es"]).optional()
|
|
2190
2361
|
});
|
|
2191
|
-
var UserSettingsSchema =
|
|
2192
|
-
permissions:
|
|
2193
|
-
call:
|
|
2194
|
-
banner:
|
|
2195
|
-
push:
|
|
2362
|
+
var UserSettingsSchema = z5.object({
|
|
2363
|
+
permissions: z5.object({
|
|
2364
|
+
call: z5.boolean(),
|
|
2365
|
+
banner: z5.boolean(),
|
|
2366
|
+
push: z5.boolean()
|
|
2196
2367
|
}),
|
|
2197
2368
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
2198
2369
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
2199
2370
|
* what people read (owner, 2026-09-30). */
|
|
2200
|
-
sessionMode:
|
|
2371
|
+
sessionMode: z5.enum(["default", "all_calls", "silent"]),
|
|
2201
2372
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
2202
2373
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
2203
2374
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -2206,7 +2377,7 @@ var UserSettingsSchema = z4.object({
|
|
|
2206
2377
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
2207
2378
|
* next dead field for the same reason. */
|
|
2208
2379
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
2209
|
-
improveConsent:
|
|
2380
|
+
improveConsent: z5.boolean(),
|
|
2210
2381
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
2211
2382
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
2212
2383
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -2214,17 +2385,17 @@ var UserSettingsSchema = z4.object({
|
|
|
2214
2385
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
2215
2386
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
2216
2387
|
* write, 'hosted' on read (see store.ts). */
|
|
2217
|
-
voiceMode:
|
|
2388
|
+
voiceMode: z5.enum(["hosted", "on_device"]).optional(),
|
|
2218
2389
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
2219
2390
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
2220
2391
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
2221
2392
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
2222
2393
|
* read (see store.ts). */
|
|
2223
|
-
talk:
|
|
2394
|
+
talk: z5.boolean().optional(),
|
|
2224
2395
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
2225
2396
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
2226
2397
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
2227
|
-
callDiagnostics:
|
|
2398
|
+
callDiagnostics: z5.boolean().optional(),
|
|
2228
2399
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
2229
2400
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
2230
2401
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -2232,7 +2403,7 @@ var UserSettingsSchema = z4.object({
|
|
|
2232
2403
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
2233
2404
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
2234
2405
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
2235
|
-
voiceTuning:
|
|
2406
|
+
voiceTuning: z5.record(z5.string(), z5.union([z5.number(), z5.string()])).optional(),
|
|
2236
2407
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
2237
2408
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
2238
2409
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -2241,53 +2412,53 @@ var UserSettingsSchema = z4.object({
|
|
|
2241
2412
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
2242
2413
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
2243
2414
|
* call — honest about what we know. */
|
|
2244
|
-
timezone:
|
|
2415
|
+
timezone: z5.string().min(1).max(64).optional(),
|
|
2245
2416
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
2246
2417
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
2247
2418
|
broker: BrokerTuningSchema.optional()
|
|
2248
2419
|
});
|
|
2249
|
-
var HistoryWorkSchema =
|
|
2250
|
-
id:
|
|
2251
|
-
title:
|
|
2252
|
-
state:
|
|
2420
|
+
var HistoryWorkSchema = z5.object({
|
|
2421
|
+
id: z5.string(),
|
|
2422
|
+
title: z5.string(),
|
|
2423
|
+
state: z5.enum(["done", "cancelled"]),
|
|
2253
2424
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
2254
|
-
assignee:
|
|
2425
|
+
assignee: z5.string()
|
|
2255
2426
|
});
|
|
2256
|
-
var HistoryEntrySchema =
|
|
2257
|
-
|
|
2258
|
-
|
|
2427
|
+
var HistoryEntrySchema = z5.union([
|
|
2428
|
+
z5.object({ at: z5.string(), card: InboxItemSchema }),
|
|
2429
|
+
z5.object({ at: z5.string(), work: HistoryWorkSchema })
|
|
2259
2430
|
]);
|
|
2260
|
-
var HistoryPageSchema =
|
|
2261
|
-
entries:
|
|
2262
|
-
next:
|
|
2431
|
+
var HistoryPageSchema = z5.object({
|
|
2432
|
+
entries: z5.array(HistoryEntrySchema),
|
|
2433
|
+
next: z5.string().nullable()
|
|
2263
2434
|
});
|
|
2264
2435
|
var ACTIVITY_LINES = 2;
|
|
2265
2436
|
var ACTIVITY_LINE_MAX = 80;
|
|
2266
|
-
var AgentActivitySchema =
|
|
2437
|
+
var AgentActivitySchema = z5.object({
|
|
2267
2438
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
2268
|
-
lines:
|
|
2439
|
+
lines: z5.array(z5.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
2269
2440
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
2270
2441
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
2271
|
-
at:
|
|
2442
|
+
at: z5.string().datetime()
|
|
2272
2443
|
});
|
|
2273
|
-
var ConnectionSummarySchema =
|
|
2444
|
+
var ConnectionSummarySchema = z5.object({
|
|
2274
2445
|
/** The connection = the agent's token id (used to address a request). */
|
|
2275
|
-
id:
|
|
2446
|
+
id: z5.string(),
|
|
2276
2447
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
2277
2448
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
2278
2449
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
2279
2450
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
2280
|
-
kind:
|
|
2451
|
+
kind: z5.enum(["device", "agent"]).optional(),
|
|
2281
2452
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
2282
2453
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
2283
2454
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
2284
|
-
mintedByDevice:
|
|
2285
|
-
device:
|
|
2455
|
+
mintedByDevice: z5.string().nullable().optional(),
|
|
2456
|
+
device: z5.string().nullable(),
|
|
2286
2457
|
/** The agent's display name (the single pairing name). */
|
|
2287
|
-
name:
|
|
2458
|
+
name: z5.string(),
|
|
2288
2459
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
2289
2460
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
2290
|
-
provider:
|
|
2461
|
+
provider: z5.string().nullable(),
|
|
2291
2462
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2292
2463
|
voice: VoiceKeySchema.nullable(),
|
|
2293
2464
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -2298,34 +2469,34 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2298
2469
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
2299
2470
|
* set once must not overrule a rule they set about one agent. */
|
|
2300
2471
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
2301
|
-
createdAt:
|
|
2472
|
+
createdAt: z5.string().datetime(),
|
|
2302
2473
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2303
2474
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
2304
|
-
lastContactAt:
|
|
2475
|
+
lastContactAt: z5.string().datetime().nullable(),
|
|
2305
2476
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
2306
2477
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
2307
|
-
lastSeenAt:
|
|
2478
|
+
lastSeenAt: z5.string().datetime().nullable().optional(),
|
|
2308
2479
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
2309
2480
|
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
2310
2481
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
2311
|
-
lastWorkedAt:
|
|
2482
|
+
lastWorkedAt: z5.string().datetime().nullable().optional(),
|
|
2312
2483
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
2313
2484
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
2314
|
-
oldestReadyAt:
|
|
2485
|
+
oldestReadyAt: z5.string().datetime().nullable().optional(),
|
|
2315
2486
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
2316
2487
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
2317
2488
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
2318
|
-
runtime:
|
|
2489
|
+
runtime: z5.object({
|
|
2319
2490
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
2320
2491
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
2321
|
-
version:
|
|
2322
|
-
harnesses:
|
|
2323
|
-
workspaces:
|
|
2492
|
+
version: z5.string().optional(),
|
|
2493
|
+
harnesses: z5.array(z5.object({ name: z5.string(), label: z5.string(), status: z5.string() })).optional(),
|
|
2494
|
+
workspaces: z5.array(z5.string()).optional(),
|
|
2324
2495
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
2325
2496
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
2326
2497
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
2327
2498
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
2328
|
-
repos:
|
|
2499
|
+
repos: z5.array(z5.object({ path: z5.string(), remote: z5.string() })).optional()
|
|
2329
2500
|
}).optional(),
|
|
2330
2501
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
2331
2502
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -2334,173 +2505,156 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2334
2505
|
activity: AgentActivitySchema.optional(),
|
|
2335
2506
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
2336
2507
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
2337
|
-
managed:
|
|
2508
|
+
managed: z5.boolean()
|
|
2338
2509
|
});
|
|
2339
|
-
var LedgerItemSchema =
|
|
2340
|
-
var AgentLedgerSchema =
|
|
2510
|
+
var LedgerItemSchema = z5.object({ id: z5.string(), parentId: z5.string(), title: z5.string(), createdAt: z5.string() });
|
|
2511
|
+
var AgentLedgerSchema = z5.object({
|
|
2341
2512
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
2342
|
-
agent:
|
|
2513
|
+
agent: z5.object({ id: z5.string(), name: z5.string().nullable(), revokedAt: z5.string().nullable() }),
|
|
2343
2514
|
/** Its own questions you have not answered. */
|
|
2344
|
-
asks:
|
|
2515
|
+
asks: z5.array(LedgerItemSchema),
|
|
2345
2516
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
2346
|
-
answered:
|
|
2517
|
+
answered: z5.array(LedgerItemSchema),
|
|
2347
2518
|
/** Requests you sent it that it never took. */
|
|
2348
|
-
requests:
|
|
2349
|
-
goals:
|
|
2350
|
-
callbacks:
|
|
2519
|
+
requests: z5.array(LedgerItemSchema),
|
|
2520
|
+
goals: z5.array(z5.object({ id: z5.string(), outcome: z5.string(), state: z5.string() })),
|
|
2521
|
+
callbacks: z5.array(z5.object({ id: z5.string(), parentId: z5.string(), trigger: z5.string(), note: z5.string(), dueAt: z5.string().nullable() }))
|
|
2351
2522
|
});
|
|
2352
|
-
var ReassignResultSchema =
|
|
2353
|
-
moved:
|
|
2354
|
-
parentId:
|
|
2523
|
+
var ReassignResultSchema = z5.object({
|
|
2524
|
+
moved: z5.object({ asks: z5.number(), answered: z5.number(), requests: z5.number(), goals: z5.number(), callbacks: z5.number() }),
|
|
2525
|
+
parentId: z5.string().nullable()
|
|
2355
2526
|
});
|
|
2356
|
-
var
|
|
2357
|
-
var
|
|
2358
|
-
id:
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
/** The
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
/**
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
* break is visible instead of a pin silently disappearing. */
|
|
2373
|
-
pinBroken: z4.boolean(),
|
|
2374
|
-
/** When the ruling was distilled. */
|
|
2375
|
-
learnedAt: z4.string(),
|
|
2376
|
-
/** Last time it answered an ask. Null = never fired. */
|
|
2377
|
-
lastUsedAt: z4.string().nullable(),
|
|
2378
|
-
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
2379
|
-
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
2380
|
-
usedCount: z4.number(),
|
|
2381
|
-
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
2382
|
-
confirms: z4.number(),
|
|
2383
|
-
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
2384
|
-
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
2385
|
-
counters: z4.number(),
|
|
2386
|
-
/** The agent that asked the question this move came from, when known. Null for a move
|
|
2387
|
-
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
2388
|
-
learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
|
|
2527
|
+
var LessonStateSchema = z5.enum(["active", "proposed", "retired"]);
|
|
2528
|
+
var LessonViewSchema = z5.object({
|
|
2529
|
+
id: z5.string(),
|
|
2530
|
+
text: z5.string(),
|
|
2531
|
+
state: LessonStateSchema,
|
|
2532
|
+
/** The Goal it is scoped to; null = the whole account. */
|
|
2533
|
+
scopeGoalId: z5.string().nullable(),
|
|
2534
|
+
goalTitle: z5.string().nullable(),
|
|
2535
|
+
version: z5.number(),
|
|
2536
|
+
pinned: z5.boolean(),
|
|
2537
|
+
/** When the person last wrote its text themselves. */
|
|
2538
|
+
editedAt: z5.string().nullable(),
|
|
2539
|
+
createdAt: z5.string(),
|
|
2540
|
+
updatedAt: z5.string(),
|
|
2541
|
+
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
2542
|
+
sources: z5.array(z5.object({ entryId: z5.string(), words: z5.string().nullable(), at: z5.string() }))
|
|
2389
2543
|
});
|
|
2390
|
-
var QueueQuestionSchema =
|
|
2544
|
+
var QueueQuestionSchema = z5.object({
|
|
2391
2545
|
/** The decision need's id — what an answer is accepted against. */
|
|
2392
|
-
id:
|
|
2546
|
+
id: z5.string(),
|
|
2393
2547
|
/** The words that were asked, from the request Entry that asked them. */
|
|
2394
|
-
question:
|
|
2548
|
+
question: z5.string(),
|
|
2395
2549
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
2396
2550
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
2397
2551
|
* can answer. */
|
|
2398
|
-
deliveryId:
|
|
2552
|
+
deliveryId: z5.string().nullable().default(null),
|
|
2399
2553
|
/** The Entry the ruling is about. */
|
|
2400
|
-
aboutId:
|
|
2554
|
+
aboutId: z5.string().nullable().default(null),
|
|
2401
2555
|
/** Empty for a free-text question. */
|
|
2402
|
-
options:
|
|
2403
|
-
select:
|
|
2404
|
-
askedAt:
|
|
2556
|
+
options: z5.array(OptionSchema).default([]),
|
|
2557
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
2558
|
+
askedAt: z5.string(),
|
|
2405
2559
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
2406
|
-
answeredAt:
|
|
2560
|
+
answeredAt: z5.string().nullable().default(null),
|
|
2407
2561
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
2408
2562
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
2409
2563
|
* for a settled question whose reply carried nothing readable. */
|
|
2410
|
-
answer:
|
|
2564
|
+
answer: z5.string().nullable().default(null),
|
|
2411
2565
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
2412
2566
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
2413
2567
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
2414
2568
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
2415
2569
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
2416
2570
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
2417
|
-
goalId:
|
|
2571
|
+
goalId: z5.string(),
|
|
2418
2572
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
2419
2573
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
2420
2574
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
2421
2575
|
* gate at all. */
|
|
2422
|
-
blocked:
|
|
2576
|
+
blocked: z5.boolean().default(false)
|
|
2423
2577
|
});
|
|
2424
|
-
var QueueReplySchema =
|
|
2578
|
+
var QueueReplySchema = z5.object({
|
|
2425
2579
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
2426
2580
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
2427
2581
|
* walk can name it in its zoom. */
|
|
2428
|
-
id:
|
|
2582
|
+
id: z5.string(),
|
|
2429
2583
|
/** The Goal the note is on. */
|
|
2430
|
-
goalId:
|
|
2584
|
+
goalId: z5.string(),
|
|
2431
2585
|
/** What the note said. */
|
|
2432
|
-
note:
|
|
2586
|
+
note: z5.string(),
|
|
2433
2587
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
2434
|
-
deliveryId:
|
|
2435
|
-
requestEntryId:
|
|
2436
|
-
askedAt:
|
|
2588
|
+
deliveryId: z5.string(),
|
|
2589
|
+
requestEntryId: z5.string(),
|
|
2590
|
+
askedAt: z5.string(),
|
|
2437
2591
|
/** When the person last replied — the window's start. */
|
|
2438
|
-
repliedAt:
|
|
2592
|
+
repliedAt: z5.string(),
|
|
2439
2593
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
2440
|
-
reply:
|
|
2594
|
+
reply: z5.string().nullable()
|
|
2441
2595
|
});
|
|
2442
|
-
var QueueItemSchema =
|
|
2443
|
-
id:
|
|
2596
|
+
var QueueItemSchema = z5.object({
|
|
2597
|
+
id: z5.string(),
|
|
2444
2598
|
/** One-line headline — the first sentence of the outcome. */
|
|
2445
|
-
title:
|
|
2599
|
+
title: z5.string(),
|
|
2446
2600
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
2447
|
-
intent:
|
|
2601
|
+
intent: z5.string(),
|
|
2448
2602
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
2449
|
-
state:
|
|
2603
|
+
state: z5.string(),
|
|
2450
2604
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
2451
|
-
assignee:
|
|
2605
|
+
assignee: z5.string().nullable().default(null),
|
|
2452
2606
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
2453
|
-
progress:
|
|
2607
|
+
progress: z5.string().nullable().default(null),
|
|
2454
2608
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
2455
2609
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
2456
|
-
progressLine:
|
|
2457
|
-
reviewPending:
|
|
2458
|
-
dueAt:
|
|
2610
|
+
progressLine: z5.string().nullable().optional(),
|
|
2611
|
+
reviewPending: z5.boolean().default(false),
|
|
2612
|
+
dueAt: z5.string().nullable().default(null),
|
|
2459
2613
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
2460
2614
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
2461
|
-
finishedAt:
|
|
2615
|
+
finishedAt: z5.string().nullable().optional(),
|
|
2462
2616
|
/** The Goal this one was opened under; null at the root. */
|
|
2463
|
-
parentGoalId:
|
|
2617
|
+
parentGoalId: z5.string().nullable().default(null),
|
|
2464
2618
|
/** Goals opened under this one — only those the same list holds. */
|
|
2465
|
-
childGoalIds:
|
|
2619
|
+
childGoalIds: z5.array(z5.string()).default([]),
|
|
2466
2620
|
/** Goals this one waits on (start or finish gates). */
|
|
2467
|
-
dependencyGoalIds:
|
|
2621
|
+
dependencyGoalIds: z5.array(z5.string()).default([]),
|
|
2468
2622
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
2469
|
-
blocked:
|
|
2623
|
+
blocked: z5.boolean().default(false),
|
|
2470
2624
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
2471
2625
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
2472
2626
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
2473
2627
|
* question is opened. */
|
|
2474
|
-
questions:
|
|
2628
|
+
questions: z5.array(QueueQuestionSchema).default([]),
|
|
2475
2629
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
2476
2630
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
2477
2631
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
2478
2632
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
2479
|
-
asked:
|
|
2480
|
-
answered:
|
|
2633
|
+
asked: z5.number().optional(),
|
|
2634
|
+
answered: z5.number().optional(),
|
|
2481
2635
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
2482
2636
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
2483
2637
|
* not spell an empty list. */
|
|
2484
|
-
replies:
|
|
2638
|
+
replies: z5.array(QueueReplySchema).optional(),
|
|
2485
2639
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
2486
|
-
repo:
|
|
2487
|
-
createdAt:
|
|
2488
|
-
updatedAt:
|
|
2640
|
+
repo: z5.string().nullable().optional(),
|
|
2641
|
+
createdAt: z5.string(),
|
|
2642
|
+
updatedAt: z5.string().nullable().default(null),
|
|
2489
2643
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
2490
2644
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
2491
2645
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
2492
|
-
lastProgressAt:
|
|
2646
|
+
lastProgressAt: z5.string().nullable().optional(),
|
|
2493
2647
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
2494
2648
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
2495
2649
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
2496
2650
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
2497
2651
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
2498
2652
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
2499
|
-
latest:
|
|
2500
|
-
from:
|
|
2501
|
-
said:
|
|
2502
|
-
at:
|
|
2503
|
-
entryId:
|
|
2653
|
+
latest: z5.object({
|
|
2654
|
+
from: z5.enum(["person", "agent"]),
|
|
2655
|
+
said: z5.string(),
|
|
2656
|
+
at: z5.string(),
|
|
2657
|
+
entryId: z5.string()
|
|
2504
2658
|
}).nullable().optional(),
|
|
2505
2659
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
2506
2660
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -2513,36 +2667,51 @@ var QueueItemSchema = z4.object({
|
|
|
2513
2667
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
2514
2668
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
2515
2669
|
* Entry with no `about_id` is in neither. */
|
|
2516
|
-
lastPersonAt:
|
|
2670
|
+
lastPersonAt: z5.string().nullable().optional(),
|
|
2671
|
+
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
2672
|
+
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
2673
|
+
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
2674
|
+
* (`POST /api/goals/changes`) and is sent only the rows whose rev differs.
|
|
2675
|
+
*
|
|
2676
|
+
* IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
|
|
2677
|
+
* writes change which field — the comparison is over the bytes the phone is holding, so a fact
|
|
2678
|
+
* the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
|
|
2679
|
+
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
2680
|
+
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
2681
|
+
rev: z5.string().optional()
|
|
2682
|
+
});
|
|
2683
|
+
var QueueDeltaSchema = z5.object({
|
|
2684
|
+
ids: z5.array(z5.string()),
|
|
2685
|
+
items: z5.array(QueueItemSchema)
|
|
2517
2686
|
});
|
|
2518
2687
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
2519
|
-
var NoteSourceSchema =
|
|
2520
|
-
var NoteStatusSchema =
|
|
2521
|
-
var NoteRepeatSchema =
|
|
2522
|
-
var DecisionSchema =
|
|
2523
|
-
id:
|
|
2688
|
+
var NoteSourceSchema = z5.enum(["app", "call"]);
|
|
2689
|
+
var NoteStatusSchema = z5.enum(["open", "assigned", "in_progress", "done"]);
|
|
2690
|
+
var NoteRepeatSchema = z5.enum(["once", "until_done"]);
|
|
2691
|
+
var DecisionSchema = z5.object({
|
|
2692
|
+
id: z5.string(),
|
|
2524
2693
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
2525
2694
|
* extensibility seam — any conversation can accrue decisions). */
|
|
2526
|
-
noteId:
|
|
2695
|
+
noteId: z5.string().nullable(),
|
|
2527
2696
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
2528
|
-
question:
|
|
2697
|
+
question: z5.string(),
|
|
2529
2698
|
/** The user's ruling; null while the question is open. */
|
|
2530
|
-
answer:
|
|
2531
|
-
decidedAt:
|
|
2532
|
-
createdAt:
|
|
2699
|
+
answer: z5.string().nullable(),
|
|
2700
|
+
decidedAt: z5.string().nullable(),
|
|
2701
|
+
createdAt: z5.string()
|
|
2533
2702
|
});
|
|
2534
|
-
var NoteSchema =
|
|
2535
|
-
id:
|
|
2703
|
+
var NoteSchema = z5.object({
|
|
2704
|
+
id: z5.string(),
|
|
2536
2705
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
2537
|
-
title:
|
|
2706
|
+
title: z5.string(),
|
|
2538
2707
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
2539
|
-
intent:
|
|
2708
|
+
intent: z5.string(),
|
|
2540
2709
|
source: NoteSourceSchema,
|
|
2541
2710
|
status: NoteStatusSchema,
|
|
2542
2711
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
2543
|
-
assignee:
|
|
2712
|
+
assignee: z5.string().nullable(),
|
|
2544
2713
|
/** The request thread minted at assignment; null until assigned. */
|
|
2545
|
-
parentId:
|
|
2714
|
+
parentId: z5.string().nullable(),
|
|
2546
2715
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
2547
2716
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
2548
2717
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -2551,149 +2720,154 @@ var NoteSchema = z4.object({
|
|
|
2551
2720
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
2552
2721
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
2553
2722
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
2554
|
-
dueAt:
|
|
2723
|
+
dueAt: z5.string().nullable().default(null),
|
|
2555
2724
|
repeat: NoteRepeatSchema.default("once"),
|
|
2556
2725
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
2557
|
-
rides:
|
|
2558
|
-
lastRideAt:
|
|
2559
|
-
createdAt:
|
|
2726
|
+
rides: z5.number().int().default(0),
|
|
2727
|
+
lastRideAt: z5.string().nullable().default(null),
|
|
2728
|
+
createdAt: z5.string()
|
|
2560
2729
|
});
|
|
2561
|
-
var TriageItemSchema =
|
|
2562
|
-
noteId:
|
|
2730
|
+
var TriageItemSchema = z5.object({
|
|
2731
|
+
noteId: z5.string(),
|
|
2563
2732
|
/** The note's headline at run time. */
|
|
2564
|
-
title:
|
|
2733
|
+
title: z5.string(),
|
|
2565
2734
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
2566
2735
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
2567
2736
|
* Never a model's reasoning transcript, never an id. */
|
|
2568
|
-
why:
|
|
2737
|
+
why: z5.string()
|
|
2569
2738
|
});
|
|
2570
|
-
var TriageAssignmentSchema =
|
|
2739
|
+
var TriageAssignmentSchema = z5.object({
|
|
2571
2740
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
2572
|
-
agent:
|
|
2741
|
+
agent: z5.string(),
|
|
2573
2742
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
2574
2743
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
2575
|
-
agentName:
|
|
2576
|
-
notes:
|
|
2744
|
+
agentName: z5.string(),
|
|
2745
|
+
notes: z5.array(TriageItemSchema)
|
|
2577
2746
|
});
|
|
2578
|
-
var TriageStatusSchema =
|
|
2579
|
-
var SubmitTriageSchema =
|
|
2747
|
+
var TriageStatusSchema = z5.enum(["open", "superseded", "dismissed"]);
|
|
2748
|
+
var SubmitTriageSchema = z5.object({
|
|
2580
2749
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
2581
2750
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
2582
2751
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
2583
2752
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
2584
|
-
provider:
|
|
2753
|
+
provider: z5.string().min(1).max(60),
|
|
2585
2754
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
2586
|
-
model:
|
|
2755
|
+
model: z5.string().max(200).nullable().optional(),
|
|
2587
2756
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
2588
2757
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
2589
|
-
reviewed:
|
|
2590
|
-
close:
|
|
2591
|
-
stale:
|
|
2592
|
-
assign:
|
|
2758
|
+
reviewed: z5.number().int().min(0).max(1e4).default(0),
|
|
2759
|
+
close: z5.array(TriageItemSchema).max(200).default([]),
|
|
2760
|
+
stale: z5.array(TriageItemSchema).max(200).default([]),
|
|
2761
|
+
assign: z5.array(TriageAssignmentSchema).max(50).default([])
|
|
2593
2762
|
});
|
|
2594
2763
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
2595
|
-
id:
|
|
2596
|
-
runAt:
|
|
2764
|
+
id: z5.string(),
|
|
2765
|
+
runAt: z5.string(),
|
|
2597
2766
|
status: TriageStatusSchema,
|
|
2598
|
-
model:
|
|
2767
|
+
model: z5.string().nullable().default(null)
|
|
2599
2768
|
});
|
|
2600
|
-
var AcceptTriageSchema =
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
group:
|
|
2605
|
-
agent:
|
|
2606
|
-
noteIds:
|
|
2769
|
+
var AcceptTriageSchema = z5.discriminatedUnion("group", [
|
|
2770
|
+
z5.object({ group: z5.literal("close"), noteIds: z5.array(z5.string()).max(200).optional() }),
|
|
2771
|
+
z5.object({ group: z5.literal("stale"), noteIds: z5.array(z5.string()).max(200).optional() }),
|
|
2772
|
+
z5.object({
|
|
2773
|
+
group: z5.literal("assign"),
|
|
2774
|
+
agent: z5.string().min(1),
|
|
2775
|
+
noteIds: z5.array(z5.string()).max(200).optional()
|
|
2607
2776
|
})
|
|
2608
2777
|
]);
|
|
2609
|
-
var AcceptTriageResultSchema =
|
|
2610
|
-
accepted:
|
|
2611
|
-
failed:
|
|
2778
|
+
var AcceptTriageResultSchema = z5.object({
|
|
2779
|
+
accepted: z5.array(z5.string()),
|
|
2780
|
+
failed: z5.array(z5.object({ noteId: z5.string(), reason: z5.string() }))
|
|
2612
2781
|
});
|
|
2613
|
-
var DeliveryModeSchema =
|
|
2782
|
+
var DeliveryModeSchema = z5.enum(["poll", "self_hosted"]);
|
|
2614
2783
|
var WAKE_EVENT = "wake";
|
|
2615
2784
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
2616
|
-
var RegisterDeliverySchema =
|
|
2617
|
-
var OAuthStartSchema =
|
|
2618
|
-
provider:
|
|
2619
|
-
returnTo:
|
|
2785
|
+
var RegisterDeliverySchema = z5.object({ mode: DeliveryModeSchema });
|
|
2786
|
+
var OAuthStartSchema = z5.object({
|
|
2787
|
+
provider: z5.enum(["cma"]),
|
|
2788
|
+
returnTo: z5.string().min(1)
|
|
2620
2789
|
});
|
|
2621
|
-
var DeliveryConfigSchema =
|
|
2622
|
-
tokenId:
|
|
2790
|
+
var DeliveryConfigSchema = z5.object({
|
|
2791
|
+
tokenId: z5.string(),
|
|
2623
2792
|
mode: DeliveryModeSchema,
|
|
2624
2793
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
2625
2794
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
2626
2795
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
2627
|
-
realtime:
|
|
2796
|
+
realtime: z5.object({ url: z5.string(), anonKey: z5.string() }).nullable()
|
|
2628
2797
|
});
|
|
2629
|
-
var HostDecisionSchema =
|
|
2798
|
+
var HostDecisionSchema = z5.object({
|
|
2630
2799
|
/** The agent's token id: the row's `recipient`. */
|
|
2631
|
-
agent:
|
|
2632
|
-
decision:
|
|
2800
|
+
agent: z5.string().uuid(),
|
|
2801
|
+
decision: z5.enum(["stood_back", "took_over"]),
|
|
2633
2802
|
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
2634
|
-
goalId:
|
|
2803
|
+
goalId: z5.string().uuid().nullable().optional(),
|
|
2635
2804
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
2636
|
-
seenAt:
|
|
2805
|
+
seenAt: z5.string().datetime().nullable().optional(),
|
|
2637
2806
|
/** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
|
|
2638
|
-
since:
|
|
2807
|
+
since: z5.string().datetime().nullable().optional(),
|
|
2639
2808
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
2640
|
-
said:
|
|
2809
|
+
said: z5.string().max(300).optional()
|
|
2641
2810
|
});
|
|
2642
|
-
var WakeNudgeSchema =
|
|
2643
|
-
kind:
|
|
2644
|
-
notificationId:
|
|
2645
|
-
parentId:
|
|
2811
|
+
var WakeNudgeSchema = z5.object({
|
|
2812
|
+
kind: z5.enum(["reply", "request", "callback"]),
|
|
2813
|
+
notificationId: z5.string().optional(),
|
|
2814
|
+
parentId: z5.string()
|
|
2646
2815
|
});
|
|
2647
|
-
var PairingStatusSchema =
|
|
2648
|
-
var DeviceCodeSchema =
|
|
2649
|
-
device_code:
|
|
2650
|
-
user_code:
|
|
2651
|
-
verification_uri:
|
|
2652
|
-
verification_uri_complete:
|
|
2653
|
-
interval:
|
|
2654
|
-
expires_in:
|
|
2816
|
+
var PairingStatusSchema = z5.enum(["pending", "approved", "denied", "expired"]);
|
|
2817
|
+
var DeviceCodeSchema = z5.object({
|
|
2818
|
+
device_code: z5.string(),
|
|
2819
|
+
user_code: z5.string(),
|
|
2820
|
+
verification_uri: z5.string().url(),
|
|
2821
|
+
verification_uri_complete: z5.string().url(),
|
|
2822
|
+
interval: z5.number(),
|
|
2823
|
+
expires_in: z5.number()
|
|
2655
2824
|
});
|
|
2656
|
-
var DeviceInfoSchema =
|
|
2657
|
-
code:
|
|
2825
|
+
var DeviceInfoSchema = z5.object({
|
|
2826
|
+
code: z5.string(),
|
|
2658
2827
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
2659
2828
|
* pre-filling the name field the human can edit. */
|
|
2660
|
-
name:
|
|
2829
|
+
name: z5.string(),
|
|
2661
2830
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
2662
2831
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
2663
2832
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
2664
2833
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
2665
|
-
agent:
|
|
2666
|
-
device:
|
|
2834
|
+
agent: z5.string().optional(),
|
|
2835
|
+
device: z5.string().nullable(),
|
|
2667
2836
|
status: PairingStatusSchema
|
|
2668
2837
|
});
|
|
2669
|
-
var DeviceTokenSchema =
|
|
2670
|
-
access_token:
|
|
2838
|
+
var DeviceTokenSchema = z5.object({
|
|
2839
|
+
access_token: z5.string(),
|
|
2671
2840
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
2672
2841
|
* a default silly name). */
|
|
2673
|
-
name:
|
|
2674
|
-
device:
|
|
2842
|
+
name: z5.string(),
|
|
2843
|
+
device: z5.string().nullable(),
|
|
2675
2844
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
2676
2845
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
2677
|
-
voice:
|
|
2846
|
+
voice: z5.string().nullable().optional(),
|
|
2678
2847
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
2679
2848
|
* that survives a rename. Cached by the host's identity beat. */
|
|
2680
|
-
token_id:
|
|
2849
|
+
token_id: z5.string().nullable().optional(),
|
|
2681
2850
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
2682
2851
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
2683
2852
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
2684
2853
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
2685
|
-
workspace:
|
|
2854
|
+
workspace: z5.string().nullable().optional(),
|
|
2686
2855
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
2687
|
-
harness:
|
|
2688
|
-
session_id:
|
|
2689
|
-
|
|
2856
|
+
harness: z5.enum(["claude", "codex", "agy"]).optional(),
|
|
2857
|
+
session_id: z5.string().uuid().optional(),
|
|
2858
|
+
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
2859
|
+
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
2860
|
+
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
2861
|
+
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
2862
|
+
full_session: z5.string().optional(),
|
|
2863
|
+
uik_pub: z5.string().nullable().optional()
|
|
2690
2864
|
});
|
|
2691
|
-
var SupportRequestSchema =
|
|
2692
|
-
email:
|
|
2693
|
-
message:
|
|
2694
|
-
name:
|
|
2865
|
+
var SupportRequestSchema = z5.object({
|
|
2866
|
+
email: z5.string().email().max(320),
|
|
2867
|
+
message: z5.string().trim().min(1).max(5e3),
|
|
2868
|
+
name: z5.string().trim().max(120).optional()
|
|
2695
2869
|
});
|
|
2696
|
-
var NotificationFeedbackKindSchema =
|
|
2870
|
+
var NotificationFeedbackKindSchema = z5.enum([
|
|
2697
2871
|
"break_down",
|
|
2698
2872
|
// "This should be more than one ask — break it down."
|
|
2699
2873
|
"regenerate_options",
|
|
@@ -2708,116 +2882,116 @@ var NotificationFeedbackKindSchema = z4.enum([
|
|
|
2708
2882
|
// anything else — the note carries it.
|
|
2709
2883
|
]);
|
|
2710
2884
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
2711
|
-
var QuestionRowSchema =
|
|
2885
|
+
var QuestionRowSchema = z5.object({
|
|
2712
2886
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
2713
|
-
id:
|
|
2714
|
-
deliveryId:
|
|
2715
|
-
entryId:
|
|
2887
|
+
id: z5.string(),
|
|
2888
|
+
deliveryId: z5.string(),
|
|
2889
|
+
entryId: z5.string(),
|
|
2716
2890
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
2717
|
-
needId:
|
|
2718
|
-
goalIds:
|
|
2891
|
+
needId: z5.string().nullable(),
|
|
2892
|
+
goalIds: z5.array(z5.string()),
|
|
2719
2893
|
/** The name of the work it is about, when the read could word it. */
|
|
2720
|
-
goalTitle:
|
|
2721
|
-
tokenId:
|
|
2722
|
-
name:
|
|
2723
|
-
title:
|
|
2724
|
-
body:
|
|
2725
|
-
select:
|
|
2726
|
-
options:
|
|
2727
|
-
hasPreview:
|
|
2728
|
-
blocking:
|
|
2729
|
-
askedAt:
|
|
2894
|
+
goalTitle: z5.string().optional(),
|
|
2895
|
+
tokenId: z5.string().optional(),
|
|
2896
|
+
name: z5.string(),
|
|
2897
|
+
title: z5.string(),
|
|
2898
|
+
body: z5.string(),
|
|
2899
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]),
|
|
2900
|
+
options: z5.array(SlimOptionSchema),
|
|
2901
|
+
hasPreview: z5.boolean(),
|
|
2902
|
+
blocking: z5.boolean(),
|
|
2903
|
+
askedAt: z5.string().datetime(),
|
|
2730
2904
|
ring: InboxItemSchema.shape.ring,
|
|
2731
|
-
onCall:
|
|
2732
|
-
sealed:
|
|
2905
|
+
onCall: z5.literal(true).optional(),
|
|
2906
|
+
sealed: z5.boolean()
|
|
2733
2907
|
});
|
|
2734
|
-
var WorkStateSchema =
|
|
2735
|
-
var WorkRowSchema =
|
|
2736
|
-
id:
|
|
2737
|
-
parentId:
|
|
2738
|
-
title:
|
|
2908
|
+
var WorkStateSchema = z5.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
2909
|
+
var WorkRowSchema = z5.object({
|
|
2910
|
+
id: z5.string(),
|
|
2911
|
+
parentId: z5.string().nullable(),
|
|
2912
|
+
title: z5.string(),
|
|
2739
2913
|
/** Straight off the Goal. */
|
|
2740
2914
|
state: WorkStateSchema,
|
|
2741
|
-
owner:
|
|
2742
|
-
revision:
|
|
2915
|
+
owner: z5.string().nullable(),
|
|
2916
|
+
revision: z5.number().int(),
|
|
2743
2917
|
/** Open questions on it, counted to 100. */
|
|
2744
|
-
waiting:
|
|
2918
|
+
waiting: z5.number().int(),
|
|
2745
2919
|
/** Held by a gate on work that is not done. */
|
|
2746
|
-
blocked:
|
|
2747
|
-
lastProgressAt:
|
|
2920
|
+
blocked: z5.boolean(),
|
|
2921
|
+
lastProgressAt: z5.string().datetime().nullable(),
|
|
2748
2922
|
/** The line written for its newest progress note, else that note's first words. */
|
|
2749
|
-
line:
|
|
2923
|
+
line: z5.string().nullable(),
|
|
2750
2924
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
2751
|
-
children:
|
|
2752
|
-
createdAt:
|
|
2753
|
-
updatedAt:
|
|
2925
|
+
children: z5.number().int(),
|
|
2926
|
+
createdAt: z5.string().datetime(),
|
|
2927
|
+
updatedAt: z5.string().datetime(),
|
|
2754
2928
|
/** When anything at or under it last moved — the order the list is in. */
|
|
2755
|
-
activeAt:
|
|
2929
|
+
activeAt: z5.string().datetime(),
|
|
2756
2930
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
2757
|
-
sealed:
|
|
2931
|
+
sealed: z5.boolean()
|
|
2758
2932
|
});
|
|
2759
2933
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
2760
2934
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
2761
2935
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
2762
|
-
asking:
|
|
2763
|
-
oldestAskAt:
|
|
2936
|
+
asking: z5.number().int().nullable(),
|
|
2937
|
+
oldestAskAt: z5.string().datetime().nullable(),
|
|
2764
2938
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
2765
2939
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
2766
2940
|
* (`agent_holds`); null when that read failed. */
|
|
2767
|
-
holds:
|
|
2768
|
-
held:
|
|
2941
|
+
holds: z5.array(z5.object({ id: z5.string(), title: z5.string() })).nullable(),
|
|
2942
|
+
held: z5.number().int().nullable(),
|
|
2769
2943
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
2770
2944
|
* while none has, or when that read failed. */
|
|
2771
|
-
cold:
|
|
2945
|
+
cold: z5.string().datetime().nullable(),
|
|
2772
2946
|
/** The newest line of its working log, and when the harness saw it. */
|
|
2773
|
-
line:
|
|
2774
|
-
lineAt:
|
|
2947
|
+
line: z5.string().nullable(),
|
|
2948
|
+
lineAt: z5.string().datetime().nullable()
|
|
2775
2949
|
});
|
|
2776
|
-
var SnapshotSchema =
|
|
2950
|
+
var SnapshotSchema = z5.object({
|
|
2777
2951
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
2778
|
-
at:
|
|
2779
|
-
questions:
|
|
2952
|
+
at: z5.string().datetime(),
|
|
2953
|
+
questions: z5.object({
|
|
2780
2954
|
/** The newest 30 open cards, questions before updates. */
|
|
2781
|
-
items:
|
|
2955
|
+
items: z5.array(QuestionRowSchema),
|
|
2782
2956
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
2783
|
-
total:
|
|
2784
|
-
updates:
|
|
2785
|
-
putOff:
|
|
2957
|
+
total: z5.number().int(),
|
|
2958
|
+
updates: z5.number().int(),
|
|
2959
|
+
putOff: z5.number().int()
|
|
2786
2960
|
}).nullable(),
|
|
2787
|
-
agents:
|
|
2961
|
+
agents: z5.object({
|
|
2788
2962
|
/** Up to 60, most recently seen first. */
|
|
2789
|
-
items:
|
|
2790
|
-
more:
|
|
2963
|
+
items: z5.array(AgentRowSchema),
|
|
2964
|
+
more: z5.boolean()
|
|
2791
2965
|
}).nullable(),
|
|
2792
|
-
work:
|
|
2966
|
+
work: z5.object({
|
|
2793
2967
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
2794
|
-
items:
|
|
2968
|
+
items: z5.array(WorkRowSchema),
|
|
2795
2969
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
2796
2970
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
2797
2971
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
2798
2972
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
2799
|
-
counts:
|
|
2973
|
+
counts: z5.object({ live: z5.number().int(), waiting: z5.number().int(), notStarted: z5.number().int(), done: z5.number().int() })
|
|
2800
2974
|
}).nullable(),
|
|
2801
|
-
you:
|
|
2975
|
+
you: z5.object({
|
|
2802
2976
|
settings: UserSettingsSchema,
|
|
2803
|
-
callable:
|
|
2977
|
+
callable: z5.boolean(),
|
|
2804
2978
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
2805
|
-
computers:
|
|
2979
|
+
computers: z5.array(ComputerRowSchema).nullable()
|
|
2806
2980
|
}).nullable()
|
|
2807
2981
|
});
|
|
2808
|
-
var CallRecapSchema =
|
|
2809
|
-
call:
|
|
2810
|
-
status:
|
|
2811
|
-
startedAt:
|
|
2812
|
-
durationMs:
|
|
2813
|
-
agents:
|
|
2982
|
+
var CallRecapSchema = z5.object({
|
|
2983
|
+
call: z5.object({
|
|
2984
|
+
status: z5.string(),
|
|
2985
|
+
startedAt: z5.string(),
|
|
2986
|
+
durationMs: z5.number().nullable(),
|
|
2987
|
+
agents: z5.array(z5.object({ id: z5.string(), name: z5.string().nullable() }))
|
|
2814
2988
|
}),
|
|
2815
|
-
topics:
|
|
2816
|
-
goalId:
|
|
2817
|
-
title:
|
|
2818
|
-
owner:
|
|
2819
|
-
state:
|
|
2820
|
-
questions:
|
|
2989
|
+
topics: z5.array(z5.object({
|
|
2990
|
+
goalId: z5.string().uuid(),
|
|
2991
|
+
title: z5.string(),
|
|
2992
|
+
owner: z5.string(),
|
|
2993
|
+
state: z5.string(),
|
|
2994
|
+
questions: z5.array(z5.object({ id: z5.string().uuid(), state: z5.string(), title: z5.string() })),
|
|
2821
2995
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
2822
2996
|
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
2823
2997
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
@@ -2825,14 +2999,14 @@ var CallRecapSchema = z4.object({
|
|
|
2825
2999
|
* anything that is not an answer. */
|
|
2826
3000
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
2827
3001
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
2828
|
-
lines:
|
|
3002
|
+
lines: z5.array(z5.object({ entryId: z5.string().uuid(), words: z5.string(), headline: z5.string().optional(), about: z5.string().nullable().optional() }))
|
|
2829
3003
|
})),
|
|
2830
|
-
unfiled:
|
|
2831
|
-
more:
|
|
3004
|
+
unfiled: z5.array(z5.object({ lineId: z5.string().uuid(), words: z5.string(), atMs: z5.number() })),
|
|
3005
|
+
more: z5.object({ lines: z5.number(), entries: z5.number(), topics: z5.number() })
|
|
2832
3006
|
});
|
|
2833
3007
|
function sessionSlot(sessionId2) {
|
|
2834
|
-
const
|
|
2835
|
-
return `session:${
|
|
3008
|
+
const id2 = sessionId2 ?? sessionId();
|
|
3009
|
+
return `session:${id2.slice(0, 8)}`;
|
|
2836
3010
|
}
|
|
2837
3011
|
function agentName() {
|
|
2838
3012
|
return process.env.PAIGY_AGENT || sessionSlot();
|
|
@@ -2890,13 +3064,16 @@ function updateSlot(agent2, patch) {
|
|
|
2890
3064
|
}
|
|
2891
3065
|
function slotIdentity(agent2) {
|
|
2892
3066
|
const t = readTokenFile()[agent2];
|
|
2893
|
-
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null };
|
|
3067
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null, fullSession: t?.full_session ?? null };
|
|
2894
3068
|
}
|
|
2895
3069
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2896
3070
|
function readToken(agent2 = agentName()) {
|
|
2897
3071
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
2898
3072
|
return readTokenFile()[agent2]?.access_token ?? "";
|
|
2899
3073
|
}
|
|
3074
|
+
function readSlot(agent2) {
|
|
3075
|
+
return readTokenFile()[agent2]?.access_token ?? "";
|
|
3076
|
+
}
|
|
2900
3077
|
function listSlots() {
|
|
2901
3078
|
return Object.keys(readTokenFile());
|
|
2902
3079
|
}
|
|
@@ -3027,11 +3204,16 @@ async function updateGoal(goalId, input, opts = {}) {
|
|
|
3027
3204
|
return await res.json();
|
|
3028
3205
|
}
|
|
3029
3206
|
var AWAIT_WINDOW_MS = 45e3;
|
|
3030
|
-
async function hatch(name, voice = null) {
|
|
3031
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
3207
|
+
async function hatch(name, voice = null, opts = {}) {
|
|
3208
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/hatch`, {
|
|
3032
3209
|
method: "POST",
|
|
3033
3210
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
|
|
3034
|
-
body: JSON.stringify({
|
|
3211
|
+
body: JSON.stringify({
|
|
3212
|
+
name,
|
|
3213
|
+
voice,
|
|
3214
|
+
...opts.brief ? { brief: opts.brief } : {},
|
|
3215
|
+
...opts.parentGoalId ? { parentGoalId: opts.parentGoalId } : {}
|
|
3216
|
+
})
|
|
3035
3217
|
}));
|
|
3036
3218
|
if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
|
|
3037
3219
|
return await res.json();
|
|
@@ -3104,7 +3286,9 @@ async function recordDecision(decision, opts = {}) {
|
|
|
3104
3286
|
async function heartbeat(runtime, opts = {}) {
|
|
3105
3287
|
const body = {
|
|
3106
3288
|
...runtime !== void 0 ? { runtime } : {},
|
|
3107
|
-
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
3289
|
+
...opts.activity !== void 0 ? { activity: opts.activity } : {},
|
|
3290
|
+
// A listener's beat (`paigy-listen`): stamps when it last swept, and is not presence.
|
|
3291
|
+
...opts.listening ? { listening: true } : {}
|
|
3108
3292
|
};
|
|
3109
3293
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
3110
3294
|
method: "POST",
|
|
@@ -3169,6 +3353,13 @@ async function dismissTriage(proposalId, opts = {}) {
|
|
|
3169
3353
|
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
3170
3354
|
return await res.json();
|
|
3171
3355
|
}
|
|
3356
|
+
async function whoIsWorking(opts = {}) {
|
|
3357
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
|
|
3358
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
3359
|
+
}));
|
|
3360
|
+
if (!res.ok) await fail("who_is_working", res);
|
|
3361
|
+
return await res.json();
|
|
3362
|
+
}
|
|
3172
3363
|
function repoFromRemote(url) {
|
|
3173
3364
|
const said = (url ?? "").trim();
|
|
3174
3365
|
if (!said) return null;
|
|
@@ -3196,6 +3387,7 @@ function currentRepo(cwd = process.cwd()) {
|
|
|
3196
3387
|
return asked;
|
|
3197
3388
|
}
|
|
3198
3389
|
var NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
|
|
3390
|
+
var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again, with or without a Goal.";
|
|
3199
3391
|
async function contact(input, opts = {}) {
|
|
3200
3392
|
const parsed = ContactSchema.parse(input);
|
|
3201
3393
|
opts.signal?.throwIfAborted();
|
|
@@ -3204,6 +3396,7 @@ async function contact(input, opts = {}) {
|
|
|
3204
3396
|
let deliveryId;
|
|
3205
3397
|
let deliveries = [];
|
|
3206
3398
|
let recorded = [];
|
|
3399
|
+
let demoted;
|
|
3207
3400
|
if ("deliveryId" in parsed) {
|
|
3208
3401
|
deliveryId = parsed.deliveryId;
|
|
3209
3402
|
} else {
|
|
@@ -3234,13 +3427,14 @@ async function contact(input, opts = {}) {
|
|
|
3234
3427
|
if (!res.ok) await fail("batch contact", res);
|
|
3235
3428
|
const receipt = await res.json();
|
|
3236
3429
|
deliveries = receipt.deliveries ?? [];
|
|
3430
|
+
if (parsed.channel === "call" && receipt.channel && receipt.channel !== "call") demoted = DEMOTED;
|
|
3237
3431
|
recorded = receipt.recorded ?? [];
|
|
3238
3432
|
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
3239
3433
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
3240
3434
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
3241
3435
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
3242
3436
|
}
|
|
3243
|
-
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...
|
|
3437
|
+
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id: id2, deliveryId: deliveryId2, goalId }) => ({ ...id2 ? { id: id2 } : {}, deliveryId: deliveryId2, goalId }));
|
|
3244
3438
|
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
3245
3439
|
const joinedCall = deliveries[0]?.joinedCall === true;
|
|
3246
3440
|
const read = async (signal2) => {
|
|
@@ -3252,7 +3446,8 @@ async function contact(input, opts = {}) {
|
|
|
3252
3446
|
...all.length > 1 ? { deliveries: all } : {},
|
|
3253
3447
|
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
3254
3448
|
...joinedCard ? { joinedCard: true } : {},
|
|
3255
|
-
...joinedCall ? { joinedCall: true } : {}
|
|
3449
|
+
...joinedCall ? { joinedCall: true } : {},
|
|
3450
|
+
...demoted ? { demoted } : {}
|
|
3256
3451
|
};
|
|
3257
3452
|
};
|
|
3258
3453
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
@@ -3283,7 +3478,7 @@ async function checkReplies(opts = {}) {
|
|
|
3283
3478
|
function compact(o) {
|
|
3284
3479
|
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== null && v !== "" && !(Array.isArray(v) && v.length === 0)));
|
|
3285
3480
|
}
|
|
3286
|
-
var who = (
|
|
3481
|
+
var who = (participant2) => participant2.startsWith("human:") ? "person" : isPaigy(participant2) ? "paigy" : participant2.startsWith("agent:") ? "agent" : participant2;
|
|
3287
3482
|
var at = (iso) => {
|
|
3288
3483
|
const t = Date.parse(iso);
|
|
3289
3484
|
return Number.isFinite(t) ? new Date(t).toISOString().replace(/\.\d{3}Z$/, "Z") : iso;
|
|
@@ -3343,7 +3538,7 @@ function goalView(g) {
|
|
|
3343
3538
|
if (!g.goalId) return compact({ state: g.state, next: g.message });
|
|
3344
3539
|
const lines2 = conversation(g);
|
|
3345
3540
|
const owed = lines2.filter((l) => l.from === "person" && l.decision?.state === "open" && l.id);
|
|
3346
|
-
const owes = owed.length ? `The person asked you ${owed.length === 1 ? "a question" : `${owed.length} questions`} (${owed.map((l) => `id ${l.id}: "${l.said.slice(0, 80)}"`).join("; ")}) \u2014 answer with contact({ asks: [{ ask: <your answer>,
|
|
3541
|
+
const owes = owed.length ? `The person asked you ${owed.length === 1 ? "a question" : `${owed.length} questions`} (${owed.map((l) => `id ${l.id}: "${l.said.slice(0, 80)}"`).join("; ")}) \u2014 answer with contact({ asks: [{ ask: <your answer>, answers: "<id>" }] }). ` : "";
|
|
3347
3542
|
const newest = [...lines2].reverse().find((l) => l.from === "agent");
|
|
3348
3543
|
return compact({
|
|
3349
3544
|
goalId: g.goalId,
|
|
@@ -3352,7 +3547,9 @@ function goalView(g) {
|
|
|
3352
3547
|
outcome: g.outcome,
|
|
3353
3548
|
progress: g.progress && g.progress.trim() !== newest?.said ? g.progress : void 0,
|
|
3354
3549
|
reviewPending: g.reviewPending || void 0,
|
|
3550
|
+
reviewSince: g.reviewSince,
|
|
3355
3551
|
owner: g.ownerParticipant,
|
|
3552
|
+
others: g.others,
|
|
3356
3553
|
parentGoalId: g.parentGoalId,
|
|
3357
3554
|
// Every child is in the subtree below, with its goalId: listed again, it was said twice.
|
|
3358
3555
|
childGoalIds: g.subtree?.length ? void 0 : g.childGoalIds,
|
|
@@ -3388,7 +3585,7 @@ function deliveryView(d) {
|
|
|
3388
3585
|
notSent: d.notSent,
|
|
3389
3586
|
joinedCard: d.joinedCard,
|
|
3390
3587
|
joinedCall: d.joinedCall,
|
|
3391
|
-
next: d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers : d.state === "open" && d.decisionNeeds.some((n) => n.state === "open") ? `Decision pending. Call contact(${JSON.stringify({ deliveryId: d.deliveryId })}) to continue this same Call \u2014 each call is ONE bounded window, and never resend the ask. When a window comes back with nothing new said, they are not typing: stop rereading, leave the question open, and collect the answer with check_replies or claim_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3588
|
+
next: d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? `${d.demoted ? `${d.demoted} ` : ""}${d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers}` : d.state === "open" && d.decisionNeeds.some((n) => n.state === "open") ? `Decision pending. Call contact(${JSON.stringify({ deliveryId: d.deliveryId })}) to continue this same Call \u2014 each call is ONE bounded window, and never resend the ask. When a window comes back with nothing new said, they are not typing: stop rereading, leave the question open, and collect the answer with check_replies or claim_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3392
3589
|
});
|
|
3393
3590
|
}
|
|
3394
3591
|
function repliesView(r) {
|
|
@@ -3429,7 +3626,7 @@ function repliesView(r) {
|
|
|
3429
3626
|
// sentence names the person and quotes the freshest one, because a count is not a message.
|
|
3430
3627
|
...review.length ? [spoke(review) ?? `${review.length} of your Goals have something new (review): read each with claim_goal or get_goal, then update_goal with reviewed: true.`] : [],
|
|
3431
3628
|
...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
|
|
3432
|
-
...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId})
|
|
3629
|
+
...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) joins one without changing its owner, then finish or cancel it with update_goal.`] : [],
|
|
3433
3630
|
// No name of its own (#2525): the API's ask, verbatim.
|
|
3434
3631
|
...r.unnamed ? [r.unnamed] : []
|
|
3435
3632
|
].join(" ")
|
|
@@ -3450,6 +3647,9 @@ async function runTool(name, args, opts) {
|
|
|
3450
3647
|
const sent = await contact(input, { ...client, signal, waits });
|
|
3451
3648
|
return "sent" in sent ? sent : deliveryView(sent);
|
|
3452
3649
|
}
|
|
3650
|
+
case "who_is_working":
|
|
3651
|
+
CheckRepliesSchema.parse(input);
|
|
3652
|
+
return whoIsWorking(client);
|
|
3453
3653
|
case "check_replies":
|
|
3454
3654
|
CheckRepliesSchema.parse(input);
|
|
3455
3655
|
return repliesView(await checkReplies(client));
|
|
@@ -3585,9 +3785,9 @@ function connect(line) {
|
|
|
3585
3785
|
ws.addEventListener("error", again);
|
|
3586
3786
|
}
|
|
3587
3787
|
function subscribeRealtime(args) {
|
|
3588
|
-
const
|
|
3589
|
-
let line = lines.get(
|
|
3590
|
-
if (!line) lines.set(
|
|
3788
|
+
const key2 = `${args.url} ${args.anonKey}`;
|
|
3789
|
+
let line = lines.get(key2);
|
|
3790
|
+
if (!line) lines.set(key2, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
3591
3791
|
let t = line.topics.get(args.topic);
|
|
3592
3792
|
if (!t) {
|
|
3593
3793
|
line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
|
|
@@ -3609,7 +3809,7 @@ function subscribeRealtime(args) {
|
|
|
3609
3809
|
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
3610
3810
|
return;
|
|
3611
3811
|
}
|
|
3612
|
-
lines.delete(
|
|
3812
|
+
lines.delete(key2);
|
|
3613
3813
|
stop(line);
|
|
3614
3814
|
try {
|
|
3615
3815
|
line.ws?.close();
|
|
@@ -3653,14 +3853,38 @@ function machineZone() {
|
|
|
3653
3853
|
return null;
|
|
3654
3854
|
}
|
|
3655
3855
|
}
|
|
3856
|
+
function withCodexEnv(text) {
|
|
3857
|
+
const head = /^\[mcp_servers\.paigy\][ \t]*(?:#[^\n]*)?\r?\n/m.exec(text);
|
|
3858
|
+
if (!head) return null;
|
|
3859
|
+
const start = head.index + head[0].length;
|
|
3860
|
+
const next = /^\[/m.exec(text.slice(start));
|
|
3861
|
+
const end = next ? start + next.index : text.length;
|
|
3862
|
+
const block = text.slice(start, end);
|
|
3863
|
+
const assignment = /^[ \t]*env_vars[ \t]*=/m.exec(block);
|
|
3864
|
+
let updated;
|
|
3865
|
+
if (!assignment) updated = `env_vars = [${CODEX_ENV.map((v) => JSON.stringify(v)).join(", ")}]
|
|
3866
|
+
${block}`;
|
|
3867
|
+
else {
|
|
3868
|
+
const array = /^[ \t]*env_vars[ \t]*=[ \t]*\[((?:#[^\n]*|[^\]"'#]|"[^"\n]*"|'[^'\n]*')*)\]/m.exec(block);
|
|
3869
|
+
if (!array) return null;
|
|
3870
|
+
const existing = [...array[1].matchAll(/"([^"\n]*)"|'([^'\n]*)'|#[^\n]*/g)].flatMap((m) => m[0].startsWith("#") ? [] : [m[1] ?? m[2]]);
|
|
3871
|
+
const missing = CODEX_ENV.filter((v) => !existing.includes(v));
|
|
3872
|
+
if (!missing.length) return text;
|
|
3873
|
+
const opening = array.index + array[0].indexOf("[") + 1;
|
|
3874
|
+
updated = block.slice(0, opening) + missing.map((v) => JSON.stringify(v)).join(", ") + (existing.length ? ", " : "") + block.slice(opening);
|
|
3875
|
+
}
|
|
3876
|
+
return text.slice(0, start) + updated + text.slice(end);
|
|
3877
|
+
}
|
|
3656
3878
|
|
|
3657
3879
|
export {
|
|
3880
|
+
CODEX_ENV,
|
|
3658
3881
|
BACKEND_URL,
|
|
3659
3882
|
NETWORK_MSG,
|
|
3660
3883
|
sessionId,
|
|
3661
3884
|
isNetworkError,
|
|
3662
3885
|
reach,
|
|
3663
3886
|
reachAs,
|
|
3887
|
+
setClient,
|
|
3664
3888
|
AGENT_TOOLS,
|
|
3665
3889
|
AGENT_TOOL_NAMES,
|
|
3666
3890
|
NotifyRequestSchema,
|
|
@@ -3674,6 +3898,7 @@ export {
|
|
|
3674
3898
|
slotIdentity,
|
|
3675
3899
|
sleep,
|
|
3676
3900
|
readToken,
|
|
3901
|
+
readSlot,
|
|
3677
3902
|
listSlots,
|
|
3678
3903
|
slotName,
|
|
3679
3904
|
deleteToken,
|
|
@@ -3706,11 +3931,13 @@ export {
|
|
|
3706
3931
|
getTriage,
|
|
3707
3932
|
acceptTriage,
|
|
3708
3933
|
dismissTriage,
|
|
3934
|
+
whoIsWorking,
|
|
3709
3935
|
repoFromRemote,
|
|
3710
3936
|
currentRepo,
|
|
3711
3937
|
contact,
|
|
3712
3938
|
checkReplies,
|
|
3713
3939
|
runTool,
|
|
3714
3940
|
subscribeWake,
|
|
3715
|
-
parseDue
|
|
3941
|
+
parseDue,
|
|
3942
|
+
withCodexEnv
|
|
3716
3943
|
};
|