@paigy/mcp 0.40.23 → 0.40.25
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 +38 -23
- package/dist/{chunk-BNMMRKEY.js → chunk-2ROD77AW.js} +1 -1
- package/dist/{chunk-PR7ROBN4.js → chunk-EJYXBEWN.js} +959 -621
- package/dist/{chunk-JLRBZZNT.js → chunk-GHSB2VP6.js} +752 -517
- package/dist/chunk-RK5LT7NH.js +110 -0
- package/dist/{chunk-HKODQKUV.js → chunk-UAVXYNLV.js} +9 -9
- package/dist/{dist-B7AOS5TG.js → dist-NMHU2UNG.js} +19 -7
- package/dist/enable.js +3 -3
- package/dist/index.js +18 -7
- package/dist/listen.js +85 -17
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +3 -3
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// ../../packages/schema/dist/index.js
|
|
2
|
-
import { z as
|
|
2
|
+
import { z as z5 } from "zod";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { z as z3 } from "zod";
|
|
5
5
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
6
6
|
import { z as z2 } from "zod";
|
|
7
|
+
import { z as z4 } from "zod";
|
|
8
|
+
import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
|
|
7
9
|
var OPTIONS_MIN = 2;
|
|
8
10
|
var OPTIONS_MAX = 6;
|
|
9
11
|
var OptionSchema = z.object({
|
|
@@ -54,32 +56,39 @@ function mcpInputSchema(s) {
|
|
|
54
56
|
}
|
|
55
57
|
var AskInputSchema = z2.object({
|
|
56
58
|
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
|
|
59
|
+
parentId: z2.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and the question starts a new Goal of yours."),
|
|
58
60
|
repo: z2.string().optional().describe("Optional repository context."),
|
|
59
61
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
60
|
-
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy
|
|
62
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy sends each ask exactly as you wrote it: a bundled ask arrives as one card. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
61
63
|
),
|
|
62
64
|
options: z2.array(OptionInputSchema).min(1).max(6).optional(),
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// shape (`goal/intake.ts` `answerOf`), and this reaches it as the agent's view, weighed, not obeyed.
|
|
65
|
+
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
66
|
+
// brain_prompts.md §1: structured agent questions need no model). Every card was pick-one while no
|
|
67
|
+
// agent sent this; no read reshapes cards any more, so this is the card's shape, and the
|
|
68
|
+
// description says so plainly.
|
|
68
69
|
select: z2.enum(["one", "many"]).optional().describe(
|
|
69
|
-
|
|
70
|
+
'How the options are answered: "many" makes a checklist (they may want several, e.g. independent fixes), "one" (the default) a pick (choosing one rules out the others). Send "many" whenever more than one option could be wanted at once. The person can always answer in their own words as well. Ignored without options.'
|
|
70
71
|
),
|
|
71
72
|
answers: z2.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
72
|
-
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them.
|
|
73
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. parentId is optional here: the question's own Goal is used. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
73
74
|
)
|
|
74
75
|
}).strict();
|
|
75
76
|
var StartContactSchema = z2.object({
|
|
76
|
-
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;
|
|
77
|
+
asks: z2.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; an ask naming no Goal starts a new Goal of yours."),
|
|
77
78
|
waiting: z2.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
78
79
|
channel: z2.enum(["notification", "call"]).default("notification")
|
|
79
80
|
}).strict();
|
|
80
|
-
var
|
|
81
|
+
var ReceiveContactSchema = z2.object({
|
|
82
|
+
wait: z2.boolean().optional().describe(
|
|
83
|
+
"Only controls when this returns. true (the default when you send and acknowledge nothing): wait up to about 45 seconds for incoming communication addressed to you, and return as soon as some is available. false: return what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived in the window; wait again without sending again), or `not_waited` (a hosted connection never holds a wait)."
|
|
84
|
+
),
|
|
85
|
+
ackEventIds: z2.array(z2.string().uuid()).min(1).max(100).optional().describe(
|
|
86
|
+
"The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (an ask with `answers`), never acknowledged away: that id is refused until it is answered."
|
|
87
|
+
)
|
|
88
|
+
}).strict();
|
|
89
|
+
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict(), ReceiveContactSchema]);
|
|
81
90
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
82
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none
|
|
91
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none starts a new Goal of yours. Notification returns immediately; collect answers by receiving (below). On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. Each ask is sent exactly as written, with no reading in between: a bundled ask arrives as one card, so separate questions are separate asks. An ask with no options that is not blocking is a report, never a claim on them, so no answer is owed and none should be awaited. A report reaches the person only when it answers something they asked you, or once the Goal is done; any other report is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first, then send one message saying what is done and anything they need to do or check. On a Goal whose report card is still open, a report that does reach them is added to that card, with no new push. Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). select:'many' makes the card a checklist, for independent options they may want several of; select:'one' (the default) a pick, for alternatives. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, a contact that reaches them joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.\n\nRECEIVING: contact with no asks sends nothing and returns `events`, a limited batch of what is addressed to you (not a history page): `question`, a Question you owe (answer it with an ask whose `answers` is its questionId); `update`, something new on one of your Goals (a reply, an answer: read it with get_goal); `instruction`, a request or note sent to you. `hasMore` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (`assigned`; claim_goal starts it) and your work gone quiet (`stalled`).";
|
|
83
92
|
var CreateGoalSchema = z3.object({
|
|
84
93
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
85
94
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -99,10 +108,6 @@ var CreateGoalSchema = z3.object({
|
|
|
99
108
|
* repo name. Delegated work inherits this from its parent Goal when omitted. */
|
|
100
109
|
repo: z3.string().trim().min(1).max(200).optional()
|
|
101
110
|
});
|
|
102
|
-
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
103
|
-
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
104
|
-
}).strict();
|
|
105
|
-
var CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
|
|
106
111
|
var UpdateGoalSchema = z3.object({
|
|
107
112
|
revision: z3.number().int().positive(),
|
|
108
113
|
changes: z3.object({
|
|
@@ -126,7 +131,65 @@ var UpdateGoalSchema = z3.object({
|
|
|
126
131
|
reason: z3.string().trim().min(1).max(2e3),
|
|
127
132
|
operationId: z3.string().uuid().optional()
|
|
128
133
|
}).strict();
|
|
129
|
-
var UpdateGoalToolSchema =
|
|
134
|
+
var UpdateGoalToolSchema = z3.object({
|
|
135
|
+
goalId: z3.string().uuid(),
|
|
136
|
+
revision: UpdateGoalSchema.shape.revision,
|
|
137
|
+
changes: UpdateGoalSchema.shape.changes.innerType().pick({ progress: true, reviewed: true, dueAt: true, withdraw: true }).strict().refine((v) => Object.keys(v).length > 0),
|
|
138
|
+
reason: UpdateGoalSchema.shape.reason
|
|
139
|
+
}).strict();
|
|
140
|
+
var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
141
|
+
var goalTitle = z3.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
|
|
142
|
+
var goalOutcome = z3.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
|
|
143
|
+
var CreateChange = z3.object({
|
|
144
|
+
kind: z3.literal("create"),
|
|
145
|
+
title: goalTitle,
|
|
146
|
+
outcome: goalOutcome,
|
|
147
|
+
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as who_is_working shows it) or the person's. Omitted: you."),
|
|
148
|
+
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
149
|
+
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
150
|
+
}).strict();
|
|
151
|
+
var EditChange = z3.object({ kind: z3.literal("edit"), goalId: changedGoal, title: goalTitle.optional(), outcome: goalOutcome.optional() }).strict();
|
|
152
|
+
var StateChange = z3.object({ kind: z3.literal("state"), goalId: changedGoal, state: z3.enum(["open", "completed", "canceled"]).describe(
|
|
153
|
+
"completed: the Goal's own work is done now (refused while required children are open); open: reopen it; canceled: it will not be done."
|
|
154
|
+
) }).strict();
|
|
155
|
+
var AssignChange = z3.object({ kind: z3.literal("assign"), goalId: changedGoal, ownerId: z3.string().trim().min(1).describe("The new owner's participant.") }).strict();
|
|
156
|
+
var DeferChange = z3.object({ kind: z3.literal("defer"), goalId: changedGoal, until: z3.string().datetime({ offset: true }).nullable().describe(
|
|
157
|
+
"Postpone it until this instant (its owner is woken then), or null to take the postponement back."
|
|
158
|
+
) }).strict();
|
|
159
|
+
var MoveChange = z3.object({ kind: z3.literal("move"), goalId: changedGoal, parentGoalId: z3.string().uuid().nullable().describe(
|
|
160
|
+
"Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
|
|
161
|
+
) }).strict();
|
|
162
|
+
var DependencyChange = z3.object({
|
|
163
|
+
kind: z3.literal("dependency"),
|
|
164
|
+
change: z3.enum(["add", "remove"]),
|
|
165
|
+
goalId: changedGoal,
|
|
166
|
+
dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
|
|
167
|
+
action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
|
|
168
|
+
reason: z3.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
|
|
169
|
+
}).strict();
|
|
170
|
+
var read = { revision: z3.number().int().positive().optional() };
|
|
171
|
+
var GoalChangeSchema = z3.discriminatedUnion("kind", [
|
|
172
|
+
CreateChange,
|
|
173
|
+
EditChange.extend(read),
|
|
174
|
+
StateChange.extend(read),
|
|
175
|
+
AssignChange.extend(read),
|
|
176
|
+
DeferChange.extend(read),
|
|
177
|
+
MoveChange.extend(read),
|
|
178
|
+
DependencyChange
|
|
179
|
+
]);
|
|
180
|
+
var editNames = (changes, ctx) => changes.forEach((c, i) => {
|
|
181
|
+
if (c.kind === "edit" && c.title === void 0 && c.outcome === void 0) {
|
|
182
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["changes", i], message: "an edit supplies title, outcome, or both" });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
var ManageGoalsSchema = z3.object({
|
|
186
|
+
requestId: z3.string().uuid().optional(),
|
|
187
|
+
changes: z3.array(GoalChangeSchema).min(1).max(50)
|
|
188
|
+
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
189
|
+
var ManageGoalsToolSchema = z3.object({
|
|
190
|
+
changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
191
|
+
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
192
|
+
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_joined, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until, or null); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change a Goal you own or have written on. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
130
193
|
var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
|
|
131
194
|
var GetGoalSchema = z3.object({
|
|
132
195
|
goalId: z3.string().uuid(),
|
|
@@ -139,34 +202,176 @@ var GetGoalSchema = z3.object({
|
|
|
139
202
|
diagnose: z3.boolean().optional()
|
|
140
203
|
}).strict();
|
|
141
204
|
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
142
|
-
var UPDATE_GOAL_DESCRIPTION =
|
|
143
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change
|
|
144
|
-
var
|
|
145
|
-
|
|
205
|
+
var UPDATE_GOAL_DESCRIPTION = "Report work on a Goal you are on, at an exact revision: progress, review acknowledgement, when to wake for it, and withdrawing a question of yours. What the Goal is (its title, outcome, state, owner, parent, dependencies) changes with manage_goals. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). Stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. progress says where the work stands. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision (contact({ackEventIds}) does the same for an `update` event). dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. Returns the Goal as get_goal reads it, at its new revision.";
|
|
206
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
207
|
+
var SearchToolSchema = z3.object({
|
|
208
|
+
query: z3.string().trim().min(1).max(500),
|
|
209
|
+
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
210
|
+
goalId: z3.string().uuid().optional(),
|
|
211
|
+
limit: z3.number().int().min(1).max(20).optional()
|
|
212
|
+
}).strict();
|
|
213
|
+
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and Answers (found by their Question or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, its Questions' Answers, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (an Answer carries its Question, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
214
|
+
var WhoIsWorkingSchema = z3.object({}).strict();
|
|
146
215
|
var AGENT_TOOLS = [
|
|
147
|
-
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(
|
|
216
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(WhoIsWorkingSchema) },
|
|
148
217
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
149
|
-
{ name: "
|
|
150
|
-
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
218
|
+
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
151
219
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
152
220
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
153
|
-
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
|
|
221
|
+
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) },
|
|
222
|
+
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) }
|
|
154
223
|
];
|
|
155
224
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
156
225
|
function serverInstructions(opts) {
|
|
157
226
|
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.";
|
|
158
|
-
return `On startup and after a wake, call
|
|
227
|
+
return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})), and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every Goal of your person is open to you, whichever of their agents owns it: read it with get_goal, write on it (contact with its id as parentId, or update_goal progress), and once you have written on it you may change it too (manage_goals). Create, edit, assign, organize or close work with manage_goals, never as a contact. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Events repeat until you acknowledge them: reading alone acknowledges nothing. Report progress 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.
|
|
159
228
|
|
|
160
229
|
HOW TO ASK:
|
|
161
230
|
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.
|
|
162
231
|
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.
|
|
163
232
|
3. \`waiting: hard\` only for a decision you are blocked on; \`waiting: none\` for a question you can keep working around.
|
|
164
|
-
4. Never hold the process open with while-loops. For an open Call with a pending decision, follow its \`next\` with another bounded \`contact({deliveryId})\` tool call; a returned wait window is not a completed conversation. STOP REREADING when a window comes back with nothing new said \u2014 they are not typing, the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`
|
|
233
|
+
4. Never hold the process open with while-loops. For an open Call with a pending decision, follow its \`next\` with another bounded \`contact({deliveryId})\` tool call; a returned wait window is not a completed conversation. STOP REREADING when a window comes back with nothing new said \u2014 they are not typing, the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`claim_goal\` on that wake.
|
|
165
234
|
5. One ask, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open.
|
|
166
235
|
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
167
|
-
7. A question carries its options. Without them it reaches the person as a bare title nobody can answer. Each option names its choice in full -- never \`All three\` or \`Option 2\`.
|
|
236
|
+
7. A question carries its options. Without them it reaches the person as a bare title nobody can answer. Each option names its choice in full -- never \`All three\` or \`Option 2\`. \`select: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others). Your ask is sent exactly as you wrote it. They can always answer in their own words, so no "Other" option.
|
|
168
237
|
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.`;
|
|
169
238
|
}
|
|
239
|
+
var id = z4.string().uuid();
|
|
240
|
+
var key = z4.string().min(1).max(40);
|
|
241
|
+
var participant = z4.string().regex(/^(human|agent):.+$/, "a participant ID such as agent:<uuid>");
|
|
242
|
+
var SourceSchema = z4.object({ entryId: id }).strict();
|
|
243
|
+
var ResultRefSchema = z4.union([z4.object({ id }).strict(), z4.object({ local: key }).strict()]);
|
|
244
|
+
var NodeRefSchema = z4.object({ type: z4.enum(["goal", "question"]), id }).strict();
|
|
245
|
+
var DependencySchema = z4.object({
|
|
246
|
+
blocker: NodeRefSchema,
|
|
247
|
+
blocked: NodeRefSchema,
|
|
248
|
+
action: z4.enum(["start", "complete", "answer"]),
|
|
249
|
+
reason: z4.string().max(500).optional()
|
|
250
|
+
}).strict();
|
|
251
|
+
var goalState = z4.enum(["open", "completed", "canceled"]);
|
|
252
|
+
var existingGoalChanges = [
|
|
253
|
+
z4.object({ kind: z4.literal("edit"), goalId: id, title: z4.string().min(1).max(120).optional(), outcome: z4.string().min(1).optional() }).strict().refine((c) => c.title !== void 0 || c.outcome !== void 0, "an edit changes the title or the outcome"),
|
|
254
|
+
z4.object({ kind: z4.literal("state"), goalId: id, state: goalState }).strict(),
|
|
255
|
+
z4.object({ kind: z4.literal("assign"), goalId: id, ownerId: participant }).strict(),
|
|
256
|
+
z4.object({ kind: z4.literal("defer"), goalId: id, until: z4.string().datetime().nullable() }).strict(),
|
|
257
|
+
z4.object({ kind: z4.literal("move"), goalId: id, parentGoalId: id.nullable() }).strict(),
|
|
258
|
+
z4.object({
|
|
259
|
+
kind: z4.literal("dependency"),
|
|
260
|
+
change: z4.enum(["add", "remove"]),
|
|
261
|
+
goalId: id,
|
|
262
|
+
dependsOnGoalId: id,
|
|
263
|
+
action: z4.enum(["start", "complete"]),
|
|
264
|
+
reason: z4.string().max(500).optional()
|
|
265
|
+
}).strict()
|
|
266
|
+
];
|
|
267
|
+
var BrainGoalChangeSchema = z4.union([
|
|
268
|
+
// newSession: Paigy starts a session to own it (owner, 2026-10-06); until it has, the person owns it.
|
|
269
|
+
z4.object({
|
|
270
|
+
kind: z4.literal("create"),
|
|
271
|
+
outcome: z4.string().min(1),
|
|
272
|
+
title: z4.string().min(1).max(120),
|
|
273
|
+
ownerId: participant,
|
|
274
|
+
parentGoal: ResultRefSchema.nullable(),
|
|
275
|
+
newSession: z4.literal(true).optional()
|
|
276
|
+
}).strict(),
|
|
277
|
+
...existingGoalChanges
|
|
278
|
+
]);
|
|
279
|
+
var option = z4.object({ id: z4.string().min(1).max(40), label: z4.string().min(1), hint: z4.string().optional() }).strict();
|
|
280
|
+
var QuestionChangeSchema = z4.union([
|
|
281
|
+
z4.object({
|
|
282
|
+
kind: z4.literal("create"),
|
|
283
|
+
text: z4.string().min(1),
|
|
284
|
+
answererId: participant,
|
|
285
|
+
goal: ResultRefSchema.optional(),
|
|
286
|
+
blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
|
|
287
|
+
options: z4.array(option).min(2).optional(),
|
|
288
|
+
pickMode: z4.enum(["one", "many", "rank"]).optional()
|
|
289
|
+
}).strict().refine((c) => c.options === void 0 === (c.pickMode === void 0), "options and pickMode come together"),
|
|
290
|
+
z4.object({ kind: z4.literal("edit"), questionId: id, text: z4.string().min(1) }).strict(),
|
|
291
|
+
z4.object({ kind: z4.literal("assign"), questionId: id, answererId: participant }).strict(),
|
|
292
|
+
z4.object({ kind: z4.literal("withdraw"), questionId: id }).strict(),
|
|
293
|
+
z4.object({ kind: z4.literal("dependency"), change: z4.enum(["add", "remove"]), edge: DependencySchema }).strict()
|
|
294
|
+
]);
|
|
295
|
+
var lessonScope = z4.union([
|
|
296
|
+
z4.object({ kind: z4.literal("user") }).strict(),
|
|
297
|
+
z4.object({ kind: z4.literal("goal"), goal: ResultRefSchema }).strict()
|
|
298
|
+
]);
|
|
299
|
+
var LessonChangeSchema = z4.union([
|
|
300
|
+
z4.object({ kind: z4.literal("remember"), text: z4.string().min(1), scope: lessonScope }).strict(),
|
|
301
|
+
z4.object({ kind: z4.literal("revise"), lessonId: id, text: z4.string().min(1), scope: lessonScope }).strict(),
|
|
302
|
+
z4.object({ kind: z4.literal("withdraw"), lessonId: id }).strict()
|
|
303
|
+
]);
|
|
304
|
+
var BrainNextSchema = z4.object({
|
|
305
|
+
instructions: z4.array(z4.object({ entryKeys: z4.array(key).min(1) }).strict()),
|
|
306
|
+
say: z4.array(key),
|
|
307
|
+
then: z4.enum(["listen", "hold", "end", "none"]),
|
|
308
|
+
waitFor: z4.array(ResultRefSchema),
|
|
309
|
+
reason: z4.string()
|
|
310
|
+
}).strict();
|
|
311
|
+
var messageBase = {
|
|
312
|
+
key,
|
|
313
|
+
text: z4.string().min(1),
|
|
314
|
+
goals: z4.array(ResultRefSchema),
|
|
315
|
+
questions: z4.array(ResultRefSchema),
|
|
316
|
+
entryKeys: z4.array(key),
|
|
317
|
+
obligationIds: z4.array(id)
|
|
318
|
+
};
|
|
319
|
+
var BrainMessageSchema = z4.union([
|
|
320
|
+
z4.object({ ...messageBase, to: z4.object({ kind: z4.literal("user") }).strict() }).strict(),
|
|
321
|
+
z4.object({
|
|
322
|
+
...messageBase,
|
|
323
|
+
to: z4.object({ kind: z4.literal("agent"), agentId: z4.string().regex(/^agent:(?!paigy$)\S+$/) }).strict(),
|
|
324
|
+
call: z4.object({ callId: id, listen: z4.literal(true) }).strict().optional()
|
|
325
|
+
}).strict()
|
|
326
|
+
]);
|
|
327
|
+
var BrainSearchSchema = z4.object({
|
|
328
|
+
query: z4.string().min(1).max(500),
|
|
329
|
+
within: z4.array(z4.enum(["entries", "goals", "answers"])).min(1),
|
|
330
|
+
goalId: id.optional()
|
|
331
|
+
}).strict();
|
|
332
|
+
var BrainResultSchema = z4.object({
|
|
333
|
+
messages: z4.array(BrainMessageSchema),
|
|
334
|
+
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
|
|
335
|
+
questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
|
|
336
|
+
answers: z4.array(z4.object({
|
|
337
|
+
question: ResultRefSchema,
|
|
338
|
+
entryKeys: z4.array(key).min(1),
|
|
339
|
+
summary: z4.string().min(1),
|
|
340
|
+
selectedOptionIds: z4.array(z4.string().min(1)).optional()
|
|
341
|
+
}).strict()),
|
|
342
|
+
goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
|
|
343
|
+
feedback: z4.array(z4.object({ entryKeys: z4.array(key).min(1) }).strict()),
|
|
344
|
+
lessons: z4.array(z4.object({ entryKeys: z4.array(key).min(1), change: LessonChangeSchema }).strict()),
|
|
345
|
+
next: BrainNextSchema,
|
|
346
|
+
search: BrainSearchSchema.optional()
|
|
347
|
+
}).strict();
|
|
348
|
+
var str = z4.string();
|
|
349
|
+
var strs = z4.array(z4.string());
|
|
350
|
+
var CompactResultSchema = z4.object({
|
|
351
|
+
next: z4.object({ say: strs, then: z4.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason: str, instructions: strs }).strict(),
|
|
352
|
+
messages: z4.array(z4.object({ key: str, to: str, text: str, about: strs, entries: strs, owed: strs, inviteToCall: z4.boolean() }).strict()),
|
|
353
|
+
entries: z4.array(z4.object({ key: str, from: str, on: strs }).strict()),
|
|
354
|
+
answers: z4.array(z4.object({ question: str, entries: strs, summary: str, picked: strs }).strict()),
|
|
355
|
+
changes: z4.array(z4.object({
|
|
356
|
+
key: str,
|
|
357
|
+
what: z4.enum(["goal", "question", "lesson"]),
|
|
358
|
+
op: z4.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
|
|
359
|
+
id: str,
|
|
360
|
+
text: str,
|
|
361
|
+
title: str,
|
|
362
|
+
who: str,
|
|
363
|
+
under: str,
|
|
364
|
+
gate: z4.enum(["start", "complete", "answer", "none"]),
|
|
365
|
+
options: z4.array(z4.object({ id: str, label: str }).strict()),
|
|
366
|
+
pick: z4.enum(["one", "many", "rank", "words"]),
|
|
367
|
+
entries: strs
|
|
368
|
+
}).strict()),
|
|
369
|
+
feedback: z4.array(strs),
|
|
370
|
+
search: z4.object({ query: str, within: strs, goal: str }).strict()
|
|
371
|
+
}).strict();
|
|
372
|
+
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
373
|
+
zodToJsonSchema2(CompactResultSchema, { $refStrategy: "none" })
|
|
374
|
+
);
|
|
170
375
|
function entryWords(entry) {
|
|
171
376
|
const content = entry.content;
|
|
172
377
|
if (content && "sealed" in content) return "";
|
|
@@ -179,22 +384,24 @@ function entryWords(entry) {
|
|
|
179
384
|
const description = plain.description;
|
|
180
385
|
const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
|
|
181
386
|
if (parts.length) return parts.join("\n\n");
|
|
387
|
+
const line = plain.line;
|
|
388
|
+
if (typeof line === "string" && line.trim()) return line;
|
|
182
389
|
}
|
|
183
390
|
return entry.sources.map((source) => source.text).join("\n");
|
|
184
391
|
}
|
|
185
392
|
var LIVE_MS = 3 * 6e4;
|
|
186
393
|
var WORKING_MS = 60 * 6e4;
|
|
187
|
-
var ContextSchema =
|
|
188
|
-
title:
|
|
189
|
-
description:
|
|
394
|
+
var ContextSchema = z5.object({
|
|
395
|
+
title: z5.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
396
|
+
description: z5.array(z5.string().min(1)).describe(
|
|
190
397
|
"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."
|
|
191
398
|
)
|
|
192
399
|
});
|
|
193
|
-
var ParticipantSchema =
|
|
194
|
-
kind:
|
|
195
|
-
id:
|
|
400
|
+
var ParticipantSchema = z5.object({
|
|
401
|
+
kind: z5.enum(["human", "agent"]),
|
|
402
|
+
id: z5.string()
|
|
196
403
|
});
|
|
197
|
-
var TransformSchema =
|
|
404
|
+
var TransformSchema = z5.enum([
|
|
198
405
|
"structure",
|
|
199
406
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
200
407
|
"request_more",
|
|
@@ -210,13 +417,13 @@ var TransformSchema = z4.enum([
|
|
|
210
417
|
"summarize"
|
|
211
418
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
212
419
|
]);
|
|
213
|
-
var VisualSchema =
|
|
214
|
-
url:
|
|
215
|
-
label:
|
|
420
|
+
var VisualSchema = z5.object({
|
|
421
|
+
url: z5.string().url(),
|
|
422
|
+
label: z5.string().optional()
|
|
216
423
|
});
|
|
217
|
-
var NotifyLevelSchema =
|
|
218
|
-
var SelectShapeSchema =
|
|
219
|
-
var ReceiptEventSchema =
|
|
424
|
+
var NotifyLevelSchema = z5.enum(["inbox", "push", "banner", "call"]);
|
|
425
|
+
var SelectShapeSchema = z5.enum(["one", "many", "rank", "confirm", "text"]);
|
|
426
|
+
var ReceiptEventSchema = z5.enum([
|
|
220
427
|
"delivered",
|
|
221
428
|
// the bundle reached the recipient at some level
|
|
222
429
|
"seen",
|
|
@@ -246,47 +453,47 @@ var ReceiptEventSchema = z4.enum([
|
|
|
246
453
|
// be rewound by a writer that forgot to advance it.
|
|
247
454
|
"restarted"
|
|
248
455
|
]);
|
|
249
|
-
var AttentionSchema =
|
|
456
|
+
var AttentionSchema = z5.object({
|
|
250
457
|
urgency: NotifyLevelSchema,
|
|
251
458
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
252
459
|
select: SelectShapeSchema.nullable(),
|
|
253
460
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
254
|
-
points:
|
|
461
|
+
points: z5.array(z5.string()).nullable(),
|
|
255
462
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
256
|
-
blocking:
|
|
463
|
+
blocking: z5.boolean(),
|
|
257
464
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
258
465
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
259
|
-
deadline:
|
|
466
|
+
deadline: z5.string().datetime().nullable().optional()
|
|
260
467
|
});
|
|
261
|
-
var NotifyRequestFields =
|
|
468
|
+
var NotifyRequestFields = z5.object({
|
|
262
469
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
263
470
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
264
471
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
265
472
|
context: ContextSchema.optional(),
|
|
266
|
-
options:
|
|
473
|
+
options: z5.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
267
474
|
"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)."
|
|
268
475
|
),
|
|
269
|
-
points:
|
|
476
|
+
points: z5.array(z5.string().min(1)).optional().describe(
|
|
270
477
|
"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."
|
|
271
478
|
),
|
|
272
|
-
visuals:
|
|
479
|
+
visuals: z5.array(VisualSchema).optional().describe(
|
|
273
480
|
"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."
|
|
274
481
|
),
|
|
275
482
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
276
|
-
repo:
|
|
483
|
+
repo: z5.string().optional(),
|
|
277
484
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
278
|
-
branch:
|
|
485
|
+
branch: z5.string().optional(),
|
|
279
486
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
280
487
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
281
488
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
282
489
|
* still accepts the old name from older clients. */
|
|
283
|
-
parentId:
|
|
490
|
+
parentId: z5.string().uuid().optional(),
|
|
284
491
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
285
492
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
286
|
-
workId:
|
|
493
|
+
workId: z5.string().uuid().optional(),
|
|
287
494
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
288
495
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
289
|
-
goalId:
|
|
496
|
+
goalId: z5.string().uuid().optional(),
|
|
290
497
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
291
498
|
"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."
|
|
292
499
|
),
|
|
@@ -294,7 +501,7 @@ var NotifyRequestFields = z4.object({
|
|
|
294
501
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
295
502
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
296
503
|
* says HOW. */
|
|
297
|
-
clarifies:
|
|
504
|
+
clarifies: z5.string().optional(),
|
|
298
505
|
select: SelectShapeSchema.optional().describe(
|
|
299
506
|
"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."
|
|
300
507
|
),
|
|
@@ -308,20 +515,20 @@ var NotifyRequestFields = z4.object({
|
|
|
308
515
|
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
309
516
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
310
517
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
311
|
-
ask:
|
|
518
|
+
ask: z5.string().min(1).max(1e4).optional().describe(
|
|
312
519
|
'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.'
|
|
313
520
|
),
|
|
314
|
-
needs:
|
|
521
|
+
needs: z5.array(z5.string().min(1)).optional().describe(
|
|
315
522
|
"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."
|
|
316
523
|
),
|
|
317
|
-
urgencyHint:
|
|
524
|
+
urgencyHint: z5.enum(["whenever", "soon", "now"]).optional().describe(
|
|
318
525
|
"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."
|
|
319
526
|
),
|
|
320
527
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
321
528
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
322
529
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
323
530
|
* urgencyHint/blocking win when both are sent. */
|
|
324
|
-
waiting:
|
|
531
|
+
waiting: z5.enum(["none", "soft", "hard"]).optional().describe(
|
|
325
532
|
"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."
|
|
326
533
|
),
|
|
327
534
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -329,98 +536,98 @@ var NotifyRequestFields = z4.object({
|
|
|
329
536
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
330
537
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
331
538
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
332
|
-
confirm:
|
|
539
|
+
confirm: z5.boolean().optional().describe(
|
|
333
540
|
"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'."
|
|
334
541
|
),
|
|
335
542
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
336
543
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
337
544
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
338
545
|
* waiting:'hard'. */
|
|
339
|
-
channel:
|
|
546
|
+
channel: z5.enum(["call", "message"]).optional().describe(
|
|
340
547
|
"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."
|
|
341
548
|
),
|
|
342
|
-
confirmStyle:
|
|
549
|
+
confirmStyle: z5.enum(["yesno", "approve"]).default("yesno").describe(
|
|
343
550
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
344
551
|
),
|
|
345
|
-
blocking:
|
|
552
|
+
blocking: z5.boolean().default(false).describe(
|
|
346
553
|
"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."
|
|
347
554
|
)
|
|
348
555
|
});
|
|
349
556
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
350
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
557
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
351
558
|
if (r.ask !== void 0) {
|
|
352
559
|
for (const f of ["context", "select", "points"]) {
|
|
353
560
|
if (r[f] !== void 0)
|
|
354
|
-
ctx.addIssue({ code:
|
|
561
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
355
562
|
}
|
|
356
563
|
return;
|
|
357
564
|
}
|
|
358
565
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
359
|
-
ctx.addIssue({ code:
|
|
566
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
360
567
|
if (!r.context)
|
|
361
|
-
ctx.addIssue({ code:
|
|
568
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
362
569
|
if (!r.select)
|
|
363
|
-
ctx.addIssue({ code:
|
|
570
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
364
571
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
365
572
|
if (needsOptions && !r.options?.length)
|
|
366
|
-
ctx.addIssue({ code:
|
|
573
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
367
574
|
if (!needsOptions && r.options?.length)
|
|
368
|
-
ctx.addIssue({ code:
|
|
575
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
369
576
|
});
|
|
370
|
-
var NotifyStatusSchema =
|
|
371
|
-
var AgentStateSchema =
|
|
372
|
-
var TurnSchema =
|
|
373
|
-
prompt:
|
|
374
|
-
reply:
|
|
577
|
+
var NotifyStatusSchema = z5.enum(["pending", "answered", "ignored"]);
|
|
578
|
+
var AgentStateSchema = z5.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
579
|
+
var TurnSchema = z5.object({
|
|
580
|
+
prompt: z5.string(),
|
|
581
|
+
reply: z5.string()
|
|
375
582
|
});
|
|
376
|
-
var UserAnswerSchema =
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
583
|
+
var UserAnswerSchema = z5.discriminatedUnion("kind", [
|
|
584
|
+
z5.object({ kind: z5.literal("option"), optionId: z5.string(), label: z5.string().optional() }),
|
|
585
|
+
z5.object({ kind: z5.literal("text"), text: z5.string() }),
|
|
586
|
+
z5.object({ kind: z5.literal("ignored") }),
|
|
587
|
+
z5.object({ kind: z5.literal("multi"), optionIds: z5.array(z5.string()), labels: z5.array(z5.string()).optional() }),
|
|
588
|
+
z5.object({ kind: z5.literal("ranked"), optionIds: z5.array(z5.string()), labels: z5.array(z5.string()).optional() }),
|
|
589
|
+
z5.object({ kind: z5.literal("clarify"), chunks: z5.array(z5.string()).min(1) }),
|
|
590
|
+
z5.object({ kind: z5.literal("confirm"), approved: z5.boolean() }),
|
|
591
|
+
z5.object({ kind: z5.literal("turns"), turns: z5.array(TurnSchema).min(1) }),
|
|
385
592
|
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
386
593
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
387
594
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
388
595
|
* card the user can reply to — the broker never overrides the user. */
|
|
389
|
-
|
|
596
|
+
z5.object({ kind: z5.literal("precedent"), answer: z5.string(), derivation: z5.string(), sources: z5.array(z5.string()).min(1) })
|
|
390
597
|
]);
|
|
391
|
-
var IntentSchema =
|
|
598
|
+
var IntentSchema = z5.object({
|
|
392
599
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
393
600
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
394
601
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
395
602
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
396
|
-
kind:
|
|
397
|
-
detail:
|
|
603
|
+
kind: z5.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
604
|
+
detail: z5.string(),
|
|
398
605
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
399
606
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
400
607
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
401
608
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
402
|
-
dueInSeconds:
|
|
609
|
+
dueInSeconds: z5.number().int().positive().optional(),
|
|
403
610
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
404
|
-
* already read the utterance, so `
|
|
611
|
+
* already read the utterance, so `signals.kind` stops defaulting to
|
|
405
612
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
406
613
|
* about what cannot answer "is the bot looping less this week?". */
|
|
407
|
-
fault:
|
|
614
|
+
fault: z5.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
408
615
|
});
|
|
409
|
-
var RideAlongSchema =
|
|
616
|
+
var RideAlongSchema = z5.object({
|
|
410
617
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
411
|
-
noteId:
|
|
618
|
+
noteId: z5.string(),
|
|
412
619
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
413
|
-
text:
|
|
620
|
+
text: z5.string(),
|
|
414
621
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
415
|
-
parentId:
|
|
622
|
+
parentId: z5.string().nullable()
|
|
416
623
|
});
|
|
417
|
-
var AwaitItemSchema =
|
|
418
|
-
|
|
419
|
-
type:
|
|
420
|
-
parentId:
|
|
421
|
-
notificationId:
|
|
422
|
-
workId:
|
|
423
|
-
decisionId:
|
|
624
|
+
var AwaitItemSchema = z5.discriminatedUnion("type", [
|
|
625
|
+
z5.object({
|
|
626
|
+
type: z5.literal("reply"),
|
|
627
|
+
parentId: z5.string(),
|
|
628
|
+
notificationId: z5.string(),
|
|
629
|
+
workId: z5.string().uuid().optional(),
|
|
630
|
+
decisionId: z5.string().uuid().optional(),
|
|
424
631
|
answer: UserAnswerSchema,
|
|
425
632
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
426
633
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -430,103 +637,103 @@ var AwaitItemSchema = z4.discriminatedUnion("type", [
|
|
|
430
637
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
431
638
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
432
639
|
* a sentence with a hole in it is worse than no sentence. */
|
|
433
|
-
note:
|
|
640
|
+
note: z5.string().optional(),
|
|
434
641
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
435
642
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
436
643
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
437
644
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
438
|
-
transcript:
|
|
645
|
+
transcript: z5.string().optional(),
|
|
439
646
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
440
647
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
441
|
-
covered:
|
|
648
|
+
covered: z5.array(z5.string()).optional(),
|
|
442
649
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
443
650
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
444
651
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
445
652
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
446
653
|
* dropped. Absent/empty = nothing owed. */
|
|
447
|
-
also:
|
|
654
|
+
also: z5.array(RideAlongSchema).optional()
|
|
448
655
|
}),
|
|
449
|
-
|
|
450
|
-
type:
|
|
451
|
-
parentId:
|
|
452
|
-
notificationId:
|
|
453
|
-
remindAt:
|
|
656
|
+
z5.object({
|
|
657
|
+
type: z5.literal("remind"),
|
|
658
|
+
parentId: z5.string(),
|
|
659
|
+
notificationId: z5.string(),
|
|
660
|
+
remindAt: z5.string().datetime({ offset: true }),
|
|
454
661
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
455
|
-
remindInSeconds:
|
|
662
|
+
remindInSeconds: z5.number()
|
|
456
663
|
}),
|
|
457
664
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
458
665
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
459
666
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
460
|
-
* re-orient via
|
|
461
|
-
|
|
462
|
-
type:
|
|
463
|
-
parentId:
|
|
464
|
-
notificationId:
|
|
667
|
+
* re-orient via contact({})). */
|
|
668
|
+
z5.object({
|
|
669
|
+
type: z5.literal("superseded"),
|
|
670
|
+
parentId: z5.string(),
|
|
671
|
+
notificationId: z5.string()
|
|
465
672
|
}),
|
|
466
673
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
467
674
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
468
675
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
469
676
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
470
677
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
471
|
-
|
|
472
|
-
type:
|
|
473
|
-
notificationId:
|
|
474
|
-
inFlight:
|
|
475
|
-
turn:
|
|
476
|
-
idx:
|
|
477
|
-
prompt:
|
|
478
|
-
reply:
|
|
479
|
-
acts:
|
|
678
|
+
z5.object({
|
|
679
|
+
type: z5.literal("partial"),
|
|
680
|
+
notificationId: z5.string(),
|
|
681
|
+
inFlight: z5.literal(true),
|
|
682
|
+
turn: z5.object({
|
|
683
|
+
idx: z5.number(),
|
|
684
|
+
prompt: z5.string(),
|
|
685
|
+
reply: z5.string(),
|
|
686
|
+
acts: z5.array(IntentSchema).nullable().optional()
|
|
480
687
|
})
|
|
481
688
|
}),
|
|
482
|
-
|
|
483
|
-
type:
|
|
484
|
-
also:
|
|
689
|
+
z5.object({
|
|
690
|
+
type: z5.literal("idle"),
|
|
691
|
+
also: z5.array(RideAlongSchema).optional(),
|
|
485
692
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
486
693
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
487
694
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
488
695
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
489
696
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
490
|
-
inFlight:
|
|
697
|
+
inFlight: z5.boolean().optional()
|
|
491
698
|
})
|
|
492
699
|
]);
|
|
493
|
-
var VoiceKeySchema =
|
|
494
|
-
var AgendaTurnSchema =
|
|
700
|
+
var VoiceKeySchema = z5.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
701
|
+
var AgendaTurnSchema = z5.object({
|
|
495
702
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
496
703
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
497
704
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
498
705
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
499
706
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
500
707
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
501
|
-
id:
|
|
708
|
+
id: z5.string().optional(),
|
|
502
709
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
503
710
|
* the planner declares duplicates instead of asking them twice. */
|
|
504
|
-
coveredIds:
|
|
711
|
+
coveredIds: z5.array(z5.string()).optional(),
|
|
505
712
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
506
713
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
507
|
-
info:
|
|
508
|
-
question:
|
|
714
|
+
info: z5.array(z5.string().min(1)).default([]),
|
|
715
|
+
question: z5.string().min(1).nullable(),
|
|
509
716
|
/** True on the one turn carrying the agent's own declared question. */
|
|
510
|
-
asks:
|
|
717
|
+
asks: z5.boolean().optional(),
|
|
511
718
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
512
719
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
513
720
|
* which route nothing. */
|
|
514
|
-
claimId:
|
|
721
|
+
claimId: z5.string().optional(),
|
|
515
722
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
516
|
-
voice:
|
|
723
|
+
voice: z5.string().optional(),
|
|
517
724
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
518
725
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
519
726
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
520
727
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
521
|
-
agent:
|
|
728
|
+
agent: z5.string().optional(),
|
|
522
729
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
523
730
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
524
731
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
525
732
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
526
733
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
527
|
-
agentId:
|
|
734
|
+
agentId: z5.string().optional(),
|
|
528
735
|
select: SelectShapeSchema.optional(),
|
|
529
|
-
options:
|
|
736
|
+
options: z5.array(OptionSchema.omit({ id: true })).optional(),
|
|
530
737
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
531
738
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
532
739
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -535,33 +742,33 @@ var AgendaTurnSchema = z4.object({
|
|
|
535
742
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
536
743
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
537
744
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
538
|
-
blocking:
|
|
745
|
+
blocking: z5.boolean().optional(),
|
|
539
746
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
540
747
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
541
748
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
542
749
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
543
750
|
* speak, it is dropped and their words are taken like any reply. */
|
|
544
|
-
ifSilent:
|
|
751
|
+
ifSilent: z5.boolean().optional()
|
|
545
752
|
});
|
|
546
753
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
547
|
-
var InboxItemSchema =
|
|
548
|
-
id:
|
|
549
|
-
tokenId:
|
|
754
|
+
var InboxItemSchema = z5.object({
|
|
755
|
+
id: z5.string(),
|
|
756
|
+
tokenId: z5.string().optional(),
|
|
550
757
|
status: NotifyStatusSchema,
|
|
551
758
|
context: ContextSchema,
|
|
552
|
-
options:
|
|
759
|
+
options: z5.array(OptionSchema).optional(),
|
|
553
760
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
554
|
-
points:
|
|
761
|
+
points: z5.array(z5.string()).optional(),
|
|
555
762
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
556
763
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
557
764
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
558
765
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
559
|
-
asks:
|
|
766
|
+
asks: z5.boolean().optional(),
|
|
560
767
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
561
768
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
562
769
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
563
770
|
* which must never CLAIM stalled. */
|
|
564
|
-
lastSeenAt:
|
|
771
|
+
lastSeenAt: z5.string().optional(),
|
|
565
772
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
566
773
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
567
774
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -571,7 +778,7 @@ var InboxItemSchema = z4.object({
|
|
|
571
778
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
572
779
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
573
780
|
* `createdAt`. */
|
|
574
|
-
agentStateAt:
|
|
781
|
+
agentStateAt: z5.string().datetime().optional(),
|
|
575
782
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
576
783
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
577
784
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -585,54 +792,54 @@ var InboxItemSchema = z4.object({
|
|
|
585
792
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
586
793
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
587
794
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
588
|
-
questions:
|
|
589
|
-
id:
|
|
590
|
-
entryId:
|
|
591
|
-
title:
|
|
592
|
-
state:
|
|
593
|
-
answer:
|
|
795
|
+
questions: z5.array(z5.object({
|
|
796
|
+
id: z5.string(),
|
|
797
|
+
entryId: z5.string(),
|
|
798
|
+
title: z5.string(),
|
|
799
|
+
state: z5.enum(["open", "answered"]),
|
|
800
|
+
answer: z5.string().nullable(),
|
|
594
801
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
595
802
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
596
803
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
597
804
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
598
|
-
agent:
|
|
805
|
+
agent: z5.object({ tokenId: z5.string(), name: z5.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
599
806
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
600
807
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
601
808
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
602
809
|
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
603
810
|
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
604
|
-
options:
|
|
811
|
+
options: z5.array(OptionSchema).optional()
|
|
605
812
|
})).optional(),
|
|
606
|
-
visuals:
|
|
813
|
+
visuals: z5.array(VisualSchema).optional(),
|
|
607
814
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
608
815
|
* agent's suggestion, or a default silly name). */
|
|
609
|
-
name:
|
|
816
|
+
name: z5.string(),
|
|
610
817
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
611
818
|
voice: VoiceKeySchema.optional(),
|
|
612
|
-
repo:
|
|
613
|
-
branch:
|
|
614
|
-
createdAt:
|
|
615
|
-
snoozedUntil:
|
|
819
|
+
repo: z5.string().optional(),
|
|
820
|
+
branch: z5.string().optional(),
|
|
821
|
+
createdAt: z5.string().datetime(),
|
|
822
|
+
snoozedUntil: z5.string().datetime().optional(),
|
|
616
823
|
agentState: AgentStateSchema.default("idle"),
|
|
617
824
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
618
825
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
619
826
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
620
|
-
turn:
|
|
827
|
+
turn: z5.enum(["you", "agent"]).default("you"),
|
|
621
828
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
622
829
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
623
830
|
* error badge + Retry. */
|
|
624
|
-
error:
|
|
831
|
+
error: z5.string().optional(),
|
|
625
832
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
626
833
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
627
834
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
628
835
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
629
|
-
cold:
|
|
630
|
-
clarifies:
|
|
836
|
+
cold: z5.string().datetime().optional(),
|
|
837
|
+
clarifies: z5.string().optional(),
|
|
631
838
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
632
839
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
633
840
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
634
841
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
635
|
-
onCall:
|
|
842
|
+
onCall: z5.literal(true).optional(),
|
|
636
843
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
637
844
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
638
845
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -642,31 +849,31 @@ var InboxItemSchema = z4.object({
|
|
|
642
849
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
643
850
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
644
851
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
645
|
-
ring:
|
|
646
|
-
declinedAt:
|
|
647
|
-
anchorAt:
|
|
648
|
-
nextRingAt:
|
|
649
|
-
step:
|
|
852
|
+
ring: z5.object({
|
|
853
|
+
declinedAt: z5.string().datetime(),
|
|
854
|
+
anchorAt: z5.string().datetime(),
|
|
855
|
+
nextRingAt: z5.string().datetime().nullable(),
|
|
856
|
+
step: z5.number().int()
|
|
650
857
|
}).optional(),
|
|
651
858
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
652
859
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
653
860
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
654
861
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
655
862
|
* feedback ABOUT. */
|
|
656
|
-
why:
|
|
863
|
+
why: z5.object({
|
|
657
864
|
asked: NotifyLevelSchema,
|
|
658
865
|
got: NotifyLevelSchema,
|
|
659
|
-
because:
|
|
660
|
-
line:
|
|
866
|
+
because: z5.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
867
|
+
line: z5.string()
|
|
661
868
|
}).optional(),
|
|
662
|
-
select:
|
|
663
|
-
confirmStyle:
|
|
869
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
870
|
+
confirmStyle: z5.enum(["yesno", "approve"]).default("yesno").describe(
|
|
664
871
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
665
872
|
),
|
|
666
873
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
667
874
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
668
875
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
669
|
-
blocking:
|
|
876
|
+
blocking: z5.boolean().default(false),
|
|
670
877
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
671
878
|
answer: UserAnswerSchema.optional(),
|
|
672
879
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -674,38 +881,49 @@ var InboxItemSchema = z4.object({
|
|
|
674
881
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
675
882
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
676
883
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
677
|
-
communication:
|
|
678
|
-
deliveryId:
|
|
679
|
-
kind:
|
|
680
|
-
entryId:
|
|
681
|
-
goalIds:
|
|
682
|
-
decisionNeedId:
|
|
683
|
-
sealed:
|
|
684
|
-
goalState:
|
|
884
|
+
communication: z5.object({
|
|
885
|
+
deliveryId: z5.string(),
|
|
886
|
+
kind: z5.enum(["notification", "call"]),
|
|
887
|
+
entryId: z5.string(),
|
|
888
|
+
goalIds: z5.array(z5.string()),
|
|
889
|
+
decisionNeedId: z5.string().optional(),
|
|
890
|
+
sealed: z5.boolean(),
|
|
891
|
+
goalState: z5.string().optional(),
|
|
685
892
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
686
893
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
687
|
-
goalTitle:
|
|
688
|
-
}).optional()
|
|
894
|
+
goalTitle: z5.string().optional()
|
|
895
|
+
}).optional(),
|
|
896
|
+
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
897
|
+
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
898
|
+
* how the incremental read knows a card has not moved: the phone echoes back the revs it holds
|
|
899
|
+
* (`POST /api/inbox/changes`) and is sent only the cards whose rev differs.
|
|
900
|
+
*
|
|
901
|
+
* THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK — the same mechanism as `QueueItem.rev`
|
|
902
|
+
* (#2928) and an entry's (#3018), for the same reason: a card shows facts no `updated_at` of its
|
|
903
|
+
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
904
|
+
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
905
|
+
* with no rev is simply always re-sent. */
|
|
906
|
+
rev: z5.string().optional()
|
|
689
907
|
});
|
|
690
908
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
691
|
-
var PushTokenSchema =
|
|
692
|
-
voipToken:
|
|
693
|
-
alertToken:
|
|
694
|
-
fcmToken:
|
|
695
|
-
platform:
|
|
909
|
+
var PushTokenSchema = z5.object({
|
|
910
|
+
voipToken: z5.string().min(1).optional(),
|
|
911
|
+
alertToken: z5.string().min(1).optional(),
|
|
912
|
+
fcmToken: z5.string().min(1).optional(),
|
|
913
|
+
platform: z5.enum(["ios", "android"])
|
|
696
914
|
}).superRefine((v, ctx) => {
|
|
697
915
|
if (v.platform !== "ios") return;
|
|
698
916
|
for (const field of ["voipToken", "alertToken"]) {
|
|
699
917
|
const token = v[field];
|
|
700
918
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
701
919
|
ctx.addIssue({
|
|
702
|
-
code:
|
|
920
|
+
code: z5.ZodIssueCode.custom,
|
|
703
921
|
path: [field],
|
|
704
922
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
705
923
|
});
|
|
706
924
|
}
|
|
707
925
|
});
|
|
708
|
-
var MissedCallSchema =
|
|
926
|
+
var MissedCallSchema = z5.enum([
|
|
709
927
|
"retry_10m",
|
|
710
928
|
"retry_30m",
|
|
711
929
|
"retry_60m",
|
|
@@ -717,31 +935,31 @@ var MissedCallSchema = z4.enum([
|
|
|
717
935
|
]);
|
|
718
936
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
719
937
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
720
|
-
var BrokerTuningSchema =
|
|
938
|
+
var BrokerTuningSchema = z5.object({
|
|
721
939
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
722
|
-
ackVerbosity:
|
|
940
|
+
ackVerbosity: z5.enum(["normal", "none"]).optional(),
|
|
723
941
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
724
942
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
725
|
-
clarifyEagerness:
|
|
943
|
+
clarifyEagerness: z5.enum(["low", "normal", "high"]).optional(),
|
|
726
944
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
727
|
-
phrasebook:
|
|
945
|
+
phrasebook: z5.array(z5.object({ say: z5.string().min(1).max(60), mean: z5.string().min(1).max(120) })).max(24).optional(),
|
|
728
946
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
729
947
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
730
948
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
731
949
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
732
950
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
733
|
-
language:
|
|
951
|
+
language: z5.enum(["en", "es"]).optional()
|
|
734
952
|
});
|
|
735
|
-
var UserSettingsSchema =
|
|
736
|
-
permissions:
|
|
737
|
-
call:
|
|
738
|
-
banner:
|
|
739
|
-
push:
|
|
953
|
+
var UserSettingsSchema = z5.object({
|
|
954
|
+
permissions: z5.object({
|
|
955
|
+
call: z5.boolean(),
|
|
956
|
+
banner: z5.boolean(),
|
|
957
|
+
push: z5.boolean()
|
|
740
958
|
}),
|
|
741
959
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
742
960
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
743
961
|
* what people read (owner, 2026-09-30). */
|
|
744
|
-
sessionMode:
|
|
962
|
+
sessionMode: z5.enum(["default", "all_calls", "silent"]),
|
|
745
963
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
746
964
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
747
965
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -750,7 +968,7 @@ var UserSettingsSchema = z4.object({
|
|
|
750
968
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
751
969
|
* next dead field for the same reason. */
|
|
752
970
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
753
|
-
improveConsent:
|
|
971
|
+
improveConsent: z5.boolean(),
|
|
754
972
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
755
973
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
756
974
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -758,17 +976,17 @@ var UserSettingsSchema = z4.object({
|
|
|
758
976
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
759
977
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
760
978
|
* write, 'hosted' on read (see store.ts). */
|
|
761
|
-
voiceMode:
|
|
979
|
+
voiceMode: z5.enum(["hosted", "on_device"]).optional(),
|
|
762
980
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
763
981
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
764
982
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
765
983
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
766
984
|
* read (see store.ts). */
|
|
767
|
-
talk:
|
|
985
|
+
talk: z5.boolean().optional(),
|
|
768
986
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
769
987
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
770
988
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
771
|
-
callDiagnostics:
|
|
989
|
+
callDiagnostics: z5.boolean().optional(),
|
|
772
990
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
773
991
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
774
992
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -776,7 +994,7 @@ var UserSettingsSchema = z4.object({
|
|
|
776
994
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
777
995
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
778
996
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
779
|
-
voiceTuning:
|
|
997
|
+
voiceTuning: z5.record(z5.string(), z5.union([z5.number(), z5.string()])).optional(),
|
|
780
998
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
781
999
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
782
1000
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -785,53 +1003,53 @@ var UserSettingsSchema = z4.object({
|
|
|
785
1003
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
786
1004
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
787
1005
|
* call — honest about what we know. */
|
|
788
|
-
timezone:
|
|
1006
|
+
timezone: z5.string().min(1).max(64).optional(),
|
|
789
1007
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
790
1008
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
791
1009
|
broker: BrokerTuningSchema.optional()
|
|
792
1010
|
});
|
|
793
|
-
var HistoryWorkSchema =
|
|
794
|
-
id:
|
|
795
|
-
title:
|
|
796
|
-
state:
|
|
1011
|
+
var HistoryWorkSchema = z5.object({
|
|
1012
|
+
id: z5.string(),
|
|
1013
|
+
title: z5.string(),
|
|
1014
|
+
state: z5.enum(["done", "cancelled"]),
|
|
797
1015
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
798
|
-
assignee:
|
|
1016
|
+
assignee: z5.string()
|
|
799
1017
|
});
|
|
800
|
-
var HistoryEntrySchema =
|
|
801
|
-
|
|
802
|
-
|
|
1018
|
+
var HistoryEntrySchema = z5.union([
|
|
1019
|
+
z5.object({ at: z5.string(), card: InboxItemSchema }),
|
|
1020
|
+
z5.object({ at: z5.string(), work: HistoryWorkSchema })
|
|
803
1021
|
]);
|
|
804
|
-
var HistoryPageSchema =
|
|
805
|
-
entries:
|
|
806
|
-
next:
|
|
1022
|
+
var HistoryPageSchema = z5.object({
|
|
1023
|
+
entries: z5.array(HistoryEntrySchema),
|
|
1024
|
+
next: z5.string().nullable()
|
|
807
1025
|
});
|
|
808
1026
|
var ACTIVITY_LINES = 2;
|
|
809
1027
|
var ACTIVITY_LINE_MAX = 80;
|
|
810
|
-
var AgentActivitySchema =
|
|
1028
|
+
var AgentActivitySchema = z5.object({
|
|
811
1029
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
812
|
-
lines:
|
|
1030
|
+
lines: z5.array(z5.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
813
1031
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
814
1032
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
815
|
-
at:
|
|
1033
|
+
at: z5.string().datetime()
|
|
816
1034
|
});
|
|
817
|
-
var ConnectionSummarySchema =
|
|
1035
|
+
var ConnectionSummarySchema = z5.object({
|
|
818
1036
|
/** The connection = the agent's token id (used to address a request). */
|
|
819
|
-
id:
|
|
1037
|
+
id: z5.string(),
|
|
820
1038
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
821
1039
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
822
1040
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
823
1041
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
824
|
-
kind:
|
|
1042
|
+
kind: z5.enum(["device", "agent"]).optional(),
|
|
825
1043
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
826
1044
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
827
1045
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
828
|
-
mintedByDevice:
|
|
829
|
-
device:
|
|
1046
|
+
mintedByDevice: z5.string().nullable().optional(),
|
|
1047
|
+
device: z5.string().nullable(),
|
|
830
1048
|
/** The agent's display name (the single pairing name). */
|
|
831
|
-
name:
|
|
1049
|
+
name: z5.string(),
|
|
832
1050
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
833
1051
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
834
|
-
provider:
|
|
1052
|
+
provider: z5.string().nullable(),
|
|
835
1053
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
836
1054
|
voice: VoiceKeySchema.nullable(),
|
|
837
1055
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -842,34 +1060,34 @@ var ConnectionSummarySchema = z4.object({
|
|
|
842
1060
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
843
1061
|
* set once must not overrule a rule they set about one agent. */
|
|
844
1062
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
845
|
-
createdAt:
|
|
1063
|
+
createdAt: z5.string().datetime(),
|
|
846
1064
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
847
1065
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
848
|
-
lastContactAt:
|
|
1066
|
+
lastContactAt: z5.string().datetime().nullable(),
|
|
849
1067
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
850
1068
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
851
|
-
lastSeenAt:
|
|
1069
|
+
lastSeenAt: z5.string().datetime().nullable().optional(),
|
|
852
1070
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
853
1071
|
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
854
1072
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
855
|
-
lastWorkedAt:
|
|
1073
|
+
lastWorkedAt: z5.string().datetime().nullable().optional(),
|
|
856
1074
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
857
1075
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
858
|
-
oldestReadyAt:
|
|
1076
|
+
oldestReadyAt: z5.string().datetime().nullable().optional(),
|
|
859
1077
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
860
1078
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
861
1079
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
862
|
-
runtime:
|
|
1080
|
+
runtime: z5.object({
|
|
863
1081
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
864
1082
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
865
|
-
version:
|
|
866
|
-
harnesses:
|
|
867
|
-
workspaces:
|
|
1083
|
+
version: z5.string().optional(),
|
|
1084
|
+
harnesses: z5.array(z5.object({ name: z5.string(), label: z5.string(), status: z5.string() })).optional(),
|
|
1085
|
+
workspaces: z5.array(z5.string()).optional(),
|
|
868
1086
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
869
1087
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
870
1088
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
871
1089
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
872
|
-
repos:
|
|
1090
|
+
repos: z5.array(z5.object({ path: z5.string(), remote: z5.string() })).optional()
|
|
873
1091
|
}).optional(),
|
|
874
1092
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
875
1093
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -878,173 +1096,156 @@ var ConnectionSummarySchema = z4.object({
|
|
|
878
1096
|
activity: AgentActivitySchema.optional(),
|
|
879
1097
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
880
1098
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
881
|
-
managed:
|
|
1099
|
+
managed: z5.boolean()
|
|
882
1100
|
});
|
|
883
|
-
var LedgerItemSchema =
|
|
884
|
-
var AgentLedgerSchema =
|
|
1101
|
+
var LedgerItemSchema = z5.object({ id: z5.string(), parentId: z5.string(), title: z5.string(), createdAt: z5.string() });
|
|
1102
|
+
var AgentLedgerSchema = z5.object({
|
|
885
1103
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
886
|
-
agent:
|
|
1104
|
+
agent: z5.object({ id: z5.string(), name: z5.string().nullable(), revokedAt: z5.string().nullable() }),
|
|
887
1105
|
/** Its own questions you have not answered. */
|
|
888
|
-
asks:
|
|
1106
|
+
asks: z5.array(LedgerItemSchema),
|
|
889
1107
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
890
|
-
answered:
|
|
1108
|
+
answered: z5.array(LedgerItemSchema),
|
|
891
1109
|
/** Requests you sent it that it never took. */
|
|
892
|
-
requests:
|
|
893
|
-
goals:
|
|
894
|
-
callbacks:
|
|
1110
|
+
requests: z5.array(LedgerItemSchema),
|
|
1111
|
+
goals: z5.array(z5.object({ id: z5.string(), outcome: z5.string(), state: z5.string() })),
|
|
1112
|
+
callbacks: z5.array(z5.object({ id: z5.string(), parentId: z5.string(), trigger: z5.string(), note: z5.string(), dueAt: z5.string().nullable() }))
|
|
895
1113
|
});
|
|
896
|
-
var ReassignResultSchema =
|
|
897
|
-
moved:
|
|
898
|
-
parentId:
|
|
1114
|
+
var ReassignResultSchema = z5.object({
|
|
1115
|
+
moved: z5.object({ asks: z5.number(), answered: z5.number(), requests: z5.number(), goals: z5.number(), callbacks: z5.number() }),
|
|
1116
|
+
parentId: z5.string().nullable()
|
|
899
1117
|
});
|
|
900
|
-
var
|
|
901
|
-
var
|
|
902
|
-
id:
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
/** The
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
* break is visible instead of a pin silently disappearing. */
|
|
917
|
-
pinBroken: z4.boolean(),
|
|
918
|
-
/** When the ruling was distilled. */
|
|
919
|
-
learnedAt: z4.string(),
|
|
920
|
-
/** Last time it answered an ask. Null = never fired. */
|
|
921
|
-
lastUsedAt: z4.string().nullable(),
|
|
922
|
-
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
923
|
-
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
924
|
-
usedCount: z4.number(),
|
|
925
|
-
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
926
|
-
confirms: z4.number(),
|
|
927
|
-
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
928
|
-
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
929
|
-
counters: z4.number(),
|
|
930
|
-
/** The agent that asked the question this move came from, when known. Null for a move
|
|
931
|
-
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
932
|
-
learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
|
|
1118
|
+
var LessonStateSchema = z5.enum(["active", "proposed", "retired"]);
|
|
1119
|
+
var LessonViewSchema = z5.object({
|
|
1120
|
+
id: z5.string(),
|
|
1121
|
+
text: z5.string(),
|
|
1122
|
+
state: LessonStateSchema,
|
|
1123
|
+
/** The Goal it is scoped to; null = the whole account. */
|
|
1124
|
+
scopeGoalId: z5.string().nullable(),
|
|
1125
|
+
goalTitle: z5.string().nullable(),
|
|
1126
|
+
version: z5.number(),
|
|
1127
|
+
pinned: z5.boolean(),
|
|
1128
|
+
/** When the person last wrote its text themselves. */
|
|
1129
|
+
editedAt: z5.string().nullable(),
|
|
1130
|
+
createdAt: z5.string(),
|
|
1131
|
+
updatedAt: z5.string(),
|
|
1132
|
+
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
1133
|
+
sources: z5.array(z5.object({ entryId: z5.string(), words: z5.string().nullable(), at: z5.string() }))
|
|
933
1134
|
});
|
|
934
|
-
var QueueQuestionSchema =
|
|
1135
|
+
var QueueQuestionSchema = z5.object({
|
|
935
1136
|
/** The decision need's id — what an answer is accepted against. */
|
|
936
|
-
id:
|
|
1137
|
+
id: z5.string(),
|
|
937
1138
|
/** The words that were asked, from the request Entry that asked them. */
|
|
938
|
-
question:
|
|
1139
|
+
question: z5.string(),
|
|
939
1140
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
940
1141
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
941
1142
|
* can answer. */
|
|
942
|
-
deliveryId:
|
|
1143
|
+
deliveryId: z5.string().nullable().default(null),
|
|
943
1144
|
/** The Entry the ruling is about. */
|
|
944
|
-
aboutId:
|
|
1145
|
+
aboutId: z5.string().nullable().default(null),
|
|
945
1146
|
/** Empty for a free-text question. */
|
|
946
|
-
options:
|
|
947
|
-
select:
|
|
948
|
-
askedAt:
|
|
1147
|
+
options: z5.array(OptionSchema).default([]),
|
|
1148
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
1149
|
+
askedAt: z5.string(),
|
|
949
1150
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
950
|
-
answeredAt:
|
|
1151
|
+
answeredAt: z5.string().nullable().default(null),
|
|
951
1152
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
952
1153
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
953
1154
|
* for a settled question whose reply carried nothing readable. */
|
|
954
|
-
answer:
|
|
1155
|
+
answer: z5.string().nullable().default(null),
|
|
955
1156
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
956
1157
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
957
1158
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
958
1159
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
959
1160
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
960
1161
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
961
|
-
goalId:
|
|
1162
|
+
goalId: z5.string(),
|
|
962
1163
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
963
1164
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
964
1165
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
965
1166
|
* gate at all. */
|
|
966
|
-
blocked:
|
|
1167
|
+
blocked: z5.boolean().default(false)
|
|
967
1168
|
});
|
|
968
|
-
var QueueReplySchema =
|
|
1169
|
+
var QueueReplySchema = z5.object({
|
|
969
1170
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
970
1171
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
971
1172
|
* walk can name it in its zoom. */
|
|
972
|
-
id:
|
|
1173
|
+
id: z5.string(),
|
|
973
1174
|
/** The Goal the note is on. */
|
|
974
|
-
goalId:
|
|
1175
|
+
goalId: z5.string(),
|
|
975
1176
|
/** What the note said. */
|
|
976
|
-
note:
|
|
1177
|
+
note: z5.string(),
|
|
977
1178
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
978
|
-
deliveryId:
|
|
979
|
-
requestEntryId:
|
|
980
|
-
askedAt:
|
|
1179
|
+
deliveryId: z5.string(),
|
|
1180
|
+
requestEntryId: z5.string(),
|
|
1181
|
+
askedAt: z5.string(),
|
|
981
1182
|
/** When the person last replied — the window's start. */
|
|
982
|
-
repliedAt:
|
|
1183
|
+
repliedAt: z5.string(),
|
|
983
1184
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
984
|
-
reply:
|
|
1185
|
+
reply: z5.string().nullable()
|
|
985
1186
|
});
|
|
986
|
-
var QueueItemSchema =
|
|
987
|
-
id:
|
|
1187
|
+
var QueueItemSchema = z5.object({
|
|
1188
|
+
id: z5.string(),
|
|
988
1189
|
/** One-line headline — the first sentence of the outcome. */
|
|
989
|
-
title:
|
|
1190
|
+
title: z5.string(),
|
|
990
1191
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
991
|
-
intent:
|
|
1192
|
+
intent: z5.string(),
|
|
992
1193
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
993
|
-
state:
|
|
1194
|
+
state: z5.string(),
|
|
994
1195
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
995
|
-
assignee:
|
|
1196
|
+
assignee: z5.string().nullable().default(null),
|
|
996
1197
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
997
|
-
progress:
|
|
1198
|
+
progress: z5.string().nullable().default(null),
|
|
998
1199
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
999
1200
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
1000
|
-
progressLine:
|
|
1001
|
-
reviewPending:
|
|
1002
|
-
dueAt:
|
|
1201
|
+
progressLine: z5.string().nullable().optional(),
|
|
1202
|
+
reviewPending: z5.boolean().default(false),
|
|
1203
|
+
dueAt: z5.string().nullable().default(null),
|
|
1003
1204
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
1004
1205
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
1005
|
-
finishedAt:
|
|
1206
|
+
finishedAt: z5.string().nullable().optional(),
|
|
1006
1207
|
/** The Goal this one was opened under; null at the root. */
|
|
1007
|
-
parentGoalId:
|
|
1208
|
+
parentGoalId: z5.string().nullable().default(null),
|
|
1008
1209
|
/** Goals opened under this one — only those the same list holds. */
|
|
1009
|
-
childGoalIds:
|
|
1210
|
+
childGoalIds: z5.array(z5.string()).default([]),
|
|
1010
1211
|
/** Goals this one waits on (start or finish gates). */
|
|
1011
|
-
dependencyGoalIds:
|
|
1212
|
+
dependencyGoalIds: z5.array(z5.string()).default([]),
|
|
1012
1213
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
1013
|
-
blocked:
|
|
1214
|
+
blocked: z5.boolean().default(false),
|
|
1014
1215
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
1015
1216
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
1016
1217
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
1017
1218
|
* question is opened. */
|
|
1018
|
-
questions:
|
|
1219
|
+
questions: z5.array(QueueQuestionSchema).default([]),
|
|
1019
1220
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
1020
1221
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
1021
1222
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
1022
1223
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
1023
|
-
asked:
|
|
1024
|
-
answered:
|
|
1224
|
+
asked: z5.number().optional(),
|
|
1225
|
+
answered: z5.number().optional(),
|
|
1025
1226
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
1026
1227
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
1027
1228
|
* not spell an empty list. */
|
|
1028
|
-
replies:
|
|
1229
|
+
replies: z5.array(QueueReplySchema).optional(),
|
|
1029
1230
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
1030
|
-
repo:
|
|
1031
|
-
createdAt:
|
|
1032
|
-
updatedAt:
|
|
1231
|
+
repo: z5.string().nullable().optional(),
|
|
1232
|
+
createdAt: z5.string(),
|
|
1233
|
+
updatedAt: z5.string().nullable().default(null),
|
|
1033
1234
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
1034
1235
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
1035
1236
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
1036
|
-
lastProgressAt:
|
|
1237
|
+
lastProgressAt: z5.string().nullable().optional(),
|
|
1037
1238
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
1038
1239
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
1039
1240
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
1040
1241
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
1041
1242
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
1042
1243
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
1043
|
-
latest:
|
|
1044
|
-
from:
|
|
1045
|
-
said:
|
|
1046
|
-
at:
|
|
1047
|
-
entryId:
|
|
1244
|
+
latest: z5.object({
|
|
1245
|
+
from: z5.enum(["person", "agent"]),
|
|
1246
|
+
said: z5.string(),
|
|
1247
|
+
at: z5.string(),
|
|
1248
|
+
entryId: z5.string()
|
|
1048
1249
|
}).nullable().optional(),
|
|
1049
1250
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
1050
1251
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -1057,36 +1258,51 @@ var QueueItemSchema = z4.object({
|
|
|
1057
1258
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
1058
1259
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
1059
1260
|
* Entry with no `about_id` is in neither. */
|
|
1060
|
-
lastPersonAt:
|
|
1261
|
+
lastPersonAt: z5.string().nullable().optional(),
|
|
1262
|
+
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
1263
|
+
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
1264
|
+
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
1265
|
+
* (`POST /api/goals/changes`) and is sent only the rows whose rev differs.
|
|
1266
|
+
*
|
|
1267
|
+
* IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
|
|
1268
|
+
* writes change which field — the comparison is over the bytes the phone is holding, so a fact
|
|
1269
|
+
* the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
|
|
1270
|
+
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
1271
|
+
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
1272
|
+
rev: z5.string().optional()
|
|
1273
|
+
});
|
|
1274
|
+
var QueueDeltaSchema = z5.object({
|
|
1275
|
+
ids: z5.array(z5.string()),
|
|
1276
|
+
items: z5.array(QueueItemSchema)
|
|
1061
1277
|
});
|
|
1062
1278
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
1063
|
-
var NoteSourceSchema =
|
|
1064
|
-
var NoteStatusSchema =
|
|
1065
|
-
var NoteRepeatSchema =
|
|
1066
|
-
var DecisionSchema =
|
|
1067
|
-
id:
|
|
1279
|
+
var NoteSourceSchema = z5.enum(["app", "call"]);
|
|
1280
|
+
var NoteStatusSchema = z5.enum(["open", "assigned", "in_progress", "done"]);
|
|
1281
|
+
var NoteRepeatSchema = z5.enum(["once", "until_done"]);
|
|
1282
|
+
var DecisionSchema = z5.object({
|
|
1283
|
+
id: z5.string(),
|
|
1068
1284
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
1069
1285
|
* extensibility seam — any conversation can accrue decisions). */
|
|
1070
|
-
noteId:
|
|
1286
|
+
noteId: z5.string().nullable(),
|
|
1071
1287
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
1072
|
-
question:
|
|
1288
|
+
question: z5.string(),
|
|
1073
1289
|
/** The user's ruling; null while the question is open. */
|
|
1074
|
-
answer:
|
|
1075
|
-
decidedAt:
|
|
1076
|
-
createdAt:
|
|
1290
|
+
answer: z5.string().nullable(),
|
|
1291
|
+
decidedAt: z5.string().nullable(),
|
|
1292
|
+
createdAt: z5.string()
|
|
1077
1293
|
});
|
|
1078
|
-
var NoteSchema =
|
|
1079
|
-
id:
|
|
1294
|
+
var NoteSchema = z5.object({
|
|
1295
|
+
id: z5.string(),
|
|
1080
1296
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
1081
|
-
title:
|
|
1297
|
+
title: z5.string(),
|
|
1082
1298
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
1083
|
-
intent:
|
|
1299
|
+
intent: z5.string(),
|
|
1084
1300
|
source: NoteSourceSchema,
|
|
1085
1301
|
status: NoteStatusSchema,
|
|
1086
1302
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
1087
|
-
assignee:
|
|
1303
|
+
assignee: z5.string().nullable(),
|
|
1088
1304
|
/** The request thread minted at assignment; null until assigned. */
|
|
1089
|
-
parentId:
|
|
1305
|
+
parentId: z5.string().nullable(),
|
|
1090
1306
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1091
1307
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
1092
1308
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -1095,147 +1311,152 @@ var NoteSchema = z4.object({
|
|
|
1095
1311
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
1096
1312
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
1097
1313
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
1098
|
-
dueAt:
|
|
1314
|
+
dueAt: z5.string().nullable().default(null),
|
|
1099
1315
|
repeat: NoteRepeatSchema.default("once"),
|
|
1100
1316
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
1101
|
-
rides:
|
|
1102
|
-
lastRideAt:
|
|
1103
|
-
createdAt:
|
|
1317
|
+
rides: z5.number().int().default(0),
|
|
1318
|
+
lastRideAt: z5.string().nullable().default(null),
|
|
1319
|
+
createdAt: z5.string()
|
|
1104
1320
|
});
|
|
1105
|
-
var TriageItemSchema =
|
|
1106
|
-
noteId:
|
|
1321
|
+
var TriageItemSchema = z5.object({
|
|
1322
|
+
noteId: z5.string(),
|
|
1107
1323
|
/** The note's headline at run time. */
|
|
1108
|
-
title:
|
|
1324
|
+
title: z5.string(),
|
|
1109
1325
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
1110
1326
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
1111
1327
|
* Never a model's reasoning transcript, never an id. */
|
|
1112
|
-
why:
|
|
1328
|
+
why: z5.string()
|
|
1113
1329
|
});
|
|
1114
|
-
var TriageAssignmentSchema =
|
|
1330
|
+
var TriageAssignmentSchema = z5.object({
|
|
1115
1331
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
1116
|
-
agent:
|
|
1332
|
+
agent: z5.string(),
|
|
1117
1333
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
1118
1334
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
1119
|
-
agentName:
|
|
1120
|
-
notes:
|
|
1335
|
+
agentName: z5.string(),
|
|
1336
|
+
notes: z5.array(TriageItemSchema)
|
|
1121
1337
|
});
|
|
1122
|
-
var TriageStatusSchema =
|
|
1123
|
-
var SubmitTriageSchema =
|
|
1338
|
+
var TriageStatusSchema = z5.enum(["open", "superseded", "dismissed"]);
|
|
1339
|
+
var SubmitTriageSchema = z5.object({
|
|
1124
1340
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
1125
1341
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
1126
1342
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
1127
1343
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
1128
|
-
provider:
|
|
1344
|
+
provider: z5.string().min(1).max(60),
|
|
1129
1345
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
1130
|
-
model:
|
|
1346
|
+
model: z5.string().max(200).nullable().optional(),
|
|
1131
1347
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
1132
1348
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
1133
|
-
reviewed:
|
|
1134
|
-
close:
|
|
1135
|
-
stale:
|
|
1136
|
-
assign:
|
|
1349
|
+
reviewed: z5.number().int().min(0).max(1e4).default(0),
|
|
1350
|
+
close: z5.array(TriageItemSchema).max(200).default([]),
|
|
1351
|
+
stale: z5.array(TriageItemSchema).max(200).default([]),
|
|
1352
|
+
assign: z5.array(TriageAssignmentSchema).max(50).default([])
|
|
1137
1353
|
});
|
|
1138
1354
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
1139
|
-
id:
|
|
1140
|
-
runAt:
|
|
1355
|
+
id: z5.string(),
|
|
1356
|
+
runAt: z5.string(),
|
|
1141
1357
|
status: TriageStatusSchema,
|
|
1142
|
-
model:
|
|
1358
|
+
model: z5.string().nullable().default(null)
|
|
1143
1359
|
});
|
|
1144
|
-
var AcceptTriageSchema =
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
group:
|
|
1149
|
-
agent:
|
|
1150
|
-
noteIds:
|
|
1360
|
+
var AcceptTriageSchema = z5.discriminatedUnion("group", [
|
|
1361
|
+
z5.object({ group: z5.literal("close"), noteIds: z5.array(z5.string()).max(200).optional() }),
|
|
1362
|
+
z5.object({ group: z5.literal("stale"), noteIds: z5.array(z5.string()).max(200).optional() }),
|
|
1363
|
+
z5.object({
|
|
1364
|
+
group: z5.literal("assign"),
|
|
1365
|
+
agent: z5.string().min(1),
|
|
1366
|
+
noteIds: z5.array(z5.string()).max(200).optional()
|
|
1151
1367
|
})
|
|
1152
1368
|
]);
|
|
1153
|
-
var AcceptTriageResultSchema =
|
|
1154
|
-
accepted:
|
|
1155
|
-
failed:
|
|
1369
|
+
var AcceptTriageResultSchema = z5.object({
|
|
1370
|
+
accepted: z5.array(z5.string()),
|
|
1371
|
+
failed: z5.array(z5.object({ noteId: z5.string(), reason: z5.string() }))
|
|
1156
1372
|
});
|
|
1157
|
-
var DeliveryModeSchema =
|
|
1158
|
-
var RegisterDeliverySchema =
|
|
1159
|
-
var OAuthStartSchema =
|
|
1160
|
-
provider:
|
|
1161
|
-
returnTo:
|
|
1373
|
+
var DeliveryModeSchema = z5.enum(["poll", "self_hosted"]);
|
|
1374
|
+
var RegisterDeliverySchema = z5.object({ mode: DeliveryModeSchema });
|
|
1375
|
+
var OAuthStartSchema = z5.object({
|
|
1376
|
+
provider: z5.enum(["cma"]),
|
|
1377
|
+
returnTo: z5.string().min(1)
|
|
1162
1378
|
});
|
|
1163
|
-
var DeliveryConfigSchema =
|
|
1164
|
-
tokenId:
|
|
1379
|
+
var DeliveryConfigSchema = z5.object({
|
|
1380
|
+
tokenId: z5.string(),
|
|
1165
1381
|
mode: DeliveryModeSchema,
|
|
1166
1382
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
1167
1383
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
1168
1384
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
1169
|
-
realtime:
|
|
1385
|
+
realtime: z5.object({ url: z5.string(), anonKey: z5.string() }).nullable()
|
|
1170
1386
|
});
|
|
1171
|
-
var HostDecisionSchema =
|
|
1387
|
+
var HostDecisionSchema = z5.object({
|
|
1172
1388
|
/** The agent's token id: the row's `recipient`. */
|
|
1173
|
-
agent:
|
|
1174
|
-
decision:
|
|
1389
|
+
agent: z5.string().uuid(),
|
|
1390
|
+
decision: z5.enum(["stood_back", "took_over"]),
|
|
1175
1391
|
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
1176
|
-
goalId:
|
|
1392
|
+
goalId: z5.string().uuid().nullable().optional(),
|
|
1177
1393
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
1178
|
-
seenAt:
|
|
1179
|
-
/** When that work last moved (`claimable.since` on `
|
|
1180
|
-
since:
|
|
1394
|
+
seenAt: z5.string().datetime().nullable().optional(),
|
|
1395
|
+
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
1396
|
+
since: z5.string().datetime().nullable().optional(),
|
|
1181
1397
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
1182
|
-
said:
|
|
1398
|
+
said: z5.string().max(300).optional()
|
|
1183
1399
|
});
|
|
1184
|
-
var WakeNudgeSchema =
|
|
1185
|
-
kind:
|
|
1186
|
-
notificationId:
|
|
1187
|
-
parentId:
|
|
1400
|
+
var WakeNudgeSchema = z5.object({
|
|
1401
|
+
kind: z5.enum(["reply", "request", "callback"]),
|
|
1402
|
+
notificationId: z5.string().optional(),
|
|
1403
|
+
parentId: z5.string()
|
|
1188
1404
|
});
|
|
1189
|
-
var PairingStatusSchema =
|
|
1190
|
-
var DeviceCodeSchema =
|
|
1191
|
-
device_code:
|
|
1192
|
-
user_code:
|
|
1193
|
-
verification_uri:
|
|
1194
|
-
verification_uri_complete:
|
|
1195
|
-
interval:
|
|
1196
|
-
expires_in:
|
|
1405
|
+
var PairingStatusSchema = z5.enum(["pending", "approved", "denied", "expired"]);
|
|
1406
|
+
var DeviceCodeSchema = z5.object({
|
|
1407
|
+
device_code: z5.string(),
|
|
1408
|
+
user_code: z5.string(),
|
|
1409
|
+
verification_uri: z5.string().url(),
|
|
1410
|
+
verification_uri_complete: z5.string().url(),
|
|
1411
|
+
interval: z5.number(),
|
|
1412
|
+
expires_in: z5.number()
|
|
1197
1413
|
});
|
|
1198
|
-
var DeviceInfoSchema =
|
|
1199
|
-
code:
|
|
1414
|
+
var DeviceInfoSchema = z5.object({
|
|
1415
|
+
code: z5.string(),
|
|
1200
1416
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
1201
1417
|
* pre-filling the name field the human can edit. */
|
|
1202
|
-
name:
|
|
1418
|
+
name: z5.string(),
|
|
1203
1419
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
1204
1420
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
1205
1421
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
1206
1422
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
1207
|
-
agent:
|
|
1208
|
-
device:
|
|
1423
|
+
agent: z5.string().optional(),
|
|
1424
|
+
device: z5.string().nullable(),
|
|
1209
1425
|
status: PairingStatusSchema
|
|
1210
1426
|
});
|
|
1211
|
-
var DeviceTokenSchema =
|
|
1212
|
-
access_token:
|
|
1427
|
+
var DeviceTokenSchema = z5.object({
|
|
1428
|
+
access_token: z5.string(),
|
|
1213
1429
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
1214
1430
|
* a default silly name). */
|
|
1215
|
-
name:
|
|
1216
|
-
device:
|
|
1431
|
+
name: z5.string(),
|
|
1432
|
+
device: z5.string().nullable(),
|
|
1217
1433
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
1218
1434
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
1219
|
-
voice:
|
|
1435
|
+
voice: z5.string().nullable().optional(),
|
|
1220
1436
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
1221
1437
|
* that survives a rename. Cached by the host's identity beat. */
|
|
1222
|
-
token_id:
|
|
1438
|
+
token_id: z5.string().nullable().optional(),
|
|
1223
1439
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
1224
1440
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
1225
1441
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
1226
1442
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
1227
|
-
workspace:
|
|
1443
|
+
workspace: z5.string().nullable().optional(),
|
|
1228
1444
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
1229
|
-
harness:
|
|
1230
|
-
session_id:
|
|
1231
|
-
|
|
1445
|
+
harness: z5.enum(["claude", "codex", "agy"]).optional(),
|
|
1446
|
+
session_id: z5.string().uuid().optional(),
|
|
1447
|
+
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
1448
|
+
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
1449
|
+
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
1450
|
+
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
1451
|
+
full_session: z5.string().optional(),
|
|
1452
|
+
uik_pub: z5.string().nullable().optional()
|
|
1232
1453
|
});
|
|
1233
|
-
var SupportRequestSchema =
|
|
1234
|
-
email:
|
|
1235
|
-
message:
|
|
1236
|
-
name:
|
|
1454
|
+
var SupportRequestSchema = z5.object({
|
|
1455
|
+
email: z5.string().email().max(320),
|
|
1456
|
+
message: z5.string().trim().min(1).max(5e3),
|
|
1457
|
+
name: z5.string().trim().max(120).optional()
|
|
1237
1458
|
});
|
|
1238
|
-
var NotificationFeedbackKindSchema =
|
|
1459
|
+
var NotificationFeedbackKindSchema = z5.enum([
|
|
1239
1460
|
"break_down",
|
|
1240
1461
|
// "This should be more than one ask — break it down."
|
|
1241
1462
|
"regenerate_options",
|
|
@@ -1250,116 +1471,116 @@ var NotificationFeedbackKindSchema = z4.enum([
|
|
|
1250
1471
|
// anything else — the note carries it.
|
|
1251
1472
|
]);
|
|
1252
1473
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
1253
|
-
var QuestionRowSchema =
|
|
1474
|
+
var QuestionRowSchema = z5.object({
|
|
1254
1475
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
1255
|
-
id:
|
|
1256
|
-
deliveryId:
|
|
1257
|
-
entryId:
|
|
1476
|
+
id: z5.string(),
|
|
1477
|
+
deliveryId: z5.string(),
|
|
1478
|
+
entryId: z5.string(),
|
|
1258
1479
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
1259
|
-
needId:
|
|
1260
|
-
goalIds:
|
|
1480
|
+
needId: z5.string().nullable(),
|
|
1481
|
+
goalIds: z5.array(z5.string()),
|
|
1261
1482
|
/** The name of the work it is about, when the read could word it. */
|
|
1262
|
-
goalTitle:
|
|
1263
|
-
tokenId:
|
|
1264
|
-
name:
|
|
1265
|
-
title:
|
|
1266
|
-
body:
|
|
1267
|
-
select:
|
|
1268
|
-
options:
|
|
1269
|
-
hasPreview:
|
|
1270
|
-
blocking:
|
|
1271
|
-
askedAt:
|
|
1483
|
+
goalTitle: z5.string().optional(),
|
|
1484
|
+
tokenId: z5.string().optional(),
|
|
1485
|
+
name: z5.string(),
|
|
1486
|
+
title: z5.string(),
|
|
1487
|
+
body: z5.string(),
|
|
1488
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]),
|
|
1489
|
+
options: z5.array(SlimOptionSchema),
|
|
1490
|
+
hasPreview: z5.boolean(),
|
|
1491
|
+
blocking: z5.boolean(),
|
|
1492
|
+
askedAt: z5.string().datetime(),
|
|
1272
1493
|
ring: InboxItemSchema.shape.ring,
|
|
1273
|
-
onCall:
|
|
1274
|
-
sealed:
|
|
1494
|
+
onCall: z5.literal(true).optional(),
|
|
1495
|
+
sealed: z5.boolean()
|
|
1275
1496
|
});
|
|
1276
|
-
var WorkStateSchema =
|
|
1277
|
-
var WorkRowSchema =
|
|
1278
|
-
id:
|
|
1279
|
-
parentId:
|
|
1280
|
-
title:
|
|
1497
|
+
var WorkStateSchema = z5.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
1498
|
+
var WorkRowSchema = z5.object({
|
|
1499
|
+
id: z5.string(),
|
|
1500
|
+
parentId: z5.string().nullable(),
|
|
1501
|
+
title: z5.string(),
|
|
1281
1502
|
/** Straight off the Goal. */
|
|
1282
1503
|
state: WorkStateSchema,
|
|
1283
|
-
owner:
|
|
1284
|
-
revision:
|
|
1504
|
+
owner: z5.string().nullable(),
|
|
1505
|
+
revision: z5.number().int(),
|
|
1285
1506
|
/** Open questions on it, counted to 100. */
|
|
1286
|
-
waiting:
|
|
1507
|
+
waiting: z5.number().int(),
|
|
1287
1508
|
/** Held by a gate on work that is not done. */
|
|
1288
|
-
blocked:
|
|
1289
|
-
lastProgressAt:
|
|
1509
|
+
blocked: z5.boolean(),
|
|
1510
|
+
lastProgressAt: z5.string().datetime().nullable(),
|
|
1290
1511
|
/** The line written for its newest progress note, else that note's first words. */
|
|
1291
|
-
line:
|
|
1512
|
+
line: z5.string().nullable(),
|
|
1292
1513
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
1293
|
-
children:
|
|
1294
|
-
createdAt:
|
|
1295
|
-
updatedAt:
|
|
1514
|
+
children: z5.number().int(),
|
|
1515
|
+
createdAt: z5.string().datetime(),
|
|
1516
|
+
updatedAt: z5.string().datetime(),
|
|
1296
1517
|
/** When anything at or under it last moved — the order the list is in. */
|
|
1297
|
-
activeAt:
|
|
1518
|
+
activeAt: z5.string().datetime(),
|
|
1298
1519
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
1299
|
-
sealed:
|
|
1520
|
+
sealed: z5.boolean()
|
|
1300
1521
|
});
|
|
1301
1522
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
1302
1523
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
1303
1524
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
1304
|
-
asking:
|
|
1305
|
-
oldestAskAt:
|
|
1525
|
+
asking: z5.number().int().nullable(),
|
|
1526
|
+
oldestAskAt: z5.string().datetime().nullable(),
|
|
1306
1527
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
1307
1528
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
1308
1529
|
* (`agent_holds`); null when that read failed. */
|
|
1309
|
-
holds:
|
|
1310
|
-
held:
|
|
1530
|
+
holds: z5.array(z5.object({ id: z5.string(), title: z5.string() })).nullable(),
|
|
1531
|
+
held: z5.number().int().nullable(),
|
|
1311
1532
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
1312
1533
|
* while none has, or when that read failed. */
|
|
1313
|
-
cold:
|
|
1534
|
+
cold: z5.string().datetime().nullable(),
|
|
1314
1535
|
/** The newest line of its working log, and when the harness saw it. */
|
|
1315
|
-
line:
|
|
1316
|
-
lineAt:
|
|
1536
|
+
line: z5.string().nullable(),
|
|
1537
|
+
lineAt: z5.string().datetime().nullable()
|
|
1317
1538
|
});
|
|
1318
|
-
var SnapshotSchema =
|
|
1539
|
+
var SnapshotSchema = z5.object({
|
|
1319
1540
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
1320
|
-
at:
|
|
1321
|
-
questions:
|
|
1541
|
+
at: z5.string().datetime(),
|
|
1542
|
+
questions: z5.object({
|
|
1322
1543
|
/** The newest 30 open cards, questions before updates. */
|
|
1323
|
-
items:
|
|
1544
|
+
items: z5.array(QuestionRowSchema),
|
|
1324
1545
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
1325
|
-
total:
|
|
1326
|
-
updates:
|
|
1327
|
-
putOff:
|
|
1546
|
+
total: z5.number().int(),
|
|
1547
|
+
updates: z5.number().int(),
|
|
1548
|
+
putOff: z5.number().int()
|
|
1328
1549
|
}).nullable(),
|
|
1329
|
-
agents:
|
|
1550
|
+
agents: z5.object({
|
|
1330
1551
|
/** Up to 60, most recently seen first. */
|
|
1331
|
-
items:
|
|
1332
|
-
more:
|
|
1552
|
+
items: z5.array(AgentRowSchema),
|
|
1553
|
+
more: z5.boolean()
|
|
1333
1554
|
}).nullable(),
|
|
1334
|
-
work:
|
|
1555
|
+
work: z5.object({
|
|
1335
1556
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
1336
|
-
items:
|
|
1557
|
+
items: z5.array(WorkRowSchema),
|
|
1337
1558
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
1338
1559
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
1339
1560
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
1340
1561
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
1341
|
-
counts:
|
|
1562
|
+
counts: z5.object({ live: z5.number().int(), waiting: z5.number().int(), notStarted: z5.number().int(), done: z5.number().int() })
|
|
1342
1563
|
}).nullable(),
|
|
1343
|
-
you:
|
|
1564
|
+
you: z5.object({
|
|
1344
1565
|
settings: UserSettingsSchema,
|
|
1345
|
-
callable:
|
|
1566
|
+
callable: z5.boolean(),
|
|
1346
1567
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
1347
|
-
computers:
|
|
1568
|
+
computers: z5.array(ComputerRowSchema).nullable()
|
|
1348
1569
|
}).nullable()
|
|
1349
1570
|
});
|
|
1350
|
-
var CallRecapSchema =
|
|
1351
|
-
call:
|
|
1352
|
-
status:
|
|
1353
|
-
startedAt:
|
|
1354
|
-
durationMs:
|
|
1355
|
-
agents:
|
|
1571
|
+
var CallRecapSchema = z5.object({
|
|
1572
|
+
call: z5.object({
|
|
1573
|
+
status: z5.string(),
|
|
1574
|
+
startedAt: z5.string(),
|
|
1575
|
+
durationMs: z5.number().nullable(),
|
|
1576
|
+
agents: z5.array(z5.object({ id: z5.string(), name: z5.string().nullable() }))
|
|
1356
1577
|
}),
|
|
1357
|
-
topics:
|
|
1358
|
-
goalId:
|
|
1359
|
-
title:
|
|
1360
|
-
owner:
|
|
1361
|
-
state:
|
|
1362
|
-
questions:
|
|
1578
|
+
topics: z5.array(z5.object({
|
|
1579
|
+
goalId: z5.string().uuid(),
|
|
1580
|
+
title: z5.string(),
|
|
1581
|
+
owner: z5.string(),
|
|
1582
|
+
state: z5.string(),
|
|
1583
|
+
questions: z5.array(z5.object({ id: z5.string().uuid(), state: z5.string(), title: z5.string() })),
|
|
1363
1584
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
1364
1585
|
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
1365
1586
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
@@ -1367,17 +1588,18 @@ var CallRecapSchema = z4.object({
|
|
|
1367
1588
|
* anything that is not an answer. */
|
|
1368
1589
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
1369
1590
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
1370
|
-
lines:
|
|
1591
|
+
lines: z5.array(z5.object({ entryId: z5.string().uuid(), words: z5.string(), headline: z5.string().optional(), about: z5.string().nullable().optional() }))
|
|
1371
1592
|
})),
|
|
1372
|
-
unfiled:
|
|
1373
|
-
more:
|
|
1593
|
+
unfiled: z5.array(z5.object({ lineId: z5.string().uuid(), words: z5.string(), atMs: z5.number() })),
|
|
1594
|
+
more: z5.object({ lines: z5.number(), entries: z5.number(), topics: z5.number() })
|
|
1374
1595
|
});
|
|
1375
1596
|
|
|
1376
1597
|
// src/listening.ts
|
|
1598
|
+
import { createHash } from "crypto";
|
|
1377
1599
|
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
1378
1600
|
import { homedir } from "os";
|
|
1379
1601
|
import { dirname, join } from "path";
|
|
1380
|
-
var HARNESS_MARKS = ["PAIGY_HARNESS"
|
|
1602
|
+
var HARNESS_MARKS = ["PAIGY_HARNESS"];
|
|
1381
1603
|
function harnessDriven(env) {
|
|
1382
1604
|
return HARNESS_MARKS.some((mark) => !!env[mark]);
|
|
1383
1605
|
}
|
|
@@ -1390,7 +1612,7 @@ function sessionStartHook() {
|
|
|
1390
1612
|
[ -f "\${HOME}/.paigy/token.json" ] || exit 0 # not paired on this machine: nothing to listen as
|
|
1391
1613
|
cat <<'PAIGY'
|
|
1392
1614
|
Paigy: nothing pushes answers into a session started in a terminal \u2014 an answer waits for your
|
|
1393
|
-
next
|
|
1615
|
+
next contact({ wait: false }). Start your own listener now, as a BACKGROUND task whose output you watch
|
|
1394
1616
|
(your harness's monitor/background-task tool), and leave it running for this session:
|
|
1395
1617
|
|
|
1396
1618
|
npx -y -p @paigy/mcp@latest paigy-listen
|
|
@@ -1433,11 +1655,12 @@ function decideListen(f) {
|
|
|
1433
1655
|
};
|
|
1434
1656
|
}
|
|
1435
1657
|
if (f.pidAlive && f.pid !== void 0) {
|
|
1436
|
-
|
|
1658
|
+
const connected = f.wake?.ready === true && (f.wake.target.kind !== "codex" || f.wake.target.thread === f.env.CODEX_THREAD_ID);
|
|
1659
|
+
return { status: connected ? "listening" : "receiving", via: "daemon", pid: f.pid, ...f.wake ? { wake: f.wake } : {}, message: connected ? `Listener running (pid ${f.pid}); wake adapter ready. A queued message is not proof the agent handled it.` : `Listener running (pid ${f.pid}), but automatic session resume is not confirmed${f.wake?.lastError ? `: ${f.wake.lastError}` : ". Watch its output or configure a supported wake adapter"}. Do not start a second listener.` };
|
|
1437
1660
|
}
|
|
1438
1661
|
return {
|
|
1439
1662
|
status: "start",
|
|
1440
|
-
command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${word(f.execPath)} ${word(f.listenJs)} --brief`,
|
|
1663
|
+
command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${f.env.CODEX_THREAD_ID ? `CODEX_THREAD_ID=${word(f.env.CODEX_THREAD_ID)} ` : ""}${f.env.PAIGY_ON_WAKE ? `PAIGY_ON_WAKE=${word(f.env.PAIGY_ON_WAKE)} ` : ""}${word(f.execPath)} ${word(f.listenJs)} --brief`,
|
|
1441
1664
|
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."
|
|
1442
1665
|
};
|
|
1443
1666
|
}
|
|
@@ -1475,16 +1698,28 @@ function listenerAlive(slot, home = homedir()) {
|
|
|
1475
1698
|
return null;
|
|
1476
1699
|
}
|
|
1477
1700
|
}
|
|
1701
|
+
function listenerKey(slot, env) {
|
|
1702
|
+
return env.CODEX_THREAD_ID ? `${slot}-${createHash("sha256").update(env.CODEX_THREAD_ID).digest("hex")}` : slot;
|
|
1703
|
+
}
|
|
1704
|
+
function wakeStatusPath(slot, home = homedir()) {
|
|
1705
|
+
return `${listenMark(slot, home)}.wake.json`;
|
|
1706
|
+
}
|
|
1707
|
+
function existingListener(slot, env, home = homedir()) {
|
|
1708
|
+
return listenerAlive(listenerKey(slot, env), home) ?? (env.CODEX_THREAD_ID ? listenerAlive(slot, home) : null);
|
|
1709
|
+
}
|
|
1478
1710
|
|
|
1479
1711
|
export {
|
|
1480
1712
|
mcpInputSchema,
|
|
1481
1713
|
AGENT_TOOLS,
|
|
1482
1714
|
serverInstructions,
|
|
1483
1715
|
entryWords,
|
|
1716
|
+
harnessDriven,
|
|
1484
1717
|
sessionStartHook,
|
|
1485
1718
|
withSessionStartHook,
|
|
1486
1719
|
decideListen,
|
|
1487
1720
|
writeListenMark,
|
|
1488
1721
|
removeListenMark,
|
|
1489
|
-
|
|
1722
|
+
listenerKey,
|
|
1723
|
+
wakeStatusPath,
|
|
1724
|
+
existingListener
|
|
1490
1725
|
};
|