@paigy/mcp 0.40.39 → 0.40.41
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 +25 -26
- package/dist/{chunk-PMZBYLLJ.js → chunk-AX55W57P.js} +30 -7
- package/dist/{chunk-X4UIGUEX.js → chunk-CPDYOHSY.js} +737 -551
- package/dist/{chunk-QE33A5XK.js → chunk-NYSDESST.js} +804 -547
- package/dist/{chunk-4IAK4ATB.js → chunk-UVVW32KH.js} +41 -44
- package/dist/{dist-DOFR2565.js → dist-VYRT3PHE.js} +3 -1
- package/dist/enable.js +3 -3
- package/dist/index.js +46 -22
- package/dist/listen.js +32 -6
- package/dist/onboard.js +18 -6
- package/dist/slot.js +1 -1
- package/dist/stalled.js +10 -5
- package/dist/statusline.js +1 -1
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ../../packages/schema/dist/index.js
|
|
2
|
-
import { z as
|
|
2
|
+
import { z as z12 } from "zod";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { z as z3 } from "zod";
|
|
5
5
|
import { z as z2 } from "zod";
|
|
@@ -16,6 +16,8 @@ import { z as z9 } from "zod";
|
|
|
16
16
|
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
17
17
|
import { z as z10 } from "zod";
|
|
18
18
|
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
19
|
+
import { z as z11 } from "zod";
|
|
20
|
+
import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
|
|
19
21
|
var OPTIONS_MIN = 2;
|
|
20
22
|
var OPTIONS_MAX = 6;
|
|
21
23
|
var OPTION_LABEL_MAX = 50;
|
|
@@ -45,7 +47,7 @@ var OptionInputSchema = z.object({
|
|
|
45
47
|
imgUrl: OptionSchema.shape.image
|
|
46
48
|
}).strict();
|
|
47
49
|
var NIGHT = { from: 23, to: 7 };
|
|
48
|
-
var ASK_MAX =
|
|
50
|
+
var ASK_MAX = 80;
|
|
49
51
|
var UNITS_MAX = 12;
|
|
50
52
|
var UNIT_TITLE_MAX = 50;
|
|
51
53
|
var UNIT_BODY_MAX = 200;
|
|
@@ -110,15 +112,15 @@ var CompletionSchema = z3.object({
|
|
|
110
112
|
}).strict();
|
|
111
113
|
var CompleteChangeSchema = CompletionSchema.extend({ kind: z3.literal("complete") });
|
|
112
114
|
var ASKING_RULES = `HOW TO ASK:
|
|
113
|
-
1.
|
|
114
|
-
2.
|
|
115
|
-
3. \`workItBlocks\` only
|
|
116
|
-
4.
|
|
117
|
-
5. One question, one row
|
|
118
|
-
6. Read the Goal
|
|
119
|
-
7.
|
|
120
|
-
8. A diagnosis says when, why and how it happens, then proposes one fix.
|
|
121
|
-
9.
|
|
115
|
+
1. One question per object in \`questions\`, so each is answered on its own.
|
|
116
|
+
2. \`goalId\` names the existing Goal it is about (create one with manage_goals first); the answer comes back there.
|
|
117
|
+
3. \`workItBlocks\` only when you are truly blocked: it asks for a call.
|
|
118
|
+
4. No while-loops. While any Call is open, even one your question joined from their items, keep receiving through empty windows; otherwise stop at one: the answer reaches you on the Goal. Yield only with a listener or wakeup.
|
|
119
|
+
5. One question, one row: never re-send one still waiting; take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
120
|
+
6. Read the Goal before asking; never re-ask what was answered or shipped. \`replied\` means they chose no option: it is still yours to settle.
|
|
121
|
+
7. Give options when you have them. Each label names its choice in full (never "Option 2"); never add "Other". \`pickMode\`: one (default), many, rank.
|
|
122
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix.
|
|
123
|
+
9. Give a report its \`reply\`: one line in THEIR voice for THAT report, so you learn whether they verified it or only received it. Required when it reaches them or completes a Goal.`;
|
|
122
124
|
function draft2020(node) {
|
|
123
125
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
124
126
|
if (node && typeof node === "object") {
|
|
@@ -161,7 +163,7 @@ var userExplicitlyRequested = z4.enum(["any", "call"]).optional().describe(
|
|
|
161
163
|
`Only what the person said about THIS message, in so many words: "any" when they asked you to send it to them ("send it to my phone", "message me the result"), "call" when they asked to be called about it. It is then delivered (a call rings for the whole contact, and carries everything waiting on them); omitted, an update is only recorded as the Goal's progress unless the person needs it. Never set it to make a message louder.`
|
|
162
164
|
);
|
|
163
165
|
var units = z4.array(UnitInputSchema).optional().describe(
|
|
164
|
-
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all.`
|
|
166
|
+
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.`
|
|
165
167
|
);
|
|
166
168
|
var UpdateInputSchema = z4.object({
|
|
167
169
|
message: z4.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
@@ -216,7 +218,7 @@ var questionFields = {
|
|
|
216
218
|
),
|
|
217
219
|
goalId: z4.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
|
|
218
220
|
units,
|
|
219
|
-
options: z4.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
|
|
221
|
+
options: z4.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200. They can always answer in their own words, so no 'Other' or 'None of these'."),
|
|
220
222
|
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
221
223
|
// brain_prompts.md §1: structured agent questions need no model). The app draws all three.
|
|
222
224
|
pickMode: z4.enum(["one", "many", "rank"]).optional().describe(
|
|
@@ -258,9 +260,11 @@ var QuestionDependencySchema = z4.object({
|
|
|
258
260
|
reason: reason.optional()
|
|
259
261
|
}).strict().refine(actionFits, ACTION_FITS);
|
|
260
262
|
var sendFields = {
|
|
261
|
-
completions: z4.array(CompletionSchema).min(1).max(20).optional().describe(
|
|
263
|
+
completions: z4.array(CompletionSchema).min(1).max(20).optional().describe(
|
|
264
|
+
"COMPLETIONS close and report in one operation, the same contract as manage_goals kind:complete: closure and the report commit together or neither does. Required children and dependencies must already be finished. options suggest next steps: a tap acknowledges the report and requests that step, and never reopens the Goal (follow-up work needs a new one). No reviewer rejudges it: say only what your evidence supports. Ordinary updates never close a Goal."
|
|
265
|
+
),
|
|
262
266
|
updates: z4.array(UpdateInputSchema).min(1).max(20).optional().describe(
|
|
263
|
-
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress
|
|
267
|
+
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress, which the person is not told. Every update reaches the other agents that wrote on its Goal in the last hour: to tell another agent something, write it as an update on that agent's Goal."
|
|
264
268
|
),
|
|
265
269
|
answers: z4.array(AnswerInputSchema).min(1).max(20).optional().describe("Answers to Questions addressed to you."),
|
|
266
270
|
withdrawQuestionIds: z4.array(questionHandle).min(1).max(10).optional().describe(
|
|
@@ -272,7 +276,7 @@ var sendFields = {
|
|
|
272
276
|
};
|
|
273
277
|
var ContactSchema = z4.object({
|
|
274
278
|
...sendFields,
|
|
275
|
-
questions: z4.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object."),
|
|
279
|
+
questions: z4.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object, each sent exactly as written, with no reading in between."),
|
|
276
280
|
/** RECEIVING (2I, I2; brain_prompts.md §2.2): the events this agent handled. */
|
|
277
281
|
ackEventIds: z4.array(z4.string().uuid()).min(1).max(100).optional().describe(
|
|
278
282
|
"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 (answers), never acknowledged away."
|
|
@@ -288,15 +292,13 @@ var ContactWireSchema = z4.object({
|
|
|
288
292
|
questions: z4.array(QuestionWireSchema).min(1).max(20).optional()
|
|
289
293
|
}).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
290
294
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
291
|
-
var CONTACT_DESCRIPTION = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals
|
|
295
|
+
var CONTACT_DESCRIPTION = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals, and what a question holds up. Every Goal named must exist: an unknown one refuses the whole contact. Each item applies on its own: read `results`, where a failed one names why.\n\n" + ASKING_RULES + `
|
|
292
296
|
|
|
293
|
-
|
|
297
|
+
LENGTH: the question, message or answer is the one point, at most ${ASK_MAX} characters; the rest goes in \`units\`, one idea each. Over a cap is refused by name and nothing is sent. Several results are several updates.
|
|
294
298
|
|
|
295
|
-
UPDATES ask nothing
|
|
299
|
+
UPDATES ask nothing. One reaches the person only when they asked for it, it answers something they said, or its Goal is done; else it is kept as progress. One that reaches them joins an open report card or a live call.
|
|
296
300
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goals); \`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\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
|
|
301
|
+
RECEIVING: contact({}) waits ~45 s; contact({wait:false}) returns \`events\` at once. Confirm the ones you handled with contact({ackEventIds}).`;
|
|
300
302
|
var CreateGoalSchema = z5.object({
|
|
301
303
|
outcome: z5.string().trim().min(1).max(1e4),
|
|
302
304
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -397,9 +399,12 @@ var GetGoalsSchema = z5.object({
|
|
|
397
399
|
history: z5.boolean().optional()
|
|
398
400
|
}).strict();
|
|
399
401
|
var GetDeliverySchema = z5.object({
|
|
400
|
-
deliveryId: z5.string().uuid().describe("The Delivery id returned by contact or receive.")
|
|
402
|
+
deliveryId: z5.string().uuid().describe("The Delivery id returned by contact or receive."),
|
|
403
|
+
// WHAT WAS SAID ON A CALL (owner, 2026-10-26, Goal c743f4a4: "A flag on get_delivery", "Any of my agents").
|
|
404
|
+
transcript: z5.boolean().optional().describe("For a Call: also return what was said on it, one page of spoken lines."),
|
|
405
|
+
after: z5.string().max(40).optional().describe("With transcript: the next value the previous page returned, to read the next page.")
|
|
401
406
|
}).strict();
|
|
402
|
-
var GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
407
|
+
var GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. For a Call, transcript: true adds what was said: each spoken line in order, who said it, and when from pickup, a page at a time. A line from the room, not the person, is marked room. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
403
408
|
var GET_GOALS_DESCRIPTION = "Read one to ten Goals by goalIds, returned in the same order under goals (a read never puts you on them). history applies to every requested Goal. A refused read fails the call with its original refusal; no partial result is presented as complete. Each Goal includes its ten most recent other contributors (names, latest entry headlines and times), 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.";
|
|
404
409
|
var SearchToolSchema = z5.object({
|
|
405
410
|
query: z5.string().trim().min(1).max(500),
|
|
@@ -430,8 +435,8 @@ var AGENT_TOOLS = [
|
|
|
430
435
|
];
|
|
431
436
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
432
437
|
function serverInstructions(opts) {
|
|
433
|
-
const waits = opts.waits ? "contact({
|
|
434
|
-
return `On startup and after a wake,
|
|
438
|
+
const waits = opts.waits ? "contact({wait:true}) waits ~45 s for a reply to what you sent; contact({}) for anything addressed to you." : "A contact here returns after one read; answers wake you, and contact({wait:false}) collects them.";
|
|
439
|
+
return `On startup and after a wake, contact({wait:false}) returns your events. They repeat until you acknowledge them with contact({ackEventIds}); reading acknowledges nothing. get_goals reads a Goal and its conversation; your first write to one puts you on it, and every Goal of your person is open to you. manage_goals creates, assigns and organizes work; to follow up later, defer the Goal with manage_goals (you're woken when due). Complete and report in one step: contact.completions or manage_goals kind:complete. Every read ends in \`next\`: your next step. ${waits} Report progress as a contact update; one they don't need yet stays on the Goal. An open Call is not proof it rang. Your user is remote: reach them only through Paigy, never the terminal; ask decisions with options.
|
|
435
440
|
|
|
436
441
|
${ASKING_RULES}`;
|
|
437
442
|
}
|
|
@@ -520,11 +525,22 @@ var BrainMessageSchema = z6.union([
|
|
|
520
525
|
// A MESSAGE TO THE PERSON KEEPS ITS SENTENCES (turns.md): the talker writes them one by one, and each is
|
|
521
526
|
// its own unit of speech (`<cycle>:<key>:<n>`). `text` is the same words joined, for every reader of a
|
|
522
527
|
// message's words. Absent on results written before sentences.
|
|
523
|
-
z6.object({
|
|
528
|
+
z6.object({
|
|
529
|
+
...messageBase,
|
|
530
|
+
to: z6.object({ kind: z6.literal("user") }).strict(),
|
|
531
|
+
sentences: z6.array(z6.string().min(1)).min(1).optional(),
|
|
532
|
+
// THE REPORTS IT TELLS, BY THEIR OWN IDENTITY (call 9d1c096c, 2026-10-15): the talker's message names the report
|
|
533
|
+
// Entries it speaks of, so the plan's line names that report rather than re-finding it by its Goal. Absent on
|
|
534
|
+
// results written before it.
|
|
535
|
+
tells: z6.array(z6.object({ id }).strict()).optional()
|
|
536
|
+
}).strict(),
|
|
524
537
|
z6.object({
|
|
525
538
|
...messageBase,
|
|
526
539
|
to: z6.object({ kind: z6.literal("agent"), agentId: z6.string().regex(/^agent:(?!paigy$)\S+$/) }).strict(),
|
|
527
540
|
call: z6.object({ callId: id, listen: z6.literal(true) }).strict().optional(),
|
|
541
|
+
// WHAT IT ASKS THE AGENT, IN UNDER TEN WORDS (owner, 2026-10-15: "Yes, add the summary"): the talker's own, stored
|
|
542
|
+
// as the sent Entry's summary, which the recap's "Sent to <agent>: …" row shows. Absent on results written before it.
|
|
543
|
+
summary: z6.string().optional(),
|
|
528
544
|
// THE PERSON ASKED TO BE CALLED BACK ONCE THIS AGENT'S WORK IS DONE (turns.md, step 0): the agent owes
|
|
529
545
|
// the call, and its next contact about it rings them (`public.callbacks`).
|
|
530
546
|
callback: z6.literal(true).optional()
|
|
@@ -551,95 +567,98 @@ var BrainResultSchema = z6.object({
|
|
|
551
567
|
next: BrainNextSchema,
|
|
552
568
|
search: BrainSearchSchema.optional()
|
|
553
569
|
}).strict();
|
|
554
|
-
var
|
|
570
|
+
var str2 = z6.string();
|
|
555
571
|
var strs = z6.array(z6.string());
|
|
556
572
|
var CompactResultSchema = z6.object({
|
|
557
|
-
next: z6.object({ say: strs, then: z6.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason:
|
|
558
|
-
messages: z6.array(z6.object({ key:
|
|
559
|
-
entries: z6.array(z6.object({ key:
|
|
560
|
-
answers: z6.array(z6.object({ question:
|
|
573
|
+
next: z6.object({ say: strs, then: z6.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason: str2, instructions: strs }).strict(),
|
|
574
|
+
messages: z6.array(z6.object({ key: str2, to: str2, text: str2, about: strs, entries: strs, owed: strs, inviteToCall: z6.boolean() }).strict()),
|
|
575
|
+
entries: z6.array(z6.object({ key: str2, from: str2, on: strs }).strict()),
|
|
576
|
+
answers: z6.array(z6.object({ question: str2, entries: strs, summary: str2, picked: strs }).strict()),
|
|
561
577
|
changes: z6.array(z6.object({
|
|
562
|
-
key:
|
|
578
|
+
key: str2,
|
|
563
579
|
what: z6.enum(["goal", "question", "lesson"]),
|
|
564
580
|
op: z6.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
|
|
565
|
-
id:
|
|
566
|
-
text:
|
|
567
|
-
title:
|
|
568
|
-
who:
|
|
569
|
-
under:
|
|
581
|
+
id: str2,
|
|
582
|
+
text: str2,
|
|
583
|
+
title: str2,
|
|
584
|
+
who: str2,
|
|
585
|
+
under: str2,
|
|
570
586
|
gate: z6.enum(["start", "complete", "answer", "none"]),
|
|
571
|
-
options: z6.array(z6.object({ id:
|
|
587
|
+
options: z6.array(z6.object({ id: str2, label: str2 }).strict()),
|
|
572
588
|
pick: z6.enum(["one", "many", "rank", "words"]),
|
|
573
589
|
entries: strs
|
|
574
590
|
}).strict()),
|
|
575
591
|
feedback: z6.array(strs),
|
|
576
|
-
search: z6.object({ query:
|
|
592
|
+
search: z6.object({ query: str2, within: strs, goal: str2 }).strict()
|
|
577
593
|
}).strict();
|
|
578
594
|
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
579
595
|
zodToJsonSchema2(CompactResultSchema, { $refStrategy: "none" })
|
|
580
596
|
);
|
|
581
|
-
var
|
|
597
|
+
var str3 = z7.string();
|
|
582
598
|
var strs2 = z7.array(z7.string());
|
|
583
599
|
var TalkerReplySchema = z7.object({
|
|
584
600
|
/** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
|
|
585
601
|
* or an agent's handle; `sentences` is what a message to the person says, one short sentence each
|
|
586
|
-
* ([] to an agent); `text` is what a message to an agent says ("" to the person); `
|
|
602
|
+
* ([] to an agent); `text` is what a message to an agent says ("" to the person); `summary` is what it
|
|
603
|
+
* asks the agent, in under ten words, as the person would put it ("" to the person; owner, 2026-10-15:
|
|
604
|
+
* "Yes, add the summary"), which the recap's "Sent to <agent>: …" row shows; `about` names
|
|
587
605
|
* handles; `blocks` is the item a question to an agent must be answered before ("" for none);
|
|
588
606
|
* `callback` is true on a message to the agent doing work the person asked to be called back about
|
|
589
607
|
* once it is done (false otherwise): that agent then owes the call (turns.md, step 0). */
|
|
590
|
-
messages: z7.array(z7.object({ key:
|
|
608
|
+
messages: z7.array(z7.object({ key: str3, to: str3, sentences: strs2, text: str3, summary: str3, about: strs2, blocks: str3, callback: z7.boolean() }).strict()),
|
|
591
609
|
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
592
610
|
then: z7.enum(["listen", "hold", "end"]),
|
|
593
611
|
/** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
|
|
594
612
|
* IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
|
|
595
613
|
* "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
|
|
596
614
|
* end of this call), a question's handle (that question's answer) or an ISO time. */
|
|
597
|
-
replies: z7.array(z7.object({ item:
|
|
615
|
+
replies: z7.array(z7.object({ item: str3, options: strs2, lines: strs2, summary: str3, defer: str3 }).strict()),
|
|
598
616
|
/** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
|
|
599
617
|
instruction: strs2,
|
|
600
618
|
/** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
|
|
601
619
|
* work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
|
|
602
620
|
file: strs2,
|
|
603
621
|
/** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
|
|
604
|
-
need: z7.object({ kind: z7.enum(["search", "item", "agent", ""]), text:
|
|
622
|
+
need: z7.object({ kind: z7.enum(["search", "item", "agent", ""]), text: str3 }).strict(),
|
|
605
623
|
/** The handle of the item to explain ("" for none). */
|
|
606
|
-
explain:
|
|
624
|
+
explain: str3
|
|
607
625
|
}).strict();
|
|
608
626
|
var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
609
627
|
zodToJsonSchema3(TalkerReplySchema, { $refStrategy: "none" })
|
|
610
628
|
);
|
|
611
|
-
var
|
|
629
|
+
var str4 = z8.string();
|
|
612
630
|
var strs3 = z8.array(z8.string());
|
|
613
631
|
var FilerReplySchema = z8.object({
|
|
614
|
-
/** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals
|
|
615
|
-
* the line says in at most
|
|
616
|
-
|
|
632
|
+
/** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals: a line that asks
|
|
633
|
+
* for new work goes on the Goal it creates, owner 2026-10-26), and what the line says in at most twenty
|
|
634
|
+
* words (owner, 2026-10-26: "bump it from 10 to 20 words"). */
|
|
635
|
+
filed: z8.array(z8.object({ line: str4, goals: strs3, summary: str4 }).strict()),
|
|
617
636
|
goals: z8.array(z8.object({
|
|
618
637
|
op: z8.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
619
|
-
key:
|
|
620
|
-
goal:
|
|
621
|
-
title:
|
|
622
|
-
outcome:
|
|
623
|
-
owner:
|
|
624
|
-
under:
|
|
625
|
-
until:
|
|
626
|
-
on:
|
|
638
|
+
key: str4,
|
|
639
|
+
goal: str4,
|
|
640
|
+
title: str4,
|
|
641
|
+
outcome: str4,
|
|
642
|
+
owner: str4,
|
|
643
|
+
under: str4,
|
|
644
|
+
until: str4,
|
|
645
|
+
on: str4,
|
|
627
646
|
gate: z8.enum(["start", "complete", "answer", ""]),
|
|
628
|
-
why:
|
|
647
|
+
why: str4
|
|
629
648
|
}).strict()),
|
|
630
649
|
questions: z8.array(z8.object({
|
|
631
650
|
op: z8.enum(["create", "edit", "assign", "withdraw"]),
|
|
632
|
-
key:
|
|
633
|
-
question:
|
|
634
|
-
text:
|
|
635
|
-
summary:
|
|
636
|
-
to:
|
|
637
|
-
goal:
|
|
638
|
-
options: z8.array(z8.object({ id:
|
|
651
|
+
key: str4,
|
|
652
|
+
question: str4,
|
|
653
|
+
text: str4,
|
|
654
|
+
summary: str4,
|
|
655
|
+
to: str4,
|
|
656
|
+
goal: str4,
|
|
657
|
+
options: z8.array(z8.object({ id: str4, label: str4 }).strict()),
|
|
639
658
|
pick: z8.enum(["one", "many", "rank", "words", ""]),
|
|
640
|
-
blocks:
|
|
659
|
+
blocks: str4
|
|
641
660
|
}).strict()),
|
|
642
|
-
lessons: z8.array(z8.object({ op: z8.enum(["remember", "revise", "withdraw"]), lesson:
|
|
661
|
+
lessons: z8.array(z8.object({ op: z8.enum(["remember", "revise", "withdraw"]), lesson: str4, text: str4, scope: str4 }).strict()),
|
|
643
662
|
/** Line handles whose words comment on Paigy itself. */
|
|
644
663
|
feedback: strs3
|
|
645
664
|
}).strict();
|
|
@@ -700,6 +719,27 @@ var TidyReplySchema = z10.object({
|
|
|
700
719
|
var TIDY_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
701
720
|
zodToJsonSchema6(TidyReplySchema, { $refStrategy: "none" })
|
|
702
721
|
);
|
|
722
|
+
var HandoutReplySchema = z11.object({
|
|
723
|
+
assign: z11.array(z11.object({
|
|
724
|
+
/** The Goal's handle (G). */
|
|
725
|
+
goal: z11.string(),
|
|
726
|
+
/** The agent's handle (A). */
|
|
727
|
+
to: z11.string(),
|
|
728
|
+
/** At most ten words, naming the Goal of theirs it is most like: the reason the hand-out records. */
|
|
729
|
+
why: z11.string()
|
|
730
|
+
}).strict()),
|
|
731
|
+
/** The Goals no listed agent's work is even loosely like: Paigy starts a session for them (owner, 2026-10-24,
|
|
732
|
+
* Goal 37603006, "One per family of related Goals"). */
|
|
733
|
+
new: z11.array(z11.object({
|
|
734
|
+
/** The Goal's handle (G). */
|
|
735
|
+
goal: z11.string(),
|
|
736
|
+
/** At most ten words: why nothing active fits it. */
|
|
737
|
+
why: z11.string()
|
|
738
|
+
}).strict())
|
|
739
|
+
}).strict();
|
|
740
|
+
var HANDOUT_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
741
|
+
zodToJsonSchema7(HandoutReplySchema, { $refStrategy: "none" })
|
|
742
|
+
);
|
|
703
743
|
function entryWords(entry) {
|
|
704
744
|
const content = entry.content;
|
|
705
745
|
if (content && "sealed" in content) return "";
|
|
@@ -728,20 +768,20 @@ var LIVE_MS = 3 * 6e4;
|
|
|
728
768
|
var WORKING_MS = 60 * 6e4;
|
|
729
769
|
var WORKING_TIER_MS = 2 * 60 * 6e4;
|
|
730
770
|
var RECENT_TIER_MS = 24 * 60 * 6e4;
|
|
731
|
-
var ContextSchema =
|
|
732
|
-
title:
|
|
733
|
-
description:
|
|
771
|
+
var ContextSchema = z12.object({
|
|
772
|
+
title: z12.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
773
|
+
description: z12.array(z12.string().min(1)).describe(
|
|
734
774
|
"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."
|
|
735
775
|
),
|
|
736
776
|
/** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
|
|
737
777
|
* a body on a tap. Absent on an ask sent without units, which renders as before. */
|
|
738
|
-
units:
|
|
778
|
+
units: z12.array(z12.object({ title: z12.string(), body: z12.string() })).optional()
|
|
739
779
|
});
|
|
740
|
-
var ParticipantSchema =
|
|
741
|
-
kind:
|
|
742
|
-
id:
|
|
780
|
+
var ParticipantSchema = z12.object({
|
|
781
|
+
kind: z12.enum(["human", "agent"]),
|
|
782
|
+
id: z12.string()
|
|
743
783
|
});
|
|
744
|
-
var TransformSchema =
|
|
784
|
+
var TransformSchema = z12.enum([
|
|
745
785
|
"structure",
|
|
746
786
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
747
787
|
"request_more",
|
|
@@ -757,13 +797,13 @@ var TransformSchema = z11.enum([
|
|
|
757
797
|
"summarize"
|
|
758
798
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
759
799
|
]);
|
|
760
|
-
var VisualSchema =
|
|
761
|
-
url:
|
|
762
|
-
label:
|
|
800
|
+
var VisualSchema = z12.object({
|
|
801
|
+
url: z12.string().url(),
|
|
802
|
+
label: z12.string().optional()
|
|
763
803
|
});
|
|
764
|
-
var NotifyLevelSchema =
|
|
765
|
-
var SelectShapeSchema =
|
|
766
|
-
var ReceiptEventSchema =
|
|
804
|
+
var NotifyLevelSchema = z12.enum(["inbox", "push", "banner", "call"]);
|
|
805
|
+
var SelectShapeSchema = z12.enum(["one", "many", "rank", "confirm", "text"]);
|
|
806
|
+
var ReceiptEventSchema = z12.enum([
|
|
767
807
|
"delivered",
|
|
768
808
|
// the bundle reached the recipient at some level
|
|
769
809
|
"seen",
|
|
@@ -793,47 +833,47 @@ var ReceiptEventSchema = z11.enum([
|
|
|
793
833
|
// be rewound by a writer that forgot to advance it.
|
|
794
834
|
"restarted"
|
|
795
835
|
]);
|
|
796
|
-
var AttentionSchema =
|
|
836
|
+
var AttentionSchema = z12.object({
|
|
797
837
|
urgency: NotifyLevelSchema,
|
|
798
838
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
799
839
|
select: SelectShapeSchema.nullable(),
|
|
800
840
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
801
|
-
points:
|
|
841
|
+
points: z12.array(z12.string()).nullable(),
|
|
802
842
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
803
|
-
blocking:
|
|
843
|
+
blocking: z12.boolean(),
|
|
804
844
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
805
845
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
806
|
-
deadline:
|
|
846
|
+
deadline: z12.string().datetime().nullable().optional()
|
|
807
847
|
});
|
|
808
|
-
var NotifyRequestFields =
|
|
848
|
+
var NotifyRequestFields = z12.object({
|
|
809
849
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
810
850
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
811
851
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
812
852
|
context: ContextSchema.optional(),
|
|
813
|
-
options:
|
|
853
|
+
options: z12.array(OptionSchema.omit({ id: true }).extend({ label: z12.string().trim().min(1).max(1e3) }).strict()).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
814
854
|
"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)."
|
|
815
855
|
),
|
|
816
|
-
points:
|
|
856
|
+
points: z12.array(z12.string().min(1)).optional().describe(
|
|
817
857
|
"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."
|
|
818
858
|
),
|
|
819
|
-
visuals:
|
|
859
|
+
visuals: z12.array(VisualSchema).optional().describe(
|
|
820
860
|
"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."
|
|
821
861
|
),
|
|
822
862
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
823
|
-
repo:
|
|
863
|
+
repo: z12.string().optional(),
|
|
824
864
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
825
|
-
branch:
|
|
865
|
+
branch: z12.string().optional(),
|
|
826
866
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
827
867
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
828
868
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
829
869
|
* still accepts the old name from older clients. */
|
|
830
|
-
parentId:
|
|
870
|
+
parentId: z12.string().uuid().optional(),
|
|
831
871
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
832
872
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
833
|
-
workId:
|
|
873
|
+
workId: z12.string().uuid().optional(),
|
|
834
874
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
835
875
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
836
|
-
goalId:
|
|
876
|
+
goalId: z12.string().uuid().optional(),
|
|
837
877
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
838
878
|
"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."
|
|
839
879
|
),
|
|
@@ -841,7 +881,7 @@ var NotifyRequestFields = z11.object({
|
|
|
841
881
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
842
882
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
843
883
|
* says HOW. */
|
|
844
|
-
clarifies:
|
|
884
|
+
clarifies: z12.string().optional(),
|
|
845
885
|
select: SelectShapeSchema.optional().describe(
|
|
846
886
|
"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."
|
|
847
887
|
),
|
|
@@ -852,23 +892,23 @@ var NotifyRequestFields = z11.object({
|
|
|
852
892
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
853
893
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
854
894
|
// the ask into topics and gives each one at most three sentences and one question
|
|
855
|
-
// (docs/
|
|
895
|
+
// (docs/history/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
856
896
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
857
897
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
858
|
-
ask:
|
|
898
|
+
ask: z12.string().min(1).max(1e4).optional().describe(
|
|
859
899
|
'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.'
|
|
860
900
|
),
|
|
861
|
-
needs:
|
|
901
|
+
needs: z12.array(z12.string().min(1)).optional().describe(
|
|
862
902
|
"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."
|
|
863
903
|
),
|
|
864
|
-
urgencyHint:
|
|
904
|
+
urgencyHint: z12.enum(["whenever", "soon", "now"]).optional().describe(
|
|
865
905
|
"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."
|
|
866
906
|
),
|
|
867
907
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
868
908
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
869
909
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
870
910
|
* urgencyHint/blocking win when both are sent. */
|
|
871
|
-
waiting:
|
|
911
|
+
waiting: z12.enum(["none", "soft", "hard"]).optional().describe(
|
|
872
912
|
"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."
|
|
873
913
|
),
|
|
874
914
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -876,98 +916,98 @@ var NotifyRequestFields = z11.object({
|
|
|
876
916
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
877
917
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
878
918
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
879
|
-
confirm:
|
|
919
|
+
confirm: z12.boolean().optional().describe(
|
|
880
920
|
"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'."
|
|
881
921
|
),
|
|
882
922
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
883
923
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
884
924
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
885
925
|
* waiting:'hard'. */
|
|
886
|
-
channel:
|
|
926
|
+
channel: z12.enum(["call", "message"]).optional().describe(
|
|
887
927
|
"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."
|
|
888
928
|
),
|
|
889
|
-
confirmStyle:
|
|
929
|
+
confirmStyle: z12.enum(["yesno", "approve"]).default("yesno").describe(
|
|
890
930
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
891
931
|
),
|
|
892
|
-
blocking:
|
|
932
|
+
blocking: z12.boolean().default(false).describe(
|
|
893
933
|
"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."
|
|
894
934
|
)
|
|
895
935
|
});
|
|
896
936
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
897
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
937
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z12.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
898
938
|
if (r.ask !== void 0) {
|
|
899
939
|
for (const f of ["context", "select", "points"]) {
|
|
900
940
|
if (r[f] !== void 0)
|
|
901
|
-
ctx.addIssue({ code:
|
|
941
|
+
ctx.addIssue({ code: z12.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.` });
|
|
902
942
|
}
|
|
903
943
|
return;
|
|
904
944
|
}
|
|
905
945
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
906
|
-
ctx.addIssue({ code:
|
|
946
|
+
ctx.addIssue({ code: z12.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
907
947
|
if (!r.context)
|
|
908
|
-
ctx.addIssue({ code:
|
|
948
|
+
ctx.addIssue({ code: z12.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
909
949
|
if (!r.select)
|
|
910
|
-
ctx.addIssue({ code:
|
|
950
|
+
ctx.addIssue({ code: z12.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
911
951
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
912
952
|
if (needsOptions && !r.options?.length)
|
|
913
|
-
ctx.addIssue({ code:
|
|
953
|
+
ctx.addIssue({ code: z12.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
914
954
|
if (!needsOptions && r.options?.length)
|
|
915
|
-
ctx.addIssue({ code:
|
|
955
|
+
ctx.addIssue({ code: z12.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
916
956
|
});
|
|
917
|
-
var NotifyStatusSchema =
|
|
918
|
-
var AgentStateSchema =
|
|
919
|
-
var TurnSchema =
|
|
920
|
-
prompt:
|
|
921
|
-
reply:
|
|
957
|
+
var NotifyStatusSchema = z12.enum(["pending", "answered", "ignored"]);
|
|
958
|
+
var AgentStateSchema = z12.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
959
|
+
var TurnSchema = z12.object({
|
|
960
|
+
prompt: z12.string(),
|
|
961
|
+
reply: z12.string()
|
|
922
962
|
});
|
|
923
|
-
var UserAnswerSchema =
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
/** An auto-answer derived from the user's PAST decisions (docs/
|
|
963
|
+
var UserAnswerSchema = z12.discriminatedUnion("kind", [
|
|
964
|
+
z12.object({ kind: z12.literal("option"), optionId: z12.string(), label: z12.string().optional() }),
|
|
965
|
+
z12.object({ kind: z12.literal("text"), text: z12.string() }),
|
|
966
|
+
z12.object({ kind: z12.literal("ignored") }),
|
|
967
|
+
z12.object({ kind: z12.literal("multi"), optionIds: z12.array(z12.string()), labels: z12.array(z12.string()).optional() }),
|
|
968
|
+
z12.object({ kind: z12.literal("ranked"), optionIds: z12.array(z12.string()), labels: z12.array(z12.string()).optional() }),
|
|
969
|
+
z12.object({ kind: z12.literal("clarify"), chunks: z12.array(z12.string()).min(1) }),
|
|
970
|
+
z12.object({ kind: z12.literal("confirm"), approved: z12.boolean() }),
|
|
971
|
+
z12.object({ kind: z12.literal("turns"), turns: z12.array(TurnSchema).min(1) }),
|
|
972
|
+
/** An auto-answer derived from the user's PAST decisions (docs/history/precedent-design.md §2):
|
|
933
973
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
934
974
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
935
975
|
* card the user can reply to — the broker never overrides the user. */
|
|
936
|
-
|
|
976
|
+
z12.object({ kind: z12.literal("precedent"), answer: z12.string(), derivation: z12.string(), sources: z12.array(z12.string()).min(1) })
|
|
937
977
|
]);
|
|
938
|
-
var IntentSchema =
|
|
978
|
+
var IntentSchema = z12.object({
|
|
939
979
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
940
980
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
941
981
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
942
982
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
943
|
-
kind:
|
|
944
|
-
detail:
|
|
983
|
+
kind: z12.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
984
|
+
detail: z12.string(),
|
|
945
985
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
946
986
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
947
987
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
948
988
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
949
|
-
dueInSeconds:
|
|
989
|
+
dueInSeconds: z12.number().int().positive().optional(),
|
|
950
990
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
951
991
|
* already read the utterance, so `signals.kind` stops defaulting to
|
|
952
992
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
953
993
|
* about what cannot answer "is the bot looping less this week?". */
|
|
954
|
-
fault:
|
|
994
|
+
fault: z12.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
955
995
|
});
|
|
956
|
-
var RideAlongSchema =
|
|
996
|
+
var RideAlongSchema = z12.object({
|
|
957
997
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
958
|
-
noteId:
|
|
998
|
+
noteId: z12.string(),
|
|
959
999
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
960
|
-
text:
|
|
1000
|
+
text: z12.string(),
|
|
961
1001
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
962
|
-
parentId:
|
|
1002
|
+
parentId: z12.string().nullable()
|
|
963
1003
|
});
|
|
964
|
-
var AwaitItemSchema =
|
|
965
|
-
|
|
966
|
-
type:
|
|
967
|
-
parentId:
|
|
968
|
-
notificationId:
|
|
969
|
-
workId:
|
|
970
|
-
decisionId:
|
|
1004
|
+
var AwaitItemSchema = z12.discriminatedUnion("type", [
|
|
1005
|
+
z12.object({
|
|
1006
|
+
type: z12.literal("reply"),
|
|
1007
|
+
parentId: z12.string(),
|
|
1008
|
+
notificationId: z12.string(),
|
|
1009
|
+
workId: z12.string().uuid().optional(),
|
|
1010
|
+
decisionId: z12.string().uuid().optional(),
|
|
971
1011
|
answer: UserAnswerSchema,
|
|
972
1012
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
973
1013
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -977,111 +1017,126 @@ var AwaitItemSchema = z11.discriminatedUnion("type", [
|
|
|
977
1017
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
978
1018
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
979
1019
|
* a sentence with a hole in it is worse than no sentence. */
|
|
980
|
-
note:
|
|
1020
|
+
note: z12.string().optional(),
|
|
981
1021
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
982
1022
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
983
1023
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
984
1024
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
985
|
-
transcript:
|
|
1025
|
+
transcript: z12.string().optional(),
|
|
986
1026
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
987
1027
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
988
|
-
covered:
|
|
1028
|
+
covered: z12.array(z12.string()).optional(),
|
|
989
1029
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
990
1030
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
991
1031
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
992
1032
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
993
1033
|
* dropped. Absent/empty = nothing owed. */
|
|
994
|
-
also:
|
|
1034
|
+
also: z12.array(RideAlongSchema).optional()
|
|
995
1035
|
}),
|
|
996
|
-
|
|
997
|
-
type:
|
|
998
|
-
parentId:
|
|
999
|
-
notificationId:
|
|
1000
|
-
remindAt:
|
|
1036
|
+
z12.object({
|
|
1037
|
+
type: z12.literal("remind"),
|
|
1038
|
+
parentId: z12.string(),
|
|
1039
|
+
notificationId: z12.string(),
|
|
1040
|
+
remindAt: z12.string().datetime({ offset: true }),
|
|
1001
1041
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
1002
|
-
remindInSeconds:
|
|
1042
|
+
remindInSeconds: z12.number()
|
|
1003
1043
|
}),
|
|
1004
1044
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
1005
1045
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
1006
1046
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
1007
1047
|
* re-orient via contact({})). */
|
|
1008
|
-
|
|
1009
|
-
type:
|
|
1010
|
-
parentId:
|
|
1011
|
-
notificationId:
|
|
1048
|
+
z12.object({
|
|
1049
|
+
type: z12.literal("superseded"),
|
|
1050
|
+
parentId: z12.string(),
|
|
1051
|
+
notificationId: z12.string()
|
|
1012
1052
|
}),
|
|
1013
1053
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
1014
1054
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
1015
1055
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
1016
1056
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
1017
1057
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
1018
|
-
|
|
1019
|
-
type:
|
|
1020
|
-
notificationId:
|
|
1021
|
-
inFlight:
|
|
1022
|
-
turn:
|
|
1023
|
-
idx:
|
|
1024
|
-
prompt:
|
|
1025
|
-
reply:
|
|
1026
|
-
acts:
|
|
1058
|
+
z12.object({
|
|
1059
|
+
type: z12.literal("partial"),
|
|
1060
|
+
notificationId: z12.string(),
|
|
1061
|
+
inFlight: z12.literal(true),
|
|
1062
|
+
turn: z12.object({
|
|
1063
|
+
idx: z12.number(),
|
|
1064
|
+
prompt: z12.string(),
|
|
1065
|
+
reply: z12.string(),
|
|
1066
|
+
acts: z12.array(IntentSchema).nullable().optional()
|
|
1027
1067
|
})
|
|
1028
1068
|
}),
|
|
1029
|
-
|
|
1030
|
-
type:
|
|
1031
|
-
also:
|
|
1069
|
+
z12.object({
|
|
1070
|
+
type: z12.literal("idle"),
|
|
1071
|
+
also: z12.array(RideAlongSchema).optional(),
|
|
1032
1072
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
1033
1073
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
1034
1074
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
1035
1075
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
1036
1076
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
1037
|
-
inFlight:
|
|
1077
|
+
inFlight: z12.boolean().optional()
|
|
1038
1078
|
})
|
|
1039
1079
|
]);
|
|
1040
|
-
var VoiceKeySchema =
|
|
1041
|
-
var AgendaTurnSchema =
|
|
1080
|
+
var VoiceKeySchema = z12.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
1081
|
+
var AgendaTurnSchema = z12.object({
|
|
1042
1082
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
1043
1083
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
1044
1084
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
1045
1085
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
1046
1086
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
1047
1087
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
1048
|
-
id:
|
|
1088
|
+
id: z12.string().optional(),
|
|
1049
1089
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
1050
1090
|
* the planner declares duplicates instead of asking them twice. */
|
|
1051
|
-
coveredIds:
|
|
1091
|
+
coveredIds: z12.array(z12.string()).optional(),
|
|
1052
1092
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
1053
1093
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
1054
|
-
info:
|
|
1094
|
+
info: z12.array(z12.string().min(1)).default([]),
|
|
1055
1095
|
/** THE ID OF EACH SENTENCE, when the plan names them (turns.md): a talker message's sentences are its
|
|
1056
1096
|
* `info`, one piece each, and `units[i]` is the id the bot's line receipt carries for `info[i]`
|
|
1057
1097
|
* (`<cycle>:<key>:<n>`), so what was heard is known per sentence. Absent on every other turn. */
|
|
1058
|
-
units:
|
|
1059
|
-
question:
|
|
1098
|
+
units: z12.array(z12.string().min(1)).optional(),
|
|
1099
|
+
question: z12.string().min(1).nullable(),
|
|
1060
1100
|
/** True on the one turn carrying the agent's own declared question. */
|
|
1061
|
-
asks:
|
|
1101
|
+
asks: z12.boolean().optional(),
|
|
1062
1102
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
1063
1103
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
1064
1104
|
* which route nothing. */
|
|
1065
|
-
claimId:
|
|
1105
|
+
claimId: z12.string().optional(),
|
|
1066
1106
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
1067
|
-
voice:
|
|
1107
|
+
voice: z12.string().optional(),
|
|
1068
1108
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
1069
1109
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
1070
1110
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
1071
1111
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
1072
|
-
agent:
|
|
1112
|
+
agent: z12.string().optional(),
|
|
1073
1113
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
1074
1114
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
1075
1115
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
1076
1116
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
1077
1117
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
1078
|
-
agentId:
|
|
1118
|
+
agentId: z12.string().optional(),
|
|
1079
1119
|
/** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
|
|
1080
1120
|
* the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
|
|
1081
1121
|
* question turn — the talker owns re-asking and the waits, so `question` stays null. */
|
|
1082
|
-
onTable:
|
|
1122
|
+
onTable: z12.boolean().optional(),
|
|
1123
|
+
/** THE CARRIED ITEM THIS LINE TELLS (Goal 2a1e50eb, owner 2026-10-14: "Build the whole design") — a
|
|
1124
|
+
* request the Call carries that asks NOTHING: a report, a progress note, another agent's waiting news.
|
|
1125
|
+
* Its request Entry id, so the phone can make that agenda row the current one while it is read
|
|
1126
|
+
* (`telling` on the `turn` topic, `docs/clients/app/walk/design.md` §11).
|
|
1127
|
+
*
|
|
1128
|
+
* A FIELD OF ITS OWN, NEVER `claimId`: the claim is the RETURN identity — answers route by it and the
|
|
1129
|
+
* record tags the line with it (`workId`) — and an answer must never route to an Entry that asks
|
|
1130
|
+
* nothing. Absent on a turn speaking for a question (`claimId` says that) and on one speaking for no
|
|
1131
|
+
* carried item at all (an opener, a sign-off, Paigy's own line). */
|
|
1132
|
+
telling: z12.string().optional(),
|
|
1133
|
+
/** PAIGY'S OWN LINE (owner, 2026-10-15; calls ed2cbdce, ad50ad25): a turn that speaks for no item -- an
|
|
1134
|
+
* opener, a sign-off, a reply about nothing an agent asked or told -- is recorded as Paigy's, not as the
|
|
1135
|
+
* agent that rang. Absent on every turn that speaks for an item (`agentId` names whose), and on plans
|
|
1136
|
+
* from before it: a bot that does not know it records the ringing agent, as before. */
|
|
1137
|
+
speaksFor: z12.literal("paigy").optional(),
|
|
1083
1138
|
select: SelectShapeSchema.optional(),
|
|
1084
|
-
options:
|
|
1139
|
+
options: z12.array(OptionSchema.omit({ id: true })).optional(),
|
|
1085
1140
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
1086
1141
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
1087
1142
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -1090,58 +1145,58 @@ var AgendaTurnSchema = z11.object({
|
|
|
1090
1145
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
1091
1146
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
1092
1147
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
1093
|
-
blocking:
|
|
1148
|
+
blocking: z12.boolean().optional(),
|
|
1094
1149
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
1095
1150
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
1096
1151
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
1097
1152
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
1098
1153
|
* speak, it is dropped and their words are taken like any reply. */
|
|
1099
|
-
ifSilent:
|
|
1154
|
+
ifSilent: z12.boolean().optional()
|
|
1100
1155
|
});
|
|
1101
1156
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
1102
|
-
var CallQuestionSchema =
|
|
1103
|
-
id:
|
|
1104
|
-
entryId:
|
|
1105
|
-
title:
|
|
1106
|
-
state:
|
|
1107
|
-
answer:
|
|
1157
|
+
var CallQuestionSchema = z12.object({
|
|
1158
|
+
id: z12.string(),
|
|
1159
|
+
entryId: z12.string(),
|
|
1160
|
+
title: z12.string(),
|
|
1161
|
+
state: z12.enum(["open", "answered"]),
|
|
1162
|
+
answer: z12.string().nullable(),
|
|
1108
1163
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
1109
1164
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
1110
1165
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
1111
1166
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
1112
|
-
agent:
|
|
1167
|
+
agent: z12.object({ tokenId: z12.string(), name: z12.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
1113
1168
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
1114
1169
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
1115
1170
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
1116
1171
|
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
1117
|
-
options:
|
|
1172
|
+
options: z12.array(OptionSchema).optional(),
|
|
1118
1173
|
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
1119
1174
|
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
1120
1175
|
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
1121
1176
|
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
1122
1177
|
* preview layout for any `options`, and must see exactly what it did. */
|
|
1123
|
-
choices:
|
|
1178
|
+
choices: z12.array(OptionSchema).optional(),
|
|
1124
1179
|
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
1125
|
-
select:
|
|
1180
|
+
select: z12.enum(["one", "many", "rank"]).optional()
|
|
1126
1181
|
});
|
|
1127
|
-
var InboxItemSchema =
|
|
1128
|
-
id:
|
|
1129
|
-
tokenId:
|
|
1182
|
+
var InboxItemSchema = z12.object({
|
|
1183
|
+
id: z12.string(),
|
|
1184
|
+
tokenId: z12.string().optional(),
|
|
1130
1185
|
status: NotifyStatusSchema,
|
|
1131
1186
|
context: ContextSchema,
|
|
1132
|
-
options:
|
|
1187
|
+
options: z12.array(OptionSchema).optional(),
|
|
1133
1188
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
1134
|
-
points:
|
|
1189
|
+
points: z12.array(z12.string()).optional(),
|
|
1135
1190
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
1136
1191
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
1137
1192
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
1138
1193
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
1139
|
-
asks:
|
|
1194
|
+
asks: z12.boolean().optional(),
|
|
1140
1195
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
1141
1196
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
1142
1197
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
1143
1198
|
* which must never CLAIM stalled. */
|
|
1144
|
-
lastSeenAt:
|
|
1199
|
+
lastSeenAt: z12.string().optional(),
|
|
1145
1200
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
1146
1201
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
1147
1202
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -1151,7 +1206,7 @@ var InboxItemSchema = z11.object({
|
|
|
1151
1206
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
1152
1207
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
1153
1208
|
* `createdAt`. */
|
|
1154
|
-
agentStateAt:
|
|
1209
|
+
agentStateAt: z12.string().datetime().optional(),
|
|
1155
1210
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
1156
1211
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
1157
1212
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -1165,40 +1220,40 @@ var InboxItemSchema = z11.object({
|
|
|
1165
1220
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
1166
1221
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
1167
1222
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
1168
|
-
questions:
|
|
1223
|
+
questions: z12.array(CallQuestionSchema).optional(),
|
|
1169
1224
|
/** The same call carrying order, including request Entries with no DecisionNeed as reports.
|
|
1170
1225
|
* Additive: questions stays decision-only for every other surface and older bundles. */
|
|
1171
|
-
callItems:
|
|
1172
|
-
visuals:
|
|
1226
|
+
callItems: z12.array(CallQuestionSchema.extend({ state: z12.enum(["open", "answered", "report"]), acknowledged: z12.boolean().optional() })).optional(),
|
|
1227
|
+
visuals: z12.array(VisualSchema).optional(),
|
|
1173
1228
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
1174
1229
|
* agent's suggestion, or a default silly name). */
|
|
1175
|
-
name:
|
|
1230
|
+
name: z12.string(),
|
|
1176
1231
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
1177
1232
|
voice: VoiceKeySchema.optional(),
|
|
1178
|
-
repo:
|
|
1179
|
-
branch:
|
|
1180
|
-
createdAt:
|
|
1181
|
-
snoozedUntil:
|
|
1233
|
+
repo: z12.string().optional(),
|
|
1234
|
+
branch: z12.string().optional(),
|
|
1235
|
+
createdAt: z12.string().datetime(),
|
|
1236
|
+
snoozedUntil: z12.string().datetime().optional(),
|
|
1182
1237
|
agentState: AgentStateSchema.default("idle"),
|
|
1183
1238
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
1184
1239
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
1185
1240
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
1186
|
-
turn:
|
|
1241
|
+
turn: z12.enum(["you", "agent"]).default("you"),
|
|
1187
1242
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
1188
1243
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
1189
1244
|
* error badge + Retry. */
|
|
1190
|
-
error:
|
|
1245
|
+
error: z12.string().optional(),
|
|
1191
1246
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
1192
1247
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
1193
1248
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
1194
1249
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
1195
|
-
cold:
|
|
1196
|
-
clarifies:
|
|
1250
|
+
cold: z12.string().datetime().optional(),
|
|
1251
|
+
clarifies: z12.string().optional(),
|
|
1197
1252
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
1198
1253
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
1199
1254
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
1200
1255
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
1201
|
-
onCall:
|
|
1256
|
+
onCall: z12.literal(true).optional(),
|
|
1202
1257
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
1203
1258
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
1204
1259
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -1208,31 +1263,52 @@ var InboxItemSchema = z11.object({
|
|
|
1208
1263
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
1209
1264
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
1210
1265
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
1211
|
-
ring:
|
|
1212
|
-
declinedAt:
|
|
1213
|
-
anchorAt:
|
|
1214
|
-
nextRingAt:
|
|
1215
|
-
step:
|
|
1266
|
+
ring: z12.object({
|
|
1267
|
+
declinedAt: z12.string().datetime(),
|
|
1268
|
+
anchorAt: z12.string().datetime(),
|
|
1269
|
+
nextRingAt: z12.string().datetime().nullable(),
|
|
1270
|
+
step: z12.number().int()
|
|
1216
1271
|
}).optional(),
|
|
1217
1272
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
1218
1273
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
1219
1274
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
1220
1275
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
1221
1276
|
* feedback ABOUT. */
|
|
1222
|
-
why:
|
|
1277
|
+
why: z12.object({
|
|
1223
1278
|
asked: NotifyLevelSchema,
|
|
1224
1279
|
got: NotifyLevelSchema,
|
|
1225
|
-
because:
|
|
1226
|
-
line:
|
|
1280
|
+
because: z12.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
1281
|
+
line: z12.string()
|
|
1227
1282
|
}).optional(),
|
|
1228
|
-
select:
|
|
1229
|
-
confirmStyle:
|
|
1283
|
+
select: z12.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
1284
|
+
confirmStyle: z12.enum(["yesno", "approve"]).default("yesno").describe(
|
|
1230
1285
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
1231
1286
|
),
|
|
1232
1287
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
1233
|
-
* urgency (see the main README's "premier use case" + docs/
|
|
1288
|
+
* urgency (see the main README's "premier use case" + docs/history/notify-states.md). Drives the
|
|
1234
1289
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
1235
|
-
blocking:
|
|
1290
|
+
blocking: z12.boolean().default(false),
|
|
1291
|
+
/** WHAT FREED THIS QUESTION (owner, 2026-10-16, Goal 2383ef14: "we don't really show the question
|
|
1292
|
+
* at the top until the question it could reply to with is resolved. And then we show it again
|
|
1293
|
+
* with the context of the other thing that was resolved"; asked how it should carry that, "The
|
|
1294
|
+
* answer above it"). A question they answered with a question of their own waits for theirs
|
|
1295
|
+
* (`private.ask_back`) and drops out of the front while it waits; when it comes back, the card
|
|
1296
|
+
* draws their question and the answer it got ABOVE the ask, as one card. Absent on a question
|
|
1297
|
+
* nothing ever held back. */
|
|
1298
|
+
freedBy: z12.object({
|
|
1299
|
+
questionId: z12.string(),
|
|
1300
|
+
asked: z12.string(),
|
|
1301
|
+
answer: z12.string(),
|
|
1302
|
+
answeredAt: z12.string(),
|
|
1303
|
+
/** The answer in ten words, when the talker wrote one beside it. */
|
|
1304
|
+
summary: z12.string().optional()
|
|
1305
|
+
}).optional(),
|
|
1306
|
+
/** WHAT THIS CARD ANSWERS (owner, 2026-10-27, Goal 81b6a9d6: "this reads very clearly as an answer to
|
|
1307
|
+
* something I asked, but I don't see the question… To show the question near the top of the card").
|
|
1308
|
+
* An agent's card that is its answer to one of the person's own questions carries that question's
|
|
1309
|
+
* words, drawn above it as "You asked" (`public.questions_answered_by`, read beside the card read).
|
|
1310
|
+
* Absent on every card that answers nothing of theirs, and on a held question, which has `freedBy`. */
|
|
1311
|
+
answering: z12.object({ asked: z12.string() }).optional(),
|
|
1236
1312
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
1237
1313
|
answer: UserAnswerSchema.optional(),
|
|
1238
1314
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -1240,17 +1316,17 @@ var InboxItemSchema = z11.object({
|
|
|
1240
1316
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
1241
1317
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
1242
1318
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
1243
|
-
communication:
|
|
1244
|
-
deliveryId:
|
|
1245
|
-
kind:
|
|
1246
|
-
entryId:
|
|
1247
|
-
goalIds:
|
|
1248
|
-
decisionNeedId:
|
|
1249
|
-
sealed:
|
|
1250
|
-
goalState:
|
|
1319
|
+
communication: z12.object({
|
|
1320
|
+
deliveryId: z12.string(),
|
|
1321
|
+
kind: z12.enum(["notification", "call"]),
|
|
1322
|
+
entryId: z12.string(),
|
|
1323
|
+
goalIds: z12.array(z12.string()),
|
|
1324
|
+
decisionNeedId: z12.string().optional(),
|
|
1325
|
+
sealed: z12.boolean(),
|
|
1326
|
+
goalState: z12.string().optional(),
|
|
1251
1327
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
1252
1328
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
1253
|
-
goalTitle:
|
|
1329
|
+
goalTitle: z12.string().optional()
|
|
1254
1330
|
}).optional(),
|
|
1255
1331
|
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
1256
1332
|
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
@@ -1262,27 +1338,27 @@ var InboxItemSchema = z11.object({
|
|
|
1262
1338
|
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
1263
1339
|
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
1264
1340
|
* with no rev is simply always re-sent. */
|
|
1265
|
-
rev:
|
|
1341
|
+
rev: z12.string().optional()
|
|
1266
1342
|
});
|
|
1267
1343
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
1268
|
-
var PushTokenSchema =
|
|
1269
|
-
voipToken:
|
|
1270
|
-
alertToken:
|
|
1271
|
-
fcmToken:
|
|
1272
|
-
platform:
|
|
1344
|
+
var PushTokenSchema = z12.object({
|
|
1345
|
+
voipToken: z12.string().min(1).optional(),
|
|
1346
|
+
alertToken: z12.string().min(1).optional(),
|
|
1347
|
+
fcmToken: z12.string().min(1).optional(),
|
|
1348
|
+
platform: z12.enum(["ios", "android"])
|
|
1273
1349
|
}).superRefine((v, ctx) => {
|
|
1274
1350
|
if (v.platform !== "ios") return;
|
|
1275
1351
|
for (const field of ["voipToken", "alertToken"]) {
|
|
1276
1352
|
const token = v[field];
|
|
1277
1353
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
1278
1354
|
ctx.addIssue({
|
|
1279
|
-
code:
|
|
1355
|
+
code: z12.ZodIssueCode.custom,
|
|
1280
1356
|
path: [field],
|
|
1281
1357
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
1282
1358
|
});
|
|
1283
1359
|
}
|
|
1284
1360
|
});
|
|
1285
|
-
var MissedCallSchema =
|
|
1361
|
+
var MissedCallSchema = z12.enum([
|
|
1286
1362
|
"retry_10m",
|
|
1287
1363
|
"retry_30m",
|
|
1288
1364
|
"retry_60m",
|
|
@@ -1294,31 +1370,32 @@ var MissedCallSchema = z11.enum([
|
|
|
1294
1370
|
]);
|
|
1295
1371
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
1296
1372
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
1297
|
-
var
|
|
1373
|
+
var THEIR_QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} their time; the count waits for morning.`;
|
|
1374
|
+
var BrokerTuningSchema = z12.object({
|
|
1298
1375
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
1299
|
-
ackVerbosity:
|
|
1376
|
+
ackVerbosity: z12.enum(["normal", "none"]).optional(),
|
|
1300
1377
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
1301
1378
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
1302
|
-
clarifyEagerness:
|
|
1379
|
+
clarifyEagerness: z12.enum(["low", "normal", "high"]).optional(),
|
|
1303
1380
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
1304
|
-
phrasebook:
|
|
1381
|
+
phrasebook: z12.array(z12.object({ say: z12.string().min(1).max(60), mean: z12.string().min(1).max(120) })).max(24).optional(),
|
|
1305
1382
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
1306
1383
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
1307
1384
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
1308
1385
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
1309
1386
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
1310
|
-
language:
|
|
1387
|
+
language: z12.enum(["en", "es"]).optional()
|
|
1311
1388
|
});
|
|
1312
|
-
var UserSettingsSchema =
|
|
1313
|
-
permissions:
|
|
1314
|
-
call:
|
|
1315
|
-
banner:
|
|
1316
|
-
push:
|
|
1389
|
+
var UserSettingsSchema = z12.object({
|
|
1390
|
+
permissions: z12.object({
|
|
1391
|
+
call: z12.boolean(),
|
|
1392
|
+
banner: z12.boolean(),
|
|
1393
|
+
push: z12.boolean()
|
|
1317
1394
|
}),
|
|
1318
1395
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
1319
1396
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
1320
1397
|
* what people read (owner, 2026-09-30). */
|
|
1321
|
-
sessionMode:
|
|
1398
|
+
sessionMode: z12.enum(["default", "all_calls", "silent"]),
|
|
1322
1399
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
1323
1400
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
1324
1401
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -1327,7 +1404,7 @@ var UserSettingsSchema = z11.object({
|
|
|
1327
1404
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
1328
1405
|
* next dead field for the same reason. */
|
|
1329
1406
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
1330
|
-
improveConsent:
|
|
1407
|
+
improveConsent: z12.boolean(),
|
|
1331
1408
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
1332
1409
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
1333
1410
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -1335,17 +1412,17 @@ var UserSettingsSchema = z11.object({
|
|
|
1335
1412
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
1336
1413
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
1337
1414
|
* write, 'hosted' on read (see store.ts). */
|
|
1338
|
-
voiceMode:
|
|
1415
|
+
voiceMode: z12.enum(["hosted", "on_device"]).optional(),
|
|
1339
1416
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
1340
1417
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
1341
1418
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
1342
1419
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
1343
1420
|
* read (see store.ts). */
|
|
1344
|
-
talk:
|
|
1421
|
+
talk: z12.boolean().optional(),
|
|
1345
1422
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
1346
1423
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
1347
1424
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
1348
|
-
callDiagnostics:
|
|
1425
|
+
callDiagnostics: z12.boolean().optional(),
|
|
1349
1426
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
1350
1427
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
1351
1428
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -1353,7 +1430,7 @@ var UserSettingsSchema = z11.object({
|
|
|
1353
1430
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
1354
1431
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
1355
1432
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
1356
|
-
voiceTuning:
|
|
1433
|
+
voiceTuning: z12.record(z12.string(), z12.union([z12.number(), z12.string()])).optional(),
|
|
1357
1434
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
1358
1435
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
1359
1436
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -1362,108 +1439,109 @@ var UserSettingsSchema = z11.object({
|
|
|
1362
1439
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
1363
1440
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
1364
1441
|
* call — honest about what we know. */
|
|
1365
|
-
timezone:
|
|
1442
|
+
timezone: z12.string().min(1).max(64).optional(),
|
|
1366
1443
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
1367
1444
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
1368
1445
|
broker: BrokerTuningSchema.optional()
|
|
1369
1446
|
});
|
|
1370
|
-
var GoalLatestSchema =
|
|
1371
|
-
from:
|
|
1372
|
-
said:
|
|
1373
|
-
at:
|
|
1374
|
-
entryId:
|
|
1447
|
+
var GoalLatestSchema = z12.object({
|
|
1448
|
+
from: z12.enum(["person", "agent"]),
|
|
1449
|
+
said: z12.string(),
|
|
1450
|
+
at: z12.string(),
|
|
1451
|
+
entryId: z12.string(),
|
|
1375
1452
|
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
1376
1453
|
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
1377
1454
|
* none was. The Work row reads it before `said`. */
|
|
1378
|
-
summary:
|
|
1455
|
+
summary: z12.string().optional(),
|
|
1379
1456
|
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
1380
1457
|
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
1381
|
-
titles:
|
|
1458
|
+
titles: z12.array(z12.string()).optional(),
|
|
1382
1459
|
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
1383
1460
|
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
1384
|
-
line:
|
|
1461
|
+
line: z12.string().optional()
|
|
1385
1462
|
});
|
|
1386
|
-
var HistoryWorkSchema =
|
|
1387
|
-
id:
|
|
1388
|
-
title:
|
|
1389
|
-
state:
|
|
1463
|
+
var HistoryWorkSchema = z12.object({
|
|
1464
|
+
id: z12.string(),
|
|
1465
|
+
title: z12.string(),
|
|
1466
|
+
state: z12.enum(["done", "cancelled"]),
|
|
1390
1467
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
1391
|
-
assignee:
|
|
1468
|
+
assignee: z12.string(),
|
|
1392
1469
|
latest: GoalLatestSchema.nullable().optional()
|
|
1393
1470
|
});
|
|
1394
|
-
var HistoryEntrySchema =
|
|
1395
|
-
|
|
1396
|
-
|
|
1471
|
+
var HistoryEntrySchema = z12.union([
|
|
1472
|
+
z12.object({ at: z12.string(), card: InboxItemSchema }),
|
|
1473
|
+
z12.object({ at: z12.string(), work: HistoryWorkSchema })
|
|
1397
1474
|
]);
|
|
1398
|
-
var HistoryPageSchema =
|
|
1399
|
-
entries:
|
|
1400
|
-
next:
|
|
1475
|
+
var HistoryPageSchema = z12.object({
|
|
1476
|
+
entries: z12.array(HistoryEntrySchema),
|
|
1477
|
+
next: z12.string().nullable()
|
|
1401
1478
|
});
|
|
1402
1479
|
var ACTIVITY_LINES = 2;
|
|
1403
1480
|
var ACTIVITY_LINE_MAX = 80;
|
|
1404
|
-
var AgentActivitySchema =
|
|
1481
|
+
var AgentActivitySchema = z12.object({
|
|
1405
1482
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
1406
|
-
lines:
|
|
1483
|
+
lines: z12.array(z12.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
1407
1484
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
1408
1485
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
1409
|
-
at:
|
|
1486
|
+
at: z12.string().datetime()
|
|
1410
1487
|
});
|
|
1411
|
-
var ConnectionSummarySchema =
|
|
1488
|
+
var ConnectionSummarySchema = z12.object({
|
|
1412
1489
|
/** The connection = the agent's token id (used to address a request). */
|
|
1413
|
-
id:
|
|
1490
|
+
id: z12.string(),
|
|
1414
1491
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
1415
1492
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
1416
1493
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
1417
1494
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
1418
|
-
kind:
|
|
1495
|
+
kind: z12.enum(["device", "agent"]).optional(),
|
|
1419
1496
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
1420
1497
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
1421
|
-
* agents (
|
|
1422
|
-
|
|
1423
|
-
|
|
1498
|
+
* agents (provider-managed, hatched by another agent, or minted before the link existed);
|
|
1499
|
+
* a phone-launched agent is linked to the computer it runs on since #1602. */
|
|
1500
|
+
mintedByDevice: z12.string().nullable().optional(),
|
|
1501
|
+
device: z12.string().nullable(),
|
|
1424
1502
|
/** The agent's display name (the single pairing name). */
|
|
1425
|
-
name:
|
|
1503
|
+
name: z12.string(),
|
|
1426
1504
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
1427
1505
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
1428
|
-
provider:
|
|
1506
|
+
provider: z12.string().nullable(),
|
|
1429
1507
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
1430
1508
|
voice: VoiceKeySchema.nullable(),
|
|
1431
1509
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
1432
1510
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
1433
1511
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
1434
1512
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
1435
|
-
* never may" (docs/
|
|
1513
|
+
* never may" (docs/history/party-navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
1436
1514
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
1437
1515
|
* set once must not overrule a rule they set about one agent. */
|
|
1438
1516
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
1439
|
-
createdAt:
|
|
1517
|
+
createdAt: z12.string().datetime(),
|
|
1440
1518
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
1441
1519
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
1442
|
-
lastContactAt:
|
|
1520
|
+
lastContactAt: z12.string().datetime().nullable(),
|
|
1443
1521
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
1444
1522
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
1445
|
-
lastSeenAt:
|
|
1523
|
+
lastSeenAt: z12.string().datetime().nullable().optional(),
|
|
1446
1524
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
1447
1525
|
* its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
|
|
1448
1526
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
1449
|
-
lastWorkedAt:
|
|
1527
|
+
lastWorkedAt: z12.string().datetime().nullable().optional(),
|
|
1450
1528
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
1451
1529
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
1452
|
-
oldestReadyAt:
|
|
1530
|
+
oldestReadyAt: z12.string().datetime().nullable().optional(),
|
|
1453
1531
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
1454
1532
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
1455
1533
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
1456
|
-
runtime:
|
|
1534
|
+
runtime: z12.object({
|
|
1457
1535
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
1458
1536
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
1459
|
-
version:
|
|
1460
|
-
harnesses:
|
|
1461
|
-
workspaces:
|
|
1537
|
+
version: z12.string().optional(),
|
|
1538
|
+
harnesses: z12.array(z12.object({ name: z12.string(), label: z12.string(), status: z12.string(), models: z12.array(z12.string()).optional() })).optional(),
|
|
1539
|
+
workspaces: z12.array(z12.string()).optional(),
|
|
1462
1540
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
1463
1541
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
1464
1542
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
1465
1543
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
1466
|
-
repos:
|
|
1544
|
+
repos: z12.array(z12.object({ path: z12.string(), remote: z12.string() })).optional()
|
|
1467
1545
|
}).optional(),
|
|
1468
1546
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
1469
1547
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -1472,145 +1550,151 @@ var ConnectionSummarySchema = z11.object({
|
|
|
1472
1550
|
activity: AgentActivitySchema.optional(),
|
|
1473
1551
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
1474
1552
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
1475
|
-
managed:
|
|
1553
|
+
managed: z12.boolean()
|
|
1476
1554
|
});
|
|
1477
|
-
var LedgerItemSchema =
|
|
1478
|
-
var AgentLedgerSchema =
|
|
1555
|
+
var LedgerItemSchema = z12.object({ id: z12.string(), parentId: z12.string(), title: z12.string(), createdAt: z12.string() });
|
|
1556
|
+
var AgentLedgerSchema = z12.object({
|
|
1479
1557
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
1480
|
-
agent:
|
|
1558
|
+
agent: z12.object({ id: z12.string(), name: z12.string().nullable(), revokedAt: z12.string().nullable() }),
|
|
1481
1559
|
/** Its own questions you have not answered. */
|
|
1482
|
-
asks:
|
|
1560
|
+
asks: z12.array(LedgerItemSchema),
|
|
1483
1561
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
1484
|
-
answered:
|
|
1562
|
+
answered: z12.array(LedgerItemSchema),
|
|
1485
1563
|
/** Requests you sent it that it never took. */
|
|
1486
|
-
requests:
|
|
1487
|
-
goals:
|
|
1488
|
-
callbacks:
|
|
1564
|
+
requests: z12.array(LedgerItemSchema),
|
|
1565
|
+
goals: z12.array(z12.object({ id: z12.string(), outcome: z12.string(), state: z12.string() })),
|
|
1566
|
+
callbacks: z12.array(z12.object({ id: z12.string(), parentId: z12.string(), trigger: z12.string(), note: z12.string(), dueAt: z12.string().nullable() }))
|
|
1489
1567
|
});
|
|
1490
|
-
var ReassignResultSchema =
|
|
1491
|
-
moved:
|
|
1492
|
-
parentId:
|
|
1568
|
+
var ReassignResultSchema = z12.object({
|
|
1569
|
+
moved: z12.object({ asks: z12.number(), answered: z12.number(), requests: z12.number(), goals: z12.number(), callbacks: z12.number() }),
|
|
1570
|
+
parentId: z12.string().nullable()
|
|
1493
1571
|
});
|
|
1494
|
-
var LessonStateSchema =
|
|
1495
|
-
var LessonViewSchema =
|
|
1496
|
-
id:
|
|
1497
|
-
text:
|
|
1572
|
+
var LessonStateSchema = z12.enum(["active", "proposed", "retired"]);
|
|
1573
|
+
var LessonViewSchema = z12.object({
|
|
1574
|
+
id: z12.string(),
|
|
1575
|
+
text: z12.string(),
|
|
1498
1576
|
state: LessonStateSchema,
|
|
1499
1577
|
/** The Goal it is scoped to; null = the whole account. */
|
|
1500
|
-
scopeGoalId:
|
|
1501
|
-
goalTitle:
|
|
1502
|
-
version:
|
|
1503
|
-
pinned:
|
|
1578
|
+
scopeGoalId: z12.string().nullable(),
|
|
1579
|
+
goalTitle: z12.string().nullable(),
|
|
1580
|
+
version: z12.number(),
|
|
1581
|
+
pinned: z12.boolean(),
|
|
1504
1582
|
/** When the person last wrote its text themselves. */
|
|
1505
|
-
editedAt:
|
|
1506
|
-
createdAt:
|
|
1507
|
-
updatedAt:
|
|
1583
|
+
editedAt: z12.string().nullable(),
|
|
1584
|
+
createdAt: z12.string(),
|
|
1585
|
+
updatedAt: z12.string(),
|
|
1508
1586
|
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
1509
|
-
sources:
|
|
1587
|
+
sources: z12.array(z12.object({ entryId: z12.string(), words: z12.string().nullable(), at: z12.string() }))
|
|
1510
1588
|
});
|
|
1511
|
-
var QueueQuestionSchema =
|
|
1589
|
+
var QueueQuestionSchema = z12.object({
|
|
1512
1590
|
/** The decision need's id — what an answer is accepted against. */
|
|
1513
|
-
id:
|
|
1591
|
+
id: z12.string(),
|
|
1514
1592
|
/** The words that were asked, from the request Entry that asked them. */
|
|
1515
|
-
question:
|
|
1593
|
+
question: z12.string(),
|
|
1516
1594
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
1517
1595
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
1518
1596
|
* can answer. */
|
|
1519
|
-
deliveryId:
|
|
1597
|
+
deliveryId: z12.string().nullable().default(null),
|
|
1520
1598
|
/** The Entry the ruling is about. */
|
|
1521
|
-
aboutId:
|
|
1599
|
+
aboutId: z12.string().nullable().default(null),
|
|
1522
1600
|
/** Empty for a free-text question. */
|
|
1523
|
-
options:
|
|
1524
|
-
select:
|
|
1525
|
-
askedAt:
|
|
1601
|
+
options: z12.array(OptionSchema).default([]),
|
|
1602
|
+
select: z12.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
1603
|
+
askedAt: z12.string(),
|
|
1526
1604
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
1527
|
-
answeredAt:
|
|
1605
|
+
answeredAt: z12.string().nullable().default(null),
|
|
1528
1606
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
1529
1607
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
1530
1608
|
* for a settled question whose reply carried nothing readable. */
|
|
1531
|
-
answer:
|
|
1609
|
+
answer: z12.string().nullable().default(null),
|
|
1532
1610
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
1533
1611
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
1534
1612
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
1535
1613
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
1536
1614
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
1537
1615
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
1538
|
-
goalId:
|
|
1616
|
+
goalId: z12.string(),
|
|
1539
1617
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
1540
1618
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
1541
1619
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
1542
1620
|
* gate at all. */
|
|
1543
|
-
blocked:
|
|
1621
|
+
blocked: z12.boolean().default(false)
|
|
1544
1622
|
});
|
|
1545
|
-
var QueueReplySchema =
|
|
1623
|
+
var QueueReplySchema = z12.object({
|
|
1546
1624
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
1547
1625
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
1548
1626
|
* walk can name it in its zoom. */
|
|
1549
|
-
id:
|
|
1627
|
+
id: z12.string(),
|
|
1550
1628
|
/** The Goal the note is on. */
|
|
1551
|
-
goalId:
|
|
1629
|
+
goalId: z12.string(),
|
|
1552
1630
|
/** What the note said. */
|
|
1553
|
-
note:
|
|
1631
|
+
note: z12.string(),
|
|
1554
1632
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
1555
|
-
deliveryId:
|
|
1556
|
-
requestEntryId:
|
|
1557
|
-
askedAt:
|
|
1633
|
+
deliveryId: z12.string(),
|
|
1634
|
+
requestEntryId: z12.string(),
|
|
1635
|
+
askedAt: z12.string(),
|
|
1558
1636
|
/** When the person last replied — the window's start. */
|
|
1559
|
-
repliedAt:
|
|
1637
|
+
repliedAt: z12.string(),
|
|
1560
1638
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
1561
|
-
reply:
|
|
1639
|
+
reply: z12.string().nullable()
|
|
1562
1640
|
});
|
|
1563
|
-
var QueueItemSchema =
|
|
1564
|
-
id:
|
|
1641
|
+
var QueueItemSchema = z12.object({
|
|
1642
|
+
id: z12.string(),
|
|
1565
1643
|
/** One-line headline — the first sentence of the outcome. */
|
|
1566
|
-
title:
|
|
1644
|
+
title: z12.string(),
|
|
1567
1645
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
1568
|
-
intent:
|
|
1646
|
+
intent: z12.string(),
|
|
1569
1647
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
1570
|
-
state:
|
|
1648
|
+
state: z12.string(),
|
|
1571
1649
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
1572
|
-
assignee:
|
|
1650
|
+
assignee: z12.string().nullable().default(null),
|
|
1573
1651
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
1574
|
-
progress:
|
|
1652
|
+
progress: z12.string().nullable().default(null),
|
|
1575
1653
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
1576
1654
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
1577
|
-
progressLine:
|
|
1578
|
-
reviewPending:
|
|
1579
|
-
dueAt:
|
|
1655
|
+
progressLine: z12.string().nullable().optional(),
|
|
1656
|
+
reviewPending: z12.boolean().default(false),
|
|
1657
|
+
dueAt: z12.string().nullable().default(null),
|
|
1580
1658
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
1581
1659
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
1582
|
-
finishedAt:
|
|
1660
|
+
finishedAt: z12.string().nullable().optional(),
|
|
1583
1661
|
/** The Goal this one was opened under; null at the root. */
|
|
1584
|
-
parentGoalId:
|
|
1662
|
+
parentGoalId: z12.string().nullable().default(null),
|
|
1585
1663
|
/** Goals opened under this one — only those the same list holds. */
|
|
1586
|
-
childGoalIds:
|
|
1664
|
+
childGoalIds: z12.array(z12.string()).default([]),
|
|
1587
1665
|
/** Goals this one waits on (start or finish gates). */
|
|
1588
|
-
dependencyGoalIds:
|
|
1666
|
+
dependencyGoalIds: z12.array(z12.string()).default([]),
|
|
1589
1667
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
1590
|
-
blocked:
|
|
1668
|
+
blocked: z12.boolean().default(false),
|
|
1591
1669
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
1592
1670
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
1593
1671
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
1594
|
-
* question is opened.
|
|
1595
|
-
|
|
1672
|
+
* question is opened. Never a CANCELLED one (withdrawn, dismissed, or its Goal or call ended): it
|
|
1673
|
+
* asks nothing of anyone (#3723). */
|
|
1674
|
+
questions: z12.array(QueueQuestionSchema).default([]),
|
|
1596
1675
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
1597
1676
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
1598
1677
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
1599
|
-
* by the projection, so hand-built queues (fixtures, the demo) need not spell them.
|
|
1600
|
-
asked
|
|
1601
|
-
|
|
1678
|
+
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. A cancelled
|
|
1679
|
+
* question is not asked (#3725). */
|
|
1680
|
+
asked: z12.number().optional(),
|
|
1681
|
+
answered: z12.number().optional(),
|
|
1602
1682
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
1603
1683
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
1604
1684
|
* not spell an empty list. */
|
|
1605
|
-
replies:
|
|
1685
|
+
replies: z12.array(QueueReplySchema).optional(),
|
|
1606
1686
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
1607
|
-
repo:
|
|
1608
|
-
createdAt:
|
|
1609
|
-
updatedAt:
|
|
1687
|
+
repo: z12.string().nullable().optional(),
|
|
1688
|
+
createdAt: z12.string(),
|
|
1689
|
+
updatedAt: z12.string().nullable().default(null),
|
|
1610
1690
|
/** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
|
|
1611
1691
|
* as progress). `updatedAt` moves for reasons nobody chose — a
|
|
1612
1692
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
1613
|
-
lastProgressAt:
|
|
1693
|
+
lastProgressAt: z12.string().nullable().optional(),
|
|
1694
|
+
/** WHEN AN AGENT LAST ACTED ON IT (owner, 2026-10-27: "Move it now", Goal 567258dc): its newest agent write, an
|
|
1695
|
+
* Entry or a Goal change, Paigy's excluded (`goals.worked_at`). What keeps work an agent is writing on from
|
|
1696
|
+
* reading as gone quiet (`coldSince`); absent on a door that does not carry it. */
|
|
1697
|
+
workedAt: z12.string().nullable().optional(),
|
|
1614
1698
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
1615
1699
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
1616
1700
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
@@ -1629,7 +1713,7 @@ var QueueItemSchema = z11.object({
|
|
|
1629
1713
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
1630
1714
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
1631
1715
|
* Entry with no `about_id` is in neither. */
|
|
1632
|
-
lastPersonAt:
|
|
1716
|
+
lastPersonAt: z12.string().nullable().optional(),
|
|
1633
1717
|
/** EACH SIDE'S NEWEST WORD IN THE LAST DAY (owner, 2026-10-13, the Work tab's order): the person's
|
|
1634
1718
|
* (`yours`) and the agents' (`theirs`), each null when that side wrote nothing on it in 24 hours.
|
|
1635
1719
|
* `latest` holds one Entry, so whichever side wrote first is hidden there; Work orders a Goal by
|
|
@@ -1647,40 +1731,40 @@ var QueueItemSchema = z11.object({
|
|
|
1647
1731
|
* the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
|
|
1648
1732
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
1649
1733
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
1650
|
-
rev:
|
|
1734
|
+
rev: z12.string().optional()
|
|
1651
1735
|
});
|
|
1652
|
-
var QueueDeltaSchema =
|
|
1653
|
-
ids:
|
|
1654
|
-
items:
|
|
1736
|
+
var QueueDeltaSchema = z12.object({
|
|
1737
|
+
ids: z12.array(z12.string()),
|
|
1738
|
+
items: z12.array(QueueItemSchema)
|
|
1655
1739
|
});
|
|
1656
1740
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
1657
|
-
var NoteSourceSchema =
|
|
1658
|
-
var NoteStatusSchema =
|
|
1659
|
-
var NoteRepeatSchema =
|
|
1660
|
-
var DecisionSchema =
|
|
1661
|
-
id:
|
|
1741
|
+
var NoteSourceSchema = z12.enum(["app", "call"]);
|
|
1742
|
+
var NoteStatusSchema = z12.enum(["open", "assigned", "in_progress", "done"]);
|
|
1743
|
+
var NoteRepeatSchema = z12.enum(["once", "until_done"]);
|
|
1744
|
+
var DecisionSchema = z12.object({
|
|
1745
|
+
id: z12.string(),
|
|
1662
1746
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
1663
1747
|
* extensibility seam — any conversation can accrue decisions). */
|
|
1664
|
-
noteId:
|
|
1748
|
+
noteId: z12.string().nullable(),
|
|
1665
1749
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
1666
|
-
question:
|
|
1750
|
+
question: z12.string(),
|
|
1667
1751
|
/** The user's ruling; null while the question is open. */
|
|
1668
|
-
answer:
|
|
1669
|
-
decidedAt:
|
|
1670
|
-
createdAt:
|
|
1752
|
+
answer: z12.string().nullable(),
|
|
1753
|
+
decidedAt: z12.string().nullable(),
|
|
1754
|
+
createdAt: z12.string()
|
|
1671
1755
|
});
|
|
1672
|
-
var NoteSchema =
|
|
1673
|
-
id:
|
|
1756
|
+
var NoteSchema = z12.object({
|
|
1757
|
+
id: z12.string(),
|
|
1674
1758
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
1675
|
-
title:
|
|
1759
|
+
title: z12.string(),
|
|
1676
1760
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
1677
|
-
intent:
|
|
1761
|
+
intent: z12.string(),
|
|
1678
1762
|
source: NoteSourceSchema,
|
|
1679
1763
|
status: NoteStatusSchema,
|
|
1680
1764
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
1681
|
-
assignee:
|
|
1765
|
+
assignee: z12.string().nullable(),
|
|
1682
1766
|
/** The request thread minted at assignment; null until assigned. */
|
|
1683
|
-
parentId:
|
|
1767
|
+
parentId: z12.string().nullable(),
|
|
1684
1768
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1685
1769
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
1686
1770
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -1689,100 +1773,100 @@ var NoteSchema = z11.object({
|
|
|
1689
1773
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
1690
1774
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
1691
1775
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
1692
|
-
dueAt:
|
|
1776
|
+
dueAt: z12.string().nullable().default(null),
|
|
1693
1777
|
repeat: NoteRepeatSchema.default("once"),
|
|
1694
1778
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
1695
|
-
rides:
|
|
1696
|
-
lastRideAt:
|
|
1697
|
-
createdAt:
|
|
1779
|
+
rides: z12.number().int().default(0),
|
|
1780
|
+
lastRideAt: z12.string().nullable().default(null),
|
|
1781
|
+
createdAt: z12.string()
|
|
1698
1782
|
});
|
|
1699
|
-
var DeliveryModeSchema =
|
|
1700
|
-
var RegisterDeliverySchema =
|
|
1701
|
-
var OAuthStartSchema =
|
|
1702
|
-
provider:
|
|
1703
|
-
returnTo:
|
|
1783
|
+
var DeliveryModeSchema = z12.enum(["poll", "self_hosted"]);
|
|
1784
|
+
var RegisterDeliverySchema = z12.object({ mode: DeliveryModeSchema });
|
|
1785
|
+
var OAuthStartSchema = z12.object({
|
|
1786
|
+
provider: z12.enum(["cma"]),
|
|
1787
|
+
returnTo: z12.string().min(1)
|
|
1704
1788
|
});
|
|
1705
|
-
var DeliveryConfigSchema =
|
|
1706
|
-
tokenId:
|
|
1789
|
+
var DeliveryConfigSchema = z12.object({
|
|
1790
|
+
tokenId: z12.string(),
|
|
1707
1791
|
mode: DeliveryModeSchema,
|
|
1708
1792
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
1709
1793
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
1710
1794
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
1711
|
-
realtime:
|
|
1795
|
+
realtime: z12.object({ url: z12.string(), anonKey: z12.string() }).nullable()
|
|
1712
1796
|
});
|
|
1713
|
-
var HostDecisionSchema =
|
|
1797
|
+
var HostDecisionSchema = z12.object({
|
|
1714
1798
|
/** The agent's token id: the row's `recipient`. */
|
|
1715
|
-
agent:
|
|
1716
|
-
decision:
|
|
1799
|
+
agent: z12.string().uuid(),
|
|
1800
|
+
decision: z12.enum(["stood_back", "took_over"]),
|
|
1717
1801
|
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
1718
|
-
goalId:
|
|
1802
|
+
goalId: z12.string().uuid().nullable().optional(),
|
|
1719
1803
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
1720
|
-
seenAt:
|
|
1804
|
+
seenAt: z12.string().datetime().nullable().optional(),
|
|
1721
1805
|
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
1722
|
-
since:
|
|
1806
|
+
since: z12.string().datetime().nullable().optional(),
|
|
1723
1807
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
1724
|
-
said:
|
|
1808
|
+
said: z12.string().max(300).optional()
|
|
1725
1809
|
});
|
|
1726
|
-
var WakeNudgeSchema =
|
|
1727
|
-
kind:
|
|
1728
|
-
notificationId:
|
|
1729
|
-
parentId:
|
|
1810
|
+
var WakeNudgeSchema = z12.object({
|
|
1811
|
+
kind: z12.enum(["reply", "request", "callback"]),
|
|
1812
|
+
notificationId: z12.string().optional(),
|
|
1813
|
+
parentId: z12.string()
|
|
1730
1814
|
});
|
|
1731
|
-
var PairingStatusSchema =
|
|
1732
|
-
var DeviceCodeSchema =
|
|
1733
|
-
device_code:
|
|
1734
|
-
user_code:
|
|
1735
|
-
verification_uri:
|
|
1736
|
-
verification_uri_complete:
|
|
1737
|
-
interval:
|
|
1738
|
-
expires_in:
|
|
1815
|
+
var PairingStatusSchema = z12.enum(["pending", "approved", "denied", "expired"]);
|
|
1816
|
+
var DeviceCodeSchema = z12.object({
|
|
1817
|
+
device_code: z12.string(),
|
|
1818
|
+
user_code: z12.string(),
|
|
1819
|
+
verification_uri: z12.string().url(),
|
|
1820
|
+
verification_uri_complete: z12.string().url(),
|
|
1821
|
+
interval: z12.number(),
|
|
1822
|
+
expires_in: z12.number()
|
|
1739
1823
|
});
|
|
1740
|
-
var DeviceInfoSchema =
|
|
1741
|
-
code:
|
|
1824
|
+
var DeviceInfoSchema = z12.object({
|
|
1825
|
+
code: z12.string(),
|
|
1742
1826
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
1743
1827
|
* pre-filling the name field the human can edit. */
|
|
1744
|
-
name:
|
|
1828
|
+
name: z12.string(),
|
|
1745
1829
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
1746
1830
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
1747
1831
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
1748
1832
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
1749
|
-
agent:
|
|
1750
|
-
device:
|
|
1833
|
+
agent: z12.string().optional(),
|
|
1834
|
+
device: z12.string().nullable(),
|
|
1751
1835
|
status: PairingStatusSchema
|
|
1752
1836
|
});
|
|
1753
|
-
var DeviceTokenSchema =
|
|
1754
|
-
access_token:
|
|
1837
|
+
var DeviceTokenSchema = z12.object({
|
|
1838
|
+
access_token: z12.string(),
|
|
1755
1839
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
1756
1840
|
* a default silly name). */
|
|
1757
|
-
name:
|
|
1758
|
-
device:
|
|
1841
|
+
name: z12.string(),
|
|
1842
|
+
device: z12.string().nullable(),
|
|
1759
1843
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
1760
1844
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
1761
|
-
voice:
|
|
1845
|
+
voice: z12.string().nullable().optional(),
|
|
1762
1846
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
1763
1847
|
* that survives a rename. Cached by the host's identity beat. */
|
|
1764
|
-
token_id:
|
|
1848
|
+
token_id: z12.string().nullable().optional(),
|
|
1765
1849
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
1766
1850
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
1767
1851
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
1768
1852
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
1769
|
-
workspace:
|
|
1853
|
+
workspace: z12.string().nullable().optional(),
|
|
1770
1854
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
1771
|
-
harness:
|
|
1772
|
-
session_id:
|
|
1855
|
+
harness: z12.enum(["claude", "codex", "agy"]).optional(),
|
|
1856
|
+
session_id: z12.string().uuid().optional(),
|
|
1773
1857
|
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
1774
1858
|
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
1775
1859
|
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
1776
1860
|
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
1777
|
-
full_session:
|
|
1778
|
-
uik_pub:
|
|
1861
|
+
full_session: z12.string().optional(),
|
|
1862
|
+
uik_pub: z12.string().nullable().optional()
|
|
1779
1863
|
});
|
|
1780
|
-
var SupportRequestSchema =
|
|
1781
|
-
email:
|
|
1782
|
-
message:
|
|
1783
|
-
name:
|
|
1864
|
+
var SupportRequestSchema = z12.object({
|
|
1865
|
+
email: z12.string().email().max(320),
|
|
1866
|
+
message: z12.string().trim().min(1).max(5e3),
|
|
1867
|
+
name: z12.string().trim().max(120).optional()
|
|
1784
1868
|
});
|
|
1785
|
-
var NotificationFeedbackKindSchema =
|
|
1869
|
+
var NotificationFeedbackKindSchema = z12.enum([
|
|
1786
1870
|
"break_down",
|
|
1787
1871
|
// "This should be more than one ask — break it down."
|
|
1788
1872
|
"regenerate_options",
|
|
@@ -1797,131 +1881,136 @@ var NotificationFeedbackKindSchema = z11.enum([
|
|
|
1797
1881
|
// anything else — the note carries it.
|
|
1798
1882
|
]);
|
|
1799
1883
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
1800
|
-
var QuestionRowSchema =
|
|
1884
|
+
var QuestionRowSchema = z12.object({
|
|
1801
1885
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
1802
|
-
id:
|
|
1803
|
-
deliveryId:
|
|
1804
|
-
entryId:
|
|
1886
|
+
id: z12.string(),
|
|
1887
|
+
deliveryId: z12.string(),
|
|
1888
|
+
entryId: z12.string(),
|
|
1805
1889
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
1806
|
-
needId:
|
|
1807
|
-
goalIds:
|
|
1890
|
+
needId: z12.string().nullable(),
|
|
1891
|
+
goalIds: z12.array(z12.string()),
|
|
1808
1892
|
/** The name of the work it is about, when the read could word it. */
|
|
1809
|
-
goalTitle:
|
|
1810
|
-
tokenId:
|
|
1811
|
-
name:
|
|
1812
|
-
title:
|
|
1813
|
-
body:
|
|
1814
|
-
select:
|
|
1815
|
-
options:
|
|
1816
|
-
hasPreview:
|
|
1817
|
-
blocking:
|
|
1818
|
-
askedAt:
|
|
1893
|
+
goalTitle: z12.string().optional(),
|
|
1894
|
+
tokenId: z12.string().optional(),
|
|
1895
|
+
name: z12.string(),
|
|
1896
|
+
title: z12.string(),
|
|
1897
|
+
body: z12.string(),
|
|
1898
|
+
select: z12.enum(["one", "many", "rank", "confirm", "text"]),
|
|
1899
|
+
options: z12.array(SlimOptionSchema),
|
|
1900
|
+
hasPreview: z12.boolean(),
|
|
1901
|
+
blocking: z12.boolean(),
|
|
1902
|
+
askedAt: z12.string().datetime(),
|
|
1819
1903
|
ring: InboxItemSchema.shape.ring,
|
|
1820
|
-
onCall:
|
|
1821
|
-
sealed:
|
|
1904
|
+
onCall: z12.literal(true).optional(),
|
|
1905
|
+
sealed: z12.boolean()
|
|
1822
1906
|
});
|
|
1823
|
-
var WorkStateSchema =
|
|
1824
|
-
var WorkRowSchema =
|
|
1825
|
-
id:
|
|
1826
|
-
parentId:
|
|
1827
|
-
title:
|
|
1907
|
+
var WorkStateSchema = z12.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
1908
|
+
var WorkRowSchema = z12.object({
|
|
1909
|
+
id: z12.string(),
|
|
1910
|
+
parentId: z12.string().nullable(),
|
|
1911
|
+
title: z12.string(),
|
|
1828
1912
|
/** Straight off the Goal. */
|
|
1829
1913
|
state: WorkStateSchema,
|
|
1830
|
-
owner:
|
|
1831
|
-
revision:
|
|
1914
|
+
owner: z12.string().nullable(),
|
|
1915
|
+
revision: z12.number().int(),
|
|
1832
1916
|
/** Open questions on it, counted to 100. */
|
|
1833
|
-
waiting:
|
|
1917
|
+
waiting: z12.number().int(),
|
|
1834
1918
|
/** Held by a gate on work that is not done. */
|
|
1835
|
-
blocked:
|
|
1836
|
-
lastProgressAt:
|
|
1919
|
+
blocked: z12.boolean(),
|
|
1920
|
+
lastProgressAt: z12.string().datetime().nullable(),
|
|
1837
1921
|
/** The line written for its newest progress note, else that note's first words. */
|
|
1838
|
-
line:
|
|
1922
|
+
line: z12.string().nullable(),
|
|
1839
1923
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
1840
|
-
children:
|
|
1841
|
-
createdAt:
|
|
1842
|
-
updatedAt:
|
|
1924
|
+
children: z12.number().int(),
|
|
1925
|
+
createdAt: z12.string().datetime(),
|
|
1926
|
+
updatedAt: z12.string().datetime(),
|
|
1843
1927
|
/** When anything at or under it last moved — the order the list is in. */
|
|
1844
|
-
activeAt:
|
|
1928
|
+
activeAt: z12.string().datetime(),
|
|
1845
1929
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
1846
|
-
sealed:
|
|
1930
|
+
sealed: z12.boolean()
|
|
1847
1931
|
});
|
|
1848
1932
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
1849
1933
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
1850
1934
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
1851
|
-
asking:
|
|
1852
|
-
oldestAskAt:
|
|
1935
|
+
asking: z12.number().int().nullable(),
|
|
1936
|
+
oldestAskAt: z12.string().datetime().nullable(),
|
|
1853
1937
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
1854
1938
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
1855
1939
|
* (`agent_holds`); null when that read failed. */
|
|
1856
|
-
holds:
|
|
1857
|
-
held:
|
|
1940
|
+
holds: z12.array(z12.object({ id: z12.string(), title: z12.string() })).nullable(),
|
|
1941
|
+
held: z12.number().int().nullable(),
|
|
1858
1942
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
1859
1943
|
* while none has, or when that read failed. */
|
|
1860
|
-
cold:
|
|
1944
|
+
cold: z12.string().datetime().nullable(),
|
|
1861
1945
|
/** The newest line of its working log, and when the harness saw it. */
|
|
1862
|
-
line:
|
|
1863
|
-
lineAt:
|
|
1946
|
+
line: z12.string().nullable(),
|
|
1947
|
+
lineAt: z12.string().datetime().nullable()
|
|
1864
1948
|
});
|
|
1865
|
-
var SnapshotSchema =
|
|
1949
|
+
var SnapshotSchema = z12.object({
|
|
1866
1950
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
1867
|
-
at:
|
|
1868
|
-
questions:
|
|
1951
|
+
at: z12.string().datetime(),
|
|
1952
|
+
questions: z12.object({
|
|
1869
1953
|
/** The newest 30 open cards, questions before updates. */
|
|
1870
|
-
items:
|
|
1954
|
+
items: z12.array(QuestionRowSchema),
|
|
1871
1955
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
1872
|
-
total:
|
|
1873
|
-
updates:
|
|
1874
|
-
putOff:
|
|
1956
|
+
total: z12.number().int(),
|
|
1957
|
+
updates: z12.number().int(),
|
|
1958
|
+
putOff: z12.number().int()
|
|
1875
1959
|
}).nullable(),
|
|
1876
|
-
agents:
|
|
1960
|
+
agents: z12.object({
|
|
1877
1961
|
/** Up to 60, most recently seen first. */
|
|
1878
|
-
items:
|
|
1879
|
-
more:
|
|
1962
|
+
items: z12.array(AgentRowSchema),
|
|
1963
|
+
more: z12.boolean()
|
|
1880
1964
|
}).nullable(),
|
|
1881
|
-
work:
|
|
1965
|
+
work: z12.object({
|
|
1882
1966
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
1883
|
-
items:
|
|
1967
|
+
items: z12.array(WorkRowSchema),
|
|
1884
1968
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
1885
1969
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
1886
1970
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
1887
1971
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
1888
|
-
counts:
|
|
1972
|
+
counts: z12.object({ live: z12.number().int(), waiting: z12.number().int(), notStarted: z12.number().int(), done: z12.number().int() })
|
|
1889
1973
|
}).nullable(),
|
|
1890
|
-
you:
|
|
1974
|
+
you: z12.object({
|
|
1891
1975
|
settings: UserSettingsSchema,
|
|
1892
|
-
callable:
|
|
1976
|
+
callable: z12.boolean(),
|
|
1893
1977
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
1894
|
-
computers:
|
|
1978
|
+
computers: z12.array(ComputerRowSchema).nullable()
|
|
1895
1979
|
}).nullable()
|
|
1896
1980
|
});
|
|
1897
|
-
var CallRecapSchema =
|
|
1898
|
-
call:
|
|
1899
|
-
status:
|
|
1900
|
-
startedAt:
|
|
1901
|
-
durationMs:
|
|
1981
|
+
var CallRecapSchema = z12.object({
|
|
1982
|
+
call: z12.object({
|
|
1983
|
+
status: z12.string(),
|
|
1984
|
+
startedAt: z12.string(),
|
|
1985
|
+
durationMs: z12.number().nullable(),
|
|
1902
1986
|
/** Bot-clock pickup offset; absent before the timestamp release, null before its report lands. */
|
|
1903
|
-
pickupMs:
|
|
1904
|
-
|
|
1987
|
+
pickupMs: z12.number().nullable().optional(),
|
|
1988
|
+
/** STILL FILING (20261026182117): the call has not ended on the server, or it has and a brain cycle on it is
|
|
1989
|
+
* still pending, the next writer, which may still settle, file or start something. What the close itself
|
|
1990
|
+
* waits for (`private.close_call`). `status` cannot say it: nothing writes "finalizing", and a call reads
|
|
1991
|
+
* "open" until the sweep closes it, after nothing is pending. Absent from an API before it. */
|
|
1992
|
+
filing: z12.boolean().optional(),
|
|
1993
|
+
agents: z12.array(z12.object({ id: z12.string(), name: z12.string().nullable() }))
|
|
1905
1994
|
}),
|
|
1906
|
-
topics:
|
|
1907
|
-
goalId:
|
|
1908
|
-
title:
|
|
1909
|
-
owner:
|
|
1910
|
-
state:
|
|
1995
|
+
topics: z12.array(z12.object({
|
|
1996
|
+
goalId: z12.string().uuid(),
|
|
1997
|
+
title: z12.string(),
|
|
1998
|
+
owner: z12.string(),
|
|
1999
|
+
state: z12.string(),
|
|
1911
2000
|
/** Presented Entry content and its author, separately from the current Goal owner. */
|
|
1912
|
-
items:
|
|
2001
|
+
items: z12.array(z12.object({ entryId: z12.string(), author: z12.string(), text: z12.string(), summary: z12.string().nullable(), acknowledged: z12.boolean().optional() })).optional(),
|
|
1913
2002
|
/** Whether the call put it to the person. An item it carried and never said has no row on the
|
|
1914
2003
|
* review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
|
|
1915
|
-
presented:
|
|
2004
|
+
presented: z12.boolean(),
|
|
1916
2005
|
/** Whether THIS call created it — the third way a call touches work, beside carrying a request
|
|
1917
2006
|
* there and filing words there (owner, 2026-10-08, Goal `5b4b1a0a`: "Show the new work, keep my
|
|
1918
2007
|
* words under it"). Created work is never carried, so it is never `presented`; without this the
|
|
1919
2008
|
* row is dropped by the "only what happened on the call" filter. False on a database before
|
|
1920
2009
|
* 20261013080133. */
|
|
1921
|
-
created:
|
|
2010
|
+
created: z12.boolean(),
|
|
1922
2011
|
/** `request` is the Entry that asked it — what a line's `about` names when the line answered or put
|
|
1923
2012
|
* it off. Optional: an older API sent none. */
|
|
1924
|
-
questions:
|
|
2013
|
+
questions: z12.array(z12.object({ id: z12.string().uuid(), state: z12.string(), title: z12.string(), request: z12.string().optional() })),
|
|
1925
2014
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
1926
2015
|
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
1927
2016
|
* gave, so the row scans like a chosen option and their own words stay under it. Absent when
|
|
@@ -1931,33 +2020,89 @@ var CallRecapSchema = z11.object({
|
|
|
1931
2020
|
/** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
|
|
1932
2021
|
* however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
|
|
1933
2022
|
* said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
|
|
1934
|
-
lines:
|
|
1935
|
-
entryId:
|
|
1936
|
-
words:
|
|
1937
|
-
summary:
|
|
1938
|
-
about:
|
|
1939
|
-
reply:
|
|
1940
|
-
replyKind:
|
|
1941
|
-
at:
|
|
2023
|
+
lines: z12.array(z12.object({
|
|
2024
|
+
entryId: z12.string().uuid(),
|
|
2025
|
+
words: z12.string(),
|
|
2026
|
+
summary: z12.string().optional(),
|
|
2027
|
+
about: z12.string().nullable().optional(),
|
|
2028
|
+
reply: z12.string().nullable().optional(),
|
|
2029
|
+
replyKind: z12.string().nullable().optional(),
|
|
2030
|
+
at: z12.array(z12.number()).optional()
|
|
1942
2031
|
}))
|
|
1943
2032
|
})),
|
|
1944
|
-
unfiled:
|
|
2033
|
+
unfiled: z12.array(z12.object({ lineId: z12.string().uuid(), words: z12.string(), atMs: z12.number() })),
|
|
1945
2034
|
/** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
|
|
1946
2035
|
* (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
|
|
1947
2036
|
* it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
|
|
1948
2037
|
* person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
|
|
1949
2038
|
* theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
|
|
1950
2039
|
* BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
|
|
1951
|
-
said:
|
|
1952
|
-
lineId:
|
|
1953
|
-
speaker:
|
|
1954
|
-
agent:
|
|
1955
|
-
words:
|
|
1956
|
-
atMs:
|
|
2040
|
+
said: z12.array(z12.object({
|
|
2041
|
+
lineId: z12.string().uuid(),
|
|
2042
|
+
speaker: z12.enum(["user", "agent"]),
|
|
2043
|
+
agent: z12.object({ id: z12.string().nullable(), name: z12.string().nullable() }).nullable(),
|
|
2044
|
+
words: z12.string(),
|
|
2045
|
+
atMs: z12.number()
|
|
1957
2046
|
})),
|
|
1958
|
-
|
|
2047
|
+
/** What did not fit, counted. `settles` counts items past the recap's 100; absent before 20261026165518. */
|
|
2048
|
+
more: z12.object({ lines: z12.number(), entries: z12.number(), topics: z12.number(), settles: z12.number().optional() }),
|
|
1959
2049
|
/** Last stored position on this page when another exists; absent before paging shipped. */
|
|
1960
|
-
next:
|
|
2050
|
+
next: z12.number().int().nonnegative().nullable().optional(),
|
|
2051
|
+
/** THE CALL AS ONE LIST OF ROWS (owner, 2026-10-14: "we only really want a thing like transcript but
|
|
2052
|
+
* built from the summaries", then "the transcript tabs should show the same but with full words").
|
|
2053
|
+
* Both tabs draw these rows, in this order: Summary from `summary`, Transcript from `words`.
|
|
2054
|
+
* - `put`: an item put to the person, at the first agent line that voiced it. Paigy's sentences
|
|
2055
|
+
* that voiced it are folded into `spoken`, never rows of their own.
|
|
2056
|
+
* - `said`: the person's words, one row per reply or per filed line no reply covers.
|
|
2057
|
+
* - `did`: what Paigy did ("Sent to Mouth: …", "Recorded: …", "Filed on …").
|
|
2058
|
+
* `by` is the row's real author, never inferred from `kind`: a relay Paigy sent to an agent is
|
|
2059
|
+
* Paigy's, not the person's. Optional and additive (owner: "make sure whatever you do is backward
|
|
2060
|
+
* compatible"): an API before it sends none, and the screen falls back to `topics` and `said`. */
|
|
2061
|
+
timeline: z12.array(z12.object({
|
|
2062
|
+
id: z12.string(),
|
|
2063
|
+
position: z12.number(),
|
|
2064
|
+
atMs: z12.number(),
|
|
2065
|
+
kind: z12.enum(["put", "said", "did"]),
|
|
2066
|
+
by: z12.object({ participant: z12.string().nullable(), agentId: z12.string().nullable(), name: z12.string().nullable() }),
|
|
2067
|
+
goalId: z12.string().uuid().nullable(),
|
|
2068
|
+
requestId: z12.string().nullable(),
|
|
2069
|
+
summary: z12.string().nullable(),
|
|
2070
|
+
words: z12.string(),
|
|
2071
|
+
/** Paigy's sentences folded into this row: on a `put` row, what it said putting the item to the
|
|
2072
|
+
* person (before their next row); on a `said` row, what it said in reply (until their next row).
|
|
2073
|
+
* `atMs` is when each was said; optional, absent before 2026-10-14's fix. */
|
|
2074
|
+
spoken: z12.array(z12.object({ lineId: z12.string(), words: z12.string(), atMs: z12.number().optional() })),
|
|
2075
|
+
/** On a `said` row: the work these words were filed on, and whether they answered (recorded) a
|
|
2076
|
+
* question. They replace the "Filed on ..." and "Recorded: ..." rows that repeated the person's own
|
|
2077
|
+
* summary (Brain, 2026-10-14). Optional: absent from an API before it. */
|
|
2078
|
+
filedOn: z12.array(z12.object({ goalId: z12.string().uuid(), title: z12.string() })).optional(),
|
|
2079
|
+
recorded: z12.boolean().optional(),
|
|
2080
|
+
settled: z12.object({ requestId: z12.string(), answer: z12.string() }).nullable().optional(),
|
|
2081
|
+
acknowledged: z12.boolean().optional(),
|
|
2082
|
+
/** On a `put` row: still waiting for the person's answer (not settled, not answered, not put off).
|
|
2083
|
+
* The server's judgement; the screen draws "Still waiting for your answer" from it. Optional: absent
|
|
2084
|
+
* from an API before it, where the screen falls back to its own reading. */
|
|
2085
|
+
waiting: z12.boolean().optional(),
|
|
2086
|
+
lineIds: z12.array(z12.string())
|
|
2087
|
+
})).optional(),
|
|
2088
|
+
/** THE RECAP (owner, 2026-10-26: "the transcript should just be the entries that settled or
|
|
2089
|
+
* acknowledged the items; that's it"). Each item the person's words settled on this call, in the
|
|
2090
|
+
* order they said them: who asked it (`by`), the item in its agent's words (`title`), and only the
|
|
2091
|
+
* words of theirs that settled it (`lines`: each line once, verbatim, with when it was said on the
|
|
2092
|
+
* bot clock like `said[].atMs`). Built from their replies, not from what the call put, so an item
|
|
2093
|
+
* settled without being put is still here. Beside them (owner: "List them too, with the words that
|
|
2094
|
+
* said them"), each Goal his words told something (`kind` "told") or the call started ("started"),
|
|
2095
|
+
* with `requestId` null and the Goal's name as `title`; a started Goal may have no `lines` yet. `kind`
|
|
2096
|
+
* is a string, not an enum, so a later kind never fails an older build's parse; `requestId` is the
|
|
2097
|
+
* request the words settled. On the first page only; absent from an API before it. */
|
|
2098
|
+
settles: z12.array(z12.object({
|
|
2099
|
+
requestId: z12.string().nullable(),
|
|
2100
|
+
kind: z12.string(),
|
|
2101
|
+
by: z12.object({ participant: z12.string(), agentId: z12.string().nullable(), name: z12.string() }),
|
|
2102
|
+
goalId: z12.string().uuid().nullable(),
|
|
2103
|
+
title: z12.string(),
|
|
2104
|
+
lines: z12.array(z12.object({ lineId: z12.string(), words: z12.string(), atMs: z12.number() }))
|
|
2105
|
+
})).optional()
|
|
1961
2106
|
});
|
|
1962
2107
|
|
|
1963
2108
|
// src/listening.ts
|
|
@@ -1978,14 +2123,21 @@ function sessionStartHook() {
|
|
|
1978
2123
|
[ -f "\${HOME}/.paigy/token.json" ] || exit 0 # not paired on this machine: nothing to listen as
|
|
1979
2124
|
cat <<'PAIGY'
|
|
1980
2125
|
Paigy: nothing pushes answers into a session started in a terminal \u2014 an answer waits for your
|
|
1981
|
-
next contact({ wait: false }). Start your own listener now
|
|
1982
|
-
|
|
2126
|
+
next contact({ wait: false }). Start your own listener now with your harness's MONITOR tool, the kind
|
|
2127
|
+
that wakes you on every line it prints (in Claude Code: Monitor, not a background Bash command, which
|
|
2128
|
+
only tells you when it exits). When the monitor expires, start it again:
|
|
1983
2129
|
|
|
1984
|
-
npx -y -p @paigy/mcp@latest paigy-listen
|
|
2130
|
+
npx -y -p @paigy/mcp@latest paigy-listen --brief
|
|
1985
2131
|
|
|
1986
2132
|
Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
|
|
1987
|
-
the answer needs a decision.
|
|
1988
|
-
|
|
2133
|
+
the answer needs a decision.
|
|
2134
|
+
|
|
2135
|
+
ONE LISTENER PER SESSION, AND ANOTHER SESSION'S IS NOT YOURS. Identities here are per session, so
|
|
2136
|
+
a paigy-listen you find in ps is almost certainly another session listening as itself, and
|
|
2137
|
+
starting yours takes nothing from it. Never decide this from ps: the listen tool reads the mark
|
|
2138
|
+
for YOUR OWN slot and answers in one call. Ask it, or just start the listener above; only a
|
|
2139
|
+
second one for the same slot is the mistake. Then call contact({ wait: false }) once: work handed
|
|
2140
|
+
to you before this session started waits there.
|
|
1989
2141
|
PAIGY
|
|
1990
2142
|
exit 0
|
|
1991
2143
|
`;
|
|
@@ -2021,7 +2173,7 @@ function decideListen(f) {
|
|
|
2021
2173
|
return {
|
|
2022
2174
|
status: "start",
|
|
2023
2175
|
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`,
|
|
2024
|
-
how: "Run this command NOW
|
|
2176
|
+
how: "Run this command NOW with your harness's MONITOR tool, the kind that wakes you on every line it prints (in Claude Code: Monitor, not a background Bash command, which only tells you when it exits); start it again when the monitor expires. Each line it prints is a Delivery waiting for you: read the Goal it names with get_goals, and answer with contact. Start it once."
|
|
2025
2177
|
};
|
|
2026
2178
|
}
|
|
2027
2179
|
function listenMark(slot, home = homedir()) {
|
|
@@ -2037,6 +2189,12 @@ function writeListenMark(slot, pid, home = homedir()) {
|
|
|
2037
2189
|
function removeListenMark(slot, home = homedir()) {
|
|
2038
2190
|
rmSync(listenMark(slot, home), { force: true });
|
|
2039
2191
|
}
|
|
2192
|
+
function clear(path) {
|
|
2193
|
+
try {
|
|
2194
|
+
rmSync(path, { force: true });
|
|
2195
|
+
} catch {
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2040
2198
|
function listenerAlive(slot, home = homedir()) {
|
|
2041
2199
|
const path = listenMark(slot, home);
|
|
2042
2200
|
let pid;
|
|
@@ -2046,7 +2204,7 @@ function listenerAlive(slot, home = homedir()) {
|
|
|
2046
2204
|
return null;
|
|
2047
2205
|
}
|
|
2048
2206
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
2049
|
-
|
|
2207
|
+
clear(path);
|
|
2050
2208
|
return null;
|
|
2051
2209
|
}
|
|
2052
2210
|
try {
|
|
@@ -2054,7 +2212,7 @@ function listenerAlive(slot, home = homedir()) {
|
|
|
2054
2212
|
return pid;
|
|
2055
2213
|
} catch (e) {
|
|
2056
2214
|
if (e.code === "EPERM") return pid;
|
|
2057
|
-
|
|
2215
|
+
clear(path);
|
|
2058
2216
|
return null;
|
|
2059
2217
|
}
|
|
2060
2218
|
}
|
|
@@ -2067,6 +2225,100 @@ function wakeStatusPath(slot, home = homedir()) {
|
|
|
2067
2225
|
function existingListener(slot, env, home = homedir()) {
|
|
2068
2226
|
return listenerAlive(listenerKey(slot, env), home) ?? (env.CODEX_THREAD_ID ? listenerAlive(slot, home) : null);
|
|
2069
2227
|
}
|
|
2228
|
+
var UNLISTENED_EVERY_MS = 10 * 6e4;
|
|
2229
|
+
var WATCHING_CLIENTS = /* @__PURE__ */ new Set(["claude-code"]);
|
|
2230
|
+
var NON_WATCHING_CLIENTS = /* @__PURE__ */ new Set(["claude-ai", "cursor", "cursor-vscode", "windsurf", "cline", "roo-cline", "continue", "vscode", "visual studio code", "zed", "antigravity"]);
|
|
2231
|
+
function canWatch(f) {
|
|
2232
|
+
if (harnessDriven(f.env) || f.env.PAIGY_ON_WAKE || f.env.CODEX_THREAD_ID) return "yes";
|
|
2233
|
+
const c = f.client?.toLowerCase();
|
|
2234
|
+
if (c && WATCHING_CLIENTS.has(c)) return "yes";
|
|
2235
|
+
if (c && NON_WATCHING_CLIENTS.has(c)) return "no";
|
|
2236
|
+
return "unknown";
|
|
2237
|
+
}
|
|
2238
|
+
function unlistenedNote(f, _now) {
|
|
2239
|
+
if (harnessDriven(f.env) || f.pidAlive) return null;
|
|
2240
|
+
const decision = decideListen(f);
|
|
2241
|
+
if (decision.status !== "start") return null;
|
|
2242
|
+
const head = `NOTHING CAN REACH THIS SESSION WHILE IT RESTS. No listener runs for your slot (${f.slot}) and no harness pump delivers to it`;
|
|
2243
|
+
switch (canWatch(f)) {
|
|
2244
|
+
case "no":
|
|
2245
|
+
return { unlistened: `${head}, and this host cannot run a listener. Work reaches you only as you act \u2014 it rides your tool calls, as it did this one \u2014 so call contact({ wait: false }) before you stop.` };
|
|
2246
|
+
case "yes":
|
|
2247
|
+
return { unlistened: `${head}. Start the listener below as a watched background task; a listener you see in ps belongs to another session's slot, never to yours.`, listen_command: decision.command };
|
|
2248
|
+
default:
|
|
2249
|
+
return { unlistened: `${head}. If this host can run a watched background task, start the listener below; if it cannot, work reaches you only as you act, so call contact({ wait: false }) before you stop.`, listen_command: decision.command };
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
var ReachTeller = class {
|
|
2253
|
+
constructor(every = UNLISTENED_EVERY_MS) {
|
|
2254
|
+
this.every = every;
|
|
2255
|
+
}
|
|
2256
|
+
every;
|
|
2257
|
+
checked = /* @__PURE__ */ new Map();
|
|
2258
|
+
async note(slot, tool, d) {
|
|
2259
|
+
const now = d.now();
|
|
2260
|
+
const last = this.checked.get(slot);
|
|
2261
|
+
if (last !== void 0 && now - last < this.every) return null;
|
|
2262
|
+
this.checked.set(slot, now);
|
|
2263
|
+
let note;
|
|
2264
|
+
try {
|
|
2265
|
+
note = unlistenedNote(d.facts(), now);
|
|
2266
|
+
} catch {
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
if (!note) return null;
|
|
2270
|
+
let waiting = null;
|
|
2271
|
+
if (tool !== "contact") {
|
|
2272
|
+
try {
|
|
2273
|
+
waiting = waitingSummary(await d.poll());
|
|
2274
|
+
} catch {
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return { paigy_reach: { ...note, ...waiting ?? {} } };
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
function withReach(shaped, reach) {
|
|
2281
|
+
if (!reach || !shaped || typeof shaped !== "object" || Array.isArray(shaped)) return shaped;
|
|
2282
|
+
return { ...reach, ...shaped };
|
|
2283
|
+
}
|
|
2284
|
+
var HOOKED_HARNESSES = "Claude Code and Codex";
|
|
2285
|
+
function listenBlock(decision, f, stale) {
|
|
2286
|
+
const block = {
|
|
2287
|
+
listen: decision,
|
|
2288
|
+
listen_permanent: `\`${ENABLE_LISTEN_COMMAND}\` installs the SessionStart hook for ${HOOKED_HARNESSES}, so every future session there is told this at its start. Other hosts are not hooked; their sessions learn it as they act.`
|
|
2289
|
+
};
|
|
2290
|
+
if (decision.status === "start") {
|
|
2291
|
+
block.listen_prompt = canWatch(f) === "no" ? "This host cannot run a listener, so nothing reaches this session while it rests. Work arrives with your tool calls; call contact({ wait: false }) before you stop." : "Start your own listener NOW, as described in `listen` \u2014 a watched background task, left running for this session. Without it, every answer waits until you call contact({ wait: false }).";
|
|
2292
|
+
}
|
|
2293
|
+
if (stale) {
|
|
2294
|
+
block.hook_refresh = `The SessionStart hook on this machine is an older text. Ask the user to re-run \`${ENABLE_LISTEN_COMMAND}\` to refresh it \u2014 the old one tells a session to skip its listener when it sees another session's.`;
|
|
2295
|
+
}
|
|
2296
|
+
return block;
|
|
2297
|
+
}
|
|
2298
|
+
var ENABLE_LISTEN_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
2299
|
+
function hookStale(home = homedir()) {
|
|
2300
|
+
try {
|
|
2301
|
+
return readFileSync(join(home, ".paigy", "hooks", "session-start.sh"), "utf8") !== sessionStartHook();
|
|
2302
|
+
} catch {
|
|
2303
|
+
return false;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function waitingSummary(received) {
|
|
2307
|
+
const r = received ?? {};
|
|
2308
|
+
const events = Array.isArray(r.events) ? r.events : [];
|
|
2309
|
+
const ids = events.map((e) => String(e?.eventId ?? "")).filter(Boolean);
|
|
2310
|
+
const claimable = r.claimable && typeof r.claimable === "object" ? String(r.claimable.title ?? "") : "";
|
|
2311
|
+
if (!ids.length && !claimable) return null;
|
|
2312
|
+
const kinds = events.map((e) => String(e?.kind ?? "event")).join(", ");
|
|
2313
|
+
const parts = [
|
|
2314
|
+
ids.length ? `${ids.length} event${ids.length === 1 ? "" : "s"} addressed to you (${kinds})` : "",
|
|
2315
|
+
claimable ? `work waiting: ${claimable}` : ""
|
|
2316
|
+
].filter(Boolean);
|
|
2317
|
+
return {
|
|
2318
|
+
waiting: `${parts.join("; ")}. Nothing acknowledged them \u2014 handle them with contact, and acknowledge with contact({ ackEventIds }) so they stop repeating. Read a Goal with get_goals before acting on a summary of it.`,
|
|
2319
|
+
...ids.length ? { waiting_events: ids } : {}
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2070
2322
|
|
|
2071
2323
|
export {
|
|
2072
2324
|
mcpInputSchema,
|
|
@@ -2082,5 +2334,10 @@ export {
|
|
|
2082
2334
|
removeListenMark,
|
|
2083
2335
|
listenerKey,
|
|
2084
2336
|
wakeStatusPath,
|
|
2085
|
-
existingListener
|
|
2337
|
+
existingListener,
|
|
2338
|
+
ReachTeller,
|
|
2339
|
+
withReach,
|
|
2340
|
+
listenBlock,
|
|
2341
|
+
ENABLE_LISTEN_COMMAND,
|
|
2342
|
+
hookStale
|
|
2086
2343
|
};
|