@paigy/mcp 0.40.25 → 0.40.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -18
- package/dist/{chunk-EJYXBEWN.js → chunk-4C6SDQ7A.js} +887 -661
- package/dist/{chunk-UAVXYNLV.js → chunk-A5FYZK6P.js} +18 -36
- package/dist/{chunk-2ROD77AW.js → chunk-UQDA7X3W.js} +3 -3
- package/dist/{chunk-GHSB2VP6.js → chunk-X3GTDW5G.js} +728 -539
- package/dist/{dist-NMHU2UNG.js → dist-N6ZJUJV7.js} +5 -7
- package/dist/enable.js +5 -5
- package/dist/index.js +57 -54
- package/dist/listen.js +8 -7
- package/dist/onboard.js +6 -6
- package/dist/slot.js +1 -1
- package/dist/stalled.js +2 -2
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -5,9 +5,9 @@ import { createHash, randomUUID } from "crypto";
|
|
|
5
5
|
import { readFileSync } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { join } from "path";
|
|
8
|
-
import { randomUUID as
|
|
8
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9
9
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
10
|
-
import { z as
|
|
10
|
+
import { z as z7 } from "zod";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { z as z3 } from "zod";
|
|
13
13
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind3 } from "zod/v3";
|
|
@@ -15,13 +15,14 @@ import { ZodFirstPartyTypeKind } from "zod/v3";
|
|
|
15
15
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind2 } from "zod/v3";
|
|
16
16
|
import { z as z2 } from "zod";
|
|
17
17
|
import { z as z4 } from "zod";
|
|
18
|
-
import {
|
|
18
|
+
import { z as z5 } from "zod";
|
|
19
|
+
import { z as z6 } from "zod";
|
|
19
20
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
20
21
|
import { homedir as homedir2 } from "os";
|
|
21
22
|
import { join as join2 } from "path";
|
|
22
|
-
import {
|
|
23
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
23
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
24
24
|
import { WebSocket } from "undici";
|
|
25
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
25
26
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
26
27
|
var CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
|
|
27
28
|
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
@@ -1320,14 +1321,20 @@ var OptionSchema = z.object({
|
|
|
1320
1321
|
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
1321
1322
|
// the constraints below are what an agent reads when deciding to use these.
|
|
1322
1323
|
html: z.string().max(16384).describe(
|
|
1323
|
-
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use
|
|
1324
|
+
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use the image URL instead."
|
|
1324
1325
|
).optional(),
|
|
1325
1326
|
image: z.string().url().describe(
|
|
1326
|
-
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use
|
|
1327
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use the HTML preview instead."
|
|
1327
1328
|
).optional()
|
|
1328
1329
|
});
|
|
1329
|
-
var OptionInputSchema =
|
|
1330
|
-
|
|
1330
|
+
var OptionInputSchema = z.object({
|
|
1331
|
+
id: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
|
|
1332
|
+
`Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
|
|
1333
|
+
),
|
|
1334
|
+
label: z.string().trim().min(1).max(1e3),
|
|
1335
|
+
hint: OptionSchema.shape.hint,
|
|
1336
|
+
htmlPreview: OptionSchema.shape.html,
|
|
1337
|
+
imgUrl: OptionSchema.shape.image
|
|
1331
1338
|
}).strict();
|
|
1332
1339
|
function tzOffsetMinutes(tz, atMs) {
|
|
1333
1340
|
try {
|
|
@@ -1527,41 +1534,162 @@ function mcpInputSchema(s) {
|
|
|
1527
1534
|
delete schema.$schema;
|
|
1528
1535
|
return draft2020(schema);
|
|
1529
1536
|
}
|
|
1530
|
-
var
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1537
|
+
var ASK_MAX = 250;
|
|
1538
|
+
var UNITS_MAX = 12;
|
|
1539
|
+
var UNIT_TITLE_MAX = 50;
|
|
1540
|
+
var UNIT_BODY_MAX = 200;
|
|
1541
|
+
var UnitInputSchema = z2.object({
|
|
1542
|
+
title: z2.string().trim().min(1).describe(`What this unit is about, at most ${UNIT_TITLE_MAX} characters. The app lists the titles; the person taps one to read its body.`),
|
|
1543
|
+
body: z2.string().trim().min(1).describe(`The unit itself, at most ${UNIT_BODY_MAX} characters.`)
|
|
1544
|
+
}).strict();
|
|
1545
|
+
function capRefusals(text, units2, field) {
|
|
1546
|
+
const out = [];
|
|
1547
|
+
const name = field === "text" ? "answer" : field;
|
|
1548
|
+
if (text.length > ASK_MAX) out.push({
|
|
1549
|
+
refusal: "ask_too_long",
|
|
1550
|
+
length: text.length,
|
|
1551
|
+
cap: ASK_MAX,
|
|
1552
|
+
path: field === "text" ? ["answer", "text"] : [field],
|
|
1553
|
+
message: `ask_too_long: the ${name} is ${text.length} characters; the cap is ${ASK_MAX}. Keep the one point in the ${name} and move the context into units.`
|
|
1554
|
+
});
|
|
1555
|
+
const all = units2 ?? [];
|
|
1556
|
+
if (all.length > UNITS_MAX) out.push({
|
|
1557
|
+
refusal: "too_many_units",
|
|
1558
|
+
length: all.length,
|
|
1559
|
+
cap: UNITS_MAX,
|
|
1560
|
+
path: ["units"],
|
|
1561
|
+
message: `too_many_units: ${all.length} units; the cap is ${UNITS_MAX}. Keep only the context this ${name} needs.`
|
|
1562
|
+
});
|
|
1563
|
+
all.forEach((unit, index) => {
|
|
1564
|
+
if (unit.title.length > UNIT_TITLE_MAX) out.push({
|
|
1565
|
+
refusal: "unit_title_too_long",
|
|
1566
|
+
index,
|
|
1567
|
+
length: unit.title.length,
|
|
1568
|
+
cap: UNIT_TITLE_MAX,
|
|
1569
|
+
path: ["units", index, "title"],
|
|
1570
|
+
message: `unit_title_too_long: units[${index}]'s title is ${unit.title.length} characters; the cap is ${UNIT_TITLE_MAX}.`
|
|
1571
|
+
});
|
|
1572
|
+
if (unit.body.length > UNIT_BODY_MAX) out.push({
|
|
1573
|
+
refusal: "unit_body_too_long",
|
|
1574
|
+
index,
|
|
1575
|
+
length: unit.body.length,
|
|
1576
|
+
cap: UNIT_BODY_MAX,
|
|
1577
|
+
path: ["units", index, "body"],
|
|
1578
|
+
message: `unit_body_too_long: units[${index}]'s body is ${unit.body.length} characters; the cap is ${UNIT_BODY_MAX}. Cut it into two units.`
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
return out;
|
|
1582
|
+
}
|
|
1583
|
+
function refuseOverCaps(text, units2, field, ctx) {
|
|
1584
|
+
for (const { path, message, ...params } of capRefusals(text, units2, field)) ctx.addIssue({ code: z2.ZodIssueCode.custom, path, message, params });
|
|
1585
|
+
}
|
|
1586
|
+
var questionHandle = z2.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
|
|
1587
|
+
var NodeRef = z2.object({
|
|
1588
|
+
type: z2.enum(["goal", "question"]),
|
|
1589
|
+
id: z2.string().uuid()
|
|
1590
|
+
}).strict().describe("A Goal or a Question, by its id.");
|
|
1591
|
+
var BlockedAction = z2.enum(["start", "complete", "answer"]).describe("What waits: starting or completing a Goal, or answering a Question.");
|
|
1592
|
+
var reason = z2.string().trim().min(1).max(500).describe("Why, as an explanation, never as policy.");
|
|
1593
|
+
var actionFits = (b) => b.blocked.type === "goal" === (b.action !== "answer");
|
|
1594
|
+
var ACTION_FITS = "a goal is blocked from start or complete; a question from answer";
|
|
1595
|
+
var userExplicitlyRequested = z2.enum(["any", "call"]).optional().describe(
|
|
1596
|
+
`Only what the person said about THIS message, in so many words: "any" when they asked you to send it to them ("send it to my phone", "message me the result"), "call" when they asked to be called about it. It is then delivered (a call rings for the whole contact, and carries everything waiting on them); omitted, an update is only recorded as the Goal's progress unless the person needs it. Never set it to make a message louder.`
|
|
1597
|
+
);
|
|
1598
|
+
var units = z2.array(UnitInputSchema).optional().describe(
|
|
1599
|
+
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all.`
|
|
1600
|
+
);
|
|
1601
|
+
var UpdateInputSchema = z2.object({
|
|
1602
|
+
message: z2.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
1603
|
+
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."),
|
|
1604
|
+
units,
|
|
1605
|
+
userExplicitlyRequested
|
|
1606
|
+
}).strict().superRefine((u, ctx) => refuseOverCaps(u.message, u.units, "message", ctx));
|
|
1607
|
+
var questionFields = {
|
|
1608
|
+
question: z2.string().trim().min(1).describe(
|
|
1609
|
+
`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.`
|
|
1536
1610
|
),
|
|
1537
|
-
|
|
1611
|
+
goalId: z2.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
|
|
1612
|
+
units,
|
|
1613
|
+
options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
|
|
1538
1614
|
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
1539
|
-
// brain_prompts.md §1: structured agent questions need no model).
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
select: z2.enum(["one", "many"]).optional().describe(
|
|
1543
|
-
'How the options are answered: "many" makes a checklist (they may want several, e.g. independent fixes), "one" (the default) a pick (choosing one rules out the others). Send "many" whenever more than one option could be wanted at once. The person can always answer in their own words as well. Ignored without options.'
|
|
1615
|
+
// brain_prompts.md §1: structured agent questions need no model). The app draws all three.
|
|
1616
|
+
pickMode: z2.enum(["one", "many", "rank"]).optional().describe(
|
|
1617
|
+
'How the options are answered: "one" (the default) a pick, for alternatives; "many" a checklist, for independent options they may want several of; "rank" an order of all of them. Only with options.'
|
|
1544
1618
|
),
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
)
|
|
1548
|
-
|
|
1549
|
-
var StartContactSchema = z2.object({
|
|
1550
|
-
asks: z2.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; an ask naming no Goal starts a new Goal of yours."),
|
|
1551
|
-
waiting: z2.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
1552
|
-
channel: z2.enum(["notification", "call"]).default("notification")
|
|
1553
|
-
}).strict();
|
|
1554
|
-
var ReceiveContactSchema = z2.object({
|
|
1555
|
-
wait: z2.boolean().optional().describe(
|
|
1556
|
-
"Only controls when this returns. true (the default when you send and acknowledge nothing): wait up to about 45 seconds for incoming communication addressed to you, and return as soon as some is available. false: return what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived in the window; wait again without sending again), or `not_waited` (a hosted connection never holds a wait)."
|
|
1619
|
+
// WHAT THE QUESTION HOLDS UP (brain_prompts.md §2.1): replaces `waiting: 'hard'`. A question that
|
|
1620
|
+
// blocks work asks for a call (the person's settings and ring cooldowns still decide).
|
|
1621
|
+
workItBlocks: z2.array(z2.object({ blocked: NodeRef, action: BlockedAction, reason: reason.optional() }).strict().refine(actionFits, ACTION_FITS)).min(1).max(10).optional().describe(
|
|
1622
|
+
"The work that cannot go on until this is answered: a Goal's start or completion, or another Question's answer. Only for work truly blocked; it asks for a call. Leave it out for a question you can keep working around."
|
|
1557
1623
|
),
|
|
1624
|
+
userExplicitlyRequested
|
|
1625
|
+
};
|
|
1626
|
+
var refineQuestion = (q, ctx) => {
|
|
1627
|
+
refuseOverCaps(q.question, q.units, "question", ctx);
|
|
1628
|
+
if (q.pickMode && !q.options) ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["pickMode"], message: "pickMode comes only with options" });
|
|
1629
|
+
const ids = (q.options ?? []).map((o) => o.id);
|
|
1630
|
+
if (ids.some((id2) => id2) && (ids.some((id2) => !id2) || new Set(ids).size !== ids.length)) {
|
|
1631
|
+
ctx.addIssue({ code: z2.ZodIssueCode.custom, path: ["options"], message: "give every option a distinct id, or none" });
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
var QuestionInputSchema = z2.object({
|
|
1635
|
+
questionId: z2.string().uuid().optional().describe("Optional: your own stable id for this question (a UUID), so a resend is the same question. Minted for you when omitted; the result returns it."),
|
|
1636
|
+
...questionFields
|
|
1637
|
+
}).strict().superRefine(refineQuestion);
|
|
1638
|
+
var QuestionWireSchema = z2.object({ questionId: z2.string().uuid(), ...questionFields }).strict().superRefine(refineQuestion);
|
|
1639
|
+
var AnswerInputSchema = z2.object({
|
|
1640
|
+
questionId: questionHandle.describe("The Question you answer: its id as the event or the conversation shows it (8 characters or whole)."),
|
|
1641
|
+
answer: z2.object({
|
|
1642
|
+
text: z2.string().trim().min(1).describe(`Your answer in your own words, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
1643
|
+
selectedOptionIds: z2.array(z2.string().trim().min(1)).min(1).max(6).optional().describe("The ids of the options you choose, when the Question offered options (in order, for a ranking).")
|
|
1644
|
+
}).strict(),
|
|
1645
|
+
units
|
|
1646
|
+
}).strict().superRefine((a, ctx) => refuseOverCaps(a.answer.text, a.units, "text", ctx));
|
|
1647
|
+
var QuestionDependencySchema = z2.object({
|
|
1648
|
+
questionId: questionHandle.describe("A Question you asked."),
|
|
1649
|
+
change: z2.enum(["add", "remove"]),
|
|
1650
|
+
blocked: NodeRef,
|
|
1651
|
+
action: BlockedAction,
|
|
1652
|
+
reason: reason.optional()
|
|
1653
|
+
}).strict().refine(actionFits, ACTION_FITS);
|
|
1654
|
+
var sendFields = {
|
|
1655
|
+
updates: z2.array(UpdateInputSchema).min(1).max(20).optional().describe(
|
|
1656
|
+
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress and nobody is notified."
|
|
1657
|
+
),
|
|
1658
|
+
answers: z2.array(AnswerInputSchema).min(1).max(20).optional().describe("Answers to Questions addressed to you."),
|
|
1659
|
+
withdrawQuestionIds: z2.array(questionHandle).min(1).max(10).optional().describe(
|
|
1660
|
+
"Questions you asked that no longer need an answer: you acted on them yourself, they no longer matter, or you asked wrongly. Each is cancelled, not answered: its card closes and nothing rings for it."
|
|
1661
|
+
),
|
|
1662
|
+
questionDependencies: z2.array(QuestionDependencySchema).min(1).max(20).optional().describe(
|
|
1663
|
+
"Add or remove what a Question you asked holds up, after asking it."
|
|
1664
|
+
)
|
|
1665
|
+
};
|
|
1666
|
+
var ContactSchema = z2.object({
|
|
1667
|
+
...sendFields,
|
|
1668
|
+
questions: z2.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object."),
|
|
1669
|
+
/** RECEIVING (2I, I2; brain_prompts.md §2.2): the events this agent handled. */
|
|
1558
1670
|
ackEventIds: z2.array(z2.string().uuid()).min(1).max(100).optional().describe(
|
|
1559
|
-
"The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (
|
|
1671
|
+
"The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (answers), never acknowledged away."
|
|
1672
|
+
),
|
|
1673
|
+
wait: z2.boolean().optional().describe(
|
|
1674
|
+
"Only controls when this returns. Sending: false (the default) returns at once; true waits up to about 45 seconds for a response to what you sent. Sending nothing: true (the default) waits up to about 45 seconds for anything addressed to you; false returns what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived; wait again without sending again), or `not_waited`."
|
|
1560
1675
|
)
|
|
1561
1676
|
}).strict();
|
|
1562
|
-
var
|
|
1677
|
+
var sendsAnything = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
|
|
1678
|
+
var ContactWireSchema = z2.object({
|
|
1679
|
+
operationId: z2.string().uuid(),
|
|
1680
|
+
...sendFields,
|
|
1681
|
+
questions: z2.array(QuestionWireSchema).min(1).max(20).optional()
|
|
1682
|
+
}).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
1563
1683
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
1564
|
-
var CONTACT_DESCRIPTION =
|
|
1684
|
+
var CONTACT_DESCRIPTION = `Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read \`results\`, where a failed item names why while the rest still apply.
|
|
1685
|
+
|
|
1686
|
+
QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
|
|
1687
|
+
|
|
1688
|
+
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.
|
|
1689
|
+
|
|
1690
|
+
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.
|
|
1691
|
+
|
|
1692
|
+
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\`).`;
|
|
1565
1693
|
var CreateGoalSchema = z3.object({
|
|
1566
1694
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1567
1695
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1595,21 +1723,11 @@ var UpdateGoalSchema = z3.object({
|
|
|
1595
1723
|
state: z3.enum(["active", "done", "cancelled"]).optional(),
|
|
1596
1724
|
progress: z3.string().trim().min(1).max(1e4).optional(),
|
|
1597
1725
|
reviewed: z3.literal(true).optional(),
|
|
1598
|
-
dueAt: z3.string().datetime({ offset: true }).nullable().optional()
|
|
1599
|
-
/** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
|
|
1600
|
-
* (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
|
|
1601
|
-
* by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
|
|
1602
|
-
withdraw: z3.array(z3.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
|
|
1726
|
+
dueAt: z3.string().datetime({ offset: true }).nullable().optional()
|
|
1603
1727
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
1604
1728
|
reason: z3.string().trim().min(1).max(2e3),
|
|
1605
1729
|
operationId: z3.string().uuid().optional()
|
|
1606
1730
|
}).strict();
|
|
1607
|
-
var UpdateGoalToolSchema = z3.object({
|
|
1608
|
-
goalId: z3.string().uuid(),
|
|
1609
|
-
revision: UpdateGoalSchema.shape.revision,
|
|
1610
|
-
changes: UpdateGoalSchema.shape.changes.innerType().pick({ progress: true, reviewed: true, dueAt: true, withdraw: true }).strict().refine((v) => Object.keys(v).length > 0),
|
|
1611
|
-
reason: UpdateGoalSchema.shape.reason
|
|
1612
|
-
}).strict();
|
|
1613
1731
|
var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
1614
1732
|
var goalTitle = z3.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
|
|
1615
1733
|
var goalOutcome = z3.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
|
|
@@ -1617,7 +1735,7 @@ var CreateChange = z3.object({
|
|
|
1617
1735
|
kind: z3.literal("create"),
|
|
1618
1736
|
title: goalTitle,
|
|
1619
1737
|
outcome: goalOutcome,
|
|
1620
|
-
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as
|
|
1738
|
+
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as check_activity shows it) or the person's. Omitted: you."),
|
|
1621
1739
|
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
1622
1740
|
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
1623
1741
|
}).strict();
|
|
@@ -1627,7 +1745,7 @@ var StateChange = z3.object({ kind: z3.literal("state"), goalId: changedGoal, st
|
|
|
1627
1745
|
) }).strict();
|
|
1628
1746
|
var AssignChange = z3.object({ kind: z3.literal("assign"), goalId: changedGoal, ownerId: z3.string().trim().min(1).describe("The new owner's participant.") }).strict();
|
|
1629
1747
|
var DeferChange = z3.object({ kind: z3.literal("defer"), goalId: changedGoal, until: z3.string().datetime({ offset: true }).nullable().describe(
|
|
1630
|
-
"Postpone it until this instant
|
|
1748
|
+
"Postpone it until this instant: it waits until then, and its owner is woken when it passes (a promise to follow up later). null takes the postponement back."
|
|
1631
1749
|
) }).strict();
|
|
1632
1750
|
var MoveChange = z3.object({ kind: z3.literal("move"), goalId: changedGoal, parentGoalId: z3.string().uuid().nullable().describe(
|
|
1633
1751
|
"Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
|
|
@@ -1662,8 +1780,7 @@ var ManageGoalsSchema = z3.object({
|
|
|
1662
1780
|
var ManageGoalsToolSchema = z3.object({
|
|
1663
1781
|
changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
1664
1782
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
1665
|
-
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open,
|
|
1666
|
-
var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
|
|
1783
|
+
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
1667
1784
|
var GetGoalSchema = z3.object({
|
|
1668
1785
|
goalId: z3.string().uuid(),
|
|
1669
1786
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
@@ -1674,9 +1791,7 @@ var GetGoalSchema = z3.object({
|
|
|
1674
1791
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
1675
1792
|
diagnose: z3.boolean().optional()
|
|
1676
1793
|
}).strict();
|
|
1677
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal
|
|
1678
|
-
var UPDATE_GOAL_DESCRIPTION = "Report work on a Goal you are on, at an exact revision: progress, review acknowledgement, when to wake for it, and withdrawing a question of yours. What the Goal is (its title, outcome, state, owner, parent, dependencies) changes with manage_goals. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). Stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. progress says where the work stands. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision (contact({ackEventIds}) does the same for an `update` event). dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. Returns the Goal as get_goal reads it, at its new revision.";
|
|
1679
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
1794
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
1680
1795
|
var SearchToolSchema = z3.object({
|
|
1681
1796
|
query: z3.string().trim().min(1).max(500),
|
|
1682
1797
|
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
@@ -1684,15 +1799,21 @@ var SearchToolSchema = z3.object({
|
|
|
1684
1799
|
limit: z3.number().int().min(1).max(20).optional()
|
|
1685
1800
|
}).strict();
|
|
1686
1801
|
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and Answers (found by their Question or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, its Questions' Answers, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (an Answer carries its Question, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
1687
|
-
var
|
|
1802
|
+
var CheckActivitySchema = z3.object({}).strict();
|
|
1803
|
+
var FEEDBACK_TEXT_MAX = 5e4;
|
|
1804
|
+
var SendFeedbackSchema = z3.object({
|
|
1805
|
+
text: z3.string().trim().min(1).max(FEEDBACK_TEXT_MAX).describe(`The report, in markdown: what happened, what was expected, how to reproduce it, versions, evidence. At most ${FEEDBACK_TEXT_MAX} characters.`),
|
|
1806
|
+
title: z3.string().trim().min(1).max(200).optional().describe("One line naming it."),
|
|
1807
|
+
kind: z3.enum(["bug", "idea", "other"]).default("other").describe("bug: something Paigy does wrong. idea: something it should do. other: anything else.")
|
|
1808
|
+
}).strict();
|
|
1809
|
+
var SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
|
|
1688
1810
|
var AGENT_TOOLS = [
|
|
1689
|
-
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(WhoIsWorkingSchema) },
|
|
1690
1811
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
1691
1812
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
1692
|
-
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
1693
1813
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
1694
|
-
{ name: "
|
|
1695
|
-
{ name: "
|
|
1814
|
+
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
1815
|
+
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
1816
|
+
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
1696
1817
|
];
|
|
1697
1818
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
1698
1819
|
var id = z4.string().uuid();
|
|
@@ -1831,6 +1952,66 @@ var CompactResultSchema = z4.object({
|
|
|
1831
1952
|
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
1832
1953
|
zodToJsonSchema(CompactResultSchema, { $refStrategy: "none" })
|
|
1833
1954
|
);
|
|
1955
|
+
var str2 = z5.string();
|
|
1956
|
+
var strs2 = z5.array(z5.string());
|
|
1957
|
+
var TalkerReplySchema = z5.object({
|
|
1958
|
+
/** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
|
|
1959
|
+
* or an agent's handle; `about` names handles; `blocks` is the item a question to an agent must be
|
|
1960
|
+
* answered before ("" for none). */
|
|
1961
|
+
messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
|
|
1962
|
+
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
1963
|
+
then: z5.enum(["listen", "hold", "end"]),
|
|
1964
|
+
/** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
|
|
1965
|
+
answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2 }).strict()),
|
|
1966
|
+
/** The line handles that hold a new instruction from the person. */
|
|
1967
|
+
instruction: strs2,
|
|
1968
|
+
/** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
|
|
1969
|
+
* reads only these (3e). */
|
|
1970
|
+
file: strs2,
|
|
1971
|
+
/** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
|
|
1972
|
+
need: z5.object({ kind: z5.enum(["search", "item", "agent", ""]), text: str2 }).strict(),
|
|
1973
|
+
/** The handle of the item to explain ("" for none). */
|
|
1974
|
+
explain: str2
|
|
1975
|
+
}).strict();
|
|
1976
|
+
var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
1977
|
+
zodToJsonSchema(TalkerReplySchema, { $refStrategy: "none" })
|
|
1978
|
+
);
|
|
1979
|
+
var str3 = z6.string();
|
|
1980
|
+
var strs3 = z6.array(z6.string());
|
|
1981
|
+
var FilerReplySchema = z6.object({
|
|
1982
|
+
/** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
|
|
1983
|
+
filed: z6.array(z6.object({ line: str3, goals: strs3 }).strict()),
|
|
1984
|
+
goals: z6.array(z6.object({
|
|
1985
|
+
op: z6.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
1986
|
+
key: str3,
|
|
1987
|
+
goal: str3,
|
|
1988
|
+
title: str3,
|
|
1989
|
+
outcome: str3,
|
|
1990
|
+
owner: str3,
|
|
1991
|
+
under: str3,
|
|
1992
|
+
until: str3,
|
|
1993
|
+
on: str3,
|
|
1994
|
+
gate: z6.enum(["start", "complete", "answer", ""]),
|
|
1995
|
+
why: str3
|
|
1996
|
+
}).strict()),
|
|
1997
|
+
questions: z6.array(z6.object({
|
|
1998
|
+
op: z6.enum(["create", "edit", "assign", "withdraw"]),
|
|
1999
|
+
key: str3,
|
|
2000
|
+
question: str3,
|
|
2001
|
+
text: str3,
|
|
2002
|
+
to: str3,
|
|
2003
|
+
goal: str3,
|
|
2004
|
+
options: z6.array(z6.object({ id: str3, label: str3 }).strict()),
|
|
2005
|
+
pick: z6.enum(["one", "many", "rank", "words", ""]),
|
|
2006
|
+
blocks: str3
|
|
2007
|
+
}).strict()),
|
|
2008
|
+
lessons: z6.array(z6.object({ op: z6.enum(["remember", "revise", "withdraw"]), lesson: str3, text: str3, scope: str3 }).strict()),
|
|
2009
|
+
/** Line handles whose words comment on Paigy itself. */
|
|
2010
|
+
feedback: strs3
|
|
2011
|
+
}).strict();
|
|
2012
|
+
var FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
2013
|
+
zodToJsonSchema(FilerReplySchema, { $refStrategy: "none" })
|
|
2014
|
+
);
|
|
1834
2015
|
function entryWords(entry) {
|
|
1835
2016
|
const content = entry.content;
|
|
1836
2017
|
if (content && "sealed" in content) return "";
|
|
@@ -1848,19 +2029,36 @@ function entryWords(entry) {
|
|
|
1848
2029
|
}
|
|
1849
2030
|
return entry.sources.map((source) => source.text).join("\n");
|
|
1850
2031
|
}
|
|
2032
|
+
function entryUnit(entry) {
|
|
2033
|
+
if (entry.kind !== "contribution" || !entry.aboutId) return null;
|
|
2034
|
+
const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
|
|
2035
|
+
if (!plain || typeof plain !== "object") return null;
|
|
2036
|
+
const { title, text } = plain;
|
|
2037
|
+
return typeof title === "string" && typeof text === "string" ? { title, body: text } : null;
|
|
2038
|
+
}
|
|
2039
|
+
function askUnits(request, entries) {
|
|
2040
|
+
return entries.flatMap((entry) => {
|
|
2041
|
+
if (entry.aboutId !== request.entryId || entry.authorParticipant !== request.authorParticipant) return [];
|
|
2042
|
+
const unit = entryUnit(entry);
|
|
2043
|
+
return unit ? [unit] : [];
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
1851
2046
|
var LIVE_MS = 3 * 6e4;
|
|
1852
2047
|
var WORKING_MS = 60 * 6e4;
|
|
1853
|
-
var ContextSchema =
|
|
1854
|
-
title:
|
|
1855
|
-
description:
|
|
2048
|
+
var ContextSchema = z7.object({
|
|
2049
|
+
title: z7.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
2050
|
+
description: z7.array(z7.string().min(1)).describe(
|
|
1856
2051
|
"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."
|
|
1857
|
-
)
|
|
2052
|
+
),
|
|
2053
|
+
/** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
|
|
2054
|
+
* a body on a tap. Absent on an ask sent without units, which renders as before. */
|
|
2055
|
+
units: z7.array(z7.object({ title: z7.string(), body: z7.string() })).optional()
|
|
1858
2056
|
});
|
|
1859
|
-
var ParticipantSchema =
|
|
1860
|
-
kind:
|
|
1861
|
-
id:
|
|
2057
|
+
var ParticipantSchema = z7.object({
|
|
2058
|
+
kind: z7.enum(["human", "agent"]),
|
|
2059
|
+
id: z7.string()
|
|
1862
2060
|
});
|
|
1863
|
-
var TransformSchema =
|
|
2061
|
+
var TransformSchema = z7.enum([
|
|
1864
2062
|
"structure",
|
|
1865
2063
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
1866
2064
|
"request_more",
|
|
@@ -1883,13 +2081,13 @@ var PAIGY_SELF = { kind: "agent", id: "paigy" };
|
|
|
1883
2081
|
function isPaigy(ref) {
|
|
1884
2082
|
return ref === participantRef(PAIGY_SELF);
|
|
1885
2083
|
}
|
|
1886
|
-
var VisualSchema =
|
|
1887
|
-
url:
|
|
1888
|
-
label:
|
|
2084
|
+
var VisualSchema = z7.object({
|
|
2085
|
+
url: z7.string().url(),
|
|
2086
|
+
label: z7.string().optional()
|
|
1889
2087
|
});
|
|
1890
|
-
var NotifyLevelSchema =
|
|
1891
|
-
var SelectShapeSchema =
|
|
1892
|
-
var ReceiptEventSchema =
|
|
2088
|
+
var NotifyLevelSchema = z7.enum(["inbox", "push", "banner", "call"]);
|
|
2089
|
+
var SelectShapeSchema = z7.enum(["one", "many", "rank", "confirm", "text"]);
|
|
2090
|
+
var ReceiptEventSchema = z7.enum([
|
|
1893
2091
|
"delivered",
|
|
1894
2092
|
// the bundle reached the recipient at some level
|
|
1895
2093
|
"seen",
|
|
@@ -1919,47 +2117,47 @@ var ReceiptEventSchema = z5.enum([
|
|
|
1919
2117
|
// be rewound by a writer that forgot to advance it.
|
|
1920
2118
|
"restarted"
|
|
1921
2119
|
]);
|
|
1922
|
-
var AttentionSchema =
|
|
2120
|
+
var AttentionSchema = z7.object({
|
|
1923
2121
|
urgency: NotifyLevelSchema,
|
|
1924
2122
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
1925
2123
|
select: SelectShapeSchema.nullable(),
|
|
1926
2124
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
1927
|
-
points:
|
|
2125
|
+
points: z7.array(z7.string()).nullable(),
|
|
1928
2126
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
1929
|
-
blocking:
|
|
2127
|
+
blocking: z7.boolean(),
|
|
1930
2128
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
1931
2129
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
1932
|
-
deadline:
|
|
2130
|
+
deadline: z7.string().datetime().nullable().optional()
|
|
1933
2131
|
});
|
|
1934
|
-
var NotifyRequestFields =
|
|
2132
|
+
var NotifyRequestFields = z7.object({
|
|
1935
2133
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
1936
2134
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
1937
2135
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
1938
2136
|
context: ContextSchema.optional(),
|
|
1939
|
-
options:
|
|
2137
|
+
options: z7.array(OptionSchema.omit({ id: true }).extend({ label: z7.string().trim().min(1).max(1e3) }).strict()).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
1940
2138
|
"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)."
|
|
1941
2139
|
),
|
|
1942
|
-
points:
|
|
2140
|
+
points: z7.array(z7.string().min(1)).optional().describe(
|
|
1943
2141
|
"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."
|
|
1944
2142
|
),
|
|
1945
|
-
visuals:
|
|
2143
|
+
visuals: z7.array(VisualSchema).optional().describe(
|
|
1946
2144
|
"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."
|
|
1947
2145
|
),
|
|
1948
2146
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
1949
|
-
repo:
|
|
2147
|
+
repo: z7.string().optional(),
|
|
1950
2148
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
1951
|
-
branch:
|
|
2149
|
+
branch: z7.string().optional(),
|
|
1952
2150
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
1953
2151
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
1954
2152
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
1955
2153
|
* still accepts the old name from older clients. */
|
|
1956
|
-
parentId:
|
|
2154
|
+
parentId: z7.string().uuid().optional(),
|
|
1957
2155
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
1958
2156
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
1959
|
-
workId:
|
|
2157
|
+
workId: z7.string().uuid().optional(),
|
|
1960
2158
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
1961
2159
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
1962
|
-
goalId:
|
|
2160
|
+
goalId: z7.string().uuid().optional(),
|
|
1963
2161
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
1964
2162
|
"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."
|
|
1965
2163
|
),
|
|
@@ -1967,7 +2165,7 @@ var NotifyRequestFields = z5.object({
|
|
|
1967
2165
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
1968
2166
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
1969
2167
|
* says HOW. */
|
|
1970
|
-
clarifies:
|
|
2168
|
+
clarifies: z7.string().optional(),
|
|
1971
2169
|
select: SelectShapeSchema.optional().describe(
|
|
1972
2170
|
"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."
|
|
1973
2171
|
),
|
|
@@ -1981,20 +2179,20 @@ var NotifyRequestFields = z5.object({
|
|
|
1981
2179
|
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
1982
2180
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
1983
2181
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
1984
|
-
ask:
|
|
2182
|
+
ask: z7.string().min(1).max(1e4).optional().describe(
|
|
1985
2183
|
'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.'
|
|
1986
2184
|
),
|
|
1987
|
-
needs:
|
|
2185
|
+
needs: z7.array(z7.string().min(1)).optional().describe(
|
|
1988
2186
|
"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."
|
|
1989
2187
|
),
|
|
1990
|
-
urgencyHint:
|
|
2188
|
+
urgencyHint: z7.enum(["whenever", "soon", "now"]).optional().describe(
|
|
1991
2189
|
"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."
|
|
1992
2190
|
),
|
|
1993
2191
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
1994
2192
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
1995
2193
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
1996
2194
|
* urgencyHint/blocking win when both are sent. */
|
|
1997
|
-
waiting:
|
|
2195
|
+
waiting: z7.enum(["none", "soft", "hard"]).optional().describe(
|
|
1998
2196
|
"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."
|
|
1999
2197
|
),
|
|
2000
2198
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -2002,98 +2200,98 @@ var NotifyRequestFields = z5.object({
|
|
|
2002
2200
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
2003
2201
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
2004
2202
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
2005
|
-
confirm:
|
|
2203
|
+
confirm: z7.boolean().optional().describe(
|
|
2006
2204
|
"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'."
|
|
2007
2205
|
),
|
|
2008
2206
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
2009
2207
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
2010
2208
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
2011
2209
|
* waiting:'hard'. */
|
|
2012
|
-
channel:
|
|
2210
|
+
channel: z7.enum(["call", "message"]).optional().describe(
|
|
2013
2211
|
"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."
|
|
2014
2212
|
),
|
|
2015
|
-
confirmStyle:
|
|
2213
|
+
confirmStyle: z7.enum(["yesno", "approve"]).default("yesno").describe(
|
|
2016
2214
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
2017
2215
|
),
|
|
2018
|
-
blocking:
|
|
2216
|
+
blocking: z7.boolean().default(false).describe(
|
|
2019
2217
|
"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."
|
|
2020
2218
|
)
|
|
2021
2219
|
});
|
|
2022
2220
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
2023
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
2221
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
2024
2222
|
if (r.ask !== void 0) {
|
|
2025
2223
|
for (const f of ["context", "select", "points"]) {
|
|
2026
2224
|
if (r[f] !== void 0)
|
|
2027
|
-
ctx.addIssue({ code:
|
|
2225
|
+
ctx.addIssue({ code: z7.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.` });
|
|
2028
2226
|
}
|
|
2029
2227
|
return;
|
|
2030
2228
|
}
|
|
2031
2229
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
2032
|
-
ctx.addIssue({ code:
|
|
2230
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
2033
2231
|
if (!r.context)
|
|
2034
|
-
ctx.addIssue({ code:
|
|
2232
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
2035
2233
|
if (!r.select)
|
|
2036
|
-
ctx.addIssue({ code:
|
|
2234
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
2037
2235
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
2038
2236
|
if (needsOptions && !r.options?.length)
|
|
2039
|
-
ctx.addIssue({ code:
|
|
2237
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
2040
2238
|
if (!needsOptions && r.options?.length)
|
|
2041
|
-
ctx.addIssue({ code:
|
|
2239
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
2042
2240
|
});
|
|
2043
|
-
var NotifyStatusSchema =
|
|
2044
|
-
var AgentStateSchema =
|
|
2045
|
-
var TurnSchema =
|
|
2046
|
-
prompt:
|
|
2047
|
-
reply:
|
|
2241
|
+
var NotifyStatusSchema = z7.enum(["pending", "answered", "ignored"]);
|
|
2242
|
+
var AgentStateSchema = z7.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
2243
|
+
var TurnSchema = z7.object({
|
|
2244
|
+
prompt: z7.string(),
|
|
2245
|
+
reply: z7.string()
|
|
2048
2246
|
});
|
|
2049
|
-
var UserAnswerSchema =
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2247
|
+
var UserAnswerSchema = z7.discriminatedUnion("kind", [
|
|
2248
|
+
z7.object({ kind: z7.literal("option"), optionId: z7.string(), label: z7.string().optional() }),
|
|
2249
|
+
z7.object({ kind: z7.literal("text"), text: z7.string() }),
|
|
2250
|
+
z7.object({ kind: z7.literal("ignored") }),
|
|
2251
|
+
z7.object({ kind: z7.literal("multi"), optionIds: z7.array(z7.string()), labels: z7.array(z7.string()).optional() }),
|
|
2252
|
+
z7.object({ kind: z7.literal("ranked"), optionIds: z7.array(z7.string()), labels: z7.array(z7.string()).optional() }),
|
|
2253
|
+
z7.object({ kind: z7.literal("clarify"), chunks: z7.array(z7.string()).min(1) }),
|
|
2254
|
+
z7.object({ kind: z7.literal("confirm"), approved: z7.boolean() }),
|
|
2255
|
+
z7.object({ kind: z7.literal("turns"), turns: z7.array(TurnSchema).min(1) }),
|
|
2058
2256
|
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
2059
2257
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
2060
2258
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
2061
2259
|
* card the user can reply to — the broker never overrides the user. */
|
|
2062
|
-
|
|
2260
|
+
z7.object({ kind: z7.literal("precedent"), answer: z7.string(), derivation: z7.string(), sources: z7.array(z7.string()).min(1) })
|
|
2063
2261
|
]);
|
|
2064
|
-
var IntentSchema =
|
|
2262
|
+
var IntentSchema = z7.object({
|
|
2065
2263
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
2066
2264
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
2067
2265
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
2068
2266
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
2069
|
-
kind:
|
|
2070
|
-
detail:
|
|
2267
|
+
kind: z7.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
2268
|
+
detail: z7.string(),
|
|
2071
2269
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
2072
2270
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
2073
2271
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
2074
2272
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
2075
|
-
dueInSeconds:
|
|
2273
|
+
dueInSeconds: z7.number().int().positive().optional(),
|
|
2076
2274
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
2077
2275
|
* already read the utterance, so `signals.kind` stops defaulting to
|
|
2078
2276
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
2079
2277
|
* about what cannot answer "is the bot looping less this week?". */
|
|
2080
|
-
fault:
|
|
2278
|
+
fault: z7.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
2081
2279
|
});
|
|
2082
|
-
var RideAlongSchema =
|
|
2280
|
+
var RideAlongSchema = z7.object({
|
|
2083
2281
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
2084
|
-
noteId:
|
|
2282
|
+
noteId: z7.string(),
|
|
2085
2283
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
2086
|
-
text:
|
|
2284
|
+
text: z7.string(),
|
|
2087
2285
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
2088
|
-
parentId:
|
|
2286
|
+
parentId: z7.string().nullable()
|
|
2089
2287
|
});
|
|
2090
|
-
var AwaitItemSchema =
|
|
2091
|
-
|
|
2092
|
-
type:
|
|
2093
|
-
parentId:
|
|
2094
|
-
notificationId:
|
|
2095
|
-
workId:
|
|
2096
|
-
decisionId:
|
|
2288
|
+
var AwaitItemSchema = z7.discriminatedUnion("type", [
|
|
2289
|
+
z7.object({
|
|
2290
|
+
type: z7.literal("reply"),
|
|
2291
|
+
parentId: z7.string(),
|
|
2292
|
+
notificationId: z7.string(),
|
|
2293
|
+
workId: z7.string().uuid().optional(),
|
|
2294
|
+
decisionId: z7.string().uuid().optional(),
|
|
2097
2295
|
answer: UserAnswerSchema,
|
|
2098
2296
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
2099
2297
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -2103,103 +2301,103 @@ var AwaitItemSchema = z5.discriminatedUnion("type", [
|
|
|
2103
2301
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
2104
2302
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
2105
2303
|
* a sentence with a hole in it is worse than no sentence. */
|
|
2106
|
-
note:
|
|
2304
|
+
note: z7.string().optional(),
|
|
2107
2305
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
2108
2306
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
2109
2307
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
2110
2308
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
2111
|
-
transcript:
|
|
2309
|
+
transcript: z7.string().optional(),
|
|
2112
2310
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
2113
2311
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
2114
|
-
covered:
|
|
2312
|
+
covered: z7.array(z7.string()).optional(),
|
|
2115
2313
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
2116
2314
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
2117
2315
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
2118
2316
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
2119
2317
|
* dropped. Absent/empty = nothing owed. */
|
|
2120
|
-
also:
|
|
2318
|
+
also: z7.array(RideAlongSchema).optional()
|
|
2121
2319
|
}),
|
|
2122
|
-
|
|
2123
|
-
type:
|
|
2124
|
-
parentId:
|
|
2125
|
-
notificationId:
|
|
2126
|
-
remindAt:
|
|
2320
|
+
z7.object({
|
|
2321
|
+
type: z7.literal("remind"),
|
|
2322
|
+
parentId: z7.string(),
|
|
2323
|
+
notificationId: z7.string(),
|
|
2324
|
+
remindAt: z7.string().datetime({ offset: true }),
|
|
2127
2325
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
2128
|
-
remindInSeconds:
|
|
2326
|
+
remindInSeconds: z7.number()
|
|
2129
2327
|
}),
|
|
2130
2328
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
2131
2329
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
2132
2330
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
2133
2331
|
* re-orient via contact({})). */
|
|
2134
|
-
|
|
2135
|
-
type:
|
|
2136
|
-
parentId:
|
|
2137
|
-
notificationId:
|
|
2332
|
+
z7.object({
|
|
2333
|
+
type: z7.literal("superseded"),
|
|
2334
|
+
parentId: z7.string(),
|
|
2335
|
+
notificationId: z7.string()
|
|
2138
2336
|
}),
|
|
2139
2337
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
2140
2338
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
2141
2339
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
2142
2340
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
2143
2341
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
2144
|
-
|
|
2145
|
-
type:
|
|
2146
|
-
notificationId:
|
|
2147
|
-
inFlight:
|
|
2148
|
-
turn:
|
|
2149
|
-
idx:
|
|
2150
|
-
prompt:
|
|
2151
|
-
reply:
|
|
2152
|
-
acts:
|
|
2342
|
+
z7.object({
|
|
2343
|
+
type: z7.literal("partial"),
|
|
2344
|
+
notificationId: z7.string(),
|
|
2345
|
+
inFlight: z7.literal(true),
|
|
2346
|
+
turn: z7.object({
|
|
2347
|
+
idx: z7.number(),
|
|
2348
|
+
prompt: z7.string(),
|
|
2349
|
+
reply: z7.string(),
|
|
2350
|
+
acts: z7.array(IntentSchema).nullable().optional()
|
|
2153
2351
|
})
|
|
2154
2352
|
}),
|
|
2155
|
-
|
|
2156
|
-
type:
|
|
2157
|
-
also:
|
|
2353
|
+
z7.object({
|
|
2354
|
+
type: z7.literal("idle"),
|
|
2355
|
+
also: z7.array(RideAlongSchema).optional(),
|
|
2158
2356
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
2159
2357
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
2160
2358
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
2161
2359
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
2162
2360
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
2163
|
-
inFlight:
|
|
2361
|
+
inFlight: z7.boolean().optional()
|
|
2164
2362
|
})
|
|
2165
2363
|
]);
|
|
2166
|
-
var VoiceKeySchema =
|
|
2167
|
-
var AgendaTurnSchema =
|
|
2364
|
+
var VoiceKeySchema = z7.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2365
|
+
var AgendaTurnSchema = z7.object({
|
|
2168
2366
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
2169
2367
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
2170
2368
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
2171
2369
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
2172
2370
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
2173
2371
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
2174
|
-
id:
|
|
2372
|
+
id: z7.string().optional(),
|
|
2175
2373
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
2176
2374
|
* the planner declares duplicates instead of asking them twice. */
|
|
2177
|
-
coveredIds:
|
|
2375
|
+
coveredIds: z7.array(z7.string()).optional(),
|
|
2178
2376
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
2179
2377
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
2180
|
-
info:
|
|
2181
|
-
question:
|
|
2378
|
+
info: z7.array(z7.string().min(1)).default([]),
|
|
2379
|
+
question: z7.string().min(1).nullable(),
|
|
2182
2380
|
/** True on the one turn carrying the agent's own declared question. */
|
|
2183
|
-
asks:
|
|
2381
|
+
asks: z7.boolean().optional(),
|
|
2184
2382
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
2185
2383
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
2186
2384
|
* which route nothing. */
|
|
2187
|
-
claimId:
|
|
2385
|
+
claimId: z7.string().optional(),
|
|
2188
2386
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
2189
|
-
voice:
|
|
2387
|
+
voice: z7.string().optional(),
|
|
2190
2388
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
2191
2389
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
2192
2390
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
2193
2391
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
2194
|
-
agent:
|
|
2392
|
+
agent: z7.string().optional(),
|
|
2195
2393
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
2196
2394
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
2197
2395
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
2198
2396
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
2199
2397
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
2200
|
-
agentId:
|
|
2398
|
+
agentId: z7.string().optional(),
|
|
2201
2399
|
select: SelectShapeSchema.optional(),
|
|
2202
|
-
options:
|
|
2400
|
+
options: z7.array(OptionSchema.omit({ id: true })).optional(),
|
|
2203
2401
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
2204
2402
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
2205
2403
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -2208,33 +2406,33 @@ var AgendaTurnSchema = z5.object({
|
|
|
2208
2406
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
2209
2407
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
2210
2408
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
2211
|
-
blocking:
|
|
2409
|
+
blocking: z7.boolean().optional(),
|
|
2212
2410
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
2213
2411
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
2214
2412
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
2215
2413
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
2216
2414
|
* speak, it is dropped and their words are taken like any reply. */
|
|
2217
|
-
ifSilent:
|
|
2415
|
+
ifSilent: z7.boolean().optional()
|
|
2218
2416
|
});
|
|
2219
2417
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
2220
|
-
var InboxItemSchema =
|
|
2221
|
-
id:
|
|
2222
|
-
tokenId:
|
|
2418
|
+
var InboxItemSchema = z7.object({
|
|
2419
|
+
id: z7.string(),
|
|
2420
|
+
tokenId: z7.string().optional(),
|
|
2223
2421
|
status: NotifyStatusSchema,
|
|
2224
2422
|
context: ContextSchema,
|
|
2225
|
-
options:
|
|
2423
|
+
options: z7.array(OptionSchema).optional(),
|
|
2226
2424
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
2227
|
-
points:
|
|
2425
|
+
points: z7.array(z7.string()).optional(),
|
|
2228
2426
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
2229
2427
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
2230
2428
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
2231
2429
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
2232
|
-
asks:
|
|
2430
|
+
asks: z7.boolean().optional(),
|
|
2233
2431
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
2234
2432
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
2235
2433
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
2236
2434
|
* which must never CLAIM stalled. */
|
|
2237
|
-
lastSeenAt:
|
|
2435
|
+
lastSeenAt: z7.string().optional(),
|
|
2238
2436
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
2239
2437
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
2240
2438
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -2244,7 +2442,7 @@ var InboxItemSchema = z5.object({
|
|
|
2244
2442
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
2245
2443
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
2246
2444
|
* `createdAt`. */
|
|
2247
|
-
agentStateAt:
|
|
2445
|
+
agentStateAt: z7.string().datetime().optional(),
|
|
2248
2446
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
2249
2447
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
2250
2448
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -2258,54 +2456,54 @@ var InboxItemSchema = z5.object({
|
|
|
2258
2456
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
2259
2457
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
2260
2458
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
2261
|
-
questions:
|
|
2262
|
-
id:
|
|
2263
|
-
entryId:
|
|
2264
|
-
title:
|
|
2265
|
-
state:
|
|
2266
|
-
answer:
|
|
2459
|
+
questions: z7.array(z7.object({
|
|
2460
|
+
id: z7.string(),
|
|
2461
|
+
entryId: z7.string(),
|
|
2462
|
+
title: z7.string(),
|
|
2463
|
+
state: z7.enum(["open", "answered"]),
|
|
2464
|
+
answer: z7.string().nullable(),
|
|
2267
2465
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
2268
2466
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
2269
2467
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
2270
2468
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
2271
|
-
agent:
|
|
2469
|
+
agent: z7.object({ tokenId: z7.string(), name: z7.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
2272
2470
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
2273
2471
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
2274
2472
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
2275
2473
|
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
2276
2474
|
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
2277
|
-
options:
|
|
2475
|
+
options: z7.array(OptionSchema).optional()
|
|
2278
2476
|
})).optional(),
|
|
2279
|
-
visuals:
|
|
2477
|
+
visuals: z7.array(VisualSchema).optional(),
|
|
2280
2478
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
2281
2479
|
* agent's suggestion, or a default silly name). */
|
|
2282
|
-
name:
|
|
2480
|
+
name: z7.string(),
|
|
2283
2481
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
2284
2482
|
voice: VoiceKeySchema.optional(),
|
|
2285
|
-
repo:
|
|
2286
|
-
branch:
|
|
2287
|
-
createdAt:
|
|
2288
|
-
snoozedUntil:
|
|
2483
|
+
repo: z7.string().optional(),
|
|
2484
|
+
branch: z7.string().optional(),
|
|
2485
|
+
createdAt: z7.string().datetime(),
|
|
2486
|
+
snoozedUntil: z7.string().datetime().optional(),
|
|
2289
2487
|
agentState: AgentStateSchema.default("idle"),
|
|
2290
2488
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
2291
2489
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
2292
2490
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
2293
|
-
turn:
|
|
2491
|
+
turn: z7.enum(["you", "agent"]).default("you"),
|
|
2294
2492
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
2295
2493
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
2296
2494
|
* error badge + Retry. */
|
|
2297
|
-
error:
|
|
2495
|
+
error: z7.string().optional(),
|
|
2298
2496
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
2299
2497
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
2300
2498
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
2301
2499
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
2302
|
-
cold:
|
|
2303
|
-
clarifies:
|
|
2500
|
+
cold: z7.string().datetime().optional(),
|
|
2501
|
+
clarifies: z7.string().optional(),
|
|
2304
2502
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
2305
2503
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
2306
2504
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
2307
2505
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
2308
|
-
onCall:
|
|
2506
|
+
onCall: z7.literal(true).optional(),
|
|
2309
2507
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
2310
2508
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
2311
2509
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -2315,31 +2513,31 @@ var InboxItemSchema = z5.object({
|
|
|
2315
2513
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
2316
2514
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
2317
2515
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
2318
|
-
ring:
|
|
2319
|
-
declinedAt:
|
|
2320
|
-
anchorAt:
|
|
2321
|
-
nextRingAt:
|
|
2322
|
-
step:
|
|
2516
|
+
ring: z7.object({
|
|
2517
|
+
declinedAt: z7.string().datetime(),
|
|
2518
|
+
anchorAt: z7.string().datetime(),
|
|
2519
|
+
nextRingAt: z7.string().datetime().nullable(),
|
|
2520
|
+
step: z7.number().int()
|
|
2323
2521
|
}).optional(),
|
|
2324
2522
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
2325
2523
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
2326
2524
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
2327
2525
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
2328
2526
|
* feedback ABOUT. */
|
|
2329
|
-
why:
|
|
2527
|
+
why: z7.object({
|
|
2330
2528
|
asked: NotifyLevelSchema,
|
|
2331
2529
|
got: NotifyLevelSchema,
|
|
2332
|
-
because:
|
|
2333
|
-
line:
|
|
2530
|
+
because: z7.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
2531
|
+
line: z7.string()
|
|
2334
2532
|
}).optional(),
|
|
2335
|
-
select:
|
|
2336
|
-
confirmStyle:
|
|
2533
|
+
select: z7.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
2534
|
+
confirmStyle: z7.enum(["yesno", "approve"]).default("yesno").describe(
|
|
2337
2535
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
2338
2536
|
),
|
|
2339
2537
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
2340
2538
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
2341
2539
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
2342
|
-
blocking:
|
|
2540
|
+
blocking: z7.boolean().default(false),
|
|
2343
2541
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
2344
2542
|
answer: UserAnswerSchema.optional(),
|
|
2345
2543
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -2347,17 +2545,17 @@ var InboxItemSchema = z5.object({
|
|
|
2347
2545
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
2348
2546
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
2349
2547
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
2350
|
-
communication:
|
|
2351
|
-
deliveryId:
|
|
2352
|
-
kind:
|
|
2353
|
-
entryId:
|
|
2354
|
-
goalIds:
|
|
2355
|
-
decisionNeedId:
|
|
2356
|
-
sealed:
|
|
2357
|
-
goalState:
|
|
2548
|
+
communication: z7.object({
|
|
2549
|
+
deliveryId: z7.string(),
|
|
2550
|
+
kind: z7.enum(["notification", "call"]),
|
|
2551
|
+
entryId: z7.string(),
|
|
2552
|
+
goalIds: z7.array(z7.string()),
|
|
2553
|
+
decisionNeedId: z7.string().optional(),
|
|
2554
|
+
sealed: z7.boolean(),
|
|
2555
|
+
goalState: z7.string().optional(),
|
|
2358
2556
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
2359
2557
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
2360
|
-
goalTitle:
|
|
2558
|
+
goalTitle: z7.string().optional()
|
|
2361
2559
|
}).optional(),
|
|
2362
2560
|
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
2363
2561
|
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
@@ -2369,27 +2567,27 @@ var InboxItemSchema = z5.object({
|
|
|
2369
2567
|
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
2370
2568
|
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
2371
2569
|
* with no rev is simply always re-sent. */
|
|
2372
|
-
rev:
|
|
2570
|
+
rev: z7.string().optional()
|
|
2373
2571
|
});
|
|
2374
2572
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
2375
|
-
var PushTokenSchema =
|
|
2376
|
-
voipToken:
|
|
2377
|
-
alertToken:
|
|
2378
|
-
fcmToken:
|
|
2379
|
-
platform:
|
|
2573
|
+
var PushTokenSchema = z7.object({
|
|
2574
|
+
voipToken: z7.string().min(1).optional(),
|
|
2575
|
+
alertToken: z7.string().min(1).optional(),
|
|
2576
|
+
fcmToken: z7.string().min(1).optional(),
|
|
2577
|
+
platform: z7.enum(["ios", "android"])
|
|
2380
2578
|
}).superRefine((v, ctx) => {
|
|
2381
2579
|
if (v.platform !== "ios") return;
|
|
2382
2580
|
for (const field of ["voipToken", "alertToken"]) {
|
|
2383
2581
|
const token = v[field];
|
|
2384
2582
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
2385
2583
|
ctx.addIssue({
|
|
2386
|
-
code:
|
|
2584
|
+
code: z7.ZodIssueCode.custom,
|
|
2387
2585
|
path: [field],
|
|
2388
2586
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
2389
2587
|
});
|
|
2390
2588
|
}
|
|
2391
2589
|
});
|
|
2392
|
-
var MissedCallSchema =
|
|
2590
|
+
var MissedCallSchema = z7.enum([
|
|
2393
2591
|
"retry_10m",
|
|
2394
2592
|
"retry_30m",
|
|
2395
2593
|
"retry_60m",
|
|
@@ -2401,31 +2599,31 @@ var MissedCallSchema = z5.enum([
|
|
|
2401
2599
|
]);
|
|
2402
2600
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
2403
2601
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
2404
|
-
var BrokerTuningSchema =
|
|
2602
|
+
var BrokerTuningSchema = z7.object({
|
|
2405
2603
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
2406
|
-
ackVerbosity:
|
|
2604
|
+
ackVerbosity: z7.enum(["normal", "none"]).optional(),
|
|
2407
2605
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
2408
2606
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
2409
|
-
clarifyEagerness:
|
|
2607
|
+
clarifyEagerness: z7.enum(["low", "normal", "high"]).optional(),
|
|
2410
2608
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
2411
|
-
phrasebook:
|
|
2609
|
+
phrasebook: z7.array(z7.object({ say: z7.string().min(1).max(60), mean: z7.string().min(1).max(120) })).max(24).optional(),
|
|
2412
2610
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
2413
2611
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
2414
2612
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
2415
2613
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
2416
2614
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
2417
|
-
language:
|
|
2615
|
+
language: z7.enum(["en", "es"]).optional()
|
|
2418
2616
|
});
|
|
2419
|
-
var UserSettingsSchema =
|
|
2420
|
-
permissions:
|
|
2421
|
-
call:
|
|
2422
|
-
banner:
|
|
2423
|
-
push:
|
|
2617
|
+
var UserSettingsSchema = z7.object({
|
|
2618
|
+
permissions: z7.object({
|
|
2619
|
+
call: z7.boolean(),
|
|
2620
|
+
banner: z7.boolean(),
|
|
2621
|
+
push: z7.boolean()
|
|
2424
2622
|
}),
|
|
2425
2623
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
2426
2624
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
2427
2625
|
* what people read (owner, 2026-09-30). */
|
|
2428
|
-
sessionMode:
|
|
2626
|
+
sessionMode: z7.enum(["default", "all_calls", "silent"]),
|
|
2429
2627
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
2430
2628
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
2431
2629
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -2434,7 +2632,7 @@ var UserSettingsSchema = z5.object({
|
|
|
2434
2632
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
2435
2633
|
* next dead field for the same reason. */
|
|
2436
2634
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
2437
|
-
improveConsent:
|
|
2635
|
+
improveConsent: z7.boolean(),
|
|
2438
2636
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
2439
2637
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
2440
2638
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -2442,17 +2640,17 @@ var UserSettingsSchema = z5.object({
|
|
|
2442
2640
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
2443
2641
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
2444
2642
|
* write, 'hosted' on read (see store.ts). */
|
|
2445
|
-
voiceMode:
|
|
2643
|
+
voiceMode: z7.enum(["hosted", "on_device"]).optional(),
|
|
2446
2644
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
2447
2645
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
2448
2646
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
2449
2647
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
2450
2648
|
* read (see store.ts). */
|
|
2451
|
-
talk:
|
|
2649
|
+
talk: z7.boolean().optional(),
|
|
2452
2650
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
2453
2651
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
2454
2652
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
2455
|
-
callDiagnostics:
|
|
2653
|
+
callDiagnostics: z7.boolean().optional(),
|
|
2456
2654
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
2457
2655
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
2458
2656
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -2460,7 +2658,7 @@ var UserSettingsSchema = z5.object({
|
|
|
2460
2658
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
2461
2659
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
2462
2660
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
2463
|
-
voiceTuning:
|
|
2661
|
+
voiceTuning: z7.record(z7.string(), z7.union([z7.number(), z7.string()])).optional(),
|
|
2464
2662
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
2465
2663
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
2466
2664
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -2469,53 +2667,53 @@ var UserSettingsSchema = z5.object({
|
|
|
2469
2667
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
2470
2668
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
2471
2669
|
* call — honest about what we know. */
|
|
2472
|
-
timezone:
|
|
2670
|
+
timezone: z7.string().min(1).max(64).optional(),
|
|
2473
2671
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
2474
2672
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
2475
2673
|
broker: BrokerTuningSchema.optional()
|
|
2476
2674
|
});
|
|
2477
|
-
var HistoryWorkSchema =
|
|
2478
|
-
id:
|
|
2479
|
-
title:
|
|
2480
|
-
state:
|
|
2675
|
+
var HistoryWorkSchema = z7.object({
|
|
2676
|
+
id: z7.string(),
|
|
2677
|
+
title: z7.string(),
|
|
2678
|
+
state: z7.enum(["done", "cancelled"]),
|
|
2481
2679
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
2482
|
-
assignee:
|
|
2680
|
+
assignee: z7.string()
|
|
2483
2681
|
});
|
|
2484
|
-
var HistoryEntrySchema =
|
|
2485
|
-
|
|
2486
|
-
|
|
2682
|
+
var HistoryEntrySchema = z7.union([
|
|
2683
|
+
z7.object({ at: z7.string(), card: InboxItemSchema }),
|
|
2684
|
+
z7.object({ at: z7.string(), work: HistoryWorkSchema })
|
|
2487
2685
|
]);
|
|
2488
|
-
var HistoryPageSchema =
|
|
2489
|
-
entries:
|
|
2490
|
-
next:
|
|
2686
|
+
var HistoryPageSchema = z7.object({
|
|
2687
|
+
entries: z7.array(HistoryEntrySchema),
|
|
2688
|
+
next: z7.string().nullable()
|
|
2491
2689
|
});
|
|
2492
2690
|
var ACTIVITY_LINES = 2;
|
|
2493
2691
|
var ACTIVITY_LINE_MAX = 80;
|
|
2494
|
-
var AgentActivitySchema =
|
|
2692
|
+
var AgentActivitySchema = z7.object({
|
|
2495
2693
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
2496
|
-
lines:
|
|
2694
|
+
lines: z7.array(z7.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
2497
2695
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
2498
2696
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
2499
|
-
at:
|
|
2697
|
+
at: z7.string().datetime()
|
|
2500
2698
|
});
|
|
2501
|
-
var ConnectionSummarySchema =
|
|
2699
|
+
var ConnectionSummarySchema = z7.object({
|
|
2502
2700
|
/** The connection = the agent's token id (used to address a request). */
|
|
2503
|
-
id:
|
|
2701
|
+
id: z7.string(),
|
|
2504
2702
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
2505
2703
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
2506
2704
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
2507
2705
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
2508
|
-
kind:
|
|
2706
|
+
kind: z7.enum(["device", "agent"]).optional(),
|
|
2509
2707
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
2510
2708
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
2511
2709
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
2512
|
-
mintedByDevice:
|
|
2513
|
-
device:
|
|
2710
|
+
mintedByDevice: z7.string().nullable().optional(),
|
|
2711
|
+
device: z7.string().nullable(),
|
|
2514
2712
|
/** The agent's display name (the single pairing name). */
|
|
2515
|
-
name:
|
|
2713
|
+
name: z7.string(),
|
|
2516
2714
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
2517
2715
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
2518
|
-
provider:
|
|
2716
|
+
provider: z7.string().nullable(),
|
|
2519
2717
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2520
2718
|
voice: VoiceKeySchema.nullable(),
|
|
2521
2719
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -2526,34 +2724,34 @@ var ConnectionSummarySchema = z5.object({
|
|
|
2526
2724
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
2527
2725
|
* set once must not overrule a rule they set about one agent. */
|
|
2528
2726
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
2529
|
-
createdAt:
|
|
2727
|
+
createdAt: z7.string().datetime(),
|
|
2530
2728
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2531
2729
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
2532
|
-
lastContactAt:
|
|
2730
|
+
lastContactAt: z7.string().datetime().nullable(),
|
|
2533
2731
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
2534
2732
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
2535
|
-
lastSeenAt:
|
|
2733
|
+
lastSeenAt: z7.string().datetime().nullable().optional(),
|
|
2536
2734
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
2537
2735
|
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
2538
2736
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
2539
|
-
lastWorkedAt:
|
|
2737
|
+
lastWorkedAt: z7.string().datetime().nullable().optional(),
|
|
2540
2738
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
2541
2739
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
2542
|
-
oldestReadyAt:
|
|
2740
|
+
oldestReadyAt: z7.string().datetime().nullable().optional(),
|
|
2543
2741
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
2544
2742
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
2545
2743
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
2546
|
-
runtime:
|
|
2744
|
+
runtime: z7.object({
|
|
2547
2745
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
2548
2746
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
2549
|
-
version:
|
|
2550
|
-
harnesses:
|
|
2551
|
-
workspaces:
|
|
2747
|
+
version: z7.string().optional(),
|
|
2748
|
+
harnesses: z7.array(z7.object({ name: z7.string(), label: z7.string(), status: z7.string() })).optional(),
|
|
2749
|
+
workspaces: z7.array(z7.string()).optional(),
|
|
2552
2750
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
2553
2751
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
2554
2752
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
2555
2753
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
2556
|
-
repos:
|
|
2754
|
+
repos: z7.array(z7.object({ path: z7.string(), remote: z7.string() })).optional()
|
|
2557
2755
|
}).optional(),
|
|
2558
2756
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
2559
2757
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -2562,156 +2760,156 @@ var ConnectionSummarySchema = z5.object({
|
|
|
2562
2760
|
activity: AgentActivitySchema.optional(),
|
|
2563
2761
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
2564
2762
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
2565
|
-
managed:
|
|
2763
|
+
managed: z7.boolean()
|
|
2566
2764
|
});
|
|
2567
|
-
var LedgerItemSchema =
|
|
2568
|
-
var AgentLedgerSchema =
|
|
2765
|
+
var LedgerItemSchema = z7.object({ id: z7.string(), parentId: z7.string(), title: z7.string(), createdAt: z7.string() });
|
|
2766
|
+
var AgentLedgerSchema = z7.object({
|
|
2569
2767
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
2570
|
-
agent:
|
|
2768
|
+
agent: z7.object({ id: z7.string(), name: z7.string().nullable(), revokedAt: z7.string().nullable() }),
|
|
2571
2769
|
/** Its own questions you have not answered. */
|
|
2572
|
-
asks:
|
|
2770
|
+
asks: z7.array(LedgerItemSchema),
|
|
2573
2771
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
2574
|
-
answered:
|
|
2772
|
+
answered: z7.array(LedgerItemSchema),
|
|
2575
2773
|
/** Requests you sent it that it never took. */
|
|
2576
|
-
requests:
|
|
2577
|
-
goals:
|
|
2578
|
-
callbacks:
|
|
2774
|
+
requests: z7.array(LedgerItemSchema),
|
|
2775
|
+
goals: z7.array(z7.object({ id: z7.string(), outcome: z7.string(), state: z7.string() })),
|
|
2776
|
+
callbacks: z7.array(z7.object({ id: z7.string(), parentId: z7.string(), trigger: z7.string(), note: z7.string(), dueAt: z7.string().nullable() }))
|
|
2579
2777
|
});
|
|
2580
|
-
var ReassignResultSchema =
|
|
2581
|
-
moved:
|
|
2582
|
-
parentId:
|
|
2778
|
+
var ReassignResultSchema = z7.object({
|
|
2779
|
+
moved: z7.object({ asks: z7.number(), answered: z7.number(), requests: z7.number(), goals: z7.number(), callbacks: z7.number() }),
|
|
2780
|
+
parentId: z7.string().nullable()
|
|
2583
2781
|
});
|
|
2584
|
-
var LessonStateSchema =
|
|
2585
|
-
var LessonViewSchema =
|
|
2586
|
-
id:
|
|
2587
|
-
text:
|
|
2782
|
+
var LessonStateSchema = z7.enum(["active", "proposed", "retired"]);
|
|
2783
|
+
var LessonViewSchema = z7.object({
|
|
2784
|
+
id: z7.string(),
|
|
2785
|
+
text: z7.string(),
|
|
2588
2786
|
state: LessonStateSchema,
|
|
2589
2787
|
/** The Goal it is scoped to; null = the whole account. */
|
|
2590
|
-
scopeGoalId:
|
|
2591
|
-
goalTitle:
|
|
2592
|
-
version:
|
|
2593
|
-
pinned:
|
|
2788
|
+
scopeGoalId: z7.string().nullable(),
|
|
2789
|
+
goalTitle: z7.string().nullable(),
|
|
2790
|
+
version: z7.number(),
|
|
2791
|
+
pinned: z7.boolean(),
|
|
2594
2792
|
/** When the person last wrote its text themselves. */
|
|
2595
|
-
editedAt:
|
|
2596
|
-
createdAt:
|
|
2597
|
-
updatedAt:
|
|
2793
|
+
editedAt: z7.string().nullable(),
|
|
2794
|
+
createdAt: z7.string(),
|
|
2795
|
+
updatedAt: z7.string(),
|
|
2598
2796
|
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
2599
|
-
sources:
|
|
2797
|
+
sources: z7.array(z7.object({ entryId: z7.string(), words: z7.string().nullable(), at: z7.string() }))
|
|
2600
2798
|
});
|
|
2601
|
-
var QueueQuestionSchema =
|
|
2799
|
+
var QueueQuestionSchema = z7.object({
|
|
2602
2800
|
/** The decision need's id — what an answer is accepted against. */
|
|
2603
|
-
id:
|
|
2801
|
+
id: z7.string(),
|
|
2604
2802
|
/** The words that were asked, from the request Entry that asked them. */
|
|
2605
|
-
question:
|
|
2803
|
+
question: z7.string(),
|
|
2606
2804
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
2607
2805
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
2608
2806
|
* can answer. */
|
|
2609
|
-
deliveryId:
|
|
2807
|
+
deliveryId: z7.string().nullable().default(null),
|
|
2610
2808
|
/** The Entry the ruling is about. */
|
|
2611
|
-
aboutId:
|
|
2809
|
+
aboutId: z7.string().nullable().default(null),
|
|
2612
2810
|
/** Empty for a free-text question. */
|
|
2613
|
-
options:
|
|
2614
|
-
select:
|
|
2615
|
-
askedAt:
|
|
2811
|
+
options: z7.array(OptionSchema).default([]),
|
|
2812
|
+
select: z7.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
2813
|
+
askedAt: z7.string(),
|
|
2616
2814
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
2617
|
-
answeredAt:
|
|
2815
|
+
answeredAt: z7.string().nullable().default(null),
|
|
2618
2816
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
2619
2817
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
2620
2818
|
* for a settled question whose reply carried nothing readable. */
|
|
2621
|
-
answer:
|
|
2819
|
+
answer: z7.string().nullable().default(null),
|
|
2622
2820
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
2623
2821
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
2624
2822
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
2625
2823
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
2626
2824
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
2627
2825
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
2628
|
-
goalId:
|
|
2826
|
+
goalId: z7.string(),
|
|
2629
2827
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
2630
2828
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
2631
2829
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
2632
2830
|
* gate at all. */
|
|
2633
|
-
blocked:
|
|
2831
|
+
blocked: z7.boolean().default(false)
|
|
2634
2832
|
});
|
|
2635
|
-
var QueueReplySchema =
|
|
2833
|
+
var QueueReplySchema = z7.object({
|
|
2636
2834
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
2637
2835
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
2638
2836
|
* walk can name it in its zoom. */
|
|
2639
|
-
id:
|
|
2837
|
+
id: z7.string(),
|
|
2640
2838
|
/** The Goal the note is on. */
|
|
2641
|
-
goalId:
|
|
2839
|
+
goalId: z7.string(),
|
|
2642
2840
|
/** What the note said. */
|
|
2643
|
-
note:
|
|
2841
|
+
note: z7.string(),
|
|
2644
2842
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
2645
|
-
deliveryId:
|
|
2646
|
-
requestEntryId:
|
|
2647
|
-
askedAt:
|
|
2843
|
+
deliveryId: z7.string(),
|
|
2844
|
+
requestEntryId: z7.string(),
|
|
2845
|
+
askedAt: z7.string(),
|
|
2648
2846
|
/** When the person last replied — the window's start. */
|
|
2649
|
-
repliedAt:
|
|
2847
|
+
repliedAt: z7.string(),
|
|
2650
2848
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
2651
|
-
reply:
|
|
2849
|
+
reply: z7.string().nullable()
|
|
2652
2850
|
});
|
|
2653
|
-
var QueueItemSchema =
|
|
2654
|
-
id:
|
|
2851
|
+
var QueueItemSchema = z7.object({
|
|
2852
|
+
id: z7.string(),
|
|
2655
2853
|
/** One-line headline — the first sentence of the outcome. */
|
|
2656
|
-
title:
|
|
2854
|
+
title: z7.string(),
|
|
2657
2855
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
2658
|
-
intent:
|
|
2856
|
+
intent: z7.string(),
|
|
2659
2857
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
2660
|
-
state:
|
|
2858
|
+
state: z7.string(),
|
|
2661
2859
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
2662
|
-
assignee:
|
|
2860
|
+
assignee: z7.string().nullable().default(null),
|
|
2663
2861
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
2664
|
-
progress:
|
|
2862
|
+
progress: z7.string().nullable().default(null),
|
|
2665
2863
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
2666
2864
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
2667
|
-
progressLine:
|
|
2668
|
-
reviewPending:
|
|
2669
|
-
dueAt:
|
|
2865
|
+
progressLine: z7.string().nullable().optional(),
|
|
2866
|
+
reviewPending: z7.boolean().default(false),
|
|
2867
|
+
dueAt: z7.string().nullable().default(null),
|
|
2670
2868
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
2671
2869
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
2672
|
-
finishedAt:
|
|
2870
|
+
finishedAt: z7.string().nullable().optional(),
|
|
2673
2871
|
/** The Goal this one was opened under; null at the root. */
|
|
2674
|
-
parentGoalId:
|
|
2872
|
+
parentGoalId: z7.string().nullable().default(null),
|
|
2675
2873
|
/** Goals opened under this one — only those the same list holds. */
|
|
2676
|
-
childGoalIds:
|
|
2874
|
+
childGoalIds: z7.array(z7.string()).default([]),
|
|
2677
2875
|
/** Goals this one waits on (start or finish gates). */
|
|
2678
|
-
dependencyGoalIds:
|
|
2876
|
+
dependencyGoalIds: z7.array(z7.string()).default([]),
|
|
2679
2877
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
2680
|
-
blocked:
|
|
2878
|
+
blocked: z7.boolean().default(false),
|
|
2681
2879
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
2682
2880
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
2683
2881
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
2684
2882
|
* question is opened. */
|
|
2685
|
-
questions:
|
|
2883
|
+
questions: z7.array(QueueQuestionSchema).default([]),
|
|
2686
2884
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
2687
2885
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
2688
2886
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
2689
2887
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
2690
|
-
asked:
|
|
2691
|
-
answered:
|
|
2888
|
+
asked: z7.number().optional(),
|
|
2889
|
+
answered: z7.number().optional(),
|
|
2692
2890
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
2693
2891
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
2694
2892
|
* not spell an empty list. */
|
|
2695
|
-
replies:
|
|
2893
|
+
replies: z7.array(QueueReplySchema).optional(),
|
|
2696
2894
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
2697
|
-
repo:
|
|
2698
|
-
createdAt:
|
|
2699
|
-
updatedAt:
|
|
2700
|
-
/** When its owner last SAID something about it (`
|
|
2701
|
-
*
|
|
2895
|
+
repo: z7.string().nullable().optional(),
|
|
2896
|
+
createdAt: z7.string(),
|
|
2897
|
+
updatedAt: z7.string().nullable().default(null),
|
|
2898
|
+
/** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
|
|
2899
|
+
* as progress). `updatedAt` moves for reasons nobody chose — a
|
|
2702
2900
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
2703
|
-
lastProgressAt:
|
|
2901
|
+
lastProgressAt: z7.string().nullable().optional(),
|
|
2704
2902
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
2705
2903
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
2706
2904
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
2707
2905
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
2708
2906
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
2709
2907
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
2710
|
-
latest:
|
|
2711
|
-
from:
|
|
2712
|
-
said:
|
|
2713
|
-
at:
|
|
2714
|
-
entryId:
|
|
2908
|
+
latest: z7.object({
|
|
2909
|
+
from: z7.enum(["person", "agent"]),
|
|
2910
|
+
said: z7.string(),
|
|
2911
|
+
at: z7.string(),
|
|
2912
|
+
entryId: z7.string()
|
|
2715
2913
|
}).nullable().optional(),
|
|
2716
2914
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
2717
2915
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -2724,7 +2922,7 @@ var QueueItemSchema = z5.object({
|
|
|
2724
2922
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
2725
2923
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
2726
2924
|
* Entry with no `about_id` is in neither. */
|
|
2727
|
-
lastPersonAt:
|
|
2925
|
+
lastPersonAt: z7.string().nullable().optional(),
|
|
2728
2926
|
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
2729
2927
|
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
2730
2928
|
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
@@ -2735,40 +2933,40 @@ var QueueItemSchema = z5.object({
|
|
|
2735
2933
|
* the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
|
|
2736
2934
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
2737
2935
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
2738
|
-
rev:
|
|
2936
|
+
rev: z7.string().optional()
|
|
2739
2937
|
});
|
|
2740
|
-
var QueueDeltaSchema =
|
|
2741
|
-
ids:
|
|
2742
|
-
items:
|
|
2938
|
+
var QueueDeltaSchema = z7.object({
|
|
2939
|
+
ids: z7.array(z7.string()),
|
|
2940
|
+
items: z7.array(QueueItemSchema)
|
|
2743
2941
|
});
|
|
2744
2942
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
2745
|
-
var NoteSourceSchema =
|
|
2746
|
-
var NoteStatusSchema =
|
|
2747
|
-
var NoteRepeatSchema =
|
|
2748
|
-
var DecisionSchema =
|
|
2749
|
-
id:
|
|
2943
|
+
var NoteSourceSchema = z7.enum(["app", "call"]);
|
|
2944
|
+
var NoteStatusSchema = z7.enum(["open", "assigned", "in_progress", "done"]);
|
|
2945
|
+
var NoteRepeatSchema = z7.enum(["once", "until_done"]);
|
|
2946
|
+
var DecisionSchema = z7.object({
|
|
2947
|
+
id: z7.string(),
|
|
2750
2948
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
2751
2949
|
* extensibility seam — any conversation can accrue decisions). */
|
|
2752
|
-
noteId:
|
|
2950
|
+
noteId: z7.string().nullable(),
|
|
2753
2951
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
2754
|
-
question:
|
|
2952
|
+
question: z7.string(),
|
|
2755
2953
|
/** The user's ruling; null while the question is open. */
|
|
2756
|
-
answer:
|
|
2757
|
-
decidedAt:
|
|
2758
|
-
createdAt:
|
|
2954
|
+
answer: z7.string().nullable(),
|
|
2955
|
+
decidedAt: z7.string().nullable(),
|
|
2956
|
+
createdAt: z7.string()
|
|
2759
2957
|
});
|
|
2760
|
-
var NoteSchema =
|
|
2761
|
-
id:
|
|
2958
|
+
var NoteSchema = z7.object({
|
|
2959
|
+
id: z7.string(),
|
|
2762
2960
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
2763
|
-
title:
|
|
2961
|
+
title: z7.string(),
|
|
2764
2962
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
2765
|
-
intent:
|
|
2963
|
+
intent: z7.string(),
|
|
2766
2964
|
source: NoteSourceSchema,
|
|
2767
2965
|
status: NoteStatusSchema,
|
|
2768
2966
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
2769
|
-
assignee:
|
|
2967
|
+
assignee: z7.string().nullable(),
|
|
2770
2968
|
/** The request thread minted at assignment; null until assigned. */
|
|
2771
|
-
parentId:
|
|
2969
|
+
parentId: z7.string().nullable(),
|
|
2772
2970
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
2773
2971
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
2774
2972
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -2777,154 +2975,154 @@ var NoteSchema = z5.object({
|
|
|
2777
2975
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
2778
2976
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
2779
2977
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
2780
|
-
dueAt:
|
|
2978
|
+
dueAt: z7.string().nullable().default(null),
|
|
2781
2979
|
repeat: NoteRepeatSchema.default("once"),
|
|
2782
2980
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
2783
|
-
rides:
|
|
2784
|
-
lastRideAt:
|
|
2785
|
-
createdAt:
|
|
2981
|
+
rides: z7.number().int().default(0),
|
|
2982
|
+
lastRideAt: z7.string().nullable().default(null),
|
|
2983
|
+
createdAt: z7.string()
|
|
2786
2984
|
});
|
|
2787
|
-
var TriageItemSchema =
|
|
2788
|
-
noteId:
|
|
2985
|
+
var TriageItemSchema = z7.object({
|
|
2986
|
+
noteId: z7.string(),
|
|
2789
2987
|
/** The note's headline at run time. */
|
|
2790
|
-
title:
|
|
2988
|
+
title: z7.string(),
|
|
2791
2989
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
2792
2990
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
2793
2991
|
* Never a model's reasoning transcript, never an id. */
|
|
2794
|
-
why:
|
|
2992
|
+
why: z7.string()
|
|
2795
2993
|
});
|
|
2796
|
-
var TriageAssignmentSchema =
|
|
2994
|
+
var TriageAssignmentSchema = z7.object({
|
|
2797
2995
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
2798
|
-
agent:
|
|
2996
|
+
agent: z7.string(),
|
|
2799
2997
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
2800
2998
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
2801
|
-
agentName:
|
|
2802
|
-
notes:
|
|
2999
|
+
agentName: z7.string(),
|
|
3000
|
+
notes: z7.array(TriageItemSchema)
|
|
2803
3001
|
});
|
|
2804
|
-
var TriageStatusSchema =
|
|
2805
|
-
var SubmitTriageSchema =
|
|
3002
|
+
var TriageStatusSchema = z7.enum(["open", "superseded", "dismissed"]);
|
|
3003
|
+
var SubmitTriageSchema = z7.object({
|
|
2806
3004
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
2807
3005
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
2808
3006
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
2809
3007
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
2810
|
-
provider:
|
|
3008
|
+
provider: z7.string().min(1).max(60),
|
|
2811
3009
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
2812
|
-
model:
|
|
3010
|
+
model: z7.string().max(200).nullable().optional(),
|
|
2813
3011
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
2814
3012
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
2815
|
-
reviewed:
|
|
2816
|
-
close:
|
|
2817
|
-
stale:
|
|
2818
|
-
assign:
|
|
3013
|
+
reviewed: z7.number().int().min(0).max(1e4).default(0),
|
|
3014
|
+
close: z7.array(TriageItemSchema).max(200).default([]),
|
|
3015
|
+
stale: z7.array(TriageItemSchema).max(200).default([]),
|
|
3016
|
+
assign: z7.array(TriageAssignmentSchema).max(50).default([])
|
|
2819
3017
|
});
|
|
2820
3018
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
2821
|
-
id:
|
|
2822
|
-
runAt:
|
|
3019
|
+
id: z7.string(),
|
|
3020
|
+
runAt: z7.string(),
|
|
2823
3021
|
status: TriageStatusSchema,
|
|
2824
|
-
model:
|
|
3022
|
+
model: z7.string().nullable().default(null)
|
|
2825
3023
|
});
|
|
2826
|
-
var AcceptTriageSchema =
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
group:
|
|
2831
|
-
agent:
|
|
2832
|
-
noteIds:
|
|
3024
|
+
var AcceptTriageSchema = z7.discriminatedUnion("group", [
|
|
3025
|
+
z7.object({ group: z7.literal("close"), noteIds: z7.array(z7.string()).max(200).optional() }),
|
|
3026
|
+
z7.object({ group: z7.literal("stale"), noteIds: z7.array(z7.string()).max(200).optional() }),
|
|
3027
|
+
z7.object({
|
|
3028
|
+
group: z7.literal("assign"),
|
|
3029
|
+
agent: z7.string().min(1),
|
|
3030
|
+
noteIds: z7.array(z7.string()).max(200).optional()
|
|
2833
3031
|
})
|
|
2834
3032
|
]);
|
|
2835
|
-
var AcceptTriageResultSchema =
|
|
2836
|
-
accepted:
|
|
2837
|
-
failed:
|
|
3033
|
+
var AcceptTriageResultSchema = z7.object({
|
|
3034
|
+
accepted: z7.array(z7.string()),
|
|
3035
|
+
failed: z7.array(z7.object({ noteId: z7.string(), reason: z7.string() }))
|
|
2838
3036
|
});
|
|
2839
|
-
var DeliveryModeSchema =
|
|
3037
|
+
var DeliveryModeSchema = z7.enum(["poll", "self_hosted"]);
|
|
2840
3038
|
var WAKE_EVENT = "wake";
|
|
2841
3039
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
2842
|
-
var RegisterDeliverySchema =
|
|
2843
|
-
var OAuthStartSchema =
|
|
2844
|
-
provider:
|
|
2845
|
-
returnTo:
|
|
3040
|
+
var RegisterDeliverySchema = z7.object({ mode: DeliveryModeSchema });
|
|
3041
|
+
var OAuthStartSchema = z7.object({
|
|
3042
|
+
provider: z7.enum(["cma"]),
|
|
3043
|
+
returnTo: z7.string().min(1)
|
|
2846
3044
|
});
|
|
2847
|
-
var DeliveryConfigSchema =
|
|
2848
|
-
tokenId:
|
|
3045
|
+
var DeliveryConfigSchema = z7.object({
|
|
3046
|
+
tokenId: z7.string(),
|
|
2849
3047
|
mode: DeliveryModeSchema,
|
|
2850
3048
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
2851
3049
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
2852
3050
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
2853
|
-
realtime:
|
|
3051
|
+
realtime: z7.object({ url: z7.string(), anonKey: z7.string() }).nullable()
|
|
2854
3052
|
});
|
|
2855
|
-
var HostDecisionSchema =
|
|
3053
|
+
var HostDecisionSchema = z7.object({
|
|
2856
3054
|
/** The agent's token id: the row's `recipient`. */
|
|
2857
|
-
agent:
|
|
2858
|
-
decision:
|
|
2859
|
-
/** The work it was about: the Goal
|
|
2860
|
-
goalId:
|
|
3055
|
+
agent: z7.string().uuid(),
|
|
3056
|
+
decision: z7.enum(["stood_back", "took_over"]),
|
|
3057
|
+
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
3058
|
+
goalId: z7.string().uuid().nullable().optional(),
|
|
2861
3059
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
2862
|
-
seenAt:
|
|
3060
|
+
seenAt: z7.string().datetime().nullable().optional(),
|
|
2863
3061
|
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
2864
|
-
since:
|
|
3062
|
+
since: z7.string().datetime().nullable().optional(),
|
|
2865
3063
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
2866
|
-
said:
|
|
3064
|
+
said: z7.string().max(300).optional()
|
|
2867
3065
|
});
|
|
2868
|
-
var WakeNudgeSchema =
|
|
2869
|
-
kind:
|
|
2870
|
-
notificationId:
|
|
2871
|
-
parentId:
|
|
3066
|
+
var WakeNudgeSchema = z7.object({
|
|
3067
|
+
kind: z7.enum(["reply", "request", "callback"]),
|
|
3068
|
+
notificationId: z7.string().optional(),
|
|
3069
|
+
parentId: z7.string()
|
|
2872
3070
|
});
|
|
2873
|
-
var PairingStatusSchema =
|
|
2874
|
-
var DeviceCodeSchema =
|
|
2875
|
-
device_code:
|
|
2876
|
-
user_code:
|
|
2877
|
-
verification_uri:
|
|
2878
|
-
verification_uri_complete:
|
|
2879
|
-
interval:
|
|
2880
|
-
expires_in:
|
|
3071
|
+
var PairingStatusSchema = z7.enum(["pending", "approved", "denied", "expired"]);
|
|
3072
|
+
var DeviceCodeSchema = z7.object({
|
|
3073
|
+
device_code: z7.string(),
|
|
3074
|
+
user_code: z7.string(),
|
|
3075
|
+
verification_uri: z7.string().url(),
|
|
3076
|
+
verification_uri_complete: z7.string().url(),
|
|
3077
|
+
interval: z7.number(),
|
|
3078
|
+
expires_in: z7.number()
|
|
2881
3079
|
});
|
|
2882
|
-
var DeviceInfoSchema =
|
|
2883
|
-
code:
|
|
3080
|
+
var DeviceInfoSchema = z7.object({
|
|
3081
|
+
code: z7.string(),
|
|
2884
3082
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
2885
3083
|
* pre-filling the name field the human can edit. */
|
|
2886
|
-
name:
|
|
3084
|
+
name: z7.string(),
|
|
2887
3085
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
2888
3086
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
2889
3087
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
2890
3088
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
2891
|
-
agent:
|
|
2892
|
-
device:
|
|
3089
|
+
agent: z7.string().optional(),
|
|
3090
|
+
device: z7.string().nullable(),
|
|
2893
3091
|
status: PairingStatusSchema
|
|
2894
3092
|
});
|
|
2895
|
-
var DeviceTokenSchema =
|
|
2896
|
-
access_token:
|
|
3093
|
+
var DeviceTokenSchema = z7.object({
|
|
3094
|
+
access_token: z7.string(),
|
|
2897
3095
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
2898
3096
|
* a default silly name). */
|
|
2899
|
-
name:
|
|
2900
|
-
device:
|
|
3097
|
+
name: z7.string(),
|
|
3098
|
+
device: z7.string().nullable(),
|
|
2901
3099
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
2902
3100
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
2903
|
-
voice:
|
|
3101
|
+
voice: z7.string().nullable().optional(),
|
|
2904
3102
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
2905
3103
|
* that survives a rename. Cached by the host's identity beat. */
|
|
2906
|
-
token_id:
|
|
3104
|
+
token_id: z7.string().nullable().optional(),
|
|
2907
3105
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
2908
3106
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
2909
3107
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
2910
3108
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
2911
|
-
workspace:
|
|
3109
|
+
workspace: z7.string().nullable().optional(),
|
|
2912
3110
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
2913
|
-
harness:
|
|
2914
|
-
session_id:
|
|
3111
|
+
harness: z7.enum(["claude", "codex", "agy"]).optional(),
|
|
3112
|
+
session_id: z7.string().uuid().optional(),
|
|
2915
3113
|
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
2916
3114
|
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
2917
3115
|
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
2918
3116
|
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
2919
|
-
full_session:
|
|
2920
|
-
uik_pub:
|
|
3117
|
+
full_session: z7.string().optional(),
|
|
3118
|
+
uik_pub: z7.string().nullable().optional()
|
|
2921
3119
|
});
|
|
2922
|
-
var SupportRequestSchema =
|
|
2923
|
-
email:
|
|
2924
|
-
message:
|
|
2925
|
-
name:
|
|
3120
|
+
var SupportRequestSchema = z7.object({
|
|
3121
|
+
email: z7.string().email().max(320),
|
|
3122
|
+
message: z7.string().trim().min(1).max(5e3),
|
|
3123
|
+
name: z7.string().trim().max(120).optional()
|
|
2926
3124
|
});
|
|
2927
|
-
var NotificationFeedbackKindSchema =
|
|
3125
|
+
var NotificationFeedbackKindSchema = z7.enum([
|
|
2928
3126
|
"break_down",
|
|
2929
3127
|
// "This should be more than one ask — break it down."
|
|
2930
3128
|
"regenerate_options",
|
|
@@ -2939,116 +3137,116 @@ var NotificationFeedbackKindSchema = z5.enum([
|
|
|
2939
3137
|
// anything else — the note carries it.
|
|
2940
3138
|
]);
|
|
2941
3139
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
2942
|
-
var QuestionRowSchema =
|
|
3140
|
+
var QuestionRowSchema = z7.object({
|
|
2943
3141
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
2944
|
-
id:
|
|
2945
|
-
deliveryId:
|
|
2946
|
-
entryId:
|
|
3142
|
+
id: z7.string(),
|
|
3143
|
+
deliveryId: z7.string(),
|
|
3144
|
+
entryId: z7.string(),
|
|
2947
3145
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
2948
|
-
needId:
|
|
2949
|
-
goalIds:
|
|
3146
|
+
needId: z7.string().nullable(),
|
|
3147
|
+
goalIds: z7.array(z7.string()),
|
|
2950
3148
|
/** The name of the work it is about, when the read could word it. */
|
|
2951
|
-
goalTitle:
|
|
2952
|
-
tokenId:
|
|
2953
|
-
name:
|
|
2954
|
-
title:
|
|
2955
|
-
body:
|
|
2956
|
-
select:
|
|
2957
|
-
options:
|
|
2958
|
-
hasPreview:
|
|
2959
|
-
blocking:
|
|
2960
|
-
askedAt:
|
|
3149
|
+
goalTitle: z7.string().optional(),
|
|
3150
|
+
tokenId: z7.string().optional(),
|
|
3151
|
+
name: z7.string(),
|
|
3152
|
+
title: z7.string(),
|
|
3153
|
+
body: z7.string(),
|
|
3154
|
+
select: z7.enum(["one", "many", "rank", "confirm", "text"]),
|
|
3155
|
+
options: z7.array(SlimOptionSchema),
|
|
3156
|
+
hasPreview: z7.boolean(),
|
|
3157
|
+
blocking: z7.boolean(),
|
|
3158
|
+
askedAt: z7.string().datetime(),
|
|
2961
3159
|
ring: InboxItemSchema.shape.ring,
|
|
2962
|
-
onCall:
|
|
2963
|
-
sealed:
|
|
3160
|
+
onCall: z7.literal(true).optional(),
|
|
3161
|
+
sealed: z7.boolean()
|
|
2964
3162
|
});
|
|
2965
|
-
var WorkStateSchema =
|
|
2966
|
-
var WorkRowSchema =
|
|
2967
|
-
id:
|
|
2968
|
-
parentId:
|
|
2969
|
-
title:
|
|
3163
|
+
var WorkStateSchema = z7.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
3164
|
+
var WorkRowSchema = z7.object({
|
|
3165
|
+
id: z7.string(),
|
|
3166
|
+
parentId: z7.string().nullable(),
|
|
3167
|
+
title: z7.string(),
|
|
2970
3168
|
/** Straight off the Goal. */
|
|
2971
3169
|
state: WorkStateSchema,
|
|
2972
|
-
owner:
|
|
2973
|
-
revision:
|
|
3170
|
+
owner: z7.string().nullable(),
|
|
3171
|
+
revision: z7.number().int(),
|
|
2974
3172
|
/** Open questions on it, counted to 100. */
|
|
2975
|
-
waiting:
|
|
3173
|
+
waiting: z7.number().int(),
|
|
2976
3174
|
/** Held by a gate on work that is not done. */
|
|
2977
|
-
blocked:
|
|
2978
|
-
lastProgressAt:
|
|
3175
|
+
blocked: z7.boolean(),
|
|
3176
|
+
lastProgressAt: z7.string().datetime().nullable(),
|
|
2979
3177
|
/** The line written for its newest progress note, else that note's first words. */
|
|
2980
|
-
line:
|
|
3178
|
+
line: z7.string().nullable(),
|
|
2981
3179
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
2982
|
-
children:
|
|
2983
|
-
createdAt:
|
|
2984
|
-
updatedAt:
|
|
3180
|
+
children: z7.number().int(),
|
|
3181
|
+
createdAt: z7.string().datetime(),
|
|
3182
|
+
updatedAt: z7.string().datetime(),
|
|
2985
3183
|
/** When anything at or under it last moved — the order the list is in. */
|
|
2986
|
-
activeAt:
|
|
3184
|
+
activeAt: z7.string().datetime(),
|
|
2987
3185
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
2988
|
-
sealed:
|
|
3186
|
+
sealed: z7.boolean()
|
|
2989
3187
|
});
|
|
2990
3188
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
2991
3189
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
2992
3190
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
2993
|
-
asking:
|
|
2994
|
-
oldestAskAt:
|
|
3191
|
+
asking: z7.number().int().nullable(),
|
|
3192
|
+
oldestAskAt: z7.string().datetime().nullable(),
|
|
2995
3193
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
2996
3194
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
2997
3195
|
* (`agent_holds`); null when that read failed. */
|
|
2998
|
-
holds:
|
|
2999
|
-
held:
|
|
3196
|
+
holds: z7.array(z7.object({ id: z7.string(), title: z7.string() })).nullable(),
|
|
3197
|
+
held: z7.number().int().nullable(),
|
|
3000
3198
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
3001
3199
|
* while none has, or when that read failed. */
|
|
3002
|
-
cold:
|
|
3200
|
+
cold: z7.string().datetime().nullable(),
|
|
3003
3201
|
/** The newest line of its working log, and when the harness saw it. */
|
|
3004
|
-
line:
|
|
3005
|
-
lineAt:
|
|
3202
|
+
line: z7.string().nullable(),
|
|
3203
|
+
lineAt: z7.string().datetime().nullable()
|
|
3006
3204
|
});
|
|
3007
|
-
var SnapshotSchema =
|
|
3205
|
+
var SnapshotSchema = z7.object({
|
|
3008
3206
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
3009
|
-
at:
|
|
3010
|
-
questions:
|
|
3207
|
+
at: z7.string().datetime(),
|
|
3208
|
+
questions: z7.object({
|
|
3011
3209
|
/** The newest 30 open cards, questions before updates. */
|
|
3012
|
-
items:
|
|
3210
|
+
items: z7.array(QuestionRowSchema),
|
|
3013
3211
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
3014
|
-
total:
|
|
3015
|
-
updates:
|
|
3016
|
-
putOff:
|
|
3212
|
+
total: z7.number().int(),
|
|
3213
|
+
updates: z7.number().int(),
|
|
3214
|
+
putOff: z7.number().int()
|
|
3017
3215
|
}).nullable(),
|
|
3018
|
-
agents:
|
|
3216
|
+
agents: z7.object({
|
|
3019
3217
|
/** Up to 60, most recently seen first. */
|
|
3020
|
-
items:
|
|
3021
|
-
more:
|
|
3218
|
+
items: z7.array(AgentRowSchema),
|
|
3219
|
+
more: z7.boolean()
|
|
3022
3220
|
}).nullable(),
|
|
3023
|
-
work:
|
|
3221
|
+
work: z7.object({
|
|
3024
3222
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
3025
|
-
items:
|
|
3223
|
+
items: z7.array(WorkRowSchema),
|
|
3026
3224
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
3027
3225
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
3028
3226
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
3029
3227
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
3030
|
-
counts:
|
|
3228
|
+
counts: z7.object({ live: z7.number().int(), waiting: z7.number().int(), notStarted: z7.number().int(), done: z7.number().int() })
|
|
3031
3229
|
}).nullable(),
|
|
3032
|
-
you:
|
|
3230
|
+
you: z7.object({
|
|
3033
3231
|
settings: UserSettingsSchema,
|
|
3034
|
-
callable:
|
|
3232
|
+
callable: z7.boolean(),
|
|
3035
3233
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
3036
|
-
computers:
|
|
3234
|
+
computers: z7.array(ComputerRowSchema).nullable()
|
|
3037
3235
|
}).nullable()
|
|
3038
3236
|
});
|
|
3039
|
-
var CallRecapSchema =
|
|
3040
|
-
call:
|
|
3041
|
-
status:
|
|
3042
|
-
startedAt:
|
|
3043
|
-
durationMs:
|
|
3044
|
-
agents:
|
|
3237
|
+
var CallRecapSchema = z7.object({
|
|
3238
|
+
call: z7.object({
|
|
3239
|
+
status: z7.string(),
|
|
3240
|
+
startedAt: z7.string(),
|
|
3241
|
+
durationMs: z7.number().nullable(),
|
|
3242
|
+
agents: z7.array(z7.object({ id: z7.string(), name: z7.string().nullable() }))
|
|
3045
3243
|
}),
|
|
3046
|
-
topics:
|
|
3047
|
-
goalId:
|
|
3048
|
-
title:
|
|
3049
|
-
owner:
|
|
3050
|
-
state:
|
|
3051
|
-
questions:
|
|
3244
|
+
topics: z7.array(z7.object({
|
|
3245
|
+
goalId: z7.string().uuid(),
|
|
3246
|
+
title: z7.string(),
|
|
3247
|
+
owner: z7.string(),
|
|
3248
|
+
state: z7.string(),
|
|
3249
|
+
questions: z7.array(z7.object({ id: z7.string().uuid(), state: z7.string(), title: z7.string() })),
|
|
3052
3250
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
3053
3251
|
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
3054
3252
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
@@ -3056,10 +3254,10 @@ var CallRecapSchema = z5.object({
|
|
|
3056
3254
|
* anything that is not an answer. */
|
|
3057
3255
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
3058
3256
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
3059
|
-
lines:
|
|
3257
|
+
lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), headline: z7.string().optional(), about: z7.string().nullable().optional() }))
|
|
3060
3258
|
})),
|
|
3061
|
-
unfiled:
|
|
3062
|
-
more:
|
|
3259
|
+
unfiled: z7.array(z7.object({ lineId: z7.string().uuid(), words: z7.string(), atMs: z7.number() })),
|
|
3260
|
+
more: z7.object({ lines: z7.number(), entries: z7.number(), topics: z7.number() })
|
|
3063
3261
|
});
|
|
3064
3262
|
function sessionSlot(sessionId2) {
|
|
3065
3263
|
const id2 = sessionId2 ?? sessionId();
|
|
@@ -3237,12 +3435,6 @@ async function manageGoals(input, opts = {}) {
|
|
|
3237
3435
|
if (!res.ok) await fail("manage_goals", res);
|
|
3238
3436
|
return await res.json();
|
|
3239
3437
|
}
|
|
3240
|
-
async function claimGoal(goalId, opts = {}) {
|
|
3241
|
-
const token = authToken(opts.token) ?? "";
|
|
3242
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
3243
|
-
if (!res.ok) await fail("claim_goal", res);
|
|
3244
|
-
return await res.json();
|
|
3245
|
-
}
|
|
3246
3438
|
async function getGoal(goalId, opts = {}, read3 = {}) {
|
|
3247
3439
|
const token = authToken(opts.token) ?? "";
|
|
3248
3440
|
const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
@@ -3253,13 +3445,6 @@ async function getGoal(goalId, opts = {}, read3 = {}) {
|
|
|
3253
3445
|
if (!res.ok) await fail("get_goal", res);
|
|
3254
3446
|
return await res.json();
|
|
3255
3447
|
}
|
|
3256
|
-
async function updateGoal(goalId, input, opts = {}) {
|
|
3257
|
-
const token = authToken(opts.token) ?? "";
|
|
3258
|
-
const operationId = input.operationId ?? randomUUID2();
|
|
3259
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ ...input, operationId }) }));
|
|
3260
|
-
if (!res.ok) await fail("update_goal", res);
|
|
3261
|
-
return await res.json();
|
|
3262
|
-
}
|
|
3263
3448
|
var AWAIT_WINDOW_MS = 45e3;
|
|
3264
3449
|
async function hatch(name, voice = null, opts = {}) {
|
|
3265
3450
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/hatch`, {
|
|
@@ -3419,106 +3604,83 @@ async function searchRecords(input, opts = {}) {
|
|
|
3419
3604
|
if (!res.ok) await fail("search", res);
|
|
3420
3605
|
return await res.json();
|
|
3421
3606
|
}
|
|
3422
|
-
async function
|
|
3607
|
+
async function sendFeedback(input, opts = {}) {
|
|
3608
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/reports`, {
|
|
3609
|
+
method: "POST",
|
|
3610
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3611
|
+
body: JSON.stringify(input)
|
|
3612
|
+
}));
|
|
3613
|
+
if (!res.ok) await fail("send_feedback", res);
|
|
3614
|
+
return await res.json();
|
|
3615
|
+
}
|
|
3616
|
+
async function checkActivity(opts = {}) {
|
|
3423
3617
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
|
|
3424
3618
|
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3425
3619
|
}));
|
|
3426
|
-
if (!res.ok) await fail("
|
|
3620
|
+
if (!res.ok) await fail("check_activity", res);
|
|
3427
3621
|
return await res.json();
|
|
3428
3622
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
const
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
var asked;
|
|
3440
|
-
function currentRepo(cwd = process.cwd()) {
|
|
3441
|
-
if (asked !== void 0) return asked;
|
|
3442
|
-
try {
|
|
3443
|
-
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
3444
|
-
cwd,
|
|
3445
|
-
encoding: "utf8",
|
|
3446
|
-
timeout: 2e3,
|
|
3447
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
3448
|
-
});
|
|
3449
|
-
asked = repoFromRemote(url);
|
|
3450
|
-
} catch {
|
|
3451
|
-
asked = null;
|
|
3452
|
-
}
|
|
3453
|
-
return asked;
|
|
3623
|
+
var NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, mark the Goal done first, then send the one update saying what is done and anything they need to do or check. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
|
|
3624
|
+
var APPLIED = "Applied; nothing was sent to the person.";
|
|
3625
|
+
var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again.";
|
|
3626
|
+
async function readDelivery(deliveryId, opts = {}) {
|
|
3627
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, {
|
|
3628
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3629
|
+
signal: opts.signal
|
|
3630
|
+
}));
|
|
3631
|
+
if (!res.ok) await fail("read_delivery", res);
|
|
3632
|
+
return await res.json();
|
|
3454
3633
|
}
|
|
3455
|
-
var NOT_SENT = "Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, mark the Goal done first, then send the one message saying what is done and anything they need to do or check.";
|
|
3456
|
-
var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again, with or without a Goal.";
|
|
3457
3634
|
async function contact(input, opts = {}) {
|
|
3458
3635
|
const parsed = ContactSchema.parse(input);
|
|
3459
|
-
if (!(
|
|
3636
|
+
if (!sendsAnything(parsed)) throw new Error("contact with nothing to send receives: call receiveContact");
|
|
3460
3637
|
opts.signal?.throwIfAborted();
|
|
3461
3638
|
const send2 = opts.reach ?? reach;
|
|
3462
|
-
const
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
waiting: parsed.waiting,
|
|
3491
|
-
channel: parsed.channel === "call" ? "call" : "message"
|
|
3492
|
-
})
|
|
3493
|
-
}));
|
|
3494
|
-
if (!res.ok) await fail("batch contact", res);
|
|
3495
|
-
const receipt = await res.json();
|
|
3496
|
-
deliveries = receipt.deliveries ?? [];
|
|
3497
|
-
if (parsed.channel === "call" && receipt.channel && receipt.channel !== "call") demoted = DEMOTED;
|
|
3498
|
-
recorded = receipt.recorded ?? [];
|
|
3499
|
-
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
3500
|
-
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
3501
|
-
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
3502
|
-
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
3503
|
-
}
|
|
3504
|
-
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id: id2, deliveryId: deliveryId2, goalId }) => ({ ...id2 ? { id: id2 } : {}, deliveryId: deliveryId2, goalId }));
|
|
3505
|
-
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
3506
|
-
const joinedCall = deliveries[0]?.joinedCall === true;
|
|
3639
|
+
const acknowledged = parsed.ackEventIds?.length ? await acknowledge(parsed.ackEventIds, opts) : void 0;
|
|
3640
|
+
const questions = parsed.questions?.map((q) => ({ ...q, questionId: q.questionId ?? randomUUID2() }));
|
|
3641
|
+
const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/goals/contact`, {
|
|
3642
|
+
method: "POST",
|
|
3643
|
+
signal: opts.signal,
|
|
3644
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3645
|
+
body: JSON.stringify({
|
|
3646
|
+
operationId: opts.operationId ?? randomUUID2(),
|
|
3647
|
+
...parsed.updates ? { updates: parsed.updates } : {},
|
|
3648
|
+
...questions ? { questions } : {},
|
|
3649
|
+
...parsed.answers ? { answers: parsed.answers } : {},
|
|
3650
|
+
...parsed.withdrawQuestionIds ? { withdrawQuestionIds: parsed.withdrawQuestionIds } : {},
|
|
3651
|
+
...parsed.questionDependencies ? { questionDependencies: parsed.questionDependencies } : {}
|
|
3652
|
+
})
|
|
3653
|
+
}));
|
|
3654
|
+
if (!res.ok) await fail("contact", res);
|
|
3655
|
+
const receipt = await res.json();
|
|
3656
|
+
const results = receipt.results ?? [];
|
|
3657
|
+
const ok = receipt.ok ?? results.every((r) => r.status !== "failed");
|
|
3658
|
+
const deliveries = receipt.deliveries ?? [];
|
|
3659
|
+
const recorded = results.filter((r) => r.status === "recorded" && r.goalId).map((r) => ({ goalId: r.goalId }));
|
|
3660
|
+
const askedCall = [...parsed.updates ?? [], ...questions ?? []].some((m) => m.userExplicitlyRequested === "call") || !!questions?.some((q) => q.workItBlocks?.length);
|
|
3661
|
+
const demoted = askedCall && receipt.channel && receipt.channel !== "call" ? DEMOTED : void 0;
|
|
3662
|
+
const acked = acknowledged ? { acknowledged } : {};
|
|
3663
|
+
if (!deliveries.length) {
|
|
3664
|
+
return { sent: false, ok, results, ...recorded.length ? { recorded } : {}, message: recorded.length ? NOT_SENT : APPLIED, waitOutcome: "not_waited", ...acked };
|
|
3665
|
+
}
|
|
3666
|
+
const lead = deliveries.find((d) => d.kind === "question") ?? deliveries[0];
|
|
3507
3667
|
const read3 = async (signal2) => {
|
|
3508
|
-
const
|
|
3509
|
-
if (!res.ok) await fail("read_delivery", res);
|
|
3510
|
-
const one = await res.json();
|
|
3668
|
+
const one = await readDelivery(lead.deliveryId, { ...opts, signal: signal2 });
|
|
3511
3669
|
return {
|
|
3512
3670
|
...one,
|
|
3513
|
-
|
|
3671
|
+
ok,
|
|
3672
|
+
results,
|
|
3673
|
+
...deliveries.length > 1 ? { deliveries: deliveries.map(({ deliveryId, goalId, kind, questionId }) => ({ deliveryId, goalId, kind, ...questionId ? { questionId } : {} })) } : {},
|
|
3514
3674
|
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
3515
|
-
...joinedCard ? { joinedCard: true } : {},
|
|
3516
|
-
|
|
3517
|
-
...
|
|
3675
|
+
...lead.joinedCard ? { joinedCard: true } : {},
|
|
3676
|
+
// IT WENT ONTO A CALL ALREADY HAPPENING (owner, 2026-09-28): the view words it as that call.
|
|
3677
|
+
...lead.joinedCall ? { joinedCall: true } : {},
|
|
3678
|
+
...demoted ? { demoted } : {},
|
|
3679
|
+
...acked
|
|
3518
3680
|
};
|
|
3519
3681
|
};
|
|
3520
|
-
const settled = (d) => d.
|
|
3521
|
-
if (opts.waits === false) return read3(opts.signal);
|
|
3682
|
+
const settled = (d) => d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution" && !entryUnit(e));
|
|
3683
|
+
if (parsed.wait !== true || opts.waits === false) return { ...await read3(opts.signal), waitOutcome: "not_waited" };
|
|
3522
3684
|
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
3523
3685
|
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
3524
3686
|
let latest;
|
|
@@ -3526,12 +3688,12 @@ async function contact(input, opts = {}) {
|
|
|
3526
3688
|
while (true) {
|
|
3527
3689
|
signal.throwIfAborted();
|
|
3528
3690
|
latest = await read3(signal);
|
|
3529
|
-
if (settled(latest)) return latest;
|
|
3691
|
+
if (settled(latest)) return { ...latest, waitOutcome: "available" };
|
|
3530
3692
|
await sleep2(5e3, void 0, { signal });
|
|
3531
3693
|
}
|
|
3532
3694
|
} catch (error) {
|
|
3533
3695
|
opts.signal?.throwIfAborted();
|
|
3534
|
-
if (window.aborted && latest) return latest;
|
|
3696
|
+
if (window.aborted && latest) return { ...latest, waitOutcome: "expired" };
|
|
3535
3697
|
throw error;
|
|
3536
3698
|
}
|
|
3537
3699
|
}
|
|
@@ -3628,8 +3790,9 @@ var handle = (entryId) => entryId.slice(0, 8);
|
|
|
3628
3790
|
function conversation(e) {
|
|
3629
3791
|
const answers = new Map((e.answers ?? []).map((a) => [a.decisionNeedId, a]));
|
|
3630
3792
|
const shown = new Set((e.entries ?? []).map((entry) => entry.entryId));
|
|
3631
|
-
const
|
|
3632
|
-
|
|
3793
|
+
const entries = (e.entries ?? []).filter((entry) => !entryUnit(entry));
|
|
3794
|
+
const repliedTo = new Set(entries.flatMap((entry) => entry.aboutId && shown.has(entry.aboutId) ? [entry.aboutId] : []));
|
|
3795
|
+
return entries.map((entry) => {
|
|
3633
3796
|
const sealed = !!entry.content && "sealed" in entry.content;
|
|
3634
3797
|
const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
|
|
3635
3798
|
const options = plain && typeof plain === "object" && Array.isArray(plain.options) ? plain.options.map((o) => o.label).filter((l) => typeof l === "string") : [];
|
|
@@ -3642,6 +3805,7 @@ function conversation(e) {
|
|
|
3642
3805
|
from: who(entry.authorParticipant),
|
|
3643
3806
|
at: at(entry.createdAt),
|
|
3644
3807
|
said: sealed ? "[encrypted]" : entryWords(entry).trim(),
|
|
3808
|
+
units: askUnits(entry, e.entries ?? []).map((u) => `${u.title}: ${u.body}`),
|
|
3645
3809
|
re: entry.aboutId && shown.has(entry.aboutId) ? handle(entry.aboutId) : void 0,
|
|
3646
3810
|
options,
|
|
3647
3811
|
decision: need ? compact({ state: stateOf(need.state, answer, decision), answer: decision }) : void 0
|
|
@@ -3661,7 +3825,7 @@ function goalView(g) {
|
|
|
3661
3825
|
if (!g.goalId) return compact({ state: g.state, next: g.message });
|
|
3662
3826
|
const lines2 = conversation(g);
|
|
3663
3827
|
const owed = lines2.filter((l) => l.from === "person" && l.decision?.state === "open" && l.id);
|
|
3664
|
-
const owes = owed.length ? `The person asked you ${owed.length === 1 ? "a question" : `${owed.length} questions`} (${owed.map((l) => `id ${l.id}: "${l.said.slice(0, 80)}"`).join("; ")}) \u2014 answer with contact({
|
|
3828
|
+
const owes = owed.length ? `The person asked you ${owed.length === 1 ? "a question" : `${owed.length} questions`} (${owed.map((l) => `id ${l.id}: "${l.said.slice(0, 80)}"`).join("; ")}) \u2014 answer with contact({ answers: [{ questionId: "<id>", answer: { text: <your answer> } }] }). ` : "";
|
|
3665
3829
|
const newest = [...lines2].reverse().find((l) => l.from === "agent");
|
|
3666
3830
|
return compact({
|
|
3667
3831
|
goalId: g.goalId,
|
|
@@ -3671,6 +3835,8 @@ function goalView(g) {
|
|
|
3671
3835
|
progress: g.progress && g.progress.trim() !== newest?.said ? g.progress : void 0,
|
|
3672
3836
|
reviewPending: g.reviewPending || void 0,
|
|
3673
3837
|
reviewSince: g.reviewSince,
|
|
3838
|
+
// What acknowledges the review: contact({ ackEventIds: [reviewEventId] }) (update_goal's reviewed: true is gone).
|
|
3839
|
+
reviewEventId: g.reviewEventId,
|
|
3674
3840
|
owner: g.ownerParticipant,
|
|
3675
3841
|
others: g.others,
|
|
3676
3842
|
parentGoalId: g.parentGoalId,
|
|
@@ -3692,26 +3858,67 @@ function goalView(g) {
|
|
|
3692
3858
|
more: more(g)
|
|
3693
3859
|
});
|
|
3694
3860
|
}
|
|
3695
|
-
var JOINED_CALL = "This
|
|
3861
|
+
var JOINED_CALL = "This joined the call the person is ALREADY ON \u2014 no ring, no card of its own. `conversation` above is that whole call, so the answers already given on it before yours arrived are in it. Yours is queued for the bot's next turn: wait for the answer with contact({wait:true}) and read it on its Goal with get_goal \u2014 do not resend it. To add information or a further question to the same call, contact again while it is live; that joins it too.";
|
|
3862
|
+
function failures(results) {
|
|
3863
|
+
const failed = (results ?? []).filter((r) => r.status === "failed");
|
|
3864
|
+
if (!failed.length) return void 0;
|
|
3865
|
+
const plural = { update: "updates", question: "questions", answer: "answers", withdraw: "withdrawQuestionIds", dependency: "questionDependencies" };
|
|
3866
|
+
return `${failed.length} of ${(results ?? []).length} items did not apply; the rest did: ` + failed.map((r) => `${plural[r.kind]}[${r.index}] ${r.error}${r.detail ? ` (${r.detail})` : ""}`).join("; ") + ".";
|
|
3867
|
+
}
|
|
3868
|
+
var itemsView = (results) => (results ?? []).map((r) => compact({
|
|
3869
|
+
kind: r.kind,
|
|
3870
|
+
index: r.index,
|
|
3871
|
+
status: r.status,
|
|
3872
|
+
questionId: r.questionId,
|
|
3873
|
+
goalId: r.goalId,
|
|
3874
|
+
error: r.error,
|
|
3875
|
+
detail: r.detail
|
|
3876
|
+
}));
|
|
3696
3877
|
function deliveryView(d) {
|
|
3697
|
-
const answers = "Answers land on the Goal:
|
|
3878
|
+
const answers = "Answers land on the Goal: collect them with contact({wait:true}) or get_goal. Do not resend it.";
|
|
3879
|
+
const pending = d.state === "open" && d.decisionNeeds.some((n) => n.state === "open");
|
|
3698
3880
|
return compact({
|
|
3881
|
+
ok: d.ok,
|
|
3882
|
+
results: itemsView(d.results),
|
|
3699
3883
|
deliveryId: d.deliveryId,
|
|
3700
3884
|
kind: d.kind,
|
|
3701
3885
|
state: d.state,
|
|
3702
3886
|
goalIds: d.goalIds,
|
|
3703
3887
|
conversation: conversation(d),
|
|
3704
|
-
// Each
|
|
3888
|
+
// Each item's own Delivery when a contact reached several.
|
|
3705
3889
|
deliveries: d.deliveries,
|
|
3706
|
-
// WHAT THIS CONTACT DID NOT SEND (#2450):
|
|
3890
|
+
// WHAT THIS CONTACT DID NOT SEND (#2450): updates kept as progress, by Goal.
|
|
3707
3891
|
recorded: d.recorded,
|
|
3708
3892
|
notSent: d.notSent,
|
|
3709
3893
|
joinedCard: d.joinedCard,
|
|
3710
3894
|
joinedCall: d.joinedCall,
|
|
3711
|
-
|
|
3895
|
+
waitOutcome: d.waitOutcome,
|
|
3896
|
+
acknowledged: d.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3897
|
+
next: [
|
|
3898
|
+
failures(d.results),
|
|
3899
|
+
d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? `${d.demoted ? `${d.demoted} ` : ""}${d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers}` : pending ? `${d.demoted ? `${d.demoted} ` : ""}Decision pending on this call. ${d.waitOutcome === "expired" ? "Nothing was said in the window. " : ""}Wait for the answer with contact({wait:true}) \u2014 one bounded window each time, and never resend the question. When a window comes back with nothing new, they are not typing: stop waiting, leave the question open, and collect the answer with contact({wait:false}) or get_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3900
|
+
].filter(Boolean).join(" ")
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
function notSentView(n) {
|
|
3904
|
+
return compact({
|
|
3905
|
+
sent: false,
|
|
3906
|
+
ok: n.ok,
|
|
3907
|
+
results: itemsView(n.results),
|
|
3908
|
+
recorded: n.recorded,
|
|
3909
|
+
acknowledged: n.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3910
|
+
next: [failures(n.results), n.message].filter(Boolean).join(" ")
|
|
3712
3911
|
});
|
|
3713
3912
|
}
|
|
3714
|
-
function
|
|
3913
|
+
function quietDays(items, now) {
|
|
3914
|
+
const oldest = Math.min(...items.map((i) => Date.parse(i.since ?? "")).filter(Number.isFinite));
|
|
3915
|
+
return Number.isFinite(oldest) ? Math.max(3, Math.round((now - oldest) / 864e5)) : 3;
|
|
3916
|
+
}
|
|
3917
|
+
var quietFor = (items, now) => {
|
|
3918
|
+
const days = quietDays(items, now);
|
|
3919
|
+
return items.length === 1 ? `for ${days} days` : `, the longest for ${days} days`;
|
|
3920
|
+
};
|
|
3921
|
+
function receivedView(r, now = Date.now()) {
|
|
3715
3922
|
const w = r.waiting;
|
|
3716
3923
|
const assigned = w.assigned ?? [];
|
|
3717
3924
|
const stalled = w.stalled ?? [];
|
|
@@ -3726,21 +3933,21 @@ function receivedView(r) {
|
|
|
3726
3933
|
notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
|
|
3727
3934
|
// WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
|
|
3728
3935
|
assigned,
|
|
3729
|
-
//
|
|
3730
|
-
// sleeping agent, so "nothing is waiting" is never said while
|
|
3936
|
+
// THE WORK WAITING ON YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
|
|
3937
|
+
// sleeping agent, so "nothing is waiting" is never said while work is.
|
|
3731
3938
|
claimable: w.claimable,
|
|
3732
3939
|
// YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
|
|
3733
3940
|
stalled,
|
|
3734
3941
|
stalledOthers: others,
|
|
3735
3942
|
next: [
|
|
3736
3943
|
r.events.length ? `Handle each event, then acknowledge the ones you handled: contact({ ackEventIds: [${r.events.filter((e) => e.kind !== "question").map((e) => `"${e.eventId}"`).join(", ")}] }).` : w.claimable || assigned.length ? "No messages." : "Nothing is waiting.",
|
|
3737
|
-
...owed.length ? [`You owe ${owed.length === 1 ? "an answer" : `${owed.length} answers`}: contact({
|
|
3944
|
+
...owed.length ? [`You owe ${owed.length === 1 ? "an answer" : `${owed.length} answers`}: contact({ answers: [{ questionId: "<questionId>", answer: { text: <your answer> } }] }) for each question; a question is answered, never acknowledged.`] : [],
|
|
3738
3945
|
...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
|
|
3739
3946
|
...r.waitOutcome === "expired" ? ["Nothing arrived in the window; receive again to keep waiting, without sending again."] : [],
|
|
3740
|
-
...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned):
|
|
3741
|
-
...w.claimable && !assigned.length ? [`
|
|
3742
|
-
...stalled.length ? [`${stalled.length} of your Goals have had no progress
|
|
3743
|
-
...others.length ? [`${others.length} of other agents' Goals have gone quiet
|
|
3947
|
+
...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): read one with get_goal({goalId}); your first write to it starts it.`] : [],
|
|
3948
|
+
...w.claimable && !assigned.length ? [`Next waiting on you: ${w.claimable.title ?? w.claimable.goalId} (get_goal({ goalId: "${w.claimable.goalId}" })).`] : [],
|
|
3949
|
+
...stalled.length ? [`${stalled.length} of your Goals have had no progress${stalled.length === 1 ? " " : ""}${quietFor(stalled, now)}: report on each with a contact update, or finish or cancel it with manage_goals.`] : [],
|
|
3950
|
+
...others.length ? [`${others.length} of other agents' Goals have gone quiet${others.length === 1 ? " " : ""}${quietFor(others, now)}: read one with get_goal({goalId}); a write to it puts you on it without changing its owner.`] : [],
|
|
3744
3951
|
// No name of its own (#2525): the API's ask, verbatim.
|
|
3745
3952
|
...w.unnamed ? [w.unnamed] : []
|
|
3746
3953
|
].join(" ")
|
|
@@ -3773,13 +3980,15 @@ async function runTool(name, args, opts) {
|
|
|
3773
3980
|
switch (name) {
|
|
3774
3981
|
case "contact": {
|
|
3775
3982
|
const parsed = ContactSchema.parse(input);
|
|
3776
|
-
if (!(
|
|
3983
|
+
if (!sendsAnything(parsed)) return receivedView(await receiveContact(parsed, { ...client, signal, waits }));
|
|
3777
3984
|
const sent = await contact(parsed, { ...client, signal, waits });
|
|
3778
|
-
return "sent" in sent ? sent : deliveryView(sent);
|
|
3985
|
+
return "sent" in sent ? notSentView(sent) : deliveryView(sent);
|
|
3779
3986
|
}
|
|
3780
|
-
case "
|
|
3781
|
-
|
|
3782
|
-
return
|
|
3987
|
+
case "check_activity":
|
|
3988
|
+
CheckActivitySchema.parse(input);
|
|
3989
|
+
return checkActivity(client);
|
|
3990
|
+
case "send_feedback":
|
|
3991
|
+
return sendFeedback(SendFeedbackSchema.parse(input), client);
|
|
3783
3992
|
case "manage_goals": {
|
|
3784
3993
|
const { changes } = ManageGoalsToolSchema.parse(input);
|
|
3785
3994
|
const versioned = changes.map((c) => {
|
|
@@ -3787,14 +3996,10 @@ async function runTool(name, args, opts) {
|
|
|
3787
3996
|
const revision = lastRead.get(readKey(client.token, c.goalId));
|
|
3788
3997
|
return revision === void 0 ? c : { ...c, revision };
|
|
3789
3998
|
});
|
|
3790
|
-
const managed = await manageGoals({ requestId:
|
|
3999
|
+
const managed = await manageGoals({ requestId: randomUUID3(), changes: versioned }, client);
|
|
3791
4000
|
for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
|
|
3792
4001
|
return managedView(managed, changes);
|
|
3793
4002
|
}
|
|
3794
|
-
case "claim_goal": {
|
|
3795
|
-
const { goalId } = ClaimGoalSchema.parse(input);
|
|
3796
|
-
return goalView(read2(client.token, await claimGoal(goalId, client)));
|
|
3797
|
-
}
|
|
3798
4003
|
case "get_goal": {
|
|
3799
4004
|
const { goalId, history, diagnose } = GetGoalSchema.parse(input);
|
|
3800
4005
|
const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
|
|
@@ -3802,10 +4007,6 @@ async function runTool(name, args, opts) {
|
|
|
3802
4007
|
const diagnosis = goal.diagnosis;
|
|
3803
4008
|
return diagnosis === void 0 ? view : { ...view, diagnosis };
|
|
3804
4009
|
}
|
|
3805
|
-
case "update_goal": {
|
|
3806
|
-
const { goalId, ...body } = UpdateGoalToolSchema.parse(input);
|
|
3807
|
-
return goalView(read2(client.token, await updateGoal(goalId, body, client)));
|
|
3808
|
-
}
|
|
3809
4010
|
case "search":
|
|
3810
4011
|
return searchRecords(SearchToolSchema.parse(input), client);
|
|
3811
4012
|
default:
|
|
@@ -3989,6 +4190,32 @@ function machineZone() {
|
|
|
3989
4190
|
return null;
|
|
3990
4191
|
}
|
|
3991
4192
|
}
|
|
4193
|
+
function repoFromRemote(url) {
|
|
4194
|
+
const said = (url ?? "").trim();
|
|
4195
|
+
if (!said) return null;
|
|
4196
|
+
const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
4197
|
+
const parts = path.split("/").filter(Boolean);
|
|
4198
|
+
if (parts.length < 2) return null;
|
|
4199
|
+
const [owner, name] = parts.slice(-2);
|
|
4200
|
+
if (!owner || !name) return null;
|
|
4201
|
+
return `${owner}/${name}`.toLowerCase();
|
|
4202
|
+
}
|
|
4203
|
+
var asked;
|
|
4204
|
+
function currentRepo(cwd = process.cwd()) {
|
|
4205
|
+
if (asked !== void 0) return asked;
|
|
4206
|
+
try {
|
|
4207
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
4208
|
+
cwd,
|
|
4209
|
+
encoding: "utf8",
|
|
4210
|
+
timeout: 2e3,
|
|
4211
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4212
|
+
});
|
|
4213
|
+
asked = repoFromRemote(url);
|
|
4214
|
+
} catch {
|
|
4215
|
+
asked = null;
|
|
4216
|
+
}
|
|
4217
|
+
return asked;
|
|
4218
|
+
}
|
|
3992
4219
|
function withCodexEnv(text) {
|
|
3993
4220
|
const head = /^\[mcp_servers\.paigy\][ \t]*(?:#[^\n]*)?\r?\n/m.exec(text);
|
|
3994
4221
|
if (!head) return null;
|
|
@@ -4047,9 +4274,7 @@ export {
|
|
|
4047
4274
|
overrideToken,
|
|
4048
4275
|
authToken,
|
|
4049
4276
|
manageGoals,
|
|
4050
|
-
claimGoal,
|
|
4051
4277
|
getGoal,
|
|
4052
|
-
updateGoal,
|
|
4053
4278
|
AWAIT_WINDOW_MS,
|
|
4054
4279
|
hatch,
|
|
4055
4280
|
whoAmI,
|
|
@@ -4068,9 +4293,8 @@ export {
|
|
|
4068
4293
|
acceptTriage,
|
|
4069
4294
|
dismissTriage,
|
|
4070
4295
|
searchRecords,
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
currentRepo,
|
|
4296
|
+
checkActivity,
|
|
4297
|
+
readDelivery,
|
|
4074
4298
|
contact,
|
|
4075
4299
|
receive,
|
|
4076
4300
|
acknowledge,
|
|
@@ -4079,5 +4303,7 @@ export {
|
|
|
4079
4303
|
runTool,
|
|
4080
4304
|
subscribeWake,
|
|
4081
4305
|
parseDue,
|
|
4306
|
+
repoFromRemote,
|
|
4307
|
+
currentRepo,
|
|
4082
4308
|
withCodexEnv
|
|
4083
4309
|
};
|