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