@paigy/mcp 0.40.24 → 0.40.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/{chunk-J7T3EB5Y.js → chunk-5MQ3J6S3.js} +3 -3
- package/dist/{chunk-VUKR2E24.js → chunk-7LQKX73K.js} +788 -539
- package/dist/{chunk-TP7DM2MP.js → chunk-BLWC44CQ.js} +1 -1
- package/dist/{chunk-WR2M3RME.js → chunk-OVSAQW3K.js} +1092 -712
- package/dist/{dist-EKLTLANV.js → dist-WGFUXFS4.js} +15 -7
- package/dist/enable.js +3 -3
- package/dist/index.js +16 -8
- package/dist/listen.js +9 -8
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +3 -3
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -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 {
|
|
@@ -1424,7 +1431,7 @@ var SPOKEN_HOUR = {
|
|
|
1424
1431
|
eleven: 11,
|
|
1425
1432
|
twelve: 12
|
|
1426
1433
|
};
|
|
1427
|
-
function parseWhen(said2, tz, nowMs,
|
|
1434
|
+
function parseWhen(said2, tz, nowMs, read22 = "earliest") {
|
|
1428
1435
|
const text = said2.trim();
|
|
1429
1436
|
if (!text) return null;
|
|
1430
1437
|
const rel = RELATIVE.exec(text);
|
|
@@ -1463,13 +1470,13 @@ function parseWhen(said2, tz, nowMs, read = "earliest") {
|
|
|
1463
1470
|
if (hour === 12) hour = 0;
|
|
1464
1471
|
} else if (hour < 12 && dayShift === 0 && hour <= today.h) hour += 12;
|
|
1465
1472
|
} else {
|
|
1466
|
-
const parts = PARTS[
|
|
1473
|
+
const parts = PARTS[read22];
|
|
1467
1474
|
const part = Object.keys(parts).find((k) => new RegExp(`\\b${k}\\b`, "i").test(text));
|
|
1468
1475
|
if (/\btonight\b/i.test(text)) hour = parts["night"];
|
|
1469
1476
|
else if (part) hour = parts[part];
|
|
1470
1477
|
else if (dayShift > 0) hour = 9;
|
|
1471
1478
|
else if (/\bend of (the )?day\b|\beod\b/i.test(text)) hour = END_OF_DAY;
|
|
1472
|
-
else if (
|
|
1479
|
+
else if (read22 === "ring" && VAGUE.test(text)) return new Date(nowMs + 30 * 6e4).toISOString();
|
|
1473
1480
|
else return null;
|
|
1474
1481
|
}
|
|
1475
1482
|
const at2 = instantAt(tz, {
|
|
@@ -1492,8 +1499,8 @@ function parseWhen(said2, tz, nowMs, read = "earliest") {
|
|
|
1492
1499
|
}) : at2;
|
|
1493
1500
|
return new Date(rolled ?? at2).toISOString();
|
|
1494
1501
|
}
|
|
1495
|
-
function parseDelay(said2, tz, nowMs,
|
|
1496
|
-
const at2 = parseWhen(said2, tz, nowMs,
|
|
1502
|
+
function parseDelay(said2, tz, nowMs, read22 = "earliest") {
|
|
1503
|
+
const at2 = parseWhen(said2, tz, nowMs, read22);
|
|
1497
1504
|
if (!at2) return null;
|
|
1498
1505
|
return Math.max(0, Math.round((Date.parse(at2) - nowMs) / 1e3));
|
|
1499
1506
|
}
|
|
@@ -1527,33 +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
|
-
|
|
1543
|
-
|
|
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.'
|
|
1618
|
+
),
|
|
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."
|
|
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."
|
|
1544
1661
|
),
|
|
1545
|
-
|
|
1546
|
-
"
|
|
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. */
|
|
1670
|
+
ackEventIds: z2.array(z2.string().uuid()).min(1).max(100).optional().describe(
|
|
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`."
|
|
1547
1675
|
)
|
|
1548
1676
|
}).strict();
|
|
1549
|
-
var
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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");
|
|
1555
1683
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
1556
|
-
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\`; claim_goal starts it) and your work gone quiet (\`stalled\`).`;
|
|
1557
1693
|
var CreateGoalSchema = z3.object({
|
|
1558
1694
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1559
1695
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1573,10 +1709,6 @@ var CreateGoalSchema = z3.object({
|
|
|
1573
1709
|
* repo name. Delegated work inherits this from its parent Goal when omitted. */
|
|
1574
1710
|
repo: z3.string().trim().min(1).max(200).optional()
|
|
1575
1711
|
});
|
|
1576
|
-
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
1577
|
-
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
1578
|
-
}).strict();
|
|
1579
|
-
var CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
|
|
1580
1712
|
var UpdateGoalSchema = z3.object({
|
|
1581
1713
|
revision: z3.number().int().positive(),
|
|
1582
1714
|
changes: z3.object({
|
|
@@ -1591,16 +1723,64 @@ var UpdateGoalSchema = z3.object({
|
|
|
1591
1723
|
state: z3.enum(["active", "done", "cancelled"]).optional(),
|
|
1592
1724
|
progress: z3.string().trim().min(1).max(1e4).optional(),
|
|
1593
1725
|
reviewed: z3.literal(true).optional(),
|
|
1594
|
-
dueAt: z3.string().datetime({ offset: true }).nullable().optional()
|
|
1595
|
-
/** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
|
|
1596
|
-
* (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
|
|
1597
|
-
* by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
|
|
1598
|
-
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()
|
|
1599
1727
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
1600
1728
|
reason: z3.string().trim().min(1).max(2e3),
|
|
1601
1729
|
operationId: z3.string().uuid().optional()
|
|
1602
1730
|
}).strict();
|
|
1603
|
-
var
|
|
1731
|
+
var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
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.");
|
|
1733
|
+
var goalOutcome = z3.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
|
|
1734
|
+
var CreateChange = z3.object({
|
|
1735
|
+
kind: z3.literal("create"),
|
|
1736
|
+
title: goalTitle,
|
|
1737
|
+
outcome: goalOutcome,
|
|
1738
|
+
ownerId: z3.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as who_is_working shows it) or the person's. Omitted: you."),
|
|
1739
|
+
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
1740
|
+
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
1741
|
+
}).strict();
|
|
1742
|
+
var EditChange = z3.object({ kind: z3.literal("edit"), goalId: changedGoal, title: goalTitle.optional(), outcome: goalOutcome.optional() }).strict();
|
|
1743
|
+
var StateChange = z3.object({ kind: z3.literal("state"), goalId: changedGoal, state: z3.enum(["open", "completed", "canceled"]).describe(
|
|
1744
|
+
"completed: the Goal's own work is done now (refused while required children are open); open: reopen it; canceled: it will not be done."
|
|
1745
|
+
) }).strict();
|
|
1746
|
+
var AssignChange = z3.object({ kind: z3.literal("assign"), goalId: changedGoal, ownerId: z3.string().trim().min(1).describe("The new owner's participant.") }).strict();
|
|
1747
|
+
var DeferChange = z3.object({ kind: z3.literal("defer"), goalId: changedGoal, until: z3.string().datetime({ offset: true }).nullable().describe(
|
|
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."
|
|
1749
|
+
) }).strict();
|
|
1750
|
+
var MoveChange = z3.object({ kind: z3.literal("move"), goalId: changedGoal, parentGoalId: z3.string().uuid().nullable().describe(
|
|
1751
|
+
"Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
|
|
1752
|
+
) }).strict();
|
|
1753
|
+
var DependencyChange = z3.object({
|
|
1754
|
+
kind: z3.literal("dependency"),
|
|
1755
|
+
change: z3.enum(["add", "remove"]),
|
|
1756
|
+
goalId: changedGoal,
|
|
1757
|
+
dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
|
|
1758
|
+
action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
|
|
1759
|
+
reason: z3.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
|
|
1760
|
+
}).strict();
|
|
1761
|
+
var read = { revision: z3.number().int().positive().optional() };
|
|
1762
|
+
var GoalChangeSchema = z3.discriminatedUnion("kind", [
|
|
1763
|
+
CreateChange,
|
|
1764
|
+
EditChange.extend(read),
|
|
1765
|
+
StateChange.extend(read),
|
|
1766
|
+
AssignChange.extend(read),
|
|
1767
|
+
DeferChange.extend(read),
|
|
1768
|
+
MoveChange.extend(read),
|
|
1769
|
+
DependencyChange
|
|
1770
|
+
]);
|
|
1771
|
+
var editNames = (changes, ctx) => changes.forEach((c, i) => {
|
|
1772
|
+
if (c.kind === "edit" && c.title === void 0 && c.outcome === void 0) {
|
|
1773
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["changes", i], message: "an edit supplies title, outcome, or both" });
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
var ManageGoalsSchema = z3.object({
|
|
1777
|
+
requestId: z3.string().uuid().optional(),
|
|
1778
|
+
changes: z3.array(GoalChangeSchema).min(1).max(50)
|
|
1779
|
+
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
1780
|
+
var ManageGoalsToolSchema = z3.object({
|
|
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.")
|
|
1782
|
+
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
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_joined, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: 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 a Goal you own or have written on. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
1604
1784
|
var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
|
|
1605
1785
|
var GetGoalSchema = z3.object({
|
|
1606
1786
|
goalId: z3.string().uuid(),
|
|
@@ -1613,18 +1793,30 @@ var GetGoalSchema = z3.object({
|
|
|
1613
1793
|
diagnose: z3.boolean().optional()
|
|
1614
1794
|
}).strict();
|
|
1615
1795
|
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
1616
|
-
var
|
|
1617
|
-
var
|
|
1618
|
-
|
|
1619
|
-
|
|
1796
|
+
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).";
|
|
1797
|
+
var SearchToolSchema = z3.object({
|
|
1798
|
+
query: z3.string().trim().min(1).max(500),
|
|
1799
|
+
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
1800
|
+
goalId: z3.string().uuid().optional(),
|
|
1801
|
+
limit: z3.number().int().min(1).max(20).optional()
|
|
1802
|
+
}).strict();
|
|
1803
|
+
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.";
|
|
1804
|
+
var WhoIsWorkingSchema = z3.object({}).strict();
|
|
1805
|
+
var FEEDBACK_TEXT_MAX = 5e4;
|
|
1806
|
+
var SendFeedbackSchema = z3.object({
|
|
1807
|
+
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.`),
|
|
1808
|
+
title: z3.string().trim().min(1).max(200).optional().describe("One line naming it."),
|
|
1809
|
+
kind: z3.enum(["bug", "idea", "other"]).default("other").describe("bug: something Paigy does wrong. idea: something it should do. other: anything else.")
|
|
1810
|
+
}).strict();
|
|
1811
|
+
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.";
|
|
1620
1812
|
var AGENT_TOOLS = [
|
|
1621
|
-
{ 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(
|
|
1813
|
+
{ 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) },
|
|
1622
1814
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
1623
|
-
{ name: "
|
|
1624
|
-
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
1815
|
+
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
1625
1816
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
1626
1817
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
1627
|
-
{ name: "
|
|
1818
|
+
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
1819
|
+
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
1628
1820
|
];
|
|
1629
1821
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
1630
1822
|
var id = z4.string().uuid();
|
|
@@ -1655,17 +1847,6 @@ var existingGoalChanges = [
|
|
|
1655
1847
|
reason: z4.string().max(500).optional()
|
|
1656
1848
|
}).strict()
|
|
1657
1849
|
];
|
|
1658
|
-
var GoalChangeSchema = z4.union([
|
|
1659
|
-
z4.object({
|
|
1660
|
-
kind: z4.literal("create"),
|
|
1661
|
-
title: z4.string().min(1).max(120),
|
|
1662
|
-
outcome: z4.string().min(1),
|
|
1663
|
-
ownerId: participant,
|
|
1664
|
-
parentGoalId: id.optional(),
|
|
1665
|
-
sourceEntryIds: z4.array(id)
|
|
1666
|
-
}).strict(),
|
|
1667
|
-
...existingGoalChanges
|
|
1668
|
-
]);
|
|
1669
1850
|
var BrainGoalChangeSchema = z4.union([
|
|
1670
1851
|
// newSession: Paigy starts a session to own it (owner, 2026-10-06); until it has, the person owns it.
|
|
1671
1852
|
z4.object({
|
|
@@ -1774,6 +1955,66 @@ var CompactResultSchema = z4.object({
|
|
|
1774
1955
|
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
1775
1956
|
zodToJsonSchema(CompactResultSchema, { $refStrategy: "none" })
|
|
1776
1957
|
);
|
|
1958
|
+
var str2 = z5.string();
|
|
1959
|
+
var strs2 = z5.array(z5.string());
|
|
1960
|
+
var TalkerReplySchema = z5.object({
|
|
1961
|
+
/** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
|
|
1962
|
+
* or an agent's handle; `about` names handles; `blocks` is the item a question to an agent must be
|
|
1963
|
+
* answered before ("" for none). */
|
|
1964
|
+
messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
|
|
1965
|
+
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
1966
|
+
then: z5.enum(["listen", "hold", "end"]),
|
|
1967
|
+
/** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
|
|
1968
|
+
answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2 }).strict()),
|
|
1969
|
+
/** The line handles that hold a new instruction from the person. */
|
|
1970
|
+
instruction: strs2,
|
|
1971
|
+
/** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
|
|
1972
|
+
* reads only these (3e). */
|
|
1973
|
+
file: strs2,
|
|
1974
|
+
/** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
|
|
1975
|
+
need: z5.object({ kind: z5.enum(["search", "item", "agent", ""]), text: str2 }).strict(),
|
|
1976
|
+
/** The handle of the item to explain ("" for none). */
|
|
1977
|
+
explain: str2
|
|
1978
|
+
}).strict();
|
|
1979
|
+
var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
1980
|
+
zodToJsonSchema(TalkerReplySchema, { $refStrategy: "none" })
|
|
1981
|
+
);
|
|
1982
|
+
var str3 = z6.string();
|
|
1983
|
+
var strs3 = z6.array(z6.string());
|
|
1984
|
+
var FilerReplySchema = z6.object({
|
|
1985
|
+
/** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
|
|
1986
|
+
filed: z6.array(z6.object({ line: str3, goals: strs3 }).strict()),
|
|
1987
|
+
goals: z6.array(z6.object({
|
|
1988
|
+
op: z6.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
1989
|
+
key: str3,
|
|
1990
|
+
goal: str3,
|
|
1991
|
+
title: str3,
|
|
1992
|
+
outcome: str3,
|
|
1993
|
+
owner: str3,
|
|
1994
|
+
under: str3,
|
|
1995
|
+
until: str3,
|
|
1996
|
+
on: str3,
|
|
1997
|
+
gate: z6.enum(["start", "complete", "answer", ""]),
|
|
1998
|
+
why: str3
|
|
1999
|
+
}).strict()),
|
|
2000
|
+
questions: z6.array(z6.object({
|
|
2001
|
+
op: z6.enum(["create", "edit", "assign", "withdraw"]),
|
|
2002
|
+
key: str3,
|
|
2003
|
+
question: str3,
|
|
2004
|
+
text: str3,
|
|
2005
|
+
to: str3,
|
|
2006
|
+
goal: str3,
|
|
2007
|
+
options: z6.array(z6.object({ id: str3, label: str3 }).strict()),
|
|
2008
|
+
pick: z6.enum(["one", "many", "rank", "words", ""]),
|
|
2009
|
+
blocks: str3
|
|
2010
|
+
}).strict()),
|
|
2011
|
+
lessons: z6.array(z6.object({ op: z6.enum(["remember", "revise", "withdraw"]), lesson: str3, text: str3, scope: str3 }).strict()),
|
|
2012
|
+
/** Line handles whose words comment on Paigy itself. */
|
|
2013
|
+
feedback: strs3
|
|
2014
|
+
}).strict();
|
|
2015
|
+
var FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
2016
|
+
zodToJsonSchema(FilerReplySchema, { $refStrategy: "none" })
|
|
2017
|
+
);
|
|
1777
2018
|
function entryWords(entry) {
|
|
1778
2019
|
const content = entry.content;
|
|
1779
2020
|
if (content && "sealed" in content) return "";
|
|
@@ -1791,19 +2032,36 @@ function entryWords(entry) {
|
|
|
1791
2032
|
}
|
|
1792
2033
|
return entry.sources.map((source) => source.text).join("\n");
|
|
1793
2034
|
}
|
|
2035
|
+
function entryUnit(entry) {
|
|
2036
|
+
if (entry.kind !== "contribution" || !entry.aboutId) return null;
|
|
2037
|
+
const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
|
|
2038
|
+
if (!plain || typeof plain !== "object") return null;
|
|
2039
|
+
const { title, text } = plain;
|
|
2040
|
+
return typeof title === "string" && typeof text === "string" ? { title, body: text } : null;
|
|
2041
|
+
}
|
|
2042
|
+
function askUnits(request, entries) {
|
|
2043
|
+
return entries.flatMap((entry) => {
|
|
2044
|
+
if (entry.aboutId !== request.entryId || entry.authorParticipant !== request.authorParticipant) return [];
|
|
2045
|
+
const unit = entryUnit(entry);
|
|
2046
|
+
return unit ? [unit] : [];
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
1794
2049
|
var LIVE_MS = 3 * 6e4;
|
|
1795
2050
|
var WORKING_MS = 60 * 6e4;
|
|
1796
|
-
var ContextSchema =
|
|
1797
|
-
title:
|
|
1798
|
-
description:
|
|
2051
|
+
var ContextSchema = z7.object({
|
|
2052
|
+
title: z7.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
2053
|
+
description: z7.array(z7.string().min(1)).describe(
|
|
1799
2054
|
"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."
|
|
1800
|
-
)
|
|
2055
|
+
),
|
|
2056
|
+
/** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
|
|
2057
|
+
* a body on a tap. Absent on an ask sent without units, which renders as before. */
|
|
2058
|
+
units: z7.array(z7.object({ title: z7.string(), body: z7.string() })).optional()
|
|
1801
2059
|
});
|
|
1802
|
-
var ParticipantSchema =
|
|
1803
|
-
kind:
|
|
1804
|
-
id:
|
|
2060
|
+
var ParticipantSchema = z7.object({
|
|
2061
|
+
kind: z7.enum(["human", "agent"]),
|
|
2062
|
+
id: z7.string()
|
|
1805
2063
|
});
|
|
1806
|
-
var TransformSchema =
|
|
2064
|
+
var TransformSchema = z7.enum([
|
|
1807
2065
|
"structure",
|
|
1808
2066
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
1809
2067
|
"request_more",
|
|
@@ -1826,13 +2084,13 @@ var PAIGY_SELF = { kind: "agent", id: "paigy" };
|
|
|
1826
2084
|
function isPaigy(ref) {
|
|
1827
2085
|
return ref === participantRef(PAIGY_SELF);
|
|
1828
2086
|
}
|
|
1829
|
-
var VisualSchema =
|
|
1830
|
-
url:
|
|
1831
|
-
label:
|
|
2087
|
+
var VisualSchema = z7.object({
|
|
2088
|
+
url: z7.string().url(),
|
|
2089
|
+
label: z7.string().optional()
|
|
1832
2090
|
});
|
|
1833
|
-
var NotifyLevelSchema =
|
|
1834
|
-
var SelectShapeSchema =
|
|
1835
|
-
var ReceiptEventSchema =
|
|
2091
|
+
var NotifyLevelSchema = z7.enum(["inbox", "push", "banner", "call"]);
|
|
2092
|
+
var SelectShapeSchema = z7.enum(["one", "many", "rank", "confirm", "text"]);
|
|
2093
|
+
var ReceiptEventSchema = z7.enum([
|
|
1836
2094
|
"delivered",
|
|
1837
2095
|
// the bundle reached the recipient at some level
|
|
1838
2096
|
"seen",
|
|
@@ -1862,47 +2120,47 @@ var ReceiptEventSchema = z5.enum([
|
|
|
1862
2120
|
// be rewound by a writer that forgot to advance it.
|
|
1863
2121
|
"restarted"
|
|
1864
2122
|
]);
|
|
1865
|
-
var AttentionSchema =
|
|
2123
|
+
var AttentionSchema = z7.object({
|
|
1866
2124
|
urgency: NotifyLevelSchema,
|
|
1867
2125
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
1868
2126
|
select: SelectShapeSchema.nullable(),
|
|
1869
2127
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
1870
|
-
points:
|
|
2128
|
+
points: z7.array(z7.string()).nullable(),
|
|
1871
2129
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
1872
|
-
blocking:
|
|
2130
|
+
blocking: z7.boolean(),
|
|
1873
2131
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
1874
2132
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
1875
|
-
deadline:
|
|
2133
|
+
deadline: z7.string().datetime().nullable().optional()
|
|
1876
2134
|
});
|
|
1877
|
-
var NotifyRequestFields =
|
|
2135
|
+
var NotifyRequestFields = z7.object({
|
|
1878
2136
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
1879
2137
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
1880
2138
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
1881
2139
|
context: ContextSchema.optional(),
|
|
1882
|
-
options:
|
|
2140
|
+
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(
|
|
1883
2141
|
"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)."
|
|
1884
2142
|
),
|
|
1885
|
-
points:
|
|
2143
|
+
points: z7.array(z7.string().min(1)).optional().describe(
|
|
1886
2144
|
"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."
|
|
1887
2145
|
),
|
|
1888
|
-
visuals:
|
|
2146
|
+
visuals: z7.array(VisualSchema).optional().describe(
|
|
1889
2147
|
"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."
|
|
1890
2148
|
),
|
|
1891
2149
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
1892
|
-
repo:
|
|
2150
|
+
repo: z7.string().optional(),
|
|
1893
2151
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
1894
|
-
branch:
|
|
2152
|
+
branch: z7.string().optional(),
|
|
1895
2153
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
1896
2154
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
1897
2155
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
1898
2156
|
* still accepts the old name from older clients. */
|
|
1899
|
-
parentId:
|
|
2157
|
+
parentId: z7.string().uuid().optional(),
|
|
1900
2158
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
1901
2159
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
1902
|
-
workId:
|
|
2160
|
+
workId: z7.string().uuid().optional(),
|
|
1903
2161
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
1904
2162
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
1905
|
-
goalId:
|
|
2163
|
+
goalId: z7.string().uuid().optional(),
|
|
1906
2164
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
1907
2165
|
"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."
|
|
1908
2166
|
),
|
|
@@ -1910,7 +2168,7 @@ var NotifyRequestFields = z5.object({
|
|
|
1910
2168
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
1911
2169
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
1912
2170
|
* says HOW. */
|
|
1913
|
-
clarifies:
|
|
2171
|
+
clarifies: z7.string().optional(),
|
|
1914
2172
|
select: SelectShapeSchema.optional().describe(
|
|
1915
2173
|
"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."
|
|
1916
2174
|
),
|
|
@@ -1924,20 +2182,20 @@ var NotifyRequestFields = z5.object({
|
|
|
1924
2182
|
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
1925
2183
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
1926
2184
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
1927
|
-
ask:
|
|
2185
|
+
ask: z7.string().min(1).max(1e4).optional().describe(
|
|
1928
2186
|
'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.'
|
|
1929
2187
|
),
|
|
1930
|
-
needs:
|
|
2188
|
+
needs: z7.array(z7.string().min(1)).optional().describe(
|
|
1931
2189
|
"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."
|
|
1932
2190
|
),
|
|
1933
|
-
urgencyHint:
|
|
2191
|
+
urgencyHint: z7.enum(["whenever", "soon", "now"]).optional().describe(
|
|
1934
2192
|
"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."
|
|
1935
2193
|
),
|
|
1936
2194
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
1937
2195
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
1938
2196
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
1939
2197
|
* urgencyHint/blocking win when both are sent. */
|
|
1940
|
-
waiting:
|
|
2198
|
+
waiting: z7.enum(["none", "soft", "hard"]).optional().describe(
|
|
1941
2199
|
"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."
|
|
1942
2200
|
),
|
|
1943
2201
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -1945,98 +2203,98 @@ var NotifyRequestFields = z5.object({
|
|
|
1945
2203
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
1946
2204
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
1947
2205
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
1948
|
-
confirm:
|
|
2206
|
+
confirm: z7.boolean().optional().describe(
|
|
1949
2207
|
"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'."
|
|
1950
2208
|
),
|
|
1951
2209
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
1952
2210
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
1953
2211
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
1954
2212
|
* waiting:'hard'. */
|
|
1955
|
-
channel:
|
|
2213
|
+
channel: z7.enum(["call", "message"]).optional().describe(
|
|
1956
2214
|
"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."
|
|
1957
2215
|
),
|
|
1958
|
-
confirmStyle:
|
|
2216
|
+
confirmStyle: z7.enum(["yesno", "approve"]).default("yesno").describe(
|
|
1959
2217
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
1960
2218
|
),
|
|
1961
|
-
blocking:
|
|
2219
|
+
blocking: z7.boolean().default(false).describe(
|
|
1962
2220
|
"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."
|
|
1963
2221
|
)
|
|
1964
2222
|
});
|
|
1965
2223
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
1966
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
2224
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
1967
2225
|
if (r.ask !== void 0) {
|
|
1968
2226
|
for (const f of ["context", "select", "points"]) {
|
|
1969
2227
|
if (r[f] !== void 0)
|
|
1970
|
-
ctx.addIssue({ code:
|
|
2228
|
+
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.` });
|
|
1971
2229
|
}
|
|
1972
2230
|
return;
|
|
1973
2231
|
}
|
|
1974
2232
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
1975
|
-
ctx.addIssue({ code:
|
|
2233
|
+
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" });
|
|
1976
2234
|
if (!r.context)
|
|
1977
|
-
ctx.addIssue({ code:
|
|
2235
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
1978
2236
|
if (!r.select)
|
|
1979
|
-
ctx.addIssue({ code:
|
|
2237
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
1980
2238
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
1981
2239
|
if (needsOptions && !r.options?.length)
|
|
1982
|
-
ctx.addIssue({ code:
|
|
2240
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
1983
2241
|
if (!needsOptions && r.options?.length)
|
|
1984
|
-
ctx.addIssue({ code:
|
|
2242
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
1985
2243
|
});
|
|
1986
|
-
var NotifyStatusSchema =
|
|
1987
|
-
var AgentStateSchema =
|
|
1988
|
-
var TurnSchema =
|
|
1989
|
-
prompt:
|
|
1990
|
-
reply:
|
|
2244
|
+
var NotifyStatusSchema = z7.enum(["pending", "answered", "ignored"]);
|
|
2245
|
+
var AgentStateSchema = z7.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
2246
|
+
var TurnSchema = z7.object({
|
|
2247
|
+
prompt: z7.string(),
|
|
2248
|
+
reply: z7.string()
|
|
1991
2249
|
});
|
|
1992
|
-
var UserAnswerSchema =
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2250
|
+
var UserAnswerSchema = z7.discriminatedUnion("kind", [
|
|
2251
|
+
z7.object({ kind: z7.literal("option"), optionId: z7.string(), label: z7.string().optional() }),
|
|
2252
|
+
z7.object({ kind: z7.literal("text"), text: z7.string() }),
|
|
2253
|
+
z7.object({ kind: z7.literal("ignored") }),
|
|
2254
|
+
z7.object({ kind: z7.literal("multi"), optionIds: z7.array(z7.string()), labels: z7.array(z7.string()).optional() }),
|
|
2255
|
+
z7.object({ kind: z7.literal("ranked"), optionIds: z7.array(z7.string()), labels: z7.array(z7.string()).optional() }),
|
|
2256
|
+
z7.object({ kind: z7.literal("clarify"), chunks: z7.array(z7.string()).min(1) }),
|
|
2257
|
+
z7.object({ kind: z7.literal("confirm"), approved: z7.boolean() }),
|
|
2258
|
+
z7.object({ kind: z7.literal("turns"), turns: z7.array(TurnSchema).min(1) }),
|
|
2001
2259
|
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
2002
2260
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
2003
2261
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
2004
2262
|
* card the user can reply to — the broker never overrides the user. */
|
|
2005
|
-
|
|
2263
|
+
z7.object({ kind: z7.literal("precedent"), answer: z7.string(), derivation: z7.string(), sources: z7.array(z7.string()).min(1) })
|
|
2006
2264
|
]);
|
|
2007
|
-
var IntentSchema =
|
|
2265
|
+
var IntentSchema = z7.object({
|
|
2008
2266
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
2009
2267
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
2010
2268
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
2011
2269
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
2012
|
-
kind:
|
|
2013
|
-
detail:
|
|
2270
|
+
kind: z7.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
2271
|
+
detail: z7.string(),
|
|
2014
2272
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
2015
2273
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
2016
2274
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
2017
2275
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
2018
|
-
dueInSeconds:
|
|
2276
|
+
dueInSeconds: z7.number().int().positive().optional(),
|
|
2019
2277
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
2020
2278
|
* already read the utterance, so `signals.kind` stops defaulting to
|
|
2021
2279
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
2022
2280
|
* about what cannot answer "is the bot looping less this week?". */
|
|
2023
|
-
fault:
|
|
2281
|
+
fault: z7.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
2024
2282
|
});
|
|
2025
|
-
var RideAlongSchema =
|
|
2283
|
+
var RideAlongSchema = z7.object({
|
|
2026
2284
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
2027
|
-
noteId:
|
|
2285
|
+
noteId: z7.string(),
|
|
2028
2286
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
2029
|
-
text:
|
|
2287
|
+
text: z7.string(),
|
|
2030
2288
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
2031
|
-
parentId:
|
|
2289
|
+
parentId: z7.string().nullable()
|
|
2032
2290
|
});
|
|
2033
|
-
var AwaitItemSchema =
|
|
2034
|
-
|
|
2035
|
-
type:
|
|
2036
|
-
parentId:
|
|
2037
|
-
notificationId:
|
|
2038
|
-
workId:
|
|
2039
|
-
decisionId:
|
|
2291
|
+
var AwaitItemSchema = z7.discriminatedUnion("type", [
|
|
2292
|
+
z7.object({
|
|
2293
|
+
type: z7.literal("reply"),
|
|
2294
|
+
parentId: z7.string(),
|
|
2295
|
+
notificationId: z7.string(),
|
|
2296
|
+
workId: z7.string().uuid().optional(),
|
|
2297
|
+
decisionId: z7.string().uuid().optional(),
|
|
2040
2298
|
answer: UserAnswerSchema,
|
|
2041
2299
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
2042
2300
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -2046,103 +2304,103 @@ var AwaitItemSchema = z5.discriminatedUnion("type", [
|
|
|
2046
2304
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
2047
2305
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
2048
2306
|
* a sentence with a hole in it is worse than no sentence. */
|
|
2049
|
-
note:
|
|
2307
|
+
note: z7.string().optional(),
|
|
2050
2308
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
2051
2309
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
2052
2310
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
2053
2311
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
2054
|
-
transcript:
|
|
2312
|
+
transcript: z7.string().optional(),
|
|
2055
2313
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
2056
2314
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
2057
|
-
covered:
|
|
2315
|
+
covered: z7.array(z7.string()).optional(),
|
|
2058
2316
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
2059
2317
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
2060
2318
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
2061
2319
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
2062
2320
|
* dropped. Absent/empty = nothing owed. */
|
|
2063
|
-
also:
|
|
2321
|
+
also: z7.array(RideAlongSchema).optional()
|
|
2064
2322
|
}),
|
|
2065
|
-
|
|
2066
|
-
type:
|
|
2067
|
-
parentId:
|
|
2068
|
-
notificationId:
|
|
2069
|
-
remindAt:
|
|
2323
|
+
z7.object({
|
|
2324
|
+
type: z7.literal("remind"),
|
|
2325
|
+
parentId: z7.string(),
|
|
2326
|
+
notificationId: z7.string(),
|
|
2327
|
+
remindAt: z7.string().datetime({ offset: true }),
|
|
2070
2328
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
2071
|
-
remindInSeconds:
|
|
2329
|
+
remindInSeconds: z7.number()
|
|
2072
2330
|
}),
|
|
2073
2331
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
2074
2332
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
2075
2333
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
2076
|
-
* re-orient via
|
|
2077
|
-
|
|
2078
|
-
type:
|
|
2079
|
-
parentId:
|
|
2080
|
-
notificationId:
|
|
2334
|
+
* re-orient via contact({})). */
|
|
2335
|
+
z7.object({
|
|
2336
|
+
type: z7.literal("superseded"),
|
|
2337
|
+
parentId: z7.string(),
|
|
2338
|
+
notificationId: z7.string()
|
|
2081
2339
|
}),
|
|
2082
2340
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
2083
2341
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
2084
2342
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
2085
2343
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
2086
2344
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
2087
|
-
|
|
2088
|
-
type:
|
|
2089
|
-
notificationId:
|
|
2090
|
-
inFlight:
|
|
2091
|
-
turn:
|
|
2092
|
-
idx:
|
|
2093
|
-
prompt:
|
|
2094
|
-
reply:
|
|
2095
|
-
acts:
|
|
2345
|
+
z7.object({
|
|
2346
|
+
type: z7.literal("partial"),
|
|
2347
|
+
notificationId: z7.string(),
|
|
2348
|
+
inFlight: z7.literal(true),
|
|
2349
|
+
turn: z7.object({
|
|
2350
|
+
idx: z7.number(),
|
|
2351
|
+
prompt: z7.string(),
|
|
2352
|
+
reply: z7.string(),
|
|
2353
|
+
acts: z7.array(IntentSchema).nullable().optional()
|
|
2096
2354
|
})
|
|
2097
2355
|
}),
|
|
2098
|
-
|
|
2099
|
-
type:
|
|
2100
|
-
also:
|
|
2356
|
+
z7.object({
|
|
2357
|
+
type: z7.literal("idle"),
|
|
2358
|
+
also: z7.array(RideAlongSchema).optional(),
|
|
2101
2359
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
2102
2360
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
2103
2361
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
2104
2362
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
2105
2363
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
2106
|
-
inFlight:
|
|
2364
|
+
inFlight: z7.boolean().optional()
|
|
2107
2365
|
})
|
|
2108
2366
|
]);
|
|
2109
|
-
var VoiceKeySchema =
|
|
2110
|
-
var AgendaTurnSchema =
|
|
2367
|
+
var VoiceKeySchema = z7.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2368
|
+
var AgendaTurnSchema = z7.object({
|
|
2111
2369
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
2112
2370
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
2113
2371
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
2114
2372
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
2115
2373
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
2116
2374
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
2117
|
-
id:
|
|
2375
|
+
id: z7.string().optional(),
|
|
2118
2376
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
2119
2377
|
* the planner declares duplicates instead of asking them twice. */
|
|
2120
|
-
coveredIds:
|
|
2378
|
+
coveredIds: z7.array(z7.string()).optional(),
|
|
2121
2379
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
2122
2380
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
2123
|
-
info:
|
|
2124
|
-
question:
|
|
2381
|
+
info: z7.array(z7.string().min(1)).default([]),
|
|
2382
|
+
question: z7.string().min(1).nullable(),
|
|
2125
2383
|
/** True on the one turn carrying the agent's own declared question. */
|
|
2126
|
-
asks:
|
|
2384
|
+
asks: z7.boolean().optional(),
|
|
2127
2385
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
2128
2386
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
2129
2387
|
* which route nothing. */
|
|
2130
|
-
claimId:
|
|
2388
|
+
claimId: z7.string().optional(),
|
|
2131
2389
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
2132
|
-
voice:
|
|
2390
|
+
voice: z7.string().optional(),
|
|
2133
2391
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
2134
2392
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
2135
2393
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
2136
2394
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
2137
|
-
agent:
|
|
2395
|
+
agent: z7.string().optional(),
|
|
2138
2396
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
2139
2397
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
2140
2398
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
2141
2399
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
2142
2400
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
2143
|
-
agentId:
|
|
2401
|
+
agentId: z7.string().optional(),
|
|
2144
2402
|
select: SelectShapeSchema.optional(),
|
|
2145
|
-
options:
|
|
2403
|
+
options: z7.array(OptionSchema.omit({ id: true })).optional(),
|
|
2146
2404
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
2147
2405
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
2148
2406
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -2151,33 +2409,33 @@ var AgendaTurnSchema = z5.object({
|
|
|
2151
2409
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
2152
2410
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
2153
2411
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
2154
|
-
blocking:
|
|
2412
|
+
blocking: z7.boolean().optional(),
|
|
2155
2413
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
2156
2414
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
2157
2415
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
2158
2416
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
2159
2417
|
* speak, it is dropped and their words are taken like any reply. */
|
|
2160
|
-
ifSilent:
|
|
2418
|
+
ifSilent: z7.boolean().optional()
|
|
2161
2419
|
});
|
|
2162
2420
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
2163
|
-
var InboxItemSchema =
|
|
2164
|
-
id:
|
|
2165
|
-
tokenId:
|
|
2421
|
+
var InboxItemSchema = z7.object({
|
|
2422
|
+
id: z7.string(),
|
|
2423
|
+
tokenId: z7.string().optional(),
|
|
2166
2424
|
status: NotifyStatusSchema,
|
|
2167
2425
|
context: ContextSchema,
|
|
2168
|
-
options:
|
|
2426
|
+
options: z7.array(OptionSchema).optional(),
|
|
2169
2427
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
2170
|
-
points:
|
|
2428
|
+
points: z7.array(z7.string()).optional(),
|
|
2171
2429
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
2172
2430
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
2173
2431
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
2174
2432
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
2175
|
-
asks:
|
|
2433
|
+
asks: z7.boolean().optional(),
|
|
2176
2434
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
2177
2435
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
2178
2436
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
2179
2437
|
* which must never CLAIM stalled. */
|
|
2180
|
-
lastSeenAt:
|
|
2438
|
+
lastSeenAt: z7.string().optional(),
|
|
2181
2439
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
2182
2440
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
2183
2441
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -2187,7 +2445,7 @@ var InboxItemSchema = z5.object({
|
|
|
2187
2445
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
2188
2446
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
2189
2447
|
* `createdAt`. */
|
|
2190
|
-
agentStateAt:
|
|
2448
|
+
agentStateAt: z7.string().datetime().optional(),
|
|
2191
2449
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
2192
2450
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
2193
2451
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -2201,54 +2459,54 @@ var InboxItemSchema = z5.object({
|
|
|
2201
2459
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
2202
2460
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
2203
2461
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
2204
|
-
questions:
|
|
2205
|
-
id:
|
|
2206
|
-
entryId:
|
|
2207
|
-
title:
|
|
2208
|
-
state:
|
|
2209
|
-
answer:
|
|
2462
|
+
questions: z7.array(z7.object({
|
|
2463
|
+
id: z7.string(),
|
|
2464
|
+
entryId: z7.string(),
|
|
2465
|
+
title: z7.string(),
|
|
2466
|
+
state: z7.enum(["open", "answered"]),
|
|
2467
|
+
answer: z7.string().nullable(),
|
|
2210
2468
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
2211
2469
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
2212
2470
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
2213
2471
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
2214
|
-
agent:
|
|
2472
|
+
agent: z7.object({ tokenId: z7.string(), name: z7.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
2215
2473
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
2216
2474
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
2217
2475
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
2218
2476
|
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
2219
2477
|
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
2220
|
-
options:
|
|
2478
|
+
options: z7.array(OptionSchema).optional()
|
|
2221
2479
|
})).optional(),
|
|
2222
|
-
visuals:
|
|
2480
|
+
visuals: z7.array(VisualSchema).optional(),
|
|
2223
2481
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
2224
2482
|
* agent's suggestion, or a default silly name). */
|
|
2225
|
-
name:
|
|
2483
|
+
name: z7.string(),
|
|
2226
2484
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
2227
2485
|
voice: VoiceKeySchema.optional(),
|
|
2228
|
-
repo:
|
|
2229
|
-
branch:
|
|
2230
|
-
createdAt:
|
|
2231
|
-
snoozedUntil:
|
|
2486
|
+
repo: z7.string().optional(),
|
|
2487
|
+
branch: z7.string().optional(),
|
|
2488
|
+
createdAt: z7.string().datetime(),
|
|
2489
|
+
snoozedUntil: z7.string().datetime().optional(),
|
|
2232
2490
|
agentState: AgentStateSchema.default("idle"),
|
|
2233
2491
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
2234
2492
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
2235
2493
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
2236
|
-
turn:
|
|
2494
|
+
turn: z7.enum(["you", "agent"]).default("you"),
|
|
2237
2495
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
2238
2496
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
2239
2497
|
* error badge + Retry. */
|
|
2240
|
-
error:
|
|
2498
|
+
error: z7.string().optional(),
|
|
2241
2499
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
2242
2500
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
2243
2501
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
2244
2502
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
2245
|
-
cold:
|
|
2246
|
-
clarifies:
|
|
2503
|
+
cold: z7.string().datetime().optional(),
|
|
2504
|
+
clarifies: z7.string().optional(),
|
|
2247
2505
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
2248
2506
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
2249
2507
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
2250
2508
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
2251
|
-
onCall:
|
|
2509
|
+
onCall: z7.literal(true).optional(),
|
|
2252
2510
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
2253
2511
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
2254
2512
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -2258,31 +2516,31 @@ var InboxItemSchema = z5.object({
|
|
|
2258
2516
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
2259
2517
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
2260
2518
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
2261
|
-
ring:
|
|
2262
|
-
declinedAt:
|
|
2263
|
-
anchorAt:
|
|
2264
|
-
nextRingAt:
|
|
2265
|
-
step:
|
|
2519
|
+
ring: z7.object({
|
|
2520
|
+
declinedAt: z7.string().datetime(),
|
|
2521
|
+
anchorAt: z7.string().datetime(),
|
|
2522
|
+
nextRingAt: z7.string().datetime().nullable(),
|
|
2523
|
+
step: z7.number().int()
|
|
2266
2524
|
}).optional(),
|
|
2267
2525
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
2268
2526
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
2269
2527
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
2270
2528
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
2271
2529
|
* feedback ABOUT. */
|
|
2272
|
-
why:
|
|
2530
|
+
why: z7.object({
|
|
2273
2531
|
asked: NotifyLevelSchema,
|
|
2274
2532
|
got: NotifyLevelSchema,
|
|
2275
|
-
because:
|
|
2276
|
-
line:
|
|
2533
|
+
because: z7.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
2534
|
+
line: z7.string()
|
|
2277
2535
|
}).optional(),
|
|
2278
|
-
select:
|
|
2279
|
-
confirmStyle:
|
|
2536
|
+
select: z7.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
2537
|
+
confirmStyle: z7.enum(["yesno", "approve"]).default("yesno").describe(
|
|
2280
2538
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
2281
2539
|
),
|
|
2282
2540
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
2283
2541
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
2284
2542
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
2285
|
-
blocking:
|
|
2543
|
+
blocking: z7.boolean().default(false),
|
|
2286
2544
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
2287
2545
|
answer: UserAnswerSchema.optional(),
|
|
2288
2546
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -2290,17 +2548,17 @@ var InboxItemSchema = z5.object({
|
|
|
2290
2548
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
2291
2549
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
2292
2550
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
2293
|
-
communication:
|
|
2294
|
-
deliveryId:
|
|
2295
|
-
kind:
|
|
2296
|
-
entryId:
|
|
2297
|
-
goalIds:
|
|
2298
|
-
decisionNeedId:
|
|
2299
|
-
sealed:
|
|
2300
|
-
goalState:
|
|
2551
|
+
communication: z7.object({
|
|
2552
|
+
deliveryId: z7.string(),
|
|
2553
|
+
kind: z7.enum(["notification", "call"]),
|
|
2554
|
+
entryId: z7.string(),
|
|
2555
|
+
goalIds: z7.array(z7.string()),
|
|
2556
|
+
decisionNeedId: z7.string().optional(),
|
|
2557
|
+
sealed: z7.boolean(),
|
|
2558
|
+
goalState: z7.string().optional(),
|
|
2301
2559
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
2302
2560
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
2303
|
-
goalTitle:
|
|
2561
|
+
goalTitle: z7.string().optional()
|
|
2304
2562
|
}).optional(),
|
|
2305
2563
|
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
2306
2564
|
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
@@ -2312,27 +2570,27 @@ var InboxItemSchema = z5.object({
|
|
|
2312
2570
|
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
2313
2571
|
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
2314
2572
|
* with no rev is simply always re-sent. */
|
|
2315
|
-
rev:
|
|
2573
|
+
rev: z7.string().optional()
|
|
2316
2574
|
});
|
|
2317
2575
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
2318
|
-
var PushTokenSchema =
|
|
2319
|
-
voipToken:
|
|
2320
|
-
alertToken:
|
|
2321
|
-
fcmToken:
|
|
2322
|
-
platform:
|
|
2576
|
+
var PushTokenSchema = z7.object({
|
|
2577
|
+
voipToken: z7.string().min(1).optional(),
|
|
2578
|
+
alertToken: z7.string().min(1).optional(),
|
|
2579
|
+
fcmToken: z7.string().min(1).optional(),
|
|
2580
|
+
platform: z7.enum(["ios", "android"])
|
|
2323
2581
|
}).superRefine((v, ctx) => {
|
|
2324
2582
|
if (v.platform !== "ios") return;
|
|
2325
2583
|
for (const field of ["voipToken", "alertToken"]) {
|
|
2326
2584
|
const token = v[field];
|
|
2327
2585
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
2328
2586
|
ctx.addIssue({
|
|
2329
|
-
code:
|
|
2587
|
+
code: z7.ZodIssueCode.custom,
|
|
2330
2588
|
path: [field],
|
|
2331
2589
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
2332
2590
|
});
|
|
2333
2591
|
}
|
|
2334
2592
|
});
|
|
2335
|
-
var MissedCallSchema =
|
|
2593
|
+
var MissedCallSchema = z7.enum([
|
|
2336
2594
|
"retry_10m",
|
|
2337
2595
|
"retry_30m",
|
|
2338
2596
|
"retry_60m",
|
|
@@ -2344,31 +2602,31 @@ var MissedCallSchema = z5.enum([
|
|
|
2344
2602
|
]);
|
|
2345
2603
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
2346
2604
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
2347
|
-
var BrokerTuningSchema =
|
|
2605
|
+
var BrokerTuningSchema = z7.object({
|
|
2348
2606
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
2349
|
-
ackVerbosity:
|
|
2607
|
+
ackVerbosity: z7.enum(["normal", "none"]).optional(),
|
|
2350
2608
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
2351
2609
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
2352
|
-
clarifyEagerness:
|
|
2610
|
+
clarifyEagerness: z7.enum(["low", "normal", "high"]).optional(),
|
|
2353
2611
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
2354
|
-
phrasebook:
|
|
2612
|
+
phrasebook: z7.array(z7.object({ say: z7.string().min(1).max(60), mean: z7.string().min(1).max(120) })).max(24).optional(),
|
|
2355
2613
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
2356
2614
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
2357
2615
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
2358
2616
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
2359
2617
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
2360
|
-
language:
|
|
2618
|
+
language: z7.enum(["en", "es"]).optional()
|
|
2361
2619
|
});
|
|
2362
|
-
var UserSettingsSchema =
|
|
2363
|
-
permissions:
|
|
2364
|
-
call:
|
|
2365
|
-
banner:
|
|
2366
|
-
push:
|
|
2620
|
+
var UserSettingsSchema = z7.object({
|
|
2621
|
+
permissions: z7.object({
|
|
2622
|
+
call: z7.boolean(),
|
|
2623
|
+
banner: z7.boolean(),
|
|
2624
|
+
push: z7.boolean()
|
|
2367
2625
|
}),
|
|
2368
2626
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
2369
2627
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
2370
2628
|
* what people read (owner, 2026-09-30). */
|
|
2371
|
-
sessionMode:
|
|
2629
|
+
sessionMode: z7.enum(["default", "all_calls", "silent"]),
|
|
2372
2630
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
2373
2631
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
2374
2632
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -2377,7 +2635,7 @@ var UserSettingsSchema = z5.object({
|
|
|
2377
2635
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
2378
2636
|
* next dead field for the same reason. */
|
|
2379
2637
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
2380
|
-
improveConsent:
|
|
2638
|
+
improveConsent: z7.boolean(),
|
|
2381
2639
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
2382
2640
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
2383
2641
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -2385,17 +2643,17 @@ var UserSettingsSchema = z5.object({
|
|
|
2385
2643
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
2386
2644
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
2387
2645
|
* write, 'hosted' on read (see store.ts). */
|
|
2388
|
-
voiceMode:
|
|
2646
|
+
voiceMode: z7.enum(["hosted", "on_device"]).optional(),
|
|
2389
2647
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
2390
2648
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
2391
2649
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
2392
2650
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
2393
2651
|
* read (see store.ts). */
|
|
2394
|
-
talk:
|
|
2652
|
+
talk: z7.boolean().optional(),
|
|
2395
2653
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
2396
2654
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
2397
2655
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
2398
|
-
callDiagnostics:
|
|
2656
|
+
callDiagnostics: z7.boolean().optional(),
|
|
2399
2657
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
2400
2658
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
2401
2659
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -2403,7 +2661,7 @@ var UserSettingsSchema = z5.object({
|
|
|
2403
2661
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
2404
2662
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
2405
2663
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
2406
|
-
voiceTuning:
|
|
2664
|
+
voiceTuning: z7.record(z7.string(), z7.union([z7.number(), z7.string()])).optional(),
|
|
2407
2665
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
2408
2666
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
2409
2667
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -2412,53 +2670,53 @@ var UserSettingsSchema = z5.object({
|
|
|
2412
2670
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
2413
2671
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
2414
2672
|
* call — honest about what we know. */
|
|
2415
|
-
timezone:
|
|
2673
|
+
timezone: z7.string().min(1).max(64).optional(),
|
|
2416
2674
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
2417
2675
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
2418
2676
|
broker: BrokerTuningSchema.optional()
|
|
2419
2677
|
});
|
|
2420
|
-
var HistoryWorkSchema =
|
|
2421
|
-
id:
|
|
2422
|
-
title:
|
|
2423
|
-
state:
|
|
2678
|
+
var HistoryWorkSchema = z7.object({
|
|
2679
|
+
id: z7.string(),
|
|
2680
|
+
title: z7.string(),
|
|
2681
|
+
state: z7.enum(["done", "cancelled"]),
|
|
2424
2682
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
2425
|
-
assignee:
|
|
2683
|
+
assignee: z7.string()
|
|
2426
2684
|
});
|
|
2427
|
-
var HistoryEntrySchema =
|
|
2428
|
-
|
|
2429
|
-
|
|
2685
|
+
var HistoryEntrySchema = z7.union([
|
|
2686
|
+
z7.object({ at: z7.string(), card: InboxItemSchema }),
|
|
2687
|
+
z7.object({ at: z7.string(), work: HistoryWorkSchema })
|
|
2430
2688
|
]);
|
|
2431
|
-
var HistoryPageSchema =
|
|
2432
|
-
entries:
|
|
2433
|
-
next:
|
|
2689
|
+
var HistoryPageSchema = z7.object({
|
|
2690
|
+
entries: z7.array(HistoryEntrySchema),
|
|
2691
|
+
next: z7.string().nullable()
|
|
2434
2692
|
});
|
|
2435
2693
|
var ACTIVITY_LINES = 2;
|
|
2436
2694
|
var ACTIVITY_LINE_MAX = 80;
|
|
2437
|
-
var AgentActivitySchema =
|
|
2695
|
+
var AgentActivitySchema = z7.object({
|
|
2438
2696
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
2439
|
-
lines:
|
|
2697
|
+
lines: z7.array(z7.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
2440
2698
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
2441
2699
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
2442
|
-
at:
|
|
2700
|
+
at: z7.string().datetime()
|
|
2443
2701
|
});
|
|
2444
|
-
var ConnectionSummarySchema =
|
|
2702
|
+
var ConnectionSummarySchema = z7.object({
|
|
2445
2703
|
/** The connection = the agent's token id (used to address a request). */
|
|
2446
|
-
id:
|
|
2704
|
+
id: z7.string(),
|
|
2447
2705
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
2448
2706
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
2449
2707
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
2450
2708
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
2451
|
-
kind:
|
|
2709
|
+
kind: z7.enum(["device", "agent"]).optional(),
|
|
2452
2710
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
2453
2711
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
2454
2712
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
2455
|
-
mintedByDevice:
|
|
2456
|
-
device:
|
|
2713
|
+
mintedByDevice: z7.string().nullable().optional(),
|
|
2714
|
+
device: z7.string().nullable(),
|
|
2457
2715
|
/** The agent's display name (the single pairing name). */
|
|
2458
|
-
name:
|
|
2716
|
+
name: z7.string(),
|
|
2459
2717
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
2460
2718
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
2461
|
-
provider:
|
|
2719
|
+
provider: z7.string().nullable(),
|
|
2462
2720
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2463
2721
|
voice: VoiceKeySchema.nullable(),
|
|
2464
2722
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -2469,34 +2727,34 @@ var ConnectionSummarySchema = z5.object({
|
|
|
2469
2727
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
2470
2728
|
* set once must not overrule a rule they set about one agent. */
|
|
2471
2729
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
2472
|
-
createdAt:
|
|
2730
|
+
createdAt: z7.string().datetime(),
|
|
2473
2731
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2474
2732
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
2475
|
-
lastContactAt:
|
|
2733
|
+
lastContactAt: z7.string().datetime().nullable(),
|
|
2476
2734
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
2477
2735
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
2478
|
-
lastSeenAt:
|
|
2736
|
+
lastSeenAt: z7.string().datetime().nullable().optional(),
|
|
2479
2737
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
2480
2738
|
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
2481
2739
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
2482
|
-
lastWorkedAt:
|
|
2740
|
+
lastWorkedAt: z7.string().datetime().nullable().optional(),
|
|
2483
2741
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
2484
2742
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
2485
|
-
oldestReadyAt:
|
|
2743
|
+
oldestReadyAt: z7.string().datetime().nullable().optional(),
|
|
2486
2744
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
2487
2745
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
2488
2746
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
2489
|
-
runtime:
|
|
2747
|
+
runtime: z7.object({
|
|
2490
2748
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
2491
2749
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
2492
|
-
version:
|
|
2493
|
-
harnesses:
|
|
2494
|
-
workspaces:
|
|
2750
|
+
version: z7.string().optional(),
|
|
2751
|
+
harnesses: z7.array(z7.object({ name: z7.string(), label: z7.string(), status: z7.string() })).optional(),
|
|
2752
|
+
workspaces: z7.array(z7.string()).optional(),
|
|
2495
2753
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
2496
2754
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
2497
2755
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
2498
2756
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
2499
|
-
repos:
|
|
2757
|
+
repos: z7.array(z7.object({ path: z7.string(), remote: z7.string() })).optional()
|
|
2500
2758
|
}).optional(),
|
|
2501
2759
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
2502
2760
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -2505,156 +2763,156 @@ var ConnectionSummarySchema = z5.object({
|
|
|
2505
2763
|
activity: AgentActivitySchema.optional(),
|
|
2506
2764
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
2507
2765
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
2508
|
-
managed:
|
|
2766
|
+
managed: z7.boolean()
|
|
2509
2767
|
});
|
|
2510
|
-
var LedgerItemSchema =
|
|
2511
|
-
var AgentLedgerSchema =
|
|
2768
|
+
var LedgerItemSchema = z7.object({ id: z7.string(), parentId: z7.string(), title: z7.string(), createdAt: z7.string() });
|
|
2769
|
+
var AgentLedgerSchema = z7.object({
|
|
2512
2770
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
2513
|
-
agent:
|
|
2771
|
+
agent: z7.object({ id: z7.string(), name: z7.string().nullable(), revokedAt: z7.string().nullable() }),
|
|
2514
2772
|
/** Its own questions you have not answered. */
|
|
2515
|
-
asks:
|
|
2773
|
+
asks: z7.array(LedgerItemSchema),
|
|
2516
2774
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
2517
|
-
answered:
|
|
2775
|
+
answered: z7.array(LedgerItemSchema),
|
|
2518
2776
|
/** Requests you sent it that it never took. */
|
|
2519
|
-
requests:
|
|
2520
|
-
goals:
|
|
2521
|
-
callbacks:
|
|
2777
|
+
requests: z7.array(LedgerItemSchema),
|
|
2778
|
+
goals: z7.array(z7.object({ id: z7.string(), outcome: z7.string(), state: z7.string() })),
|
|
2779
|
+
callbacks: z7.array(z7.object({ id: z7.string(), parentId: z7.string(), trigger: z7.string(), note: z7.string(), dueAt: z7.string().nullable() }))
|
|
2522
2780
|
});
|
|
2523
|
-
var ReassignResultSchema =
|
|
2524
|
-
moved:
|
|
2525
|
-
parentId:
|
|
2781
|
+
var ReassignResultSchema = z7.object({
|
|
2782
|
+
moved: z7.object({ asks: z7.number(), answered: z7.number(), requests: z7.number(), goals: z7.number(), callbacks: z7.number() }),
|
|
2783
|
+
parentId: z7.string().nullable()
|
|
2526
2784
|
});
|
|
2527
|
-
var LessonStateSchema =
|
|
2528
|
-
var LessonViewSchema =
|
|
2529
|
-
id:
|
|
2530
|
-
text:
|
|
2785
|
+
var LessonStateSchema = z7.enum(["active", "proposed", "retired"]);
|
|
2786
|
+
var LessonViewSchema = z7.object({
|
|
2787
|
+
id: z7.string(),
|
|
2788
|
+
text: z7.string(),
|
|
2531
2789
|
state: LessonStateSchema,
|
|
2532
2790
|
/** The Goal it is scoped to; null = the whole account. */
|
|
2533
|
-
scopeGoalId:
|
|
2534
|
-
goalTitle:
|
|
2535
|
-
version:
|
|
2536
|
-
pinned:
|
|
2791
|
+
scopeGoalId: z7.string().nullable(),
|
|
2792
|
+
goalTitle: z7.string().nullable(),
|
|
2793
|
+
version: z7.number(),
|
|
2794
|
+
pinned: z7.boolean(),
|
|
2537
2795
|
/** When the person last wrote its text themselves. */
|
|
2538
|
-
editedAt:
|
|
2539
|
-
createdAt:
|
|
2540
|
-
updatedAt:
|
|
2796
|
+
editedAt: z7.string().nullable(),
|
|
2797
|
+
createdAt: z7.string(),
|
|
2798
|
+
updatedAt: z7.string(),
|
|
2541
2799
|
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
2542
|
-
sources:
|
|
2800
|
+
sources: z7.array(z7.object({ entryId: z7.string(), words: z7.string().nullable(), at: z7.string() }))
|
|
2543
2801
|
});
|
|
2544
|
-
var QueueQuestionSchema =
|
|
2802
|
+
var QueueQuestionSchema = z7.object({
|
|
2545
2803
|
/** The decision need's id — what an answer is accepted against. */
|
|
2546
|
-
id:
|
|
2804
|
+
id: z7.string(),
|
|
2547
2805
|
/** The words that were asked, from the request Entry that asked them. */
|
|
2548
|
-
question:
|
|
2806
|
+
question: z7.string(),
|
|
2549
2807
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
2550
2808
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
2551
2809
|
* can answer. */
|
|
2552
|
-
deliveryId:
|
|
2810
|
+
deliveryId: z7.string().nullable().default(null),
|
|
2553
2811
|
/** The Entry the ruling is about. */
|
|
2554
|
-
aboutId:
|
|
2812
|
+
aboutId: z7.string().nullable().default(null),
|
|
2555
2813
|
/** Empty for a free-text question. */
|
|
2556
|
-
options:
|
|
2557
|
-
select:
|
|
2558
|
-
askedAt:
|
|
2814
|
+
options: z7.array(OptionSchema).default([]),
|
|
2815
|
+
select: z7.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
2816
|
+
askedAt: z7.string(),
|
|
2559
2817
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
2560
|
-
answeredAt:
|
|
2818
|
+
answeredAt: z7.string().nullable().default(null),
|
|
2561
2819
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
2562
2820
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
2563
2821
|
* for a settled question whose reply carried nothing readable. */
|
|
2564
|
-
answer:
|
|
2822
|
+
answer: z7.string().nullable().default(null),
|
|
2565
2823
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
2566
2824
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
2567
2825
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
2568
2826
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
2569
2827
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
2570
2828
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
2571
|
-
goalId:
|
|
2829
|
+
goalId: z7.string(),
|
|
2572
2830
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
2573
2831
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
2574
2832
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
2575
2833
|
* gate at all. */
|
|
2576
|
-
blocked:
|
|
2834
|
+
blocked: z7.boolean().default(false)
|
|
2577
2835
|
});
|
|
2578
|
-
var QueueReplySchema =
|
|
2836
|
+
var QueueReplySchema = z7.object({
|
|
2579
2837
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
2580
2838
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
2581
2839
|
* walk can name it in its zoom. */
|
|
2582
|
-
id:
|
|
2840
|
+
id: z7.string(),
|
|
2583
2841
|
/** The Goal the note is on. */
|
|
2584
|
-
goalId:
|
|
2842
|
+
goalId: z7.string(),
|
|
2585
2843
|
/** What the note said. */
|
|
2586
|
-
note:
|
|
2844
|
+
note: z7.string(),
|
|
2587
2845
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
2588
|
-
deliveryId:
|
|
2589
|
-
requestEntryId:
|
|
2590
|
-
askedAt:
|
|
2846
|
+
deliveryId: z7.string(),
|
|
2847
|
+
requestEntryId: z7.string(),
|
|
2848
|
+
askedAt: z7.string(),
|
|
2591
2849
|
/** When the person last replied — the window's start. */
|
|
2592
|
-
repliedAt:
|
|
2850
|
+
repliedAt: z7.string(),
|
|
2593
2851
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
2594
|
-
reply:
|
|
2852
|
+
reply: z7.string().nullable()
|
|
2595
2853
|
});
|
|
2596
|
-
var QueueItemSchema =
|
|
2597
|
-
id:
|
|
2854
|
+
var QueueItemSchema = z7.object({
|
|
2855
|
+
id: z7.string(),
|
|
2598
2856
|
/** One-line headline — the first sentence of the outcome. */
|
|
2599
|
-
title:
|
|
2857
|
+
title: z7.string(),
|
|
2600
2858
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
2601
|
-
intent:
|
|
2859
|
+
intent: z7.string(),
|
|
2602
2860
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
2603
|
-
state:
|
|
2861
|
+
state: z7.string(),
|
|
2604
2862
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
2605
|
-
assignee:
|
|
2863
|
+
assignee: z7.string().nullable().default(null),
|
|
2606
2864
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
2607
|
-
progress:
|
|
2865
|
+
progress: z7.string().nullable().default(null),
|
|
2608
2866
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
2609
2867
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
2610
|
-
progressLine:
|
|
2611
|
-
reviewPending:
|
|
2612
|
-
dueAt:
|
|
2868
|
+
progressLine: z7.string().nullable().optional(),
|
|
2869
|
+
reviewPending: z7.boolean().default(false),
|
|
2870
|
+
dueAt: z7.string().nullable().default(null),
|
|
2613
2871
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
2614
2872
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
2615
|
-
finishedAt:
|
|
2873
|
+
finishedAt: z7.string().nullable().optional(),
|
|
2616
2874
|
/** The Goal this one was opened under; null at the root. */
|
|
2617
|
-
parentGoalId:
|
|
2875
|
+
parentGoalId: z7.string().nullable().default(null),
|
|
2618
2876
|
/** Goals opened under this one — only those the same list holds. */
|
|
2619
|
-
childGoalIds:
|
|
2877
|
+
childGoalIds: z7.array(z7.string()).default([]),
|
|
2620
2878
|
/** Goals this one waits on (start or finish gates). */
|
|
2621
|
-
dependencyGoalIds:
|
|
2879
|
+
dependencyGoalIds: z7.array(z7.string()).default([]),
|
|
2622
2880
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
2623
|
-
blocked:
|
|
2881
|
+
blocked: z7.boolean().default(false),
|
|
2624
2882
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
2625
2883
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
2626
2884
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
2627
2885
|
* question is opened. */
|
|
2628
|
-
questions:
|
|
2886
|
+
questions: z7.array(QueueQuestionSchema).default([]),
|
|
2629
2887
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
2630
2888
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
2631
2889
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
2632
2890
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
2633
|
-
asked:
|
|
2634
|
-
answered:
|
|
2891
|
+
asked: z7.number().optional(),
|
|
2892
|
+
answered: z7.number().optional(),
|
|
2635
2893
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
2636
2894
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
2637
2895
|
* not spell an empty list. */
|
|
2638
|
-
replies:
|
|
2896
|
+
replies: z7.array(QueueReplySchema).optional(),
|
|
2639
2897
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
2640
|
-
repo:
|
|
2641
|
-
createdAt:
|
|
2642
|
-
updatedAt:
|
|
2643
|
-
/** When its owner last SAID something about it (`
|
|
2644
|
-
*
|
|
2898
|
+
repo: z7.string().nullable().optional(),
|
|
2899
|
+
createdAt: z7.string(),
|
|
2900
|
+
updatedAt: z7.string().nullable().default(null),
|
|
2901
|
+
/** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
|
|
2902
|
+
* as progress). `updatedAt` moves for reasons nobody chose — a
|
|
2645
2903
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
2646
|
-
lastProgressAt:
|
|
2904
|
+
lastProgressAt: z7.string().nullable().optional(),
|
|
2647
2905
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
2648
2906
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
2649
2907
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
2650
2908
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
2651
2909
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
2652
2910
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
2653
|
-
latest:
|
|
2654
|
-
from:
|
|
2655
|
-
said:
|
|
2656
|
-
at:
|
|
2657
|
-
entryId:
|
|
2911
|
+
latest: z7.object({
|
|
2912
|
+
from: z7.enum(["person", "agent"]),
|
|
2913
|
+
said: z7.string(),
|
|
2914
|
+
at: z7.string(),
|
|
2915
|
+
entryId: z7.string()
|
|
2658
2916
|
}).nullable().optional(),
|
|
2659
2917
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
2660
2918
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -2667,7 +2925,7 @@ var QueueItemSchema = z5.object({
|
|
|
2667
2925
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
2668
2926
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
2669
2927
|
* Entry with no `about_id` is in neither. */
|
|
2670
|
-
lastPersonAt:
|
|
2928
|
+
lastPersonAt: z7.string().nullable().optional(),
|
|
2671
2929
|
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
2672
2930
|
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
2673
2931
|
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
@@ -2678,40 +2936,40 @@ var QueueItemSchema = z5.object({
|
|
|
2678
2936
|
* the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
|
|
2679
2937
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
2680
2938
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
2681
|
-
rev:
|
|
2939
|
+
rev: z7.string().optional()
|
|
2682
2940
|
});
|
|
2683
|
-
var QueueDeltaSchema =
|
|
2684
|
-
ids:
|
|
2685
|
-
items:
|
|
2941
|
+
var QueueDeltaSchema = z7.object({
|
|
2942
|
+
ids: z7.array(z7.string()),
|
|
2943
|
+
items: z7.array(QueueItemSchema)
|
|
2686
2944
|
});
|
|
2687
2945
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
2688
|
-
var NoteSourceSchema =
|
|
2689
|
-
var NoteStatusSchema =
|
|
2690
|
-
var NoteRepeatSchema =
|
|
2691
|
-
var DecisionSchema =
|
|
2692
|
-
id:
|
|
2946
|
+
var NoteSourceSchema = z7.enum(["app", "call"]);
|
|
2947
|
+
var NoteStatusSchema = z7.enum(["open", "assigned", "in_progress", "done"]);
|
|
2948
|
+
var NoteRepeatSchema = z7.enum(["once", "until_done"]);
|
|
2949
|
+
var DecisionSchema = z7.object({
|
|
2950
|
+
id: z7.string(),
|
|
2693
2951
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
2694
2952
|
* extensibility seam — any conversation can accrue decisions). */
|
|
2695
|
-
noteId:
|
|
2953
|
+
noteId: z7.string().nullable(),
|
|
2696
2954
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
2697
|
-
question:
|
|
2955
|
+
question: z7.string(),
|
|
2698
2956
|
/** The user's ruling; null while the question is open. */
|
|
2699
|
-
answer:
|
|
2700
|
-
decidedAt:
|
|
2701
|
-
createdAt:
|
|
2957
|
+
answer: z7.string().nullable(),
|
|
2958
|
+
decidedAt: z7.string().nullable(),
|
|
2959
|
+
createdAt: z7.string()
|
|
2702
2960
|
});
|
|
2703
|
-
var NoteSchema =
|
|
2704
|
-
id:
|
|
2961
|
+
var NoteSchema = z7.object({
|
|
2962
|
+
id: z7.string(),
|
|
2705
2963
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
2706
|
-
title:
|
|
2964
|
+
title: z7.string(),
|
|
2707
2965
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
2708
|
-
intent:
|
|
2966
|
+
intent: z7.string(),
|
|
2709
2967
|
source: NoteSourceSchema,
|
|
2710
2968
|
status: NoteStatusSchema,
|
|
2711
2969
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
2712
|
-
assignee:
|
|
2970
|
+
assignee: z7.string().nullable(),
|
|
2713
2971
|
/** The request thread minted at assignment; null until assigned. */
|
|
2714
|
-
parentId:
|
|
2972
|
+
parentId: z7.string().nullable(),
|
|
2715
2973
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
2716
2974
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
2717
2975
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -2720,154 +2978,154 @@ var NoteSchema = z5.object({
|
|
|
2720
2978
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
2721
2979
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
2722
2980
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
2723
|
-
dueAt:
|
|
2981
|
+
dueAt: z7.string().nullable().default(null),
|
|
2724
2982
|
repeat: NoteRepeatSchema.default("once"),
|
|
2725
2983
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
2726
|
-
rides:
|
|
2727
|
-
lastRideAt:
|
|
2728
|
-
createdAt:
|
|
2984
|
+
rides: z7.number().int().default(0),
|
|
2985
|
+
lastRideAt: z7.string().nullable().default(null),
|
|
2986
|
+
createdAt: z7.string()
|
|
2729
2987
|
});
|
|
2730
|
-
var TriageItemSchema =
|
|
2731
|
-
noteId:
|
|
2988
|
+
var TriageItemSchema = z7.object({
|
|
2989
|
+
noteId: z7.string(),
|
|
2732
2990
|
/** The note's headline at run time. */
|
|
2733
|
-
title:
|
|
2991
|
+
title: z7.string(),
|
|
2734
2992
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
2735
2993
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
2736
2994
|
* Never a model's reasoning transcript, never an id. */
|
|
2737
|
-
why:
|
|
2995
|
+
why: z7.string()
|
|
2738
2996
|
});
|
|
2739
|
-
var TriageAssignmentSchema =
|
|
2997
|
+
var TriageAssignmentSchema = z7.object({
|
|
2740
2998
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
2741
|
-
agent:
|
|
2999
|
+
agent: z7.string(),
|
|
2742
3000
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
2743
3001
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
2744
|
-
agentName:
|
|
2745
|
-
notes:
|
|
3002
|
+
agentName: z7.string(),
|
|
3003
|
+
notes: z7.array(TriageItemSchema)
|
|
2746
3004
|
});
|
|
2747
|
-
var TriageStatusSchema =
|
|
2748
|
-
var SubmitTriageSchema =
|
|
3005
|
+
var TriageStatusSchema = z7.enum(["open", "superseded", "dismissed"]);
|
|
3006
|
+
var SubmitTriageSchema = z7.object({
|
|
2749
3007
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
2750
3008
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
2751
3009
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
2752
3010
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
2753
|
-
provider:
|
|
3011
|
+
provider: z7.string().min(1).max(60),
|
|
2754
3012
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
2755
|
-
model:
|
|
3013
|
+
model: z7.string().max(200).nullable().optional(),
|
|
2756
3014
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
2757
3015
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
2758
|
-
reviewed:
|
|
2759
|
-
close:
|
|
2760
|
-
stale:
|
|
2761
|
-
assign:
|
|
3016
|
+
reviewed: z7.number().int().min(0).max(1e4).default(0),
|
|
3017
|
+
close: z7.array(TriageItemSchema).max(200).default([]),
|
|
3018
|
+
stale: z7.array(TriageItemSchema).max(200).default([]),
|
|
3019
|
+
assign: z7.array(TriageAssignmentSchema).max(50).default([])
|
|
2762
3020
|
});
|
|
2763
3021
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
2764
|
-
id:
|
|
2765
|
-
runAt:
|
|
3022
|
+
id: z7.string(),
|
|
3023
|
+
runAt: z7.string(),
|
|
2766
3024
|
status: TriageStatusSchema,
|
|
2767
|
-
model:
|
|
3025
|
+
model: z7.string().nullable().default(null)
|
|
2768
3026
|
});
|
|
2769
|
-
var AcceptTriageSchema =
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
group:
|
|
2774
|
-
agent:
|
|
2775
|
-
noteIds:
|
|
3027
|
+
var AcceptTriageSchema = z7.discriminatedUnion("group", [
|
|
3028
|
+
z7.object({ group: z7.literal("close"), noteIds: z7.array(z7.string()).max(200).optional() }),
|
|
3029
|
+
z7.object({ group: z7.literal("stale"), noteIds: z7.array(z7.string()).max(200).optional() }),
|
|
3030
|
+
z7.object({
|
|
3031
|
+
group: z7.literal("assign"),
|
|
3032
|
+
agent: z7.string().min(1),
|
|
3033
|
+
noteIds: z7.array(z7.string()).max(200).optional()
|
|
2776
3034
|
})
|
|
2777
3035
|
]);
|
|
2778
|
-
var AcceptTriageResultSchema =
|
|
2779
|
-
accepted:
|
|
2780
|
-
failed:
|
|
3036
|
+
var AcceptTriageResultSchema = z7.object({
|
|
3037
|
+
accepted: z7.array(z7.string()),
|
|
3038
|
+
failed: z7.array(z7.object({ noteId: z7.string(), reason: z7.string() }))
|
|
2781
3039
|
});
|
|
2782
|
-
var DeliveryModeSchema =
|
|
3040
|
+
var DeliveryModeSchema = z7.enum(["poll", "self_hosted"]);
|
|
2783
3041
|
var WAKE_EVENT = "wake";
|
|
2784
3042
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
2785
|
-
var RegisterDeliverySchema =
|
|
2786
|
-
var OAuthStartSchema =
|
|
2787
|
-
provider:
|
|
2788
|
-
returnTo:
|
|
3043
|
+
var RegisterDeliverySchema = z7.object({ mode: DeliveryModeSchema });
|
|
3044
|
+
var OAuthStartSchema = z7.object({
|
|
3045
|
+
provider: z7.enum(["cma"]),
|
|
3046
|
+
returnTo: z7.string().min(1)
|
|
2789
3047
|
});
|
|
2790
|
-
var DeliveryConfigSchema =
|
|
2791
|
-
tokenId:
|
|
3048
|
+
var DeliveryConfigSchema = z7.object({
|
|
3049
|
+
tokenId: z7.string(),
|
|
2792
3050
|
mode: DeliveryModeSchema,
|
|
2793
3051
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
2794
3052
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
2795
3053
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
2796
|
-
realtime:
|
|
3054
|
+
realtime: z7.object({ url: z7.string(), anonKey: z7.string() }).nullable()
|
|
2797
3055
|
});
|
|
2798
|
-
var HostDecisionSchema =
|
|
3056
|
+
var HostDecisionSchema = z7.object({
|
|
2799
3057
|
/** The agent's token id: the row's `recipient`. */
|
|
2800
|
-
agent:
|
|
2801
|
-
decision:
|
|
3058
|
+
agent: z7.string().uuid(),
|
|
3059
|
+
decision: z7.enum(["stood_back", "took_over"]),
|
|
2802
3060
|
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
2803
|
-
goalId:
|
|
3061
|
+
goalId: z7.string().uuid().nullable().optional(),
|
|
2804
3062
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
2805
|
-
seenAt:
|
|
2806
|
-
/** When that work last moved (`claimable.since` on `
|
|
2807
|
-
since:
|
|
3063
|
+
seenAt: z7.string().datetime().nullable().optional(),
|
|
3064
|
+
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
3065
|
+
since: z7.string().datetime().nullable().optional(),
|
|
2808
3066
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
2809
|
-
said:
|
|
3067
|
+
said: z7.string().max(300).optional()
|
|
2810
3068
|
});
|
|
2811
|
-
var WakeNudgeSchema =
|
|
2812
|
-
kind:
|
|
2813
|
-
notificationId:
|
|
2814
|
-
parentId:
|
|
3069
|
+
var WakeNudgeSchema = z7.object({
|
|
3070
|
+
kind: z7.enum(["reply", "request", "callback"]),
|
|
3071
|
+
notificationId: z7.string().optional(),
|
|
3072
|
+
parentId: z7.string()
|
|
2815
3073
|
});
|
|
2816
|
-
var PairingStatusSchema =
|
|
2817
|
-
var DeviceCodeSchema =
|
|
2818
|
-
device_code:
|
|
2819
|
-
user_code:
|
|
2820
|
-
verification_uri:
|
|
2821
|
-
verification_uri_complete:
|
|
2822
|
-
interval:
|
|
2823
|
-
expires_in:
|
|
3074
|
+
var PairingStatusSchema = z7.enum(["pending", "approved", "denied", "expired"]);
|
|
3075
|
+
var DeviceCodeSchema = z7.object({
|
|
3076
|
+
device_code: z7.string(),
|
|
3077
|
+
user_code: z7.string(),
|
|
3078
|
+
verification_uri: z7.string().url(),
|
|
3079
|
+
verification_uri_complete: z7.string().url(),
|
|
3080
|
+
interval: z7.number(),
|
|
3081
|
+
expires_in: z7.number()
|
|
2824
3082
|
});
|
|
2825
|
-
var DeviceInfoSchema =
|
|
2826
|
-
code:
|
|
3083
|
+
var DeviceInfoSchema = z7.object({
|
|
3084
|
+
code: z7.string(),
|
|
2827
3085
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
2828
3086
|
* pre-filling the name field the human can edit. */
|
|
2829
|
-
name:
|
|
3087
|
+
name: z7.string(),
|
|
2830
3088
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
2831
3089
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
2832
3090
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
2833
3091
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
2834
|
-
agent:
|
|
2835
|
-
device:
|
|
3092
|
+
agent: z7.string().optional(),
|
|
3093
|
+
device: z7.string().nullable(),
|
|
2836
3094
|
status: PairingStatusSchema
|
|
2837
3095
|
});
|
|
2838
|
-
var DeviceTokenSchema =
|
|
2839
|
-
access_token:
|
|
3096
|
+
var DeviceTokenSchema = z7.object({
|
|
3097
|
+
access_token: z7.string(),
|
|
2840
3098
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
2841
3099
|
* a default silly name). */
|
|
2842
|
-
name:
|
|
2843
|
-
device:
|
|
3100
|
+
name: z7.string(),
|
|
3101
|
+
device: z7.string().nullable(),
|
|
2844
3102
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
2845
3103
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
2846
|
-
voice:
|
|
3104
|
+
voice: z7.string().nullable().optional(),
|
|
2847
3105
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
2848
3106
|
* that survives a rename. Cached by the host's identity beat. */
|
|
2849
|
-
token_id:
|
|
3107
|
+
token_id: z7.string().nullable().optional(),
|
|
2850
3108
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
2851
3109
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
2852
3110
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
2853
3111
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
2854
|
-
workspace:
|
|
3112
|
+
workspace: z7.string().nullable().optional(),
|
|
2855
3113
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
2856
|
-
harness:
|
|
2857
|
-
session_id:
|
|
3114
|
+
harness: z7.enum(["claude", "codex", "agy"]).optional(),
|
|
3115
|
+
session_id: z7.string().uuid().optional(),
|
|
2858
3116
|
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
2859
3117
|
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
2860
3118
|
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
2861
3119
|
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
2862
|
-
full_session:
|
|
2863
|
-
uik_pub:
|
|
3120
|
+
full_session: z7.string().optional(),
|
|
3121
|
+
uik_pub: z7.string().nullable().optional()
|
|
2864
3122
|
});
|
|
2865
|
-
var SupportRequestSchema =
|
|
2866
|
-
email:
|
|
2867
|
-
message:
|
|
2868
|
-
name:
|
|
3123
|
+
var SupportRequestSchema = z7.object({
|
|
3124
|
+
email: z7.string().email().max(320),
|
|
3125
|
+
message: z7.string().trim().min(1).max(5e3),
|
|
3126
|
+
name: z7.string().trim().max(120).optional()
|
|
2869
3127
|
});
|
|
2870
|
-
var NotificationFeedbackKindSchema =
|
|
3128
|
+
var NotificationFeedbackKindSchema = z7.enum([
|
|
2871
3129
|
"break_down",
|
|
2872
3130
|
// "This should be more than one ask — break it down."
|
|
2873
3131
|
"regenerate_options",
|
|
@@ -2882,116 +3140,116 @@ var NotificationFeedbackKindSchema = z5.enum([
|
|
|
2882
3140
|
// anything else — the note carries it.
|
|
2883
3141
|
]);
|
|
2884
3142
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
2885
|
-
var QuestionRowSchema =
|
|
3143
|
+
var QuestionRowSchema = z7.object({
|
|
2886
3144
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
2887
|
-
id:
|
|
2888
|
-
deliveryId:
|
|
2889
|
-
entryId:
|
|
3145
|
+
id: z7.string(),
|
|
3146
|
+
deliveryId: z7.string(),
|
|
3147
|
+
entryId: z7.string(),
|
|
2890
3148
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
2891
|
-
needId:
|
|
2892
|
-
goalIds:
|
|
3149
|
+
needId: z7.string().nullable(),
|
|
3150
|
+
goalIds: z7.array(z7.string()),
|
|
2893
3151
|
/** The name of the work it is about, when the read could word it. */
|
|
2894
|
-
goalTitle:
|
|
2895
|
-
tokenId:
|
|
2896
|
-
name:
|
|
2897
|
-
title:
|
|
2898
|
-
body:
|
|
2899
|
-
select:
|
|
2900
|
-
options:
|
|
2901
|
-
hasPreview:
|
|
2902
|
-
blocking:
|
|
2903
|
-
askedAt:
|
|
3152
|
+
goalTitle: z7.string().optional(),
|
|
3153
|
+
tokenId: z7.string().optional(),
|
|
3154
|
+
name: z7.string(),
|
|
3155
|
+
title: z7.string(),
|
|
3156
|
+
body: z7.string(),
|
|
3157
|
+
select: z7.enum(["one", "many", "rank", "confirm", "text"]),
|
|
3158
|
+
options: z7.array(SlimOptionSchema),
|
|
3159
|
+
hasPreview: z7.boolean(),
|
|
3160
|
+
blocking: z7.boolean(),
|
|
3161
|
+
askedAt: z7.string().datetime(),
|
|
2904
3162
|
ring: InboxItemSchema.shape.ring,
|
|
2905
|
-
onCall:
|
|
2906
|
-
sealed:
|
|
3163
|
+
onCall: z7.literal(true).optional(),
|
|
3164
|
+
sealed: z7.boolean()
|
|
2907
3165
|
});
|
|
2908
|
-
var WorkStateSchema =
|
|
2909
|
-
var WorkRowSchema =
|
|
2910
|
-
id:
|
|
2911
|
-
parentId:
|
|
2912
|
-
title:
|
|
3166
|
+
var WorkStateSchema = z7.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
3167
|
+
var WorkRowSchema = z7.object({
|
|
3168
|
+
id: z7.string(),
|
|
3169
|
+
parentId: z7.string().nullable(),
|
|
3170
|
+
title: z7.string(),
|
|
2913
3171
|
/** Straight off the Goal. */
|
|
2914
3172
|
state: WorkStateSchema,
|
|
2915
|
-
owner:
|
|
2916
|
-
revision:
|
|
3173
|
+
owner: z7.string().nullable(),
|
|
3174
|
+
revision: z7.number().int(),
|
|
2917
3175
|
/** Open questions on it, counted to 100. */
|
|
2918
|
-
waiting:
|
|
3176
|
+
waiting: z7.number().int(),
|
|
2919
3177
|
/** Held by a gate on work that is not done. */
|
|
2920
|
-
blocked:
|
|
2921
|
-
lastProgressAt:
|
|
3178
|
+
blocked: z7.boolean(),
|
|
3179
|
+
lastProgressAt: z7.string().datetime().nullable(),
|
|
2922
3180
|
/** The line written for its newest progress note, else that note's first words. */
|
|
2923
|
-
line:
|
|
3181
|
+
line: z7.string().nullable(),
|
|
2924
3182
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
2925
|
-
children:
|
|
2926
|
-
createdAt:
|
|
2927
|
-
updatedAt:
|
|
3183
|
+
children: z7.number().int(),
|
|
3184
|
+
createdAt: z7.string().datetime(),
|
|
3185
|
+
updatedAt: z7.string().datetime(),
|
|
2928
3186
|
/** When anything at or under it last moved — the order the list is in. */
|
|
2929
|
-
activeAt:
|
|
3187
|
+
activeAt: z7.string().datetime(),
|
|
2930
3188
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
2931
|
-
sealed:
|
|
3189
|
+
sealed: z7.boolean()
|
|
2932
3190
|
});
|
|
2933
3191
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
2934
3192
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
2935
3193
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
2936
|
-
asking:
|
|
2937
|
-
oldestAskAt:
|
|
3194
|
+
asking: z7.number().int().nullable(),
|
|
3195
|
+
oldestAskAt: z7.string().datetime().nullable(),
|
|
2938
3196
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
2939
3197
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
2940
3198
|
* (`agent_holds`); null when that read failed. */
|
|
2941
|
-
holds:
|
|
2942
|
-
held:
|
|
3199
|
+
holds: z7.array(z7.object({ id: z7.string(), title: z7.string() })).nullable(),
|
|
3200
|
+
held: z7.number().int().nullable(),
|
|
2943
3201
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
2944
3202
|
* while none has, or when that read failed. */
|
|
2945
|
-
cold:
|
|
3203
|
+
cold: z7.string().datetime().nullable(),
|
|
2946
3204
|
/** The newest line of its working log, and when the harness saw it. */
|
|
2947
|
-
line:
|
|
2948
|
-
lineAt:
|
|
3205
|
+
line: z7.string().nullable(),
|
|
3206
|
+
lineAt: z7.string().datetime().nullable()
|
|
2949
3207
|
});
|
|
2950
|
-
var SnapshotSchema =
|
|
3208
|
+
var SnapshotSchema = z7.object({
|
|
2951
3209
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
2952
|
-
at:
|
|
2953
|
-
questions:
|
|
3210
|
+
at: z7.string().datetime(),
|
|
3211
|
+
questions: z7.object({
|
|
2954
3212
|
/** The newest 30 open cards, questions before updates. */
|
|
2955
|
-
items:
|
|
3213
|
+
items: z7.array(QuestionRowSchema),
|
|
2956
3214
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
2957
|
-
total:
|
|
2958
|
-
updates:
|
|
2959
|
-
putOff:
|
|
3215
|
+
total: z7.number().int(),
|
|
3216
|
+
updates: z7.number().int(),
|
|
3217
|
+
putOff: z7.number().int()
|
|
2960
3218
|
}).nullable(),
|
|
2961
|
-
agents:
|
|
3219
|
+
agents: z7.object({
|
|
2962
3220
|
/** Up to 60, most recently seen first. */
|
|
2963
|
-
items:
|
|
2964
|
-
more:
|
|
3221
|
+
items: z7.array(AgentRowSchema),
|
|
3222
|
+
more: z7.boolean()
|
|
2965
3223
|
}).nullable(),
|
|
2966
|
-
work:
|
|
3224
|
+
work: z7.object({
|
|
2967
3225
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
2968
|
-
items:
|
|
3226
|
+
items: z7.array(WorkRowSchema),
|
|
2969
3227
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
2970
3228
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
2971
3229
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
2972
3230
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
2973
|
-
counts:
|
|
3231
|
+
counts: z7.object({ live: z7.number().int(), waiting: z7.number().int(), notStarted: z7.number().int(), done: z7.number().int() })
|
|
2974
3232
|
}).nullable(),
|
|
2975
|
-
you:
|
|
3233
|
+
you: z7.object({
|
|
2976
3234
|
settings: UserSettingsSchema,
|
|
2977
|
-
callable:
|
|
3235
|
+
callable: z7.boolean(),
|
|
2978
3236
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
2979
|
-
computers:
|
|
3237
|
+
computers: z7.array(ComputerRowSchema).nullable()
|
|
2980
3238
|
}).nullable()
|
|
2981
3239
|
});
|
|
2982
|
-
var CallRecapSchema =
|
|
2983
|
-
call:
|
|
2984
|
-
status:
|
|
2985
|
-
startedAt:
|
|
2986
|
-
durationMs:
|
|
2987
|
-
agents:
|
|
3240
|
+
var CallRecapSchema = z7.object({
|
|
3241
|
+
call: z7.object({
|
|
3242
|
+
status: z7.string(),
|
|
3243
|
+
startedAt: z7.string(),
|
|
3244
|
+
durationMs: z7.number().nullable(),
|
|
3245
|
+
agents: z7.array(z7.object({ id: z7.string(), name: z7.string().nullable() }))
|
|
2988
3246
|
}),
|
|
2989
|
-
topics:
|
|
2990
|
-
goalId:
|
|
2991
|
-
title:
|
|
2992
|
-
owner:
|
|
2993
|
-
state:
|
|
2994
|
-
questions:
|
|
3247
|
+
topics: z7.array(z7.object({
|
|
3248
|
+
goalId: z7.string().uuid(),
|
|
3249
|
+
title: z7.string(),
|
|
3250
|
+
owner: z7.string(),
|
|
3251
|
+
state: z7.string(),
|
|
3252
|
+
questions: z7.array(z7.object({ id: z7.string().uuid(), state: z7.string(), title: z7.string() })),
|
|
2995
3253
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
2996
3254
|
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
2997
3255
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
@@ -2999,10 +3257,10 @@ var CallRecapSchema = z5.object({
|
|
|
2999
3257
|
* anything that is not an answer. */
|
|
3000
3258
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
3001
3259
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
3002
|
-
lines:
|
|
3260
|
+
lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), headline: z7.string().optional(), about: z7.string().nullable().optional() }))
|
|
3003
3261
|
})),
|
|
3004
|
-
unfiled:
|
|
3005
|
-
more:
|
|
3262
|
+
unfiled: z7.array(z7.object({ lineId: z7.string().uuid(), words: z7.string(), atMs: z7.number() })),
|
|
3263
|
+
more: z7.object({ lines: z7.number(), entries: z7.number(), topics: z7.number() })
|
|
3006
3264
|
});
|
|
3007
3265
|
function sessionSlot(sessionId2) {
|
|
3008
3266
|
const id2 = sessionId2 ?? sessionId();
|
|
@@ -3170,39 +3428,32 @@ var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
|
3170
3428
|
async function fail(what, res) {
|
|
3171
3429
|
throw new ApiError(what, res.status, await res.text());
|
|
3172
3430
|
}
|
|
3173
|
-
async function
|
|
3431
|
+
async function manageGoals(input, opts = {}) {
|
|
3174
3432
|
const token = authToken(opts.token) ?? "";
|
|
3175
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
3433
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/manage`, {
|
|
3176
3434
|
method: "POST",
|
|
3177
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}
|
|
3435
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3178
3436
|
body: JSON.stringify(input)
|
|
3179
3437
|
}));
|
|
3180
|
-
if (!res.ok) await fail("
|
|
3438
|
+
if (!res.ok) await fail("manage_goals", res);
|
|
3181
3439
|
return await res.json();
|
|
3182
3440
|
}
|
|
3183
3441
|
async function claimGoal(goalId, opts = {}) {
|
|
3184
3442
|
const token = authToken(opts.token) ?? "";
|
|
3185
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}
|
|
3443
|
+
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 } : {}) }));
|
|
3186
3444
|
if (!res.ok) await fail("claim_goal", res);
|
|
3187
3445
|
return await res.json();
|
|
3188
3446
|
}
|
|
3189
|
-
async function getGoal(goalId, opts = {},
|
|
3447
|
+
async function getGoal(goalId, opts = {}, read3 = {}) {
|
|
3190
3448
|
const token = authToken(opts.token) ?? "";
|
|
3191
|
-
const query = [
|
|
3449
|
+
const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
3192
3450
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
|
|
3193
3451
|
method: "GET",
|
|
3194
|
-
headers: { authorization: `Bearer ${token}
|
|
3452
|
+
headers: { authorization: `Bearer ${token}` }
|
|
3195
3453
|
}));
|
|
3196
3454
|
if (!res.ok) await fail("get_goal", res);
|
|
3197
3455
|
return await res.json();
|
|
3198
3456
|
}
|
|
3199
|
-
async function updateGoal(goalId, input, opts = {}) {
|
|
3200
|
-
const token = authToken(opts.token) ?? "";
|
|
3201
|
-
const operationId = input.operationId ?? randomUUID2();
|
|
3202
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
3203
|
-
if (!res.ok) await fail("update_goal", res);
|
|
3204
|
-
return await res.json();
|
|
3205
|
-
}
|
|
3206
3457
|
var AWAIT_WINDOW_MS = 45e3;
|
|
3207
3458
|
async function hatch(name, voice = null, opts = {}) {
|
|
3208
3459
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/hatch`, {
|
|
@@ -3315,7 +3566,7 @@ async function listNotes(opts = {}) {
|
|
|
3315
3566
|
}
|
|
3316
3567
|
async function listConnections(opts = {}) {
|
|
3317
3568
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
3318
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}
|
|
3569
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
3319
3570
|
}));
|
|
3320
3571
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
3321
3572
|
return await res.json();
|
|
@@ -3353,128 +3604,171 @@ async function dismissTriage(proposalId, opts = {}) {
|
|
|
3353
3604
|
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
3354
3605
|
return await res.json();
|
|
3355
3606
|
}
|
|
3607
|
+
async function searchRecords(input, opts = {}) {
|
|
3608
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/records/search`, {
|
|
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("search", res);
|
|
3614
|
+
return await res.json();
|
|
3615
|
+
}
|
|
3616
|
+
async function sendFeedback(input, opts = {}) {
|
|
3617
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/reports`, {
|
|
3618
|
+
method: "POST",
|
|
3619
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3620
|
+
body: JSON.stringify(input)
|
|
3621
|
+
}));
|
|
3622
|
+
if (!res.ok) await fail("send_feedback", res);
|
|
3623
|
+
return await res.json();
|
|
3624
|
+
}
|
|
3356
3625
|
async function whoIsWorking(opts = {}) {
|
|
3357
3626
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
|
|
3358
|
-
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}
|
|
3627
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3359
3628
|
}));
|
|
3360
3629
|
if (!res.ok) await fail("who_is_working", res);
|
|
3361
3630
|
return await res.json();
|
|
3362
3631
|
}
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
const
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
var asked;
|
|
3374
|
-
function currentRepo(cwd = process.cwd()) {
|
|
3375
|
-
if (asked !== void 0) return asked;
|
|
3376
|
-
try {
|
|
3377
|
-
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
3378
|
-
cwd,
|
|
3379
|
-
encoding: "utf8",
|
|
3380
|
-
timeout: 2e3,
|
|
3381
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
3382
|
-
});
|
|
3383
|
-
asked = repoFromRemote(url);
|
|
3384
|
-
} catch {
|
|
3385
|
-
asked = null;
|
|
3386
|
-
}
|
|
3387
|
-
return asked;
|
|
3632
|
+
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.`;
|
|
3633
|
+
var APPLIED = "Applied; nothing was sent to the person.";
|
|
3634
|
+
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.";
|
|
3635
|
+
async function readDelivery(deliveryId, opts = {}) {
|
|
3636
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, {
|
|
3637
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3638
|
+
signal: opts.signal
|
|
3639
|
+
}));
|
|
3640
|
+
if (!res.ok) await fail("read_delivery", res);
|
|
3641
|
+
return await res.json();
|
|
3388
3642
|
}
|
|
3389
|
-
var NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
|
|
3390
|
-
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.";
|
|
3391
3643
|
async function contact(input, opts = {}) {
|
|
3392
3644
|
const parsed = ContactSchema.parse(input);
|
|
3645
|
+
if (!sendsAnything(parsed)) throw new Error("contact with nothing to send receives: call receiveContact");
|
|
3393
3646
|
opts.signal?.throwIfAborted();
|
|
3394
3647
|
const send2 = opts.reach ?? reach;
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
})
|
|
3426
|
-
}));
|
|
3427
|
-
if (!res.ok) await fail("batch contact", res);
|
|
3428
|
-
const receipt = await res.json();
|
|
3429
|
-
deliveries = receipt.deliveries ?? [];
|
|
3430
|
-
if (parsed.channel === "call" && receipt.channel && receipt.channel !== "call") demoted = DEMOTED;
|
|
3431
|
-
recorded = receipt.recorded ?? [];
|
|
3432
|
-
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
3433
|
-
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
3434
|
-
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
3435
|
-
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
3436
|
-
}
|
|
3437
|
-
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id: id2, deliveryId: deliveryId2, goalId }) => ({ ...id2 ? { id: id2 } : {}, deliveryId: deliveryId2, goalId }));
|
|
3438
|
-
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
3439
|
-
const joinedCall = deliveries[0]?.joinedCall === true;
|
|
3440
|
-
const read = async (signal2) => {
|
|
3441
|
-
const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
3442
|
-
if (!res.ok) await fail("read_delivery", res);
|
|
3443
|
-
const one = await res.json();
|
|
3648
|
+
const acknowledged = parsed.ackEventIds?.length ? await acknowledge(parsed.ackEventIds, opts) : void 0;
|
|
3649
|
+
const questions = parsed.questions?.map((q) => ({ ...q, questionId: q.questionId ?? randomUUID2() }));
|
|
3650
|
+
const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/goals/contact`, {
|
|
3651
|
+
method: "POST",
|
|
3652
|
+
signal: opts.signal,
|
|
3653
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3654
|
+
body: JSON.stringify({
|
|
3655
|
+
operationId: opts.operationId ?? randomUUID2(),
|
|
3656
|
+
...parsed.updates ? { updates: parsed.updates } : {},
|
|
3657
|
+
...questions ? { questions } : {},
|
|
3658
|
+
...parsed.answers ? { answers: parsed.answers } : {},
|
|
3659
|
+
...parsed.withdrawQuestionIds ? { withdrawQuestionIds: parsed.withdrawQuestionIds } : {},
|
|
3660
|
+
...parsed.questionDependencies ? { questionDependencies: parsed.questionDependencies } : {}
|
|
3661
|
+
})
|
|
3662
|
+
}));
|
|
3663
|
+
if (!res.ok) await fail("contact", res);
|
|
3664
|
+
const receipt = await res.json();
|
|
3665
|
+
const results = receipt.results ?? [];
|
|
3666
|
+
const ok = receipt.ok ?? results.every((r) => r.status !== "failed");
|
|
3667
|
+
const deliveries = receipt.deliveries ?? [];
|
|
3668
|
+
const recorded = results.filter((r) => r.status === "recorded" && r.goalId).map((r) => ({ goalId: r.goalId }));
|
|
3669
|
+
const askedCall = [...parsed.updates ?? [], ...questions ?? []].some((m) => m.userExplicitlyRequested === "call") || !!questions?.some((q) => q.workItBlocks?.length);
|
|
3670
|
+
const demoted = askedCall && receipt.channel && receipt.channel !== "call" ? DEMOTED : void 0;
|
|
3671
|
+
const acked = acknowledged ? { acknowledged } : {};
|
|
3672
|
+
if (!deliveries.length) {
|
|
3673
|
+
return { sent: false, ok, results, ...recorded.length ? { recorded } : {}, message: recorded.length ? NOT_SENT : APPLIED, waitOutcome: "not_waited", ...acked };
|
|
3674
|
+
}
|
|
3675
|
+
const lead = deliveries.find((d) => d.kind === "question") ?? deliveries[0];
|
|
3676
|
+
const read3 = async (signal2) => {
|
|
3677
|
+
const one = await readDelivery(lead.deliveryId, { ...opts, signal: signal2 });
|
|
3444
3678
|
return {
|
|
3445
3679
|
...one,
|
|
3446
|
-
|
|
3680
|
+
ok,
|
|
3681
|
+
results,
|
|
3682
|
+
...deliveries.length > 1 ? { deliveries: deliveries.map(({ deliveryId, goalId, kind, questionId }) => ({ deliveryId, goalId, kind, ...questionId ? { questionId } : {} })) } : {},
|
|
3447
3683
|
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
3448
|
-
...joinedCard ? { joinedCard: true } : {},
|
|
3449
|
-
|
|
3450
|
-
...
|
|
3684
|
+
...lead.joinedCard ? { joinedCard: true } : {},
|
|
3685
|
+
// IT WENT ONTO A CALL ALREADY HAPPENING (owner, 2026-09-28): the view words it as that call.
|
|
3686
|
+
...lead.joinedCall ? { joinedCall: true } : {},
|
|
3687
|
+
...demoted ? { demoted } : {},
|
|
3688
|
+
...acked
|
|
3451
3689
|
};
|
|
3452
3690
|
};
|
|
3453
|
-
const settled = (d) => d.
|
|
3454
|
-
if (opts.waits === false) return
|
|
3691
|
+
const settled = (d) => d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution" && !entryUnit(e));
|
|
3692
|
+
if (parsed.wait !== true || opts.waits === false) return { ...await read3(opts.signal), waitOutcome: "not_waited" };
|
|
3455
3693
|
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
3456
3694
|
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
3457
3695
|
let latest;
|
|
3458
3696
|
try {
|
|
3459
3697
|
while (true) {
|
|
3460
3698
|
signal.throwIfAborted();
|
|
3461
|
-
latest = await
|
|
3462
|
-
if (settled(latest)) return latest;
|
|
3699
|
+
latest = await read3(signal);
|
|
3700
|
+
if (settled(latest)) return { ...latest, waitOutcome: "available" };
|
|
3463
3701
|
await sleep2(5e3, void 0, { signal });
|
|
3464
3702
|
}
|
|
3465
3703
|
} catch (error) {
|
|
3466
3704
|
opts.signal?.throwIfAborted();
|
|
3467
|
-
if (window.aborted && latest) return latest;
|
|
3705
|
+
if (window.aborted && latest) return { ...latest, waitOutcome: "expired" };
|
|
3468
3706
|
throw error;
|
|
3469
3707
|
}
|
|
3470
3708
|
}
|
|
3471
|
-
async function
|
|
3709
|
+
async function receive(opts = {}) {
|
|
3472
3710
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
3473
|
-
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}
|
|
3711
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3474
3712
|
}));
|
|
3475
|
-
if (!res.ok) await fail("
|
|
3713
|
+
if (!res.ok) await fail("receive", res);
|
|
3476
3714
|
return await res.json();
|
|
3477
3715
|
}
|
|
3716
|
+
async function acknowledge(eventIds, opts = {}) {
|
|
3717
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries/ack`, {
|
|
3718
|
+
method: "POST",
|
|
3719
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3720
|
+
body: JSON.stringify({ eventIds })
|
|
3721
|
+
}));
|
|
3722
|
+
if (!res.ok) await fail("acknowledge", res);
|
|
3723
|
+
return (await res.json()).results ?? [];
|
|
3724
|
+
}
|
|
3725
|
+
var EVENT_BATCH = 20;
|
|
3726
|
+
function pendingEvents(w) {
|
|
3727
|
+
const fromDelivery = (d) => ({
|
|
3728
|
+
eventId: d.deliveryId,
|
|
3729
|
+
kind: d.questions?.length ? "question" : "instruction",
|
|
3730
|
+
...d.goalIds.length ? { goalIds: d.goalIds } : {},
|
|
3731
|
+
...d.about ? { text: d.about } : {},
|
|
3732
|
+
...d.questions?.length ? { questions: d.questions } : {}
|
|
3733
|
+
});
|
|
3734
|
+
const deliveries = (w.deliveries ?? []).map(fromDelivery);
|
|
3735
|
+
return [
|
|
3736
|
+
...deliveries.filter((e) => e.kind === "question"),
|
|
3737
|
+
...(w.review ?? []).map((r) => ({
|
|
3738
|
+
eventId: r.eventId ?? r.goalId,
|
|
3739
|
+
kind: "update",
|
|
3740
|
+
goalIds: [r.goalId],
|
|
3741
|
+
text: r.said ? `${r.title}: ${r.said}` : r.title,
|
|
3742
|
+
...r.saidAt ? { at: r.saidAt } : {}
|
|
3743
|
+
})),
|
|
3744
|
+
...deliveries.filter((e) => e.kind !== "question")
|
|
3745
|
+
];
|
|
3746
|
+
}
|
|
3747
|
+
async function receiveContact(input, opts = {}) {
|
|
3748
|
+
opts.signal?.throwIfAborted();
|
|
3749
|
+
const acknowledged = input.ackEventIds?.length ? await acknowledge(input.ackEventIds, opts) : void 0;
|
|
3750
|
+
const shaped = (waiting, waitOutcome) => {
|
|
3751
|
+
const all = pendingEvents(waiting);
|
|
3752
|
+
return { waiting, events: all.slice(0, EVENT_BATCH), hasMore: all.length > EVENT_BATCH, waitOutcome, ...acknowledged ? { acknowledged } : {} };
|
|
3753
|
+
};
|
|
3754
|
+
const wait = (input.wait ?? !input.ackEventIds?.length) && opts.waits !== false;
|
|
3755
|
+
if (!wait) return shaped(await receive(opts), "not_waited");
|
|
3756
|
+
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
3757
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
3758
|
+
let latest;
|
|
3759
|
+
try {
|
|
3760
|
+
while (true) {
|
|
3761
|
+
signal.throwIfAborted();
|
|
3762
|
+
latest = await receive({ ...opts });
|
|
3763
|
+
if (pendingEvents(latest).length) return shaped(latest, "available");
|
|
3764
|
+
await sleep2(5e3, void 0, { signal });
|
|
3765
|
+
}
|
|
3766
|
+
} catch (error) {
|
|
3767
|
+
opts.signal?.throwIfAborted();
|
|
3768
|
+
if (window.aborted && latest) return shaped(latest, "expired");
|
|
3769
|
+
throw error;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3478
3772
|
function compact(o) {
|
|
3479
3773
|
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== null && v !== "" && !(Array.isArray(v) && v.length === 0)));
|
|
3480
3774
|
}
|
|
@@ -3505,8 +3799,9 @@ var handle = (entryId) => entryId.slice(0, 8);
|
|
|
3505
3799
|
function conversation(e) {
|
|
3506
3800
|
const answers = new Map((e.answers ?? []).map((a) => [a.decisionNeedId, a]));
|
|
3507
3801
|
const shown = new Set((e.entries ?? []).map((entry) => entry.entryId));
|
|
3508
|
-
const
|
|
3509
|
-
|
|
3802
|
+
const entries = (e.entries ?? []).filter((entry) => !entryUnit(entry));
|
|
3803
|
+
const repliedTo = new Set(entries.flatMap((entry) => entry.aboutId && shown.has(entry.aboutId) ? [entry.aboutId] : []));
|
|
3804
|
+
return entries.map((entry) => {
|
|
3510
3805
|
const sealed = !!entry.content && "sealed" in entry.content;
|
|
3511
3806
|
const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
|
|
3512
3807
|
const options = plain && typeof plain === "object" && Array.isArray(plain.options) ? plain.options.map((o) => o.label).filter((l) => typeof l === "string") : [];
|
|
@@ -3519,6 +3814,7 @@ function conversation(e) {
|
|
|
3519
3814
|
from: who(entry.authorParticipant),
|
|
3520
3815
|
at: at(entry.createdAt),
|
|
3521
3816
|
said: sealed ? "[encrypted]" : entryWords(entry).trim(),
|
|
3817
|
+
units: askUnits(entry, e.entries ?? []).map((u) => `${u.title}: ${u.body}`),
|
|
3522
3818
|
re: entry.aboutId && shown.has(entry.aboutId) ? handle(entry.aboutId) : void 0,
|
|
3523
3819
|
options,
|
|
3524
3820
|
decision: need ? compact({ state: stateOf(need.state, answer, decision), answer: decision }) : void 0
|
|
@@ -3531,14 +3827,14 @@ function more(g) {
|
|
|
3531
3827
|
earlier,
|
|
3532
3828
|
g.subtree?.length ? "A Goal below, in full: get_goal({ goalId }) with its goalId above." : null,
|
|
3533
3829
|
g.parentGoalId ? `The Goal above: get_goal({ goalId: "${g.parentGoalId}" }).` : null,
|
|
3534
|
-
"Anything else
|
|
3830
|
+
"Anything else addressed to you (requests, answers, questions you owe): contact({ wait: false })."
|
|
3535
3831
|
].filter((line) => !!line);
|
|
3536
3832
|
}
|
|
3537
3833
|
function goalView(g) {
|
|
3538
3834
|
if (!g.goalId) return compact({ state: g.state, next: g.message });
|
|
3539
3835
|
const lines2 = conversation(g);
|
|
3540
3836
|
const owed = lines2.filter((l) => l.from === "person" && l.decision?.state === "open" && l.id);
|
|
3541
|
-
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({
|
|
3837
|
+
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> } }] }). ` : "";
|
|
3542
3838
|
const newest = [...lines2].reverse().find((l) => l.from === "agent");
|
|
3543
3839
|
return compact({
|
|
3544
3840
|
goalId: g.goalId,
|
|
@@ -3548,6 +3844,8 @@ function goalView(g) {
|
|
|
3548
3844
|
progress: g.progress && g.progress.trim() !== newest?.said ? g.progress : void 0,
|
|
3549
3845
|
reviewPending: g.reviewPending || void 0,
|
|
3550
3846
|
reviewSince: g.reviewSince,
|
|
3847
|
+
// What acknowledges the review: contact({ ackEventIds: [reviewEventId] }) (update_goal's reviewed: true is gone).
|
|
3848
|
+
reviewEventId: g.reviewEventId,
|
|
3551
3849
|
owner: g.ownerParticipant,
|
|
3552
3850
|
others: g.others,
|
|
3553
3851
|
parentGoalId: g.parentGoalId,
|
|
@@ -3569,109 +3867,161 @@ function goalView(g) {
|
|
|
3569
3867
|
more: more(g)
|
|
3570
3868
|
});
|
|
3571
3869
|
}
|
|
3572
|
-
var JOINED_CALL = "This
|
|
3870
|
+
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.";
|
|
3871
|
+
function failures(results) {
|
|
3872
|
+
const failed = (results ?? []).filter((r) => r.status === "failed");
|
|
3873
|
+
if (!failed.length) return void 0;
|
|
3874
|
+
const plural = { update: "updates", question: "questions", answer: "answers", withdraw: "withdrawQuestionIds", dependency: "questionDependencies" };
|
|
3875
|
+
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("; ") + ".";
|
|
3876
|
+
}
|
|
3877
|
+
var itemsView = (results) => (results ?? []).map((r) => compact({
|
|
3878
|
+
kind: r.kind,
|
|
3879
|
+
index: r.index,
|
|
3880
|
+
status: r.status,
|
|
3881
|
+
questionId: r.questionId,
|
|
3882
|
+
goalId: r.goalId,
|
|
3883
|
+
error: r.error,
|
|
3884
|
+
detail: r.detail
|
|
3885
|
+
}));
|
|
3573
3886
|
function deliveryView(d) {
|
|
3574
|
-
const answers = "Answers land on the Goal:
|
|
3887
|
+
const answers = "Answers land on the Goal: collect them with contact({wait:true}) or get_goal. Do not resend it.";
|
|
3888
|
+
const pending = d.state === "open" && d.decisionNeeds.some((n) => n.state === "open");
|
|
3575
3889
|
return compact({
|
|
3890
|
+
ok: d.ok,
|
|
3891
|
+
results: itemsView(d.results),
|
|
3576
3892
|
deliveryId: d.deliveryId,
|
|
3577
3893
|
kind: d.kind,
|
|
3578
3894
|
state: d.state,
|
|
3579
3895
|
goalIds: d.goalIds,
|
|
3580
3896
|
conversation: conversation(d),
|
|
3581
|
-
// Each
|
|
3897
|
+
// Each item's own Delivery when a contact reached several.
|
|
3582
3898
|
deliveries: d.deliveries,
|
|
3583
|
-
// WHAT THIS CONTACT DID NOT SEND (#2450):
|
|
3899
|
+
// WHAT THIS CONTACT DID NOT SEND (#2450): updates kept as progress, by Goal.
|
|
3584
3900
|
recorded: d.recorded,
|
|
3585
3901
|
notSent: d.notSent,
|
|
3586
3902
|
joinedCard: d.joinedCard,
|
|
3587
3903
|
joinedCall: d.joinedCall,
|
|
3588
|
-
|
|
3904
|
+
waitOutcome: d.waitOutcome,
|
|
3905
|
+
acknowledged: d.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3906
|
+
next: [
|
|
3907
|
+
failures(d.results),
|
|
3908
|
+
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 claim_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3909
|
+
].filter(Boolean).join(" ")
|
|
3589
3910
|
});
|
|
3590
3911
|
}
|
|
3591
|
-
function
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3912
|
+
function notSentView(n) {
|
|
3913
|
+
return compact({
|
|
3914
|
+
sent: false,
|
|
3915
|
+
ok: n.ok,
|
|
3916
|
+
results: itemsView(n.results),
|
|
3917
|
+
recorded: n.recorded,
|
|
3918
|
+
acknowledged: n.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3919
|
+
next: [failures(n.results), n.message].filter(Boolean).join(" ")
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
function quietDays(items, now) {
|
|
3923
|
+
const oldest = Math.min(...items.map((i) => Date.parse(i.since ?? "")).filter(Number.isFinite));
|
|
3924
|
+
return Number.isFinite(oldest) ? Math.max(3, Math.round((now - oldest) / 864e5)) : 3;
|
|
3925
|
+
}
|
|
3926
|
+
var quietFor = (items, now) => {
|
|
3927
|
+
const days = quietDays(items, now);
|
|
3928
|
+
return items.length === 1 ? `for ${days} days` : `, the longest for ${days} days`;
|
|
3929
|
+
};
|
|
3930
|
+
function receivedView(r, now = Date.now()) {
|
|
3931
|
+
const w = r.waiting;
|
|
3932
|
+
const assigned = w.assigned ?? [];
|
|
3933
|
+
const stalled = w.stalled ?? [];
|
|
3934
|
+
const others = w.stalledOthers ?? [];
|
|
3935
|
+
const owed = r.events.filter((e) => e.kind === "question");
|
|
3936
|
+
const refused = (r.acknowledged ?? []).filter((a) => a.refused);
|
|
3937
|
+
return compact({
|
|
3938
|
+
events: r.events.map((e) => compact({ ...e })),
|
|
3939
|
+
hasMore: r.hasMore || void 0,
|
|
3940
|
+
waitOutcome: r.waitOutcome,
|
|
3941
|
+
acknowledged: r.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3942
|
+
notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
|
|
3943
|
+
// WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
|
|
3944
|
+
assigned,
|
|
3945
|
+
// WHAT claim_goal HANDS YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
|
|
3946
|
+
// sleeping agent, so "nothing is waiting" is never said while a claim would hand you work.
|
|
3947
|
+
claimable: w.claimable,
|
|
3948
|
+
// YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
|
|
3949
|
+
stalled,
|
|
3950
|
+
stalledOthers: others,
|
|
3620
3951
|
next: [
|
|
3621
|
-
|
|
3952
|
+
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.",
|
|
3953
|
+
...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.`] : [],
|
|
3954
|
+
...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
|
|
3955
|
+
...r.waitOutcome === "expired" ? ["Nothing arrived in the window; receive again to keep waiting, without sending again."] : [],
|
|
3622
3956
|
...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): claim_goal({goalId}) to start one.`] : [],
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
// sentence names the person and quotes the freshest one, because a count is not a message.
|
|
3627
|
-
...review.length ? [spoke(review) ?? `${review.length} of your Goals have something new (review): read each with claim_goal or get_goal, then update_goal with reviewed: true.`] : [],
|
|
3628
|
-
...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
|
|
3629
|
-
...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) joins one without changing its owner, then finish or cancel it with update_goal.`] : [],
|
|
3957
|
+
...w.claimable && !assigned.length ? [`claim_goal() hands you ${w.claimable.title ?? w.claimable.goalId} next.`] : [],
|
|
3958
|
+
...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.`] : [],
|
|
3959
|
+
...others.length ? [`${others.length} of other agents' Goals have gone quiet${others.length === 1 ? " " : ""}${quietFor(others, now)}: claim_goal({goalId}) joins one without changing its owner.`] : [],
|
|
3630
3960
|
// No name of its own (#2525): the API's ask, verbatim.
|
|
3631
|
-
...
|
|
3961
|
+
...w.unnamed ? [w.unnamed] : []
|
|
3632
3962
|
].join(" ")
|
|
3633
|
-
};
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
function managedView(m, changes) {
|
|
3966
|
+
const results = m.results ?? [];
|
|
3967
|
+
const failed = results.filter((r) => r.status !== "applied");
|
|
3968
|
+
return compact({
|
|
3969
|
+
ok: m.ok,
|
|
3970
|
+
results: results.map((r) => compact({ index: r.index, kind: changes[r.index]?.kind, status: r.status, goalId: r.goalId, error: r.error })),
|
|
3971
|
+
goals: m.goals,
|
|
3972
|
+
next: failed.length ? `${failed.length} of ${results.length} changes did not apply (see results[].error); the rest did. A goal_revision_conflict means the Goal changed since you read it: read it with get_goal and decide again.` : void 0
|
|
3973
|
+
});
|
|
3634
3974
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3975
|
+
var lastRead = /* @__PURE__ */ new Map();
|
|
3976
|
+
var readKey = (token, goalId) => `${token ?? ""}:${goalId}`;
|
|
3977
|
+
function remember(token, goalId, revision) {
|
|
3978
|
+
if (revision === void 0) return;
|
|
3979
|
+
if (lastRead.size >= 5e3) lastRead.clear();
|
|
3980
|
+
lastRead.set(readKey(token, goalId), revision);
|
|
3981
|
+
}
|
|
3982
|
+
function read2(token, goal) {
|
|
3983
|
+
if (goal.goalId) remember(token, goal.goalId, goal.revision);
|
|
3984
|
+
return goal;
|
|
3641
3985
|
}
|
|
3642
3986
|
async function runTool(name, args, opts) {
|
|
3643
3987
|
const { waits, signal, ...client } = opts;
|
|
3644
3988
|
const input = args ?? {};
|
|
3645
3989
|
switch (name) {
|
|
3646
3990
|
case "contact": {
|
|
3647
|
-
const
|
|
3648
|
-
return
|
|
3991
|
+
const parsed = ContactSchema.parse(input);
|
|
3992
|
+
if (!sendsAnything(parsed)) return receivedView(await receiveContact(parsed, { ...client, signal, waits }));
|
|
3993
|
+
const sent = await contact(parsed, { ...client, signal, waits });
|
|
3994
|
+
return "sent" in sent ? notSentView(sent) : deliveryView(sent);
|
|
3649
3995
|
}
|
|
3650
3996
|
case "who_is_working":
|
|
3651
|
-
|
|
3997
|
+
WhoIsWorkingSchema.parse(input);
|
|
3652
3998
|
return whoIsWorking(client);
|
|
3653
|
-
case "
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3999
|
+
case "send_feedback":
|
|
4000
|
+
return sendFeedback(SendFeedbackSchema.parse(input), client);
|
|
4001
|
+
case "manage_goals": {
|
|
4002
|
+
const { changes } = ManageGoalsToolSchema.parse(input);
|
|
4003
|
+
const versioned = changes.map((c) => {
|
|
4004
|
+
if (c.kind === "create" || c.kind === "dependency") return c;
|
|
4005
|
+
const revision = lastRead.get(readKey(client.token, c.goalId));
|
|
4006
|
+
return revision === void 0 ? c : { ...c, revision };
|
|
4007
|
+
});
|
|
4008
|
+
const managed = await manageGoals({ requestId: randomUUID3(), changes: versioned }, client);
|
|
4009
|
+
for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
|
|
4010
|
+
return managedView(managed, changes);
|
|
3659
4011
|
}
|
|
3660
4012
|
case "claim_goal": {
|
|
3661
4013
|
const { goalId } = ClaimGoalSchema.parse(input);
|
|
3662
|
-
return goalView(await claimGoal(goalId, client));
|
|
4014
|
+
return goalView(read2(client.token, await claimGoal(goalId, client)));
|
|
3663
4015
|
}
|
|
3664
4016
|
case "get_goal": {
|
|
3665
4017
|
const { goalId, history, diagnose } = GetGoalSchema.parse(input);
|
|
3666
|
-
const goal = await getGoal(goalId, client, { history, diagnose });
|
|
4018
|
+
const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
|
|
3667
4019
|
const view = goalView(goal);
|
|
3668
4020
|
const diagnosis = goal.diagnosis;
|
|
3669
4021
|
return diagnosis === void 0 ? view : { ...view, diagnosis };
|
|
3670
4022
|
}
|
|
3671
|
-
case "
|
|
3672
|
-
|
|
3673
|
-
return goalView(await updateGoal(goalId, body, client));
|
|
3674
|
-
}
|
|
4023
|
+
case "search":
|
|
4024
|
+
return searchRecords(SearchToolSchema.parse(input), client);
|
|
3675
4025
|
default:
|
|
3676
4026
|
throw new Error(`Unknown tool: ${name}`);
|
|
3677
4027
|
}
|
|
@@ -3853,6 +4203,32 @@ function machineZone() {
|
|
|
3853
4203
|
return null;
|
|
3854
4204
|
}
|
|
3855
4205
|
}
|
|
4206
|
+
function repoFromRemote(url) {
|
|
4207
|
+
const said = (url ?? "").trim();
|
|
4208
|
+
if (!said) return null;
|
|
4209
|
+
const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
4210
|
+
const parts = path.split("/").filter(Boolean);
|
|
4211
|
+
if (parts.length < 2) return null;
|
|
4212
|
+
const [owner, name] = parts.slice(-2);
|
|
4213
|
+
if (!owner || !name) return null;
|
|
4214
|
+
return `${owner}/${name}`.toLowerCase();
|
|
4215
|
+
}
|
|
4216
|
+
var asked;
|
|
4217
|
+
function currentRepo(cwd = process.cwd()) {
|
|
4218
|
+
if (asked !== void 0) return asked;
|
|
4219
|
+
try {
|
|
4220
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
4221
|
+
cwd,
|
|
4222
|
+
encoding: "utf8",
|
|
4223
|
+
timeout: 2e3,
|
|
4224
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4225
|
+
});
|
|
4226
|
+
asked = repoFromRemote(url);
|
|
4227
|
+
} catch {
|
|
4228
|
+
asked = null;
|
|
4229
|
+
}
|
|
4230
|
+
return asked;
|
|
4231
|
+
}
|
|
3856
4232
|
function withCodexEnv(text) {
|
|
3857
4233
|
const head = /^\[mcp_servers\.paigy\][ \t]*(?:#[^\n]*)?\r?\n/m.exec(text);
|
|
3858
4234
|
if (!head) return null;
|
|
@@ -3910,10 +4286,9 @@ export {
|
|
|
3910
4286
|
ApiError,
|
|
3911
4287
|
overrideToken,
|
|
3912
4288
|
authToken,
|
|
3913
|
-
|
|
4289
|
+
manageGoals,
|
|
3914
4290
|
claimGoal,
|
|
3915
4291
|
getGoal,
|
|
3916
|
-
updateGoal,
|
|
3917
4292
|
AWAIT_WINDOW_MS,
|
|
3918
4293
|
hatch,
|
|
3919
4294
|
whoAmI,
|
|
@@ -3931,13 +4306,18 @@ export {
|
|
|
3931
4306
|
getTriage,
|
|
3932
4307
|
acceptTriage,
|
|
3933
4308
|
dismissTriage,
|
|
4309
|
+
searchRecords,
|
|
3934
4310
|
whoIsWorking,
|
|
3935
|
-
|
|
3936
|
-
currentRepo,
|
|
4311
|
+
readDelivery,
|
|
3937
4312
|
contact,
|
|
3938
|
-
|
|
4313
|
+
receive,
|
|
4314
|
+
acknowledge,
|
|
4315
|
+
pendingEvents,
|
|
4316
|
+
receiveContact,
|
|
3939
4317
|
runTool,
|
|
3940
4318
|
subscribeWake,
|
|
3941
4319
|
parseDue,
|
|
4320
|
+
repoFromRemote,
|
|
4321
|
+
currentRepo,
|
|
3942
4322
|
withCodexEnv
|
|
3943
4323
|
};
|