@paigy/mcp 0.40.28 → 0.40.31
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/dist/{chunk-632JM6QA.js → chunk-AWI252HQ.js} +590 -477
- package/dist/{chunk-AHLJPVEP.js → chunk-CULBHBFU.js} +614 -480
- package/dist/{chunk-WKAXAJEM.js → chunk-FAS3IB2F.js} +1 -1
- package/dist/{chunk-ZP6NBVEE.js → chunk-NHPLQOQP.js} +2 -2
- package/dist/{dist-JTZJFUWD.js → dist-2WLAZWSH.js} +1 -1
- package/dist/enable.js +3 -3
- package/dist/index.js +4 -4
- package/dist/listen.js +2 -2
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ../../packages/schema/dist/index.js
|
|
2
|
-
import { z as
|
|
2
|
+
import { z as z9 } from "zod";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { z as z3 } from "zod";
|
|
5
5
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
@@ -10,6 +10,10 @@ import { z as z5 } from "zod";
|
|
|
10
10
|
import { zodToJsonSchema as zodToJsonSchema3 } from "zod-to-json-schema";
|
|
11
11
|
import { z as z6 } from "zod";
|
|
12
12
|
import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
|
|
13
|
+
import { z as z7 } from "zod";
|
|
14
|
+
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
15
|
+
import { z as z8 } from "zod";
|
|
16
|
+
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
13
17
|
var OPTIONS_MIN = 2;
|
|
14
18
|
var OPTIONS_MAX = 6;
|
|
15
19
|
var OPTION_LABEL_MAX = 50;
|
|
@@ -136,8 +140,41 @@ var UpdateInputSchema = z2.object({
|
|
|
136
140
|
message: z2.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
137
141
|
goalIds: z2.array(z2.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
|
|
138
142
|
units,
|
|
143
|
+
// A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
|
|
144
|
+
// until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
|
|
145
|
+
// still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
|
|
146
|
+
// tap lands as a contribution, the person's own words on the Goal, because there is no Question
|
|
147
|
+
// for it to answer.
|
|
148
|
+
//
|
|
149
|
+
// AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
|
|
150
|
+
// acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
|
|
151
|
+
// it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
|
|
152
|
+
// work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
|
|
153
|
+
// call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
|
|
154
|
+
// tested yet*. The words are worth writing because the agent reads them back.
|
|
155
|
+
//
|
|
156
|
+
// ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
|
|
157
|
+
// write up to four of them: "just on the cards, but it should be more like just they should only
|
|
158
|
+
// get one basically string value that they can enter that replaces got it"). A report has exactly
|
|
159
|
+
// one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
|
|
160
|
+
// `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
|
|
161
|
+
// MECHANISM). It rides the door as the report's single stored option (`storedOption`,
|
|
162
|
+
// `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
|
|
163
|
+
reply: z2.string().trim().min(1).describe(
|
|
164
|
+
`What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. Left out, the button says "Got it", as every card did.`
|
|
165
|
+
).optional(),
|
|
139
166
|
userExplicitlyRequested
|
|
140
|
-
}).strict().superRefine((u, ctx) =>
|
|
167
|
+
}).strict().superRefine((u, ctx) => {
|
|
168
|
+
refuseOverCaps(u.message, u.units, "message", ctx);
|
|
169
|
+
if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
|
|
170
|
+
ctx.addIssue({
|
|
171
|
+
code: z2.ZodIssueCode.custom,
|
|
172
|
+
path: ["reply"],
|
|
173
|
+
params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX },
|
|
174
|
+
message: `reply_too_long: the reply is ${u.reply.length} characters; the cap is ${OPTION_LABEL_MAX}. It is a button's words, so say it in a breath.`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
141
178
|
var questionFields = {
|
|
142
179
|
question: z2.string().trim().min(1).describe(
|
|
143
180
|
`ONE question, in at most ${ASK_MAX} characters, and only what is needed to answer it; the context goes in units. Several questions are several objects, one each: an answer settles the one question it was given, so a person who answers the part of a bundled question that interested them settles nothing and is asked again. Separate questions cost nothing: ANY contact for a person already on a call JOINS that call, so several arrive as one call.`
|
|
@@ -221,6 +258,8 @@ QUESTIONS: one question per object; several questions are several objects in one
|
|
|
221
258
|
|
|
222
259
|
CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX} characters; everything else it needs goes in 'units', up to ${UNITS_MAX} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX} characters and every body at most ${UNIT_BODY_MAX}. The person sees the main text and the titles, and opens a unit to read its body. 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.
|
|
223
260
|
|
|
261
|
+
GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. It is what their button says instead of "Got it", and what they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Nothing is owed on it either way, and a report that writes none offers "Got it".
|
|
262
|
+
|
|
224
263
|
UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
|
|
225
264
|
|
|
226
265
|
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_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
|
|
@@ -333,7 +372,7 @@ var SearchToolSchema = z3.object({
|
|
|
333
372
|
goalId: z3.string().uuid().optional(),
|
|
334
373
|
limit: z3.number().int().min(1).max(20).optional()
|
|
335
374
|
}).strict();
|
|
336
|
-
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and
|
|
375
|
+
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and replies -- the person's answers, acknowledgements and deferrals (type answer; found by their summary, by the question or report they reply to, or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, the replies to its items, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (a reply carries its kind -- answered, acknowledged or deferred -- whether it is closed (a deferral is not), its summary in under ten words, its question or report, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
337
376
|
var CheckActivitySchema = z3.object({}).strict();
|
|
338
377
|
var FEEDBACK_TEXT_MAX = 5e4;
|
|
339
378
|
var SendFeedbackSchema = z3.object({
|
|
@@ -353,7 +392,7 @@ var AGENT_TOOLS = [
|
|
|
353
392
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
354
393
|
function serverInstructions(opts) {
|
|
355
394
|
const waits = opts.waits ? "contact({..., wait:true}) holds one bounded ~45 s window for a response to what you sent; contact({}) holds one for anything addressed to you." : "A contact here returns after one read (`waitOutcome: not_waited`); answers wake you, and contact({wait:false}) collects them.";
|
|
356
|
-
return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})), which hand you your work: answers, assignments, reviews and due Goals. get_goal reads a Goal and the conversation on it; a read never puts you on it. You are on a Goal from your first write to it (a contact update, question or answer naming it, or a manage_goals change); there is nothing to claim or join. Every Goal of your person is open to you, whichever of their agents owns it: read it with get_goal, write on it (a contact naming its id) and change it (manage_goals). Create, edit, assign, organize or close work with manage_goals, never as a contact; a contact only names Goals that already exist. Every read ends in \`next\`, the one step to take. Sending returns immediately unless you ask it to wait: keep working and collect answers by receiving or through get_goal. ${waits} Events repeat until you acknowledge them: reading alone acknowledges nothing; an \`update\` event acknowledged is a review cleared. Report progress as a contact update: one the person does not need yet is kept as the Goal's progress and reaches no one. To follow up later, defer the Goal with manage_goals: you are woken when its time comes. Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
|
|
395
|
+
return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})), which hand you your work: answers, assignments, reviews and due Goals. get_goal reads a Goal and the conversation on it; a read never puts you on it. You are on a Goal from your first write to it (a contact update, question or answer naming it, or a manage_goals change); there is nothing to claim or join. Every Goal of your person is open to you, whichever of their agents owns it: read it with get_goal, write on it (a contact naming its id) and change it (manage_goals). Create, edit, assign, organize or close work with manage_goals, never as a contact; a contact only names Goals that already exist. Every read ends in \`next\`, the one step to take. Sending returns immediately unless you ask it to wait: keep working and collect answers by receiving or through get_goal. ${waits} Events repeat until you acknowledge them: reading alone acknowledges nothing; an \`update\` event acknowledged is a review cleared. Report progress as a contact update: one the person does not need yet is kept as the Goal's progress and reaches no one. To follow up later, defer the Goal with manage_goals: you are woken when its time comes. Never infer ringing from an open Call Delivery. While a Call you placed is open, stay with it until it ends: keep receiving window after window, because the person may give you follow-ups or instructions on the call. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Give a report its reply too (an update's \`reply\`): one line the person can tap instead of "Got it", written in their voice for that report, so what they tap tells you whether they verified your work or only received it. Never assume anyone is reading the terminal stdout.
|
|
357
396
|
|
|
358
397
|
HOW TO ASK:
|
|
359
398
|
1. One question per question object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
@@ -511,13 +550,15 @@ var TalkerReplySchema = z5.object({
|
|
|
511
550
|
messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
|
|
512
551
|
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
513
552
|
then: z5.enum(["listen", "hold", "end"]),
|
|
514
|
-
/**
|
|
515
|
-
* the
|
|
516
|
-
|
|
517
|
-
|
|
553
|
+
/** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
|
|
554
|
+
* IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
|
|
555
|
+
* "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
|
|
556
|
+
* end of this call), a question's handle (that question's answer) or an ISO time. */
|
|
557
|
+
replies: z5.array(z5.object({ item: str2, options: strs2, lines: strs2, summary: str2, defer: str2 }).strict()),
|
|
558
|
+
/** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
|
|
518
559
|
instruction: strs2,
|
|
519
|
-
/** The line handles
|
|
520
|
-
*
|
|
560
|
+
/** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
|
|
561
|
+
* work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
|
|
521
562
|
file: strs2,
|
|
522
563
|
/** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
|
|
523
564
|
need: z5.object({ kind: z5.enum(["search", "item", "agent", ""]), text: str2 }).strict(),
|
|
@@ -565,6 +606,60 @@ var FilerReplySchema = z6.object({
|
|
|
565
606
|
var FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
566
607
|
zodToJsonSchema4(FilerReplySchema, { $refStrategy: "none" })
|
|
567
608
|
);
|
|
609
|
+
var ExplainerReplySchema = z7.object({
|
|
610
|
+
/** The explanation, as sentences, spoken in order. */
|
|
611
|
+
say: z7.array(z7.string()),
|
|
612
|
+
/** Questions to an agent about gaps in what it read: the agent's handle and the question. */
|
|
613
|
+
followUps: z7.array(z7.object({ to: z7.string(), text: z7.string() }).strict())
|
|
614
|
+
}).strict();
|
|
615
|
+
var EXPLAINER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
616
|
+
zodToJsonSchema5(ExplainerReplySchema, { $refStrategy: "none" })
|
|
617
|
+
);
|
|
618
|
+
var DISTILLER_DEFECTS = [
|
|
619
|
+
"reasked",
|
|
620
|
+
// a question put to the person again after they answered it
|
|
621
|
+
"unsupported",
|
|
622
|
+
// a fact stated that nothing in the input supports
|
|
623
|
+
"unsaid",
|
|
624
|
+
// an agent's answer arrived during the call and was never said
|
|
625
|
+
"skipped",
|
|
626
|
+
// an agenda item never put to the person
|
|
627
|
+
"unsaved",
|
|
628
|
+
// something the person answered or asked for did not save, was refused or dropped
|
|
629
|
+
"ignored",
|
|
630
|
+
// the person asked or said something and was never answered (the critic's ignored_a_question)
|
|
631
|
+
"promised",
|
|
632
|
+
// said it did or would do something, and nothing shows it done (the critic's broken_promise)
|
|
633
|
+
"dropped"
|
|
634
|
+
// something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
|
|
635
|
+
];
|
|
636
|
+
var DistillerReplySchema = z8.object({
|
|
637
|
+
defects: z8.array(z8.object({
|
|
638
|
+
kind: z8.enum(DISTILLER_DEFECTS),
|
|
639
|
+
/** The handles of the lines it shows in. */
|
|
640
|
+
lines: z8.array(z8.string()),
|
|
641
|
+
/** The handle of the record it concerns, or "". */
|
|
642
|
+
about: z8.string(),
|
|
643
|
+
/** One plain sentence, in words, never handles: it is what the feedback row keeps. */
|
|
644
|
+
why: z8.string()
|
|
645
|
+
}).strict())
|
|
646
|
+
}).strict();
|
|
647
|
+
var DISTILLER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
648
|
+
zodToJsonSchema6(DistillerReplySchema, { $refStrategy: "none" })
|
|
649
|
+
);
|
|
650
|
+
var TIDY_AUDIENCES = ["talker", "filer", "agents"];
|
|
651
|
+
var TidyReplySchema = z8.object({
|
|
652
|
+
lessons: z8.array(z8.object({
|
|
653
|
+
lesson: z8.string(),
|
|
654
|
+
op: z8.enum(["keep", "revise", "withdraw"]),
|
|
655
|
+
text: z8.string(),
|
|
656
|
+
for: z8.enum([...TIDY_AUDIENCES, ""]),
|
|
657
|
+
into: z8.string()
|
|
658
|
+
}).strict())
|
|
659
|
+
}).strict();
|
|
660
|
+
var TIDY_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
661
|
+
zodToJsonSchema6(TidyReplySchema, { $refStrategy: "none" })
|
|
662
|
+
);
|
|
568
663
|
function entryWords(entry) {
|
|
569
664
|
const content = entry.content;
|
|
570
665
|
if (content && "sealed" in content) return "";
|
|
@@ -591,20 +686,20 @@ function entryUnit(entry) {
|
|
|
591
686
|
}
|
|
592
687
|
var LIVE_MS = 3 * 6e4;
|
|
593
688
|
var WORKING_MS = 60 * 6e4;
|
|
594
|
-
var ContextSchema =
|
|
595
|
-
title:
|
|
596
|
-
description:
|
|
689
|
+
var ContextSchema = z9.object({
|
|
690
|
+
title: z9.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
691
|
+
description: z9.array(z9.string().min(1)).describe(
|
|
597
692
|
"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."
|
|
598
693
|
),
|
|
599
694
|
/** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
|
|
600
695
|
* a body on a tap. Absent on an ask sent without units, which renders as before. */
|
|
601
|
-
units:
|
|
696
|
+
units: z9.array(z9.object({ title: z9.string(), body: z9.string() })).optional()
|
|
602
697
|
});
|
|
603
|
-
var ParticipantSchema =
|
|
604
|
-
kind:
|
|
605
|
-
id:
|
|
698
|
+
var ParticipantSchema = z9.object({
|
|
699
|
+
kind: z9.enum(["human", "agent"]),
|
|
700
|
+
id: z9.string()
|
|
606
701
|
});
|
|
607
|
-
var TransformSchema =
|
|
702
|
+
var TransformSchema = z9.enum([
|
|
608
703
|
"structure",
|
|
609
704
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
610
705
|
"request_more",
|
|
@@ -620,13 +715,13 @@ var TransformSchema = z7.enum([
|
|
|
620
715
|
"summarize"
|
|
621
716
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
622
717
|
]);
|
|
623
|
-
var VisualSchema =
|
|
624
|
-
url:
|
|
625
|
-
label:
|
|
718
|
+
var VisualSchema = z9.object({
|
|
719
|
+
url: z9.string().url(),
|
|
720
|
+
label: z9.string().optional()
|
|
626
721
|
});
|
|
627
|
-
var NotifyLevelSchema =
|
|
628
|
-
var SelectShapeSchema =
|
|
629
|
-
var ReceiptEventSchema =
|
|
722
|
+
var NotifyLevelSchema = z9.enum(["inbox", "push", "banner", "call"]);
|
|
723
|
+
var SelectShapeSchema = z9.enum(["one", "many", "rank", "confirm", "text"]);
|
|
724
|
+
var ReceiptEventSchema = z9.enum([
|
|
630
725
|
"delivered",
|
|
631
726
|
// the bundle reached the recipient at some level
|
|
632
727
|
"seen",
|
|
@@ -656,47 +751,47 @@ var ReceiptEventSchema = z7.enum([
|
|
|
656
751
|
// be rewound by a writer that forgot to advance it.
|
|
657
752
|
"restarted"
|
|
658
753
|
]);
|
|
659
|
-
var AttentionSchema =
|
|
754
|
+
var AttentionSchema = z9.object({
|
|
660
755
|
urgency: NotifyLevelSchema,
|
|
661
756
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
662
757
|
select: SelectShapeSchema.nullable(),
|
|
663
758
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
664
|
-
points:
|
|
759
|
+
points: z9.array(z9.string()).nullable(),
|
|
665
760
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
666
|
-
blocking:
|
|
761
|
+
blocking: z9.boolean(),
|
|
667
762
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
668
763
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
669
|
-
deadline:
|
|
764
|
+
deadline: z9.string().datetime().nullable().optional()
|
|
670
765
|
});
|
|
671
|
-
var NotifyRequestFields =
|
|
766
|
+
var NotifyRequestFields = z9.object({
|
|
672
767
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
673
768
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
674
769
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
675
770
|
context: ContextSchema.optional(),
|
|
676
|
-
options:
|
|
771
|
+
options: z9.array(OptionSchema.omit({ id: true }).extend({ label: z9.string().trim().min(1).max(1e3) }).strict()).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
677
772
|
"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)."
|
|
678
773
|
),
|
|
679
|
-
points:
|
|
774
|
+
points: z9.array(z9.string().min(1)).optional().describe(
|
|
680
775
|
"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."
|
|
681
776
|
),
|
|
682
|
-
visuals:
|
|
777
|
+
visuals: z9.array(VisualSchema).optional().describe(
|
|
683
778
|
"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."
|
|
684
779
|
),
|
|
685
780
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
686
|
-
repo:
|
|
781
|
+
repo: z9.string().optional(),
|
|
687
782
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
688
|
-
branch:
|
|
783
|
+
branch: z9.string().optional(),
|
|
689
784
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
690
785
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
691
786
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
692
787
|
* still accepts the old name from older clients. */
|
|
693
|
-
parentId:
|
|
788
|
+
parentId: z9.string().uuid().optional(),
|
|
694
789
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
695
790
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
696
|
-
workId:
|
|
791
|
+
workId: z9.string().uuid().optional(),
|
|
697
792
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
698
793
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
699
|
-
goalId:
|
|
794
|
+
goalId: z9.string().uuid().optional(),
|
|
700
795
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
701
796
|
"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."
|
|
702
797
|
),
|
|
@@ -704,7 +799,7 @@ var NotifyRequestFields = z7.object({
|
|
|
704
799
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
705
800
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
706
801
|
* says HOW. */
|
|
707
|
-
clarifies:
|
|
802
|
+
clarifies: z9.string().optional(),
|
|
708
803
|
select: SelectShapeSchema.optional().describe(
|
|
709
804
|
"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."
|
|
710
805
|
),
|
|
@@ -718,20 +813,20 @@ var NotifyRequestFields = z7.object({
|
|
|
718
813
|
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
719
814
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
720
815
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
721
|
-
ask:
|
|
816
|
+
ask: z9.string().min(1).max(1e4).optional().describe(
|
|
722
817
|
'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.'
|
|
723
818
|
),
|
|
724
|
-
needs:
|
|
819
|
+
needs: z9.array(z9.string().min(1)).optional().describe(
|
|
725
820
|
"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."
|
|
726
821
|
),
|
|
727
|
-
urgencyHint:
|
|
822
|
+
urgencyHint: z9.enum(["whenever", "soon", "now"]).optional().describe(
|
|
728
823
|
"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."
|
|
729
824
|
),
|
|
730
825
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
731
826
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
732
827
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
733
828
|
* urgencyHint/blocking win when both are sent. */
|
|
734
|
-
waiting:
|
|
829
|
+
waiting: z9.enum(["none", "soft", "hard"]).optional().describe(
|
|
735
830
|
"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."
|
|
736
831
|
),
|
|
737
832
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -739,98 +834,98 @@ var NotifyRequestFields = z7.object({
|
|
|
739
834
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
740
835
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
741
836
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
742
|
-
confirm:
|
|
837
|
+
confirm: z9.boolean().optional().describe(
|
|
743
838
|
"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'."
|
|
744
839
|
),
|
|
745
840
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
746
841
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
747
842
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
748
843
|
* waiting:'hard'. */
|
|
749
|
-
channel:
|
|
844
|
+
channel: z9.enum(["call", "message"]).optional().describe(
|
|
750
845
|
"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."
|
|
751
846
|
),
|
|
752
|
-
confirmStyle:
|
|
847
|
+
confirmStyle: z9.enum(["yesno", "approve"]).default("yesno").describe(
|
|
753
848
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
754
849
|
),
|
|
755
|
-
blocking:
|
|
850
|
+
blocking: z9.boolean().default(false).describe(
|
|
756
851
|
"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."
|
|
757
852
|
)
|
|
758
853
|
});
|
|
759
854
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
760
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
855
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z9.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
761
856
|
if (r.ask !== void 0) {
|
|
762
857
|
for (const f of ["context", "select", "points"]) {
|
|
763
858
|
if (r[f] !== void 0)
|
|
764
|
-
ctx.addIssue({ code:
|
|
859
|
+
ctx.addIssue({ code: z9.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.` });
|
|
765
860
|
}
|
|
766
861
|
return;
|
|
767
862
|
}
|
|
768
863
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
769
|
-
ctx.addIssue({ code:
|
|
864
|
+
ctx.addIssue({ code: z9.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
770
865
|
if (!r.context)
|
|
771
|
-
ctx.addIssue({ code:
|
|
866
|
+
ctx.addIssue({ code: z9.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
772
867
|
if (!r.select)
|
|
773
|
-
ctx.addIssue({ code:
|
|
868
|
+
ctx.addIssue({ code: z9.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
774
869
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
775
870
|
if (needsOptions && !r.options?.length)
|
|
776
|
-
ctx.addIssue({ code:
|
|
871
|
+
ctx.addIssue({ code: z9.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
777
872
|
if (!needsOptions && r.options?.length)
|
|
778
|
-
ctx.addIssue({ code:
|
|
873
|
+
ctx.addIssue({ code: z9.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
779
874
|
});
|
|
780
|
-
var NotifyStatusSchema =
|
|
781
|
-
var AgentStateSchema =
|
|
782
|
-
var TurnSchema =
|
|
783
|
-
prompt:
|
|
784
|
-
reply:
|
|
875
|
+
var NotifyStatusSchema = z9.enum(["pending", "answered", "ignored"]);
|
|
876
|
+
var AgentStateSchema = z9.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
877
|
+
var TurnSchema = z9.object({
|
|
878
|
+
prompt: z9.string(),
|
|
879
|
+
reply: z9.string()
|
|
785
880
|
});
|
|
786
|
-
var UserAnswerSchema =
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
881
|
+
var UserAnswerSchema = z9.discriminatedUnion("kind", [
|
|
882
|
+
z9.object({ kind: z9.literal("option"), optionId: z9.string(), label: z9.string().optional() }),
|
|
883
|
+
z9.object({ kind: z9.literal("text"), text: z9.string() }),
|
|
884
|
+
z9.object({ kind: z9.literal("ignored") }),
|
|
885
|
+
z9.object({ kind: z9.literal("multi"), optionIds: z9.array(z9.string()), labels: z9.array(z9.string()).optional() }),
|
|
886
|
+
z9.object({ kind: z9.literal("ranked"), optionIds: z9.array(z9.string()), labels: z9.array(z9.string()).optional() }),
|
|
887
|
+
z9.object({ kind: z9.literal("clarify"), chunks: z9.array(z9.string()).min(1) }),
|
|
888
|
+
z9.object({ kind: z9.literal("confirm"), approved: z9.boolean() }),
|
|
889
|
+
z9.object({ kind: z9.literal("turns"), turns: z9.array(TurnSchema).min(1) }),
|
|
795
890
|
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
796
891
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
797
892
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
798
893
|
* card the user can reply to — the broker never overrides the user. */
|
|
799
|
-
|
|
894
|
+
z9.object({ kind: z9.literal("precedent"), answer: z9.string(), derivation: z9.string(), sources: z9.array(z9.string()).min(1) })
|
|
800
895
|
]);
|
|
801
|
-
var IntentSchema =
|
|
896
|
+
var IntentSchema = z9.object({
|
|
802
897
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
803
898
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
804
899
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
805
900
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
806
|
-
kind:
|
|
807
|
-
detail:
|
|
901
|
+
kind: z9.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
902
|
+
detail: z9.string(),
|
|
808
903
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
809
904
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
810
905
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
811
906
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
812
|
-
dueInSeconds:
|
|
907
|
+
dueInSeconds: z9.number().int().positive().optional(),
|
|
813
908
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
814
909
|
* already read the utterance, so `signals.kind` stops defaulting to
|
|
815
910
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
816
911
|
* about what cannot answer "is the bot looping less this week?". */
|
|
817
|
-
fault:
|
|
912
|
+
fault: z9.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
818
913
|
});
|
|
819
|
-
var RideAlongSchema =
|
|
914
|
+
var RideAlongSchema = z9.object({
|
|
820
915
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
821
|
-
noteId:
|
|
916
|
+
noteId: z9.string(),
|
|
822
917
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
823
|
-
text:
|
|
918
|
+
text: z9.string(),
|
|
824
919
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
825
|
-
parentId:
|
|
920
|
+
parentId: z9.string().nullable()
|
|
826
921
|
});
|
|
827
|
-
var AwaitItemSchema =
|
|
828
|
-
|
|
829
|
-
type:
|
|
830
|
-
parentId:
|
|
831
|
-
notificationId:
|
|
832
|
-
workId:
|
|
833
|
-
decisionId:
|
|
922
|
+
var AwaitItemSchema = z9.discriminatedUnion("type", [
|
|
923
|
+
z9.object({
|
|
924
|
+
type: z9.literal("reply"),
|
|
925
|
+
parentId: z9.string(),
|
|
926
|
+
notificationId: z9.string(),
|
|
927
|
+
workId: z9.string().uuid().optional(),
|
|
928
|
+
decisionId: z9.string().uuid().optional(),
|
|
834
929
|
answer: UserAnswerSchema,
|
|
835
930
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
836
931
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -840,103 +935,103 @@ var AwaitItemSchema = z7.discriminatedUnion("type", [
|
|
|
840
935
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
841
936
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
842
937
|
* a sentence with a hole in it is worse than no sentence. */
|
|
843
|
-
note:
|
|
938
|
+
note: z9.string().optional(),
|
|
844
939
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
845
940
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
846
941
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
847
942
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
848
|
-
transcript:
|
|
943
|
+
transcript: z9.string().optional(),
|
|
849
944
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
850
945
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
851
|
-
covered:
|
|
946
|
+
covered: z9.array(z9.string()).optional(),
|
|
852
947
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
853
948
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
854
949
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
855
950
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
856
951
|
* dropped. Absent/empty = nothing owed. */
|
|
857
|
-
also:
|
|
952
|
+
also: z9.array(RideAlongSchema).optional()
|
|
858
953
|
}),
|
|
859
|
-
|
|
860
|
-
type:
|
|
861
|
-
parentId:
|
|
862
|
-
notificationId:
|
|
863
|
-
remindAt:
|
|
954
|
+
z9.object({
|
|
955
|
+
type: z9.literal("remind"),
|
|
956
|
+
parentId: z9.string(),
|
|
957
|
+
notificationId: z9.string(),
|
|
958
|
+
remindAt: z9.string().datetime({ offset: true }),
|
|
864
959
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
865
|
-
remindInSeconds:
|
|
960
|
+
remindInSeconds: z9.number()
|
|
866
961
|
}),
|
|
867
962
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
868
963
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
869
964
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
870
965
|
* re-orient via contact({})). */
|
|
871
|
-
|
|
872
|
-
type:
|
|
873
|
-
parentId:
|
|
874
|
-
notificationId:
|
|
966
|
+
z9.object({
|
|
967
|
+
type: z9.literal("superseded"),
|
|
968
|
+
parentId: z9.string(),
|
|
969
|
+
notificationId: z9.string()
|
|
875
970
|
}),
|
|
876
971
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
877
972
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
878
973
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
879
974
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
880
975
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
881
|
-
|
|
882
|
-
type:
|
|
883
|
-
notificationId:
|
|
884
|
-
inFlight:
|
|
885
|
-
turn:
|
|
886
|
-
idx:
|
|
887
|
-
prompt:
|
|
888
|
-
reply:
|
|
889
|
-
acts:
|
|
976
|
+
z9.object({
|
|
977
|
+
type: z9.literal("partial"),
|
|
978
|
+
notificationId: z9.string(),
|
|
979
|
+
inFlight: z9.literal(true),
|
|
980
|
+
turn: z9.object({
|
|
981
|
+
idx: z9.number(),
|
|
982
|
+
prompt: z9.string(),
|
|
983
|
+
reply: z9.string(),
|
|
984
|
+
acts: z9.array(IntentSchema).nullable().optional()
|
|
890
985
|
})
|
|
891
986
|
}),
|
|
892
|
-
|
|
893
|
-
type:
|
|
894
|
-
also:
|
|
987
|
+
z9.object({
|
|
988
|
+
type: z9.literal("idle"),
|
|
989
|
+
also: z9.array(RideAlongSchema).optional(),
|
|
895
990
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
896
991
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
897
992
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
898
993
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
899
994
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
900
|
-
inFlight:
|
|
995
|
+
inFlight: z9.boolean().optional()
|
|
901
996
|
})
|
|
902
997
|
]);
|
|
903
|
-
var VoiceKeySchema =
|
|
904
|
-
var AgendaTurnSchema =
|
|
998
|
+
var VoiceKeySchema = z9.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
999
|
+
var AgendaTurnSchema = z9.object({
|
|
905
1000
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
906
1001
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
907
1002
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
908
1003
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
909
1004
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
910
1005
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
911
|
-
id:
|
|
1006
|
+
id: z9.string().optional(),
|
|
912
1007
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
913
1008
|
* the planner declares duplicates instead of asking them twice. */
|
|
914
|
-
coveredIds:
|
|
1009
|
+
coveredIds: z9.array(z9.string()).optional(),
|
|
915
1010
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
916
1011
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
917
|
-
info:
|
|
918
|
-
question:
|
|
1012
|
+
info: z9.array(z9.string().min(1)).default([]),
|
|
1013
|
+
question: z9.string().min(1).nullable(),
|
|
919
1014
|
/** True on the one turn carrying the agent's own declared question. */
|
|
920
|
-
asks:
|
|
1015
|
+
asks: z9.boolean().optional(),
|
|
921
1016
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
922
1017
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
923
1018
|
* which route nothing. */
|
|
924
|
-
claimId:
|
|
1019
|
+
claimId: z9.string().optional(),
|
|
925
1020
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
926
|
-
voice:
|
|
1021
|
+
voice: z9.string().optional(),
|
|
927
1022
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
928
1023
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
929
1024
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
930
1025
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
931
|
-
agent:
|
|
1026
|
+
agent: z9.string().optional(),
|
|
932
1027
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
933
1028
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
934
1029
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
935
1030
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
936
1031
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
937
|
-
agentId:
|
|
1032
|
+
agentId: z9.string().optional(),
|
|
938
1033
|
select: SelectShapeSchema.optional(),
|
|
939
|
-
options:
|
|
1034
|
+
options: z9.array(OptionSchema.omit({ id: true })).optional(),
|
|
940
1035
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
941
1036
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
942
1037
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -945,33 +1040,33 @@ var AgendaTurnSchema = z7.object({
|
|
|
945
1040
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
946
1041
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
947
1042
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
948
|
-
blocking:
|
|
1043
|
+
blocking: z9.boolean().optional(),
|
|
949
1044
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
950
1045
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
951
1046
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
952
1047
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
953
1048
|
* speak, it is dropped and their words are taken like any reply. */
|
|
954
|
-
ifSilent:
|
|
1049
|
+
ifSilent: z9.boolean().optional()
|
|
955
1050
|
});
|
|
956
1051
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
957
|
-
var InboxItemSchema =
|
|
958
|
-
id:
|
|
959
|
-
tokenId:
|
|
1052
|
+
var InboxItemSchema = z9.object({
|
|
1053
|
+
id: z9.string(),
|
|
1054
|
+
tokenId: z9.string().optional(),
|
|
960
1055
|
status: NotifyStatusSchema,
|
|
961
1056
|
context: ContextSchema,
|
|
962
|
-
options:
|
|
1057
|
+
options: z9.array(OptionSchema).optional(),
|
|
963
1058
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
964
|
-
points:
|
|
1059
|
+
points: z9.array(z9.string()).optional(),
|
|
965
1060
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
966
1061
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
967
1062
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
968
1063
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
969
|
-
asks:
|
|
1064
|
+
asks: z9.boolean().optional(),
|
|
970
1065
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
971
1066
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
972
1067
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
973
1068
|
* which must never CLAIM stalled. */
|
|
974
|
-
lastSeenAt:
|
|
1069
|
+
lastSeenAt: z9.string().optional(),
|
|
975
1070
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
976
1071
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
977
1072
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -981,7 +1076,7 @@ var InboxItemSchema = z7.object({
|
|
|
981
1076
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
982
1077
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
983
1078
|
* `createdAt`. */
|
|
984
|
-
agentStateAt:
|
|
1079
|
+
agentStateAt: z9.string().datetime().optional(),
|
|
985
1080
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
986
1081
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
987
1082
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -995,54 +1090,61 @@ var InboxItemSchema = z7.object({
|
|
|
995
1090
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
996
1091
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
997
1092
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
998
|
-
questions:
|
|
999
|
-
id:
|
|
1000
|
-
entryId:
|
|
1001
|
-
title:
|
|
1002
|
-
state:
|
|
1003
|
-
answer:
|
|
1093
|
+
questions: z9.array(z9.object({
|
|
1094
|
+
id: z9.string(),
|
|
1095
|
+
entryId: z9.string(),
|
|
1096
|
+
title: z9.string(),
|
|
1097
|
+
state: z9.enum(["open", "answered"]),
|
|
1098
|
+
answer: z9.string().nullable(),
|
|
1004
1099
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
1005
1100
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
1006
1101
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
1007
1102
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
1008
|
-
agent:
|
|
1103
|
+
agent: z9.object({ tokenId: z9.string(), name: z9.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
1009
1104
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
1010
1105
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
1011
1106
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
1012
|
-
* never re-sent as a second card to be seen mid-call. Words-only options are
|
|
1013
|
-
|
|
1014
|
-
|
|
1107
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
1108
|
+
options: z9.array(OptionSchema).optional(),
|
|
1109
|
+
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
1110
|
+
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
1111
|
+
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
1112
|
+
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
1113
|
+
* preview layout for any `options`, and must see exactly what it did. */
|
|
1114
|
+
choices: z9.array(OptionSchema).optional(),
|
|
1115
|
+
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
1116
|
+
select: z9.enum(["one", "many", "rank"]).optional()
|
|
1015
1117
|
})).optional(),
|
|
1016
|
-
visuals:
|
|
1118
|
+
visuals: z9.array(VisualSchema).optional(),
|
|
1017
1119
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
1018
1120
|
* agent's suggestion, or a default silly name). */
|
|
1019
|
-
name:
|
|
1121
|
+
name: z9.string(),
|
|
1020
1122
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
1021
1123
|
voice: VoiceKeySchema.optional(),
|
|
1022
|
-
repo:
|
|
1023
|
-
branch:
|
|
1024
|
-
createdAt:
|
|
1025
|
-
snoozedUntil:
|
|
1124
|
+
repo: z9.string().optional(),
|
|
1125
|
+
branch: z9.string().optional(),
|
|
1126
|
+
createdAt: z9.string().datetime(),
|
|
1127
|
+
snoozedUntil: z9.string().datetime().optional(),
|
|
1026
1128
|
agentState: AgentStateSchema.default("idle"),
|
|
1027
1129
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
1028
1130
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
1029
1131
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
1030
|
-
turn:
|
|
1132
|
+
turn: z9.enum(["you", "agent"]).default("you"),
|
|
1031
1133
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
1032
1134
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
1033
1135
|
* error badge + Retry. */
|
|
1034
|
-
error:
|
|
1136
|
+
error: z9.string().optional(),
|
|
1035
1137
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
1036
1138
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
1037
1139
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
1038
1140
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
1039
|
-
cold:
|
|
1040
|
-
clarifies:
|
|
1141
|
+
cold: z9.string().datetime().optional(),
|
|
1142
|
+
clarifies: z9.string().optional(),
|
|
1041
1143
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
1042
1144
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
1043
1145
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
1044
1146
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
1045
|
-
onCall:
|
|
1147
|
+
onCall: z9.literal(true).optional(),
|
|
1046
1148
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
1047
1149
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
1048
1150
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -1052,31 +1154,31 @@ var InboxItemSchema = z7.object({
|
|
|
1052
1154
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
1053
1155
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
1054
1156
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
1055
|
-
ring:
|
|
1056
|
-
declinedAt:
|
|
1057
|
-
anchorAt:
|
|
1058
|
-
nextRingAt:
|
|
1059
|
-
step:
|
|
1157
|
+
ring: z9.object({
|
|
1158
|
+
declinedAt: z9.string().datetime(),
|
|
1159
|
+
anchorAt: z9.string().datetime(),
|
|
1160
|
+
nextRingAt: z9.string().datetime().nullable(),
|
|
1161
|
+
step: z9.number().int()
|
|
1060
1162
|
}).optional(),
|
|
1061
1163
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
1062
1164
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
1063
1165
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
1064
1166
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
1065
1167
|
* feedback ABOUT. */
|
|
1066
|
-
why:
|
|
1168
|
+
why: z9.object({
|
|
1067
1169
|
asked: NotifyLevelSchema,
|
|
1068
1170
|
got: NotifyLevelSchema,
|
|
1069
|
-
because:
|
|
1070
|
-
line:
|
|
1171
|
+
because: z9.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
1172
|
+
line: z9.string()
|
|
1071
1173
|
}).optional(),
|
|
1072
|
-
select:
|
|
1073
|
-
confirmStyle:
|
|
1174
|
+
select: z9.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
1175
|
+
confirmStyle: z9.enum(["yesno", "approve"]).default("yesno").describe(
|
|
1074
1176
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
1075
1177
|
),
|
|
1076
1178
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
1077
1179
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
1078
1180
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
1079
|
-
blocking:
|
|
1181
|
+
blocking: z9.boolean().default(false),
|
|
1080
1182
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
1081
1183
|
answer: UserAnswerSchema.optional(),
|
|
1082
1184
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -1084,17 +1186,17 @@ var InboxItemSchema = z7.object({
|
|
|
1084
1186
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
1085
1187
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
1086
1188
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
1087
|
-
communication:
|
|
1088
|
-
deliveryId:
|
|
1089
|
-
kind:
|
|
1090
|
-
entryId:
|
|
1091
|
-
goalIds:
|
|
1092
|
-
decisionNeedId:
|
|
1093
|
-
sealed:
|
|
1094
|
-
goalState:
|
|
1189
|
+
communication: z9.object({
|
|
1190
|
+
deliveryId: z9.string(),
|
|
1191
|
+
kind: z9.enum(["notification", "call"]),
|
|
1192
|
+
entryId: z9.string(),
|
|
1193
|
+
goalIds: z9.array(z9.string()),
|
|
1194
|
+
decisionNeedId: z9.string().optional(),
|
|
1195
|
+
sealed: z9.boolean(),
|
|
1196
|
+
goalState: z9.string().optional(),
|
|
1095
1197
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
1096
1198
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
1097
|
-
goalTitle:
|
|
1199
|
+
goalTitle: z9.string().optional()
|
|
1098
1200
|
}).optional(),
|
|
1099
1201
|
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
1100
1202
|
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
@@ -1106,27 +1208,27 @@ var InboxItemSchema = z7.object({
|
|
|
1106
1208
|
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
1107
1209
|
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
1108
1210
|
* with no rev is simply always re-sent. */
|
|
1109
|
-
rev:
|
|
1211
|
+
rev: z9.string().optional()
|
|
1110
1212
|
});
|
|
1111
1213
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
1112
|
-
var PushTokenSchema =
|
|
1113
|
-
voipToken:
|
|
1114
|
-
alertToken:
|
|
1115
|
-
fcmToken:
|
|
1116
|
-
platform:
|
|
1214
|
+
var PushTokenSchema = z9.object({
|
|
1215
|
+
voipToken: z9.string().min(1).optional(),
|
|
1216
|
+
alertToken: z9.string().min(1).optional(),
|
|
1217
|
+
fcmToken: z9.string().min(1).optional(),
|
|
1218
|
+
platform: z9.enum(["ios", "android"])
|
|
1117
1219
|
}).superRefine((v, ctx) => {
|
|
1118
1220
|
if (v.platform !== "ios") return;
|
|
1119
1221
|
for (const field of ["voipToken", "alertToken"]) {
|
|
1120
1222
|
const token = v[field];
|
|
1121
1223
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
1122
1224
|
ctx.addIssue({
|
|
1123
|
-
code:
|
|
1225
|
+
code: z9.ZodIssueCode.custom,
|
|
1124
1226
|
path: [field],
|
|
1125
1227
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
1126
1228
|
});
|
|
1127
1229
|
}
|
|
1128
1230
|
});
|
|
1129
|
-
var MissedCallSchema =
|
|
1231
|
+
var MissedCallSchema = z9.enum([
|
|
1130
1232
|
"retry_10m",
|
|
1131
1233
|
"retry_30m",
|
|
1132
1234
|
"retry_60m",
|
|
@@ -1138,31 +1240,31 @@ var MissedCallSchema = z7.enum([
|
|
|
1138
1240
|
]);
|
|
1139
1241
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
1140
1242
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
1141
|
-
var BrokerTuningSchema =
|
|
1243
|
+
var BrokerTuningSchema = z9.object({
|
|
1142
1244
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
1143
|
-
ackVerbosity:
|
|
1245
|
+
ackVerbosity: z9.enum(["normal", "none"]).optional(),
|
|
1144
1246
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
1145
1247
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
1146
|
-
clarifyEagerness:
|
|
1248
|
+
clarifyEagerness: z9.enum(["low", "normal", "high"]).optional(),
|
|
1147
1249
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
1148
|
-
phrasebook:
|
|
1250
|
+
phrasebook: z9.array(z9.object({ say: z9.string().min(1).max(60), mean: z9.string().min(1).max(120) })).max(24).optional(),
|
|
1149
1251
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
1150
1252
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
1151
1253
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
1152
1254
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
1153
1255
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
1154
|
-
language:
|
|
1256
|
+
language: z9.enum(["en", "es"]).optional()
|
|
1155
1257
|
});
|
|
1156
|
-
var UserSettingsSchema =
|
|
1157
|
-
permissions:
|
|
1158
|
-
call:
|
|
1159
|
-
banner:
|
|
1160
|
-
push:
|
|
1258
|
+
var UserSettingsSchema = z9.object({
|
|
1259
|
+
permissions: z9.object({
|
|
1260
|
+
call: z9.boolean(),
|
|
1261
|
+
banner: z9.boolean(),
|
|
1262
|
+
push: z9.boolean()
|
|
1161
1263
|
}),
|
|
1162
1264
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
1163
1265
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
1164
1266
|
* what people read (owner, 2026-09-30). */
|
|
1165
|
-
sessionMode:
|
|
1267
|
+
sessionMode: z9.enum(["default", "all_calls", "silent"]),
|
|
1166
1268
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
1167
1269
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
1168
1270
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -1171,7 +1273,7 @@ var UserSettingsSchema = z7.object({
|
|
|
1171
1273
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
1172
1274
|
* next dead field for the same reason. */
|
|
1173
1275
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
1174
|
-
improveConsent:
|
|
1276
|
+
improveConsent: z9.boolean(),
|
|
1175
1277
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
1176
1278
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
1177
1279
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -1179,17 +1281,17 @@ var UserSettingsSchema = z7.object({
|
|
|
1179
1281
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
1180
1282
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
1181
1283
|
* write, 'hosted' on read (see store.ts). */
|
|
1182
|
-
voiceMode:
|
|
1284
|
+
voiceMode: z9.enum(["hosted", "on_device"]).optional(),
|
|
1183
1285
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
1184
1286
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
1185
1287
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
1186
1288
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
1187
1289
|
* read (see store.ts). */
|
|
1188
|
-
talk:
|
|
1290
|
+
talk: z9.boolean().optional(),
|
|
1189
1291
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
1190
1292
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
1191
1293
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
1192
|
-
callDiagnostics:
|
|
1294
|
+
callDiagnostics: z9.boolean().optional(),
|
|
1193
1295
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
1194
1296
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
1195
1297
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -1197,7 +1299,7 @@ var UserSettingsSchema = z7.object({
|
|
|
1197
1299
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
1198
1300
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
1199
1301
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
1200
|
-
voiceTuning:
|
|
1302
|
+
voiceTuning: z9.record(z9.string(), z9.union([z9.number(), z9.string()])).optional(),
|
|
1201
1303
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
1202
1304
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
1203
1305
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -1206,53 +1308,53 @@ var UserSettingsSchema = z7.object({
|
|
|
1206
1308
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
1207
1309
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
1208
1310
|
* call — honest about what we know. */
|
|
1209
|
-
timezone:
|
|
1311
|
+
timezone: z9.string().min(1).max(64).optional(),
|
|
1210
1312
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
1211
1313
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
1212
1314
|
broker: BrokerTuningSchema.optional()
|
|
1213
1315
|
});
|
|
1214
|
-
var HistoryWorkSchema =
|
|
1215
|
-
id:
|
|
1216
|
-
title:
|
|
1217
|
-
state:
|
|
1316
|
+
var HistoryWorkSchema = z9.object({
|
|
1317
|
+
id: z9.string(),
|
|
1318
|
+
title: z9.string(),
|
|
1319
|
+
state: z9.enum(["done", "cancelled"]),
|
|
1218
1320
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
1219
|
-
assignee:
|
|
1321
|
+
assignee: z9.string()
|
|
1220
1322
|
});
|
|
1221
|
-
var HistoryEntrySchema =
|
|
1222
|
-
|
|
1223
|
-
|
|
1323
|
+
var HistoryEntrySchema = z9.union([
|
|
1324
|
+
z9.object({ at: z9.string(), card: InboxItemSchema }),
|
|
1325
|
+
z9.object({ at: z9.string(), work: HistoryWorkSchema })
|
|
1224
1326
|
]);
|
|
1225
|
-
var HistoryPageSchema =
|
|
1226
|
-
entries:
|
|
1227
|
-
next:
|
|
1327
|
+
var HistoryPageSchema = z9.object({
|
|
1328
|
+
entries: z9.array(HistoryEntrySchema),
|
|
1329
|
+
next: z9.string().nullable()
|
|
1228
1330
|
});
|
|
1229
1331
|
var ACTIVITY_LINES = 2;
|
|
1230
1332
|
var ACTIVITY_LINE_MAX = 80;
|
|
1231
|
-
var AgentActivitySchema =
|
|
1333
|
+
var AgentActivitySchema = z9.object({
|
|
1232
1334
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
1233
|
-
lines:
|
|
1335
|
+
lines: z9.array(z9.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
1234
1336
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
1235
1337
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
1236
|
-
at:
|
|
1338
|
+
at: z9.string().datetime()
|
|
1237
1339
|
});
|
|
1238
|
-
var ConnectionSummarySchema =
|
|
1340
|
+
var ConnectionSummarySchema = z9.object({
|
|
1239
1341
|
/** The connection = the agent's token id (used to address a request). */
|
|
1240
|
-
id:
|
|
1342
|
+
id: z9.string(),
|
|
1241
1343
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
1242
1344
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
1243
1345
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
1244
1346
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
1245
|
-
kind:
|
|
1347
|
+
kind: z9.enum(["device", "agent"]).optional(),
|
|
1246
1348
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
1247
1349
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
1248
1350
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
1249
|
-
mintedByDevice:
|
|
1250
|
-
device:
|
|
1351
|
+
mintedByDevice: z9.string().nullable().optional(),
|
|
1352
|
+
device: z9.string().nullable(),
|
|
1251
1353
|
/** The agent's display name (the single pairing name). */
|
|
1252
|
-
name:
|
|
1354
|
+
name: z9.string(),
|
|
1253
1355
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
1254
1356
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
1255
|
-
provider:
|
|
1357
|
+
provider: z9.string().nullable(),
|
|
1256
1358
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
1257
1359
|
voice: VoiceKeySchema.nullable(),
|
|
1258
1360
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -1263,34 +1365,34 @@ var ConnectionSummarySchema = z7.object({
|
|
|
1263
1365
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
1264
1366
|
* set once must not overrule a rule they set about one agent. */
|
|
1265
1367
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
1266
|
-
createdAt:
|
|
1368
|
+
createdAt: z9.string().datetime(),
|
|
1267
1369
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
1268
1370
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
1269
|
-
lastContactAt:
|
|
1371
|
+
lastContactAt: z9.string().datetime().nullable(),
|
|
1270
1372
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
1271
1373
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
1272
|
-
lastSeenAt:
|
|
1374
|
+
lastSeenAt: z9.string().datetime().nullable().optional(),
|
|
1273
1375
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
1274
1376
|
* its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
|
|
1275
1377
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
1276
|
-
lastWorkedAt:
|
|
1378
|
+
lastWorkedAt: z9.string().datetime().nullable().optional(),
|
|
1277
1379
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
1278
1380
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
1279
|
-
oldestReadyAt:
|
|
1381
|
+
oldestReadyAt: z9.string().datetime().nullable().optional(),
|
|
1280
1382
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
1281
1383
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
1282
1384
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
1283
|
-
runtime:
|
|
1385
|
+
runtime: z9.object({
|
|
1284
1386
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
1285
1387
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
1286
|
-
version:
|
|
1287
|
-
harnesses:
|
|
1288
|
-
workspaces:
|
|
1388
|
+
version: z9.string().optional(),
|
|
1389
|
+
harnesses: z9.array(z9.object({ name: z9.string(), label: z9.string(), status: z9.string() })).optional(),
|
|
1390
|
+
workspaces: z9.array(z9.string()).optional(),
|
|
1289
1391
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
1290
1392
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
1291
1393
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
1292
1394
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
1293
|
-
repos:
|
|
1395
|
+
repos: z9.array(z9.object({ path: z9.string(), remote: z9.string() })).optional()
|
|
1294
1396
|
}).optional(),
|
|
1295
1397
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
1296
1398
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -1299,156 +1401,156 @@ var ConnectionSummarySchema = z7.object({
|
|
|
1299
1401
|
activity: AgentActivitySchema.optional(),
|
|
1300
1402
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
1301
1403
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
1302
|
-
managed:
|
|
1404
|
+
managed: z9.boolean()
|
|
1303
1405
|
});
|
|
1304
|
-
var LedgerItemSchema =
|
|
1305
|
-
var AgentLedgerSchema =
|
|
1406
|
+
var LedgerItemSchema = z9.object({ id: z9.string(), parentId: z9.string(), title: z9.string(), createdAt: z9.string() });
|
|
1407
|
+
var AgentLedgerSchema = z9.object({
|
|
1306
1408
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
1307
|
-
agent:
|
|
1409
|
+
agent: z9.object({ id: z9.string(), name: z9.string().nullable(), revokedAt: z9.string().nullable() }),
|
|
1308
1410
|
/** Its own questions you have not answered. */
|
|
1309
|
-
asks:
|
|
1411
|
+
asks: z9.array(LedgerItemSchema),
|
|
1310
1412
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
1311
|
-
answered:
|
|
1413
|
+
answered: z9.array(LedgerItemSchema),
|
|
1312
1414
|
/** Requests you sent it that it never took. */
|
|
1313
|
-
requests:
|
|
1314
|
-
goals:
|
|
1315
|
-
callbacks:
|
|
1415
|
+
requests: z9.array(LedgerItemSchema),
|
|
1416
|
+
goals: z9.array(z9.object({ id: z9.string(), outcome: z9.string(), state: z9.string() })),
|
|
1417
|
+
callbacks: z9.array(z9.object({ id: z9.string(), parentId: z9.string(), trigger: z9.string(), note: z9.string(), dueAt: z9.string().nullable() }))
|
|
1316
1418
|
});
|
|
1317
|
-
var ReassignResultSchema =
|
|
1318
|
-
moved:
|
|
1319
|
-
parentId:
|
|
1419
|
+
var ReassignResultSchema = z9.object({
|
|
1420
|
+
moved: z9.object({ asks: z9.number(), answered: z9.number(), requests: z9.number(), goals: z9.number(), callbacks: z9.number() }),
|
|
1421
|
+
parentId: z9.string().nullable()
|
|
1320
1422
|
});
|
|
1321
|
-
var LessonStateSchema =
|
|
1322
|
-
var LessonViewSchema =
|
|
1323
|
-
id:
|
|
1324
|
-
text:
|
|
1423
|
+
var LessonStateSchema = z9.enum(["active", "proposed", "retired"]);
|
|
1424
|
+
var LessonViewSchema = z9.object({
|
|
1425
|
+
id: z9.string(),
|
|
1426
|
+
text: z9.string(),
|
|
1325
1427
|
state: LessonStateSchema,
|
|
1326
1428
|
/** The Goal it is scoped to; null = the whole account. */
|
|
1327
|
-
scopeGoalId:
|
|
1328
|
-
goalTitle:
|
|
1329
|
-
version:
|
|
1330
|
-
pinned:
|
|
1429
|
+
scopeGoalId: z9.string().nullable(),
|
|
1430
|
+
goalTitle: z9.string().nullable(),
|
|
1431
|
+
version: z9.number(),
|
|
1432
|
+
pinned: z9.boolean(),
|
|
1331
1433
|
/** When the person last wrote its text themselves. */
|
|
1332
|
-
editedAt:
|
|
1333
|
-
createdAt:
|
|
1334
|
-
updatedAt:
|
|
1434
|
+
editedAt: z9.string().nullable(),
|
|
1435
|
+
createdAt: z9.string(),
|
|
1436
|
+
updatedAt: z9.string(),
|
|
1335
1437
|
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
1336
|
-
sources:
|
|
1438
|
+
sources: z9.array(z9.object({ entryId: z9.string(), words: z9.string().nullable(), at: z9.string() }))
|
|
1337
1439
|
});
|
|
1338
|
-
var QueueQuestionSchema =
|
|
1440
|
+
var QueueQuestionSchema = z9.object({
|
|
1339
1441
|
/** The decision need's id — what an answer is accepted against. */
|
|
1340
|
-
id:
|
|
1442
|
+
id: z9.string(),
|
|
1341
1443
|
/** The words that were asked, from the request Entry that asked them. */
|
|
1342
|
-
question:
|
|
1444
|
+
question: z9.string(),
|
|
1343
1445
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
1344
1446
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
1345
1447
|
* can answer. */
|
|
1346
|
-
deliveryId:
|
|
1448
|
+
deliveryId: z9.string().nullable().default(null),
|
|
1347
1449
|
/** The Entry the ruling is about. */
|
|
1348
|
-
aboutId:
|
|
1450
|
+
aboutId: z9.string().nullable().default(null),
|
|
1349
1451
|
/** Empty for a free-text question. */
|
|
1350
|
-
options:
|
|
1351
|
-
select:
|
|
1352
|
-
askedAt:
|
|
1452
|
+
options: z9.array(OptionSchema).default([]),
|
|
1453
|
+
select: z9.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
1454
|
+
askedAt: z9.string(),
|
|
1353
1455
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
1354
|
-
answeredAt:
|
|
1456
|
+
answeredAt: z9.string().nullable().default(null),
|
|
1355
1457
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
1356
1458
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
1357
1459
|
* for a settled question whose reply carried nothing readable. */
|
|
1358
|
-
answer:
|
|
1460
|
+
answer: z9.string().nullable().default(null),
|
|
1359
1461
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
1360
1462
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
1361
1463
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
1362
1464
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
1363
1465
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
1364
1466
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
1365
|
-
goalId:
|
|
1467
|
+
goalId: z9.string(),
|
|
1366
1468
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
1367
1469
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
1368
1470
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
1369
1471
|
* gate at all. */
|
|
1370
|
-
blocked:
|
|
1472
|
+
blocked: z9.boolean().default(false)
|
|
1371
1473
|
});
|
|
1372
|
-
var QueueReplySchema =
|
|
1474
|
+
var QueueReplySchema = z9.object({
|
|
1373
1475
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
1374
1476
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
1375
1477
|
* walk can name it in its zoom. */
|
|
1376
|
-
id:
|
|
1478
|
+
id: z9.string(),
|
|
1377
1479
|
/** The Goal the note is on. */
|
|
1378
|
-
goalId:
|
|
1480
|
+
goalId: z9.string(),
|
|
1379
1481
|
/** What the note said. */
|
|
1380
|
-
note:
|
|
1482
|
+
note: z9.string(),
|
|
1381
1483
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
1382
|
-
deliveryId:
|
|
1383
|
-
requestEntryId:
|
|
1384
|
-
askedAt:
|
|
1484
|
+
deliveryId: z9.string(),
|
|
1485
|
+
requestEntryId: z9.string(),
|
|
1486
|
+
askedAt: z9.string(),
|
|
1385
1487
|
/** When the person last replied — the window's start. */
|
|
1386
|
-
repliedAt:
|
|
1488
|
+
repliedAt: z9.string(),
|
|
1387
1489
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
1388
|
-
reply:
|
|
1490
|
+
reply: z9.string().nullable()
|
|
1389
1491
|
});
|
|
1390
|
-
var QueueItemSchema =
|
|
1391
|
-
id:
|
|
1492
|
+
var QueueItemSchema = z9.object({
|
|
1493
|
+
id: z9.string(),
|
|
1392
1494
|
/** One-line headline — the first sentence of the outcome. */
|
|
1393
|
-
title:
|
|
1495
|
+
title: z9.string(),
|
|
1394
1496
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
1395
|
-
intent:
|
|
1497
|
+
intent: z9.string(),
|
|
1396
1498
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
1397
|
-
state:
|
|
1499
|
+
state: z9.string(),
|
|
1398
1500
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
1399
|
-
assignee:
|
|
1501
|
+
assignee: z9.string().nullable().default(null),
|
|
1400
1502
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
1401
|
-
progress:
|
|
1503
|
+
progress: z9.string().nullable().default(null),
|
|
1402
1504
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
1403
1505
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
1404
|
-
progressLine:
|
|
1405
|
-
reviewPending:
|
|
1406
|
-
dueAt:
|
|
1506
|
+
progressLine: z9.string().nullable().optional(),
|
|
1507
|
+
reviewPending: z9.boolean().default(false),
|
|
1508
|
+
dueAt: z9.string().nullable().default(null),
|
|
1407
1509
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
1408
1510
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
1409
|
-
finishedAt:
|
|
1511
|
+
finishedAt: z9.string().nullable().optional(),
|
|
1410
1512
|
/** The Goal this one was opened under; null at the root. */
|
|
1411
|
-
parentGoalId:
|
|
1513
|
+
parentGoalId: z9.string().nullable().default(null),
|
|
1412
1514
|
/** Goals opened under this one — only those the same list holds. */
|
|
1413
|
-
childGoalIds:
|
|
1515
|
+
childGoalIds: z9.array(z9.string()).default([]),
|
|
1414
1516
|
/** Goals this one waits on (start or finish gates). */
|
|
1415
|
-
dependencyGoalIds:
|
|
1517
|
+
dependencyGoalIds: z9.array(z9.string()).default([]),
|
|
1416
1518
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
1417
|
-
blocked:
|
|
1519
|
+
blocked: z9.boolean().default(false),
|
|
1418
1520
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
1419
1521
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
1420
1522
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
1421
1523
|
* question is opened. */
|
|
1422
|
-
questions:
|
|
1524
|
+
questions: z9.array(QueueQuestionSchema).default([]),
|
|
1423
1525
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
1424
1526
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
1425
1527
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
1426
1528
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
1427
|
-
asked:
|
|
1428
|
-
answered:
|
|
1529
|
+
asked: z9.number().optional(),
|
|
1530
|
+
answered: z9.number().optional(),
|
|
1429
1531
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
1430
1532
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
1431
1533
|
* not spell an empty list. */
|
|
1432
|
-
replies:
|
|
1534
|
+
replies: z9.array(QueueReplySchema).optional(),
|
|
1433
1535
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
1434
|
-
repo:
|
|
1435
|
-
createdAt:
|
|
1436
|
-
updatedAt:
|
|
1536
|
+
repo: z9.string().nullable().optional(),
|
|
1537
|
+
createdAt: z9.string(),
|
|
1538
|
+
updatedAt: z9.string().nullable().default(null),
|
|
1437
1539
|
/** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
|
|
1438
1540
|
* as progress). `updatedAt` moves for reasons nobody chose — a
|
|
1439
1541
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
1440
|
-
lastProgressAt:
|
|
1542
|
+
lastProgressAt: z9.string().nullable().optional(),
|
|
1441
1543
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
1442
1544
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
1443
1545
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
1444
1546
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
1445
1547
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
1446
1548
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
1447
|
-
latest:
|
|
1448
|
-
from:
|
|
1449
|
-
said:
|
|
1450
|
-
at:
|
|
1451
|
-
entryId:
|
|
1549
|
+
latest: z9.object({
|
|
1550
|
+
from: z9.enum(["person", "agent"]),
|
|
1551
|
+
said: z9.string(),
|
|
1552
|
+
at: z9.string(),
|
|
1553
|
+
entryId: z9.string()
|
|
1452
1554
|
}).nullable().optional(),
|
|
1453
1555
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
1454
1556
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -1461,7 +1563,7 @@ var QueueItemSchema = z7.object({
|
|
|
1461
1563
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
1462
1564
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
1463
1565
|
* Entry with no `about_id` is in neither. */
|
|
1464
|
-
lastPersonAt:
|
|
1566
|
+
lastPersonAt: z9.string().nullable().optional(),
|
|
1465
1567
|
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
1466
1568
|
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
1467
1569
|
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
@@ -1472,40 +1574,40 @@ var QueueItemSchema = z7.object({
|
|
|
1472
1574
|
* the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
|
|
1473
1575
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
1474
1576
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
1475
|
-
rev:
|
|
1577
|
+
rev: z9.string().optional()
|
|
1476
1578
|
});
|
|
1477
|
-
var QueueDeltaSchema =
|
|
1478
|
-
ids:
|
|
1479
|
-
items:
|
|
1579
|
+
var QueueDeltaSchema = z9.object({
|
|
1580
|
+
ids: z9.array(z9.string()),
|
|
1581
|
+
items: z9.array(QueueItemSchema)
|
|
1480
1582
|
});
|
|
1481
1583
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
1482
|
-
var NoteSourceSchema =
|
|
1483
|
-
var NoteStatusSchema =
|
|
1484
|
-
var NoteRepeatSchema =
|
|
1485
|
-
var DecisionSchema =
|
|
1486
|
-
id:
|
|
1584
|
+
var NoteSourceSchema = z9.enum(["app", "call"]);
|
|
1585
|
+
var NoteStatusSchema = z9.enum(["open", "assigned", "in_progress", "done"]);
|
|
1586
|
+
var NoteRepeatSchema = z9.enum(["once", "until_done"]);
|
|
1587
|
+
var DecisionSchema = z9.object({
|
|
1588
|
+
id: z9.string(),
|
|
1487
1589
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
1488
1590
|
* extensibility seam — any conversation can accrue decisions). */
|
|
1489
|
-
noteId:
|
|
1591
|
+
noteId: z9.string().nullable(),
|
|
1490
1592
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
1491
|
-
question:
|
|
1593
|
+
question: z9.string(),
|
|
1492
1594
|
/** The user's ruling; null while the question is open. */
|
|
1493
|
-
answer:
|
|
1494
|
-
decidedAt:
|
|
1495
|
-
createdAt:
|
|
1595
|
+
answer: z9.string().nullable(),
|
|
1596
|
+
decidedAt: z9.string().nullable(),
|
|
1597
|
+
createdAt: z9.string()
|
|
1496
1598
|
});
|
|
1497
|
-
var NoteSchema =
|
|
1498
|
-
id:
|
|
1599
|
+
var NoteSchema = z9.object({
|
|
1600
|
+
id: z9.string(),
|
|
1499
1601
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
1500
|
-
title:
|
|
1602
|
+
title: z9.string(),
|
|
1501
1603
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
1502
|
-
intent:
|
|
1604
|
+
intent: z9.string(),
|
|
1503
1605
|
source: NoteSourceSchema,
|
|
1504
1606
|
status: NoteStatusSchema,
|
|
1505
1607
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
1506
|
-
assignee:
|
|
1608
|
+
assignee: z9.string().nullable(),
|
|
1507
1609
|
/** The request thread minted at assignment; null until assigned. */
|
|
1508
|
-
parentId:
|
|
1610
|
+
parentId: z9.string().nullable(),
|
|
1509
1611
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1510
1612
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
1511
1613
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -1514,152 +1616,152 @@ var NoteSchema = z7.object({
|
|
|
1514
1616
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
1515
1617
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
1516
1618
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
1517
|
-
dueAt:
|
|
1619
|
+
dueAt: z9.string().nullable().default(null),
|
|
1518
1620
|
repeat: NoteRepeatSchema.default("once"),
|
|
1519
1621
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
1520
|
-
rides:
|
|
1521
|
-
lastRideAt:
|
|
1522
|
-
createdAt:
|
|
1622
|
+
rides: z9.number().int().default(0),
|
|
1623
|
+
lastRideAt: z9.string().nullable().default(null),
|
|
1624
|
+
createdAt: z9.string()
|
|
1523
1625
|
});
|
|
1524
|
-
var TriageItemSchema =
|
|
1525
|
-
noteId:
|
|
1626
|
+
var TriageItemSchema = z9.object({
|
|
1627
|
+
noteId: z9.string(),
|
|
1526
1628
|
/** The note's headline at run time. */
|
|
1527
|
-
title:
|
|
1629
|
+
title: z9.string(),
|
|
1528
1630
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
1529
1631
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
1530
1632
|
* Never a model's reasoning transcript, never an id. */
|
|
1531
|
-
why:
|
|
1633
|
+
why: z9.string()
|
|
1532
1634
|
});
|
|
1533
|
-
var TriageAssignmentSchema =
|
|
1635
|
+
var TriageAssignmentSchema = z9.object({
|
|
1534
1636
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
1535
|
-
agent:
|
|
1637
|
+
agent: z9.string(),
|
|
1536
1638
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
1537
1639
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
1538
|
-
agentName:
|
|
1539
|
-
notes:
|
|
1640
|
+
agentName: z9.string(),
|
|
1641
|
+
notes: z9.array(TriageItemSchema)
|
|
1540
1642
|
});
|
|
1541
|
-
var TriageStatusSchema =
|
|
1542
|
-
var SubmitTriageSchema =
|
|
1643
|
+
var TriageStatusSchema = z9.enum(["open", "superseded", "dismissed"]);
|
|
1644
|
+
var SubmitTriageSchema = z9.object({
|
|
1543
1645
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
1544
1646
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
1545
1647
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
1546
1648
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
1547
|
-
provider:
|
|
1649
|
+
provider: z9.string().min(1).max(60),
|
|
1548
1650
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
1549
|
-
model:
|
|
1651
|
+
model: z9.string().max(200).nullable().optional(),
|
|
1550
1652
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
1551
1653
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
1552
|
-
reviewed:
|
|
1553
|
-
close:
|
|
1554
|
-
stale:
|
|
1555
|
-
assign:
|
|
1654
|
+
reviewed: z9.number().int().min(0).max(1e4).default(0),
|
|
1655
|
+
close: z9.array(TriageItemSchema).max(200).default([]),
|
|
1656
|
+
stale: z9.array(TriageItemSchema).max(200).default([]),
|
|
1657
|
+
assign: z9.array(TriageAssignmentSchema).max(50).default([])
|
|
1556
1658
|
});
|
|
1557
1659
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
1558
|
-
id:
|
|
1559
|
-
runAt:
|
|
1660
|
+
id: z9.string(),
|
|
1661
|
+
runAt: z9.string(),
|
|
1560
1662
|
status: TriageStatusSchema,
|
|
1561
|
-
model:
|
|
1663
|
+
model: z9.string().nullable().default(null)
|
|
1562
1664
|
});
|
|
1563
|
-
var AcceptTriageSchema =
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
group:
|
|
1568
|
-
agent:
|
|
1569
|
-
noteIds:
|
|
1665
|
+
var AcceptTriageSchema = z9.discriminatedUnion("group", [
|
|
1666
|
+
z9.object({ group: z9.literal("close"), noteIds: z9.array(z9.string()).max(200).optional() }),
|
|
1667
|
+
z9.object({ group: z9.literal("stale"), noteIds: z9.array(z9.string()).max(200).optional() }),
|
|
1668
|
+
z9.object({
|
|
1669
|
+
group: z9.literal("assign"),
|
|
1670
|
+
agent: z9.string().min(1),
|
|
1671
|
+
noteIds: z9.array(z9.string()).max(200).optional()
|
|
1570
1672
|
})
|
|
1571
1673
|
]);
|
|
1572
|
-
var AcceptTriageResultSchema =
|
|
1573
|
-
accepted:
|
|
1574
|
-
failed:
|
|
1674
|
+
var AcceptTriageResultSchema = z9.object({
|
|
1675
|
+
accepted: z9.array(z9.string()),
|
|
1676
|
+
failed: z9.array(z9.object({ noteId: z9.string(), reason: z9.string() }))
|
|
1575
1677
|
});
|
|
1576
|
-
var DeliveryModeSchema =
|
|
1577
|
-
var RegisterDeliverySchema =
|
|
1578
|
-
var OAuthStartSchema =
|
|
1579
|
-
provider:
|
|
1580
|
-
returnTo:
|
|
1678
|
+
var DeliveryModeSchema = z9.enum(["poll", "self_hosted"]);
|
|
1679
|
+
var RegisterDeliverySchema = z9.object({ mode: DeliveryModeSchema });
|
|
1680
|
+
var OAuthStartSchema = z9.object({
|
|
1681
|
+
provider: z9.enum(["cma"]),
|
|
1682
|
+
returnTo: z9.string().min(1)
|
|
1581
1683
|
});
|
|
1582
|
-
var DeliveryConfigSchema =
|
|
1583
|
-
tokenId:
|
|
1684
|
+
var DeliveryConfigSchema = z9.object({
|
|
1685
|
+
tokenId: z9.string(),
|
|
1584
1686
|
mode: DeliveryModeSchema,
|
|
1585
1687
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
1586
1688
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
1587
1689
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
1588
|
-
realtime:
|
|
1690
|
+
realtime: z9.object({ url: z9.string(), anonKey: z9.string() }).nullable()
|
|
1589
1691
|
});
|
|
1590
|
-
var HostDecisionSchema =
|
|
1692
|
+
var HostDecisionSchema = z9.object({
|
|
1591
1693
|
/** The agent's token id: the row's `recipient`. */
|
|
1592
|
-
agent:
|
|
1593
|
-
decision:
|
|
1694
|
+
agent: z9.string().uuid(),
|
|
1695
|
+
decision: z9.enum(["stood_back", "took_over"]),
|
|
1594
1696
|
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
1595
|
-
goalId:
|
|
1697
|
+
goalId: z9.string().uuid().nullable().optional(),
|
|
1596
1698
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
1597
|
-
seenAt:
|
|
1699
|
+
seenAt: z9.string().datetime().nullable().optional(),
|
|
1598
1700
|
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
1599
|
-
since:
|
|
1701
|
+
since: z9.string().datetime().nullable().optional(),
|
|
1600
1702
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
1601
|
-
said:
|
|
1703
|
+
said: z9.string().max(300).optional()
|
|
1602
1704
|
});
|
|
1603
|
-
var WakeNudgeSchema =
|
|
1604
|
-
kind:
|
|
1605
|
-
notificationId:
|
|
1606
|
-
parentId:
|
|
1705
|
+
var WakeNudgeSchema = z9.object({
|
|
1706
|
+
kind: z9.enum(["reply", "request", "callback"]),
|
|
1707
|
+
notificationId: z9.string().optional(),
|
|
1708
|
+
parentId: z9.string()
|
|
1607
1709
|
});
|
|
1608
|
-
var PairingStatusSchema =
|
|
1609
|
-
var DeviceCodeSchema =
|
|
1610
|
-
device_code:
|
|
1611
|
-
user_code:
|
|
1612
|
-
verification_uri:
|
|
1613
|
-
verification_uri_complete:
|
|
1614
|
-
interval:
|
|
1615
|
-
expires_in:
|
|
1710
|
+
var PairingStatusSchema = z9.enum(["pending", "approved", "denied", "expired"]);
|
|
1711
|
+
var DeviceCodeSchema = z9.object({
|
|
1712
|
+
device_code: z9.string(),
|
|
1713
|
+
user_code: z9.string(),
|
|
1714
|
+
verification_uri: z9.string().url(),
|
|
1715
|
+
verification_uri_complete: z9.string().url(),
|
|
1716
|
+
interval: z9.number(),
|
|
1717
|
+
expires_in: z9.number()
|
|
1616
1718
|
});
|
|
1617
|
-
var DeviceInfoSchema =
|
|
1618
|
-
code:
|
|
1719
|
+
var DeviceInfoSchema = z9.object({
|
|
1720
|
+
code: z9.string(),
|
|
1619
1721
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
1620
1722
|
* pre-filling the name field the human can edit. */
|
|
1621
|
-
name:
|
|
1723
|
+
name: z9.string(),
|
|
1622
1724
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
1623
1725
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
1624
1726
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
1625
1727
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
1626
|
-
agent:
|
|
1627
|
-
device:
|
|
1728
|
+
agent: z9.string().optional(),
|
|
1729
|
+
device: z9.string().nullable(),
|
|
1628
1730
|
status: PairingStatusSchema
|
|
1629
1731
|
});
|
|
1630
|
-
var DeviceTokenSchema =
|
|
1631
|
-
access_token:
|
|
1732
|
+
var DeviceTokenSchema = z9.object({
|
|
1733
|
+
access_token: z9.string(),
|
|
1632
1734
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
1633
1735
|
* a default silly name). */
|
|
1634
|
-
name:
|
|
1635
|
-
device:
|
|
1736
|
+
name: z9.string(),
|
|
1737
|
+
device: z9.string().nullable(),
|
|
1636
1738
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
1637
1739
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
1638
|
-
voice:
|
|
1740
|
+
voice: z9.string().nullable().optional(),
|
|
1639
1741
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
1640
1742
|
* that survives a rename. Cached by the host's identity beat. */
|
|
1641
|
-
token_id:
|
|
1743
|
+
token_id: z9.string().nullable().optional(),
|
|
1642
1744
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
1643
1745
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
1644
1746
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
1645
1747
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
1646
|
-
workspace:
|
|
1748
|
+
workspace: z9.string().nullable().optional(),
|
|
1647
1749
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
1648
|
-
harness:
|
|
1649
|
-
session_id:
|
|
1750
|
+
harness: z9.enum(["claude", "codex", "agy"]).optional(),
|
|
1751
|
+
session_id: z9.string().uuid().optional(),
|
|
1650
1752
|
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
1651
1753
|
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
1652
1754
|
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
1653
1755
|
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
1654
|
-
full_session:
|
|
1655
|
-
uik_pub:
|
|
1756
|
+
full_session: z9.string().optional(),
|
|
1757
|
+
uik_pub: z9.string().nullable().optional()
|
|
1656
1758
|
});
|
|
1657
|
-
var SupportRequestSchema =
|
|
1658
|
-
email:
|
|
1659
|
-
message:
|
|
1660
|
-
name:
|
|
1759
|
+
var SupportRequestSchema = z9.object({
|
|
1760
|
+
email: z9.string().email().max(320),
|
|
1761
|
+
message: z9.string().trim().min(1).max(5e3),
|
|
1762
|
+
name: z9.string().trim().max(120).optional()
|
|
1661
1763
|
});
|
|
1662
|
-
var NotificationFeedbackKindSchema =
|
|
1764
|
+
var NotificationFeedbackKindSchema = z9.enum([
|
|
1663
1765
|
"break_down",
|
|
1664
1766
|
// "This should be more than one ask — break it down."
|
|
1665
1767
|
"regenerate_options",
|
|
@@ -1674,126 +1776,137 @@ var NotificationFeedbackKindSchema = z7.enum([
|
|
|
1674
1776
|
// anything else — the note carries it.
|
|
1675
1777
|
]);
|
|
1676
1778
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
1677
|
-
var QuestionRowSchema =
|
|
1779
|
+
var QuestionRowSchema = z9.object({
|
|
1678
1780
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
1679
|
-
id:
|
|
1680
|
-
deliveryId:
|
|
1681
|
-
entryId:
|
|
1781
|
+
id: z9.string(),
|
|
1782
|
+
deliveryId: z9.string(),
|
|
1783
|
+
entryId: z9.string(),
|
|
1682
1784
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
1683
|
-
needId:
|
|
1684
|
-
goalIds:
|
|
1785
|
+
needId: z9.string().nullable(),
|
|
1786
|
+
goalIds: z9.array(z9.string()),
|
|
1685
1787
|
/** The name of the work it is about, when the read could word it. */
|
|
1686
|
-
goalTitle:
|
|
1687
|
-
tokenId:
|
|
1688
|
-
name:
|
|
1689
|
-
title:
|
|
1690
|
-
body:
|
|
1691
|
-
select:
|
|
1692
|
-
options:
|
|
1693
|
-
hasPreview:
|
|
1694
|
-
blocking:
|
|
1695
|
-
askedAt:
|
|
1788
|
+
goalTitle: z9.string().optional(),
|
|
1789
|
+
tokenId: z9.string().optional(),
|
|
1790
|
+
name: z9.string(),
|
|
1791
|
+
title: z9.string(),
|
|
1792
|
+
body: z9.string(),
|
|
1793
|
+
select: z9.enum(["one", "many", "rank", "confirm", "text"]),
|
|
1794
|
+
options: z9.array(SlimOptionSchema),
|
|
1795
|
+
hasPreview: z9.boolean(),
|
|
1796
|
+
blocking: z9.boolean(),
|
|
1797
|
+
askedAt: z9.string().datetime(),
|
|
1696
1798
|
ring: InboxItemSchema.shape.ring,
|
|
1697
|
-
onCall:
|
|
1698
|
-
sealed:
|
|
1799
|
+
onCall: z9.literal(true).optional(),
|
|
1800
|
+
sealed: z9.boolean()
|
|
1699
1801
|
});
|
|
1700
|
-
var WorkStateSchema =
|
|
1701
|
-
var WorkRowSchema =
|
|
1702
|
-
id:
|
|
1703
|
-
parentId:
|
|
1704
|
-
title:
|
|
1802
|
+
var WorkStateSchema = z9.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
1803
|
+
var WorkRowSchema = z9.object({
|
|
1804
|
+
id: z9.string(),
|
|
1805
|
+
parentId: z9.string().nullable(),
|
|
1806
|
+
title: z9.string(),
|
|
1705
1807
|
/** Straight off the Goal. */
|
|
1706
1808
|
state: WorkStateSchema,
|
|
1707
|
-
owner:
|
|
1708
|
-
revision:
|
|
1809
|
+
owner: z9.string().nullable(),
|
|
1810
|
+
revision: z9.number().int(),
|
|
1709
1811
|
/** Open questions on it, counted to 100. */
|
|
1710
|
-
waiting:
|
|
1812
|
+
waiting: z9.number().int(),
|
|
1711
1813
|
/** Held by a gate on work that is not done. */
|
|
1712
|
-
blocked:
|
|
1713
|
-
lastProgressAt:
|
|
1814
|
+
blocked: z9.boolean(),
|
|
1815
|
+
lastProgressAt: z9.string().datetime().nullable(),
|
|
1714
1816
|
/** The line written for its newest progress note, else that note's first words. */
|
|
1715
|
-
line:
|
|
1817
|
+
line: z9.string().nullable(),
|
|
1716
1818
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
1717
|
-
children:
|
|
1718
|
-
createdAt:
|
|
1719
|
-
updatedAt:
|
|
1819
|
+
children: z9.number().int(),
|
|
1820
|
+
createdAt: z9.string().datetime(),
|
|
1821
|
+
updatedAt: z9.string().datetime(),
|
|
1720
1822
|
/** When anything at or under it last moved — the order the list is in. */
|
|
1721
|
-
activeAt:
|
|
1823
|
+
activeAt: z9.string().datetime(),
|
|
1722
1824
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
1723
|
-
sealed:
|
|
1825
|
+
sealed: z9.boolean()
|
|
1724
1826
|
});
|
|
1725
1827
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
1726
1828
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
1727
1829
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
1728
|
-
asking:
|
|
1729
|
-
oldestAskAt:
|
|
1830
|
+
asking: z9.number().int().nullable(),
|
|
1831
|
+
oldestAskAt: z9.string().datetime().nullable(),
|
|
1730
1832
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
1731
1833
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
1732
1834
|
* (`agent_holds`); null when that read failed. */
|
|
1733
|
-
holds:
|
|
1734
|
-
held:
|
|
1835
|
+
holds: z9.array(z9.object({ id: z9.string(), title: z9.string() })).nullable(),
|
|
1836
|
+
held: z9.number().int().nullable(),
|
|
1735
1837
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
1736
1838
|
* while none has, or when that read failed. */
|
|
1737
|
-
cold:
|
|
1839
|
+
cold: z9.string().datetime().nullable(),
|
|
1738
1840
|
/** The newest line of its working log, and when the harness saw it. */
|
|
1739
|
-
line:
|
|
1740
|
-
lineAt:
|
|
1841
|
+
line: z9.string().nullable(),
|
|
1842
|
+
lineAt: z9.string().datetime().nullable()
|
|
1741
1843
|
});
|
|
1742
|
-
var SnapshotSchema =
|
|
1844
|
+
var SnapshotSchema = z9.object({
|
|
1743
1845
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
1744
|
-
at:
|
|
1745
|
-
questions:
|
|
1846
|
+
at: z9.string().datetime(),
|
|
1847
|
+
questions: z9.object({
|
|
1746
1848
|
/** The newest 30 open cards, questions before updates. */
|
|
1747
|
-
items:
|
|
1849
|
+
items: z9.array(QuestionRowSchema),
|
|
1748
1850
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
1749
|
-
total:
|
|
1750
|
-
updates:
|
|
1751
|
-
putOff:
|
|
1851
|
+
total: z9.number().int(),
|
|
1852
|
+
updates: z9.number().int(),
|
|
1853
|
+
putOff: z9.number().int()
|
|
1752
1854
|
}).nullable(),
|
|
1753
|
-
agents:
|
|
1855
|
+
agents: z9.object({
|
|
1754
1856
|
/** Up to 60, most recently seen first. */
|
|
1755
|
-
items:
|
|
1756
|
-
more:
|
|
1857
|
+
items: z9.array(AgentRowSchema),
|
|
1858
|
+
more: z9.boolean()
|
|
1757
1859
|
}).nullable(),
|
|
1758
|
-
work:
|
|
1860
|
+
work: z9.object({
|
|
1759
1861
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
1760
|
-
items:
|
|
1862
|
+
items: z9.array(WorkRowSchema),
|
|
1761
1863
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
1762
1864
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
1763
1865
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
1764
1866
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
1765
|
-
counts:
|
|
1867
|
+
counts: z9.object({ live: z9.number().int(), waiting: z9.number().int(), notStarted: z9.number().int(), done: z9.number().int() })
|
|
1766
1868
|
}).nullable(),
|
|
1767
|
-
you:
|
|
1869
|
+
you: z9.object({
|
|
1768
1870
|
settings: UserSettingsSchema,
|
|
1769
|
-
callable:
|
|
1871
|
+
callable: z9.boolean(),
|
|
1770
1872
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
1771
|
-
computers:
|
|
1873
|
+
computers: z9.array(ComputerRowSchema).nullable()
|
|
1772
1874
|
}).nullable()
|
|
1773
1875
|
});
|
|
1774
|
-
var CallRecapSchema =
|
|
1775
|
-
call:
|
|
1776
|
-
status:
|
|
1777
|
-
startedAt:
|
|
1778
|
-
durationMs:
|
|
1779
|
-
agents:
|
|
1876
|
+
var CallRecapSchema = z9.object({
|
|
1877
|
+
call: z9.object({
|
|
1878
|
+
status: z9.string(),
|
|
1879
|
+
startedAt: z9.string(),
|
|
1880
|
+
durationMs: z9.number().nullable(),
|
|
1881
|
+
agents: z9.array(z9.object({ id: z9.string(), name: z9.string().nullable() }))
|
|
1780
1882
|
}),
|
|
1781
|
-
topics:
|
|
1782
|
-
goalId:
|
|
1783
|
-
title:
|
|
1784
|
-
owner:
|
|
1785
|
-
state:
|
|
1786
|
-
questions:
|
|
1883
|
+
topics: z9.array(z9.object({
|
|
1884
|
+
goalId: z9.string().uuid(),
|
|
1885
|
+
title: z9.string(),
|
|
1886
|
+
owner: z9.string(),
|
|
1887
|
+
state: z9.string(),
|
|
1888
|
+
questions: z9.array(z9.object({ id: z9.string().uuid(), state: z9.string(), title: z9.string() })),
|
|
1787
1889
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
1788
1890
|
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
1789
1891
|
* gave, so the row scans like a chosen option and their own words stay under it. Absent when
|
|
1790
1892
|
* neither wrote one. */
|
|
1791
1893
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
1792
1894
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
1793
|
-
|
|
1895
|
+
/** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
|
|
1896
|
+
* however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
|
|
1897
|
+
* said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
|
|
1898
|
+
lines: z9.array(z9.object({
|
|
1899
|
+
entryId: z9.string().uuid(),
|
|
1900
|
+
words: z9.string(),
|
|
1901
|
+
summary: z9.string().optional(),
|
|
1902
|
+
about: z9.string().nullable().optional(),
|
|
1903
|
+
reply: z9.string().nullable().optional(),
|
|
1904
|
+
replyKind: z9.string().nullable().optional(),
|
|
1905
|
+
at: z9.array(z9.number()).optional()
|
|
1906
|
+
}))
|
|
1794
1907
|
})),
|
|
1795
|
-
unfiled:
|
|
1796
|
-
more:
|
|
1908
|
+
unfiled: z9.array(z9.object({ lineId: z9.string().uuid(), words: z9.string(), atMs: z9.number() })),
|
|
1909
|
+
more: z9.object({ lines: z9.number(), entries: z9.number(), topics: z9.number() })
|
|
1797
1910
|
});
|
|
1798
1911
|
|
|
1799
1912
|
// src/listening.ts
|