@paigy/mcp 0.40.23 → 0.40.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -23
- package/dist/{chunk-BNMMRKEY.js → chunk-2ROD77AW.js} +1 -1
- package/dist/{chunk-PR7ROBN4.js → chunk-EJYXBEWN.js} +959 -621
- package/dist/{chunk-JLRBZZNT.js → chunk-GHSB2VP6.js} +752 -517
- package/dist/chunk-RK5LT7NH.js +110 -0
- package/dist/{chunk-HKODQKUV.js → chunk-UAVXYNLV.js} +9 -9
- package/dist/{dist-B7AOS5TG.js → dist-NMHU2UNG.js} +19 -7
- package/dist/enable.js +3 -3
- package/dist/index.js +18 -7
- package/dist/listen.js +85 -17
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +3 -3
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -7,13 +7,14 @@ import { homedir } from "os";
|
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
9
9
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
10
|
-
import { z as
|
|
10
|
+
import { z as z5 } 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";
|
|
14
14
|
import { ZodFirstPartyTypeKind } from "zod/v3";
|
|
15
15
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind2 } from "zod/v3";
|
|
16
16
|
import { z as z2 } from "zod";
|
|
17
|
+
import { z as z4 } from "zod";
|
|
17
18
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
18
19
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
19
20
|
import { homedir as homedir2 } from "os";
|
|
@@ -22,7 +23,7 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
22
23
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
23
24
|
import { WebSocket } from "undici";
|
|
24
25
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
25
|
-
var CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID"];
|
|
26
|
+
var CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
|
|
26
27
|
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
27
28
|
var NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
|
|
28
29
|
var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
@@ -39,8 +40,8 @@ var WORKSPACE_ID = (() => {
|
|
|
39
40
|
return INSTANCE_ID;
|
|
40
41
|
}
|
|
41
42
|
})();
|
|
42
|
-
var envSession = (
|
|
43
|
-
const v = process.env[
|
|
43
|
+
var envSession = (key2) => {
|
|
44
|
+
const v = process.env[key2];
|
|
44
45
|
return v && v.trim() ? v : void 0;
|
|
45
46
|
};
|
|
46
47
|
function parentOf(pid) {
|
|
@@ -83,12 +84,17 @@ async function reach(url, init) {
|
|
|
83
84
|
function reachAs(session, instance) {
|
|
84
85
|
return (url, init) => send(url, init, session, instance);
|
|
85
86
|
}
|
|
87
|
+
var CLIENT;
|
|
88
|
+
function setClient(name) {
|
|
89
|
+
CLIENT = name;
|
|
90
|
+
}
|
|
86
91
|
async function send(url, init, session, instance = envSession("PAIGY_INSTANCE_ID") ?? INSTANCE_ID) {
|
|
87
92
|
try {
|
|
88
93
|
const headers = {
|
|
89
94
|
...init?.headers,
|
|
90
95
|
"x-paigy-instance": instance,
|
|
91
|
-
"x-paigy-session": session
|
|
96
|
+
"x-paigy-session": session,
|
|
97
|
+
...CLIENT ? { "x-paigy-client": CLIENT } : {}
|
|
92
98
|
};
|
|
93
99
|
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
94
100
|
} catch (e) {
|
|
@@ -146,19 +152,19 @@ var getRefs = (options) => {
|
|
|
146
152
|
]))
|
|
147
153
|
};
|
|
148
154
|
};
|
|
149
|
-
function addErrorMessage(res,
|
|
155
|
+
function addErrorMessage(res, key2, errorMessage, refs) {
|
|
150
156
|
if (!refs?.errorMessages)
|
|
151
157
|
return;
|
|
152
158
|
if (errorMessage) {
|
|
153
159
|
res.errorMessage = {
|
|
154
160
|
...res.errorMessage,
|
|
155
|
-
[
|
|
161
|
+
[key2]: errorMessage
|
|
156
162
|
};
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
|
-
function setResponseValueAndErrors(res,
|
|
160
|
-
res[
|
|
161
|
-
addErrorMessage(res,
|
|
165
|
+
function setResponseValueAndErrors(res, key2, value, errorMessage, refs) {
|
|
166
|
+
res[key2] = value;
|
|
167
|
+
addErrorMessage(res, key2, errorMessage, refs);
|
|
162
168
|
}
|
|
163
169
|
var getRelativePath = (pathA, pathB) => {
|
|
164
170
|
let i = 0;
|
|
@@ -720,11 +726,11 @@ function parseRecordDef(def, refs) {
|
|
|
720
726
|
return {
|
|
721
727
|
type: "object",
|
|
722
728
|
required: def.keyType._def.values,
|
|
723
|
-
properties: def.keyType._def.values.reduce((acc,
|
|
729
|
+
properties: def.keyType._def.values.reduce((acc, key2) => ({
|
|
724
730
|
...acc,
|
|
725
|
-
[
|
|
731
|
+
[key2]: parseDef(def.valueType._def, {
|
|
726
732
|
...refs,
|
|
727
|
-
currentPath: [...refs.currentPath, "properties",
|
|
733
|
+
currentPath: [...refs.currentPath, "properties", key2]
|
|
728
734
|
}) ?? parseAnyDef(refs)
|
|
729
735
|
}), {}),
|
|
730
736
|
additionalProperties: refs.rejectedAdditionalProperties
|
|
@@ -787,10 +793,10 @@ function parseMapDef(def, refs) {
|
|
|
787
793
|
}
|
|
788
794
|
function parseNativeEnumDef(def) {
|
|
789
795
|
const object = def.values;
|
|
790
|
-
const actualKeys = Object.keys(def.values).filter((
|
|
791
|
-
return typeof object[object[
|
|
796
|
+
const actualKeys = Object.keys(def.values).filter((key2) => {
|
|
797
|
+
return typeof object[object[key2]] !== "number";
|
|
792
798
|
});
|
|
793
|
-
const actualValues = actualKeys.map((
|
|
799
|
+
const actualValues = actualKeys.map((key2) => object[key2]);
|
|
794
800
|
const parsedTypes = Array.from(new Set(actualValues.map((values) => typeof values)));
|
|
795
801
|
return {
|
|
796
802
|
type: parsedTypes.length === 1 ? parsedTypes[0] === "string" ? "string" : "number" : ["string", "number"],
|
|
@@ -1418,7 +1424,7 @@ var SPOKEN_HOUR = {
|
|
|
1418
1424
|
eleven: 11,
|
|
1419
1425
|
twelve: 12
|
|
1420
1426
|
};
|
|
1421
|
-
function parseWhen(said2, tz, nowMs,
|
|
1427
|
+
function parseWhen(said2, tz, nowMs, read22 = "earliest") {
|
|
1422
1428
|
const text = said2.trim();
|
|
1423
1429
|
if (!text) return null;
|
|
1424
1430
|
const rel = RELATIVE.exec(text);
|
|
@@ -1457,13 +1463,13 @@ function parseWhen(said2, tz, nowMs, read = "earliest") {
|
|
|
1457
1463
|
if (hour === 12) hour = 0;
|
|
1458
1464
|
} else if (hour < 12 && dayShift === 0 && hour <= today.h) hour += 12;
|
|
1459
1465
|
} else {
|
|
1460
|
-
const parts = PARTS[
|
|
1466
|
+
const parts = PARTS[read22];
|
|
1461
1467
|
const part = Object.keys(parts).find((k) => new RegExp(`\\b${k}\\b`, "i").test(text));
|
|
1462
1468
|
if (/\btonight\b/i.test(text)) hour = parts["night"];
|
|
1463
1469
|
else if (part) hour = parts[part];
|
|
1464
1470
|
else if (dayShift > 0) hour = 9;
|
|
1465
1471
|
else if (/\bend of (the )?day\b|\beod\b/i.test(text)) hour = END_OF_DAY;
|
|
1466
|
-
else if (
|
|
1472
|
+
else if (read22 === "ring" && VAGUE.test(text)) return new Date(nowMs + 30 * 6e4).toISOString();
|
|
1467
1473
|
else return null;
|
|
1468
1474
|
}
|
|
1469
1475
|
const at2 = instantAt(tz, {
|
|
@@ -1486,8 +1492,8 @@ function parseWhen(said2, tz, nowMs, read = "earliest") {
|
|
|
1486
1492
|
}) : at2;
|
|
1487
1493
|
return new Date(rolled ?? at2).toISOString();
|
|
1488
1494
|
}
|
|
1489
|
-
function parseDelay(said2, tz, nowMs,
|
|
1490
|
-
const at2 = parseWhen(said2, tz, nowMs,
|
|
1495
|
+
function parseDelay(said2, tz, nowMs, read22 = "earliest") {
|
|
1496
|
+
const at2 = parseWhen(said2, tz, nowMs, read22);
|
|
1491
1497
|
if (!at2) return null;
|
|
1492
1498
|
return Math.max(0, Math.round((Date.parse(at2) - nowMs) / 1e3));
|
|
1493
1499
|
}
|
|
@@ -1523,32 +1529,39 @@ function mcpInputSchema(s) {
|
|
|
1523
1529
|
}
|
|
1524
1530
|
var AskInputSchema = z2.object({
|
|
1525
1531
|
id: z2.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
|
|
1526
|
-
parentId: z2.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and
|
|
1532
|
+
parentId: z2.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and the question starts a new Goal of yours."),
|
|
1527
1533
|
repo: z2.string().optional().describe("Optional repository context."),
|
|
1528
1534
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
1529
|
-
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy
|
|
1535
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy sends each ask exactly as you wrote it: a bundled ask arrives as one card. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
1530
1536
|
),
|
|
1531
1537
|
options: z2.array(OptionInputSchema).min(1).max(6).optional(),
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
// shape (`goal/intake.ts` `answerOf`), and this reaches it as the agent's view, weighed, not obeyed.
|
|
1538
|
+
// 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). Every card was pick-one while no
|
|
1540
|
+
// agent sent this; no read reshapes cards any more, so this is the card's shape, and the
|
|
1541
|
+
// description says so plainly.
|
|
1537
1542
|
select: z2.enum(["one", "many"]).optional().describe(
|
|
1538
|
-
|
|
1543
|
+
'How the options are answered: "many" makes a checklist (they may want several, e.g. independent fixes), "one" (the default) a pick (choosing one rules out the others). Send "many" whenever more than one option could be wanted at once. The person can always answer in their own words as well. Ignored without options.'
|
|
1539
1544
|
),
|
|
1540
1545
|
answers: z2.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
1541
|
-
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them.
|
|
1546
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. parentId is optional here: the question's own Goal is used. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
1542
1547
|
)
|
|
1543
1548
|
}).strict();
|
|
1544
1549
|
var StartContactSchema = z2.object({
|
|
1545
|
-
asks: z2.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is;
|
|
1550
|
+
asks: z2.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; an ask naming no Goal starts a new Goal of yours."),
|
|
1546
1551
|
waiting: z2.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
1547
1552
|
channel: z2.enum(["notification", "call"]).default("notification")
|
|
1548
1553
|
}).strict();
|
|
1549
|
-
var
|
|
1554
|
+
var ReceiveContactSchema = z2.object({
|
|
1555
|
+
wait: z2.boolean().optional().describe(
|
|
1556
|
+
"Only controls when this returns. true (the default when you send and acknowledge nothing): wait up to about 45 seconds for incoming communication addressed to you, and return as soon as some is available. false: return what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived in the window; wait again without sending again), or `not_waited` (a hosted connection never holds a wait)."
|
|
1557
|
+
),
|
|
1558
|
+
ackEventIds: z2.array(z2.string().uuid()).min(1).max(100).optional().describe(
|
|
1559
|
+
"The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (an ask with `answers`), never acknowledged away: that id is refused until it is answered."
|
|
1560
|
+
)
|
|
1561
|
+
}).strict();
|
|
1562
|
+
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict(), ReceiveContactSchema]);
|
|
1550
1563
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
1551
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none
|
|
1564
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none starts a new Goal of yours. Notification returns immediately; collect answers by receiving (below). On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. Each ask is sent exactly as written, with no reading in between: a bundled ask arrives as one card, so separate questions are separate asks. An ask with no options that is not blocking is a report, never a claim on them, so no answer is owed and none should be awaited. A report reaches the person only when it answers something they asked you, or once the Goal is done; any other report is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first, then send one message saying what is done and anything they need to do or check. On a Goal whose report card is still open, a report that does reach them is added to that card, with no new push. Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). select:'many' makes the card a checklist, for independent options they may want several of; select:'one' (the default) a pick, for alternatives. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, a contact that reaches them joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.\n\nRECEIVING: contact with no asks sends nothing and returns `events`, a limited batch of what is addressed to you (not a history page): `question`, a Question you owe (answer it with an ask whose `answers` is its questionId); `update`, something new on one of your Goals (a reply, an answer: read it with get_goal); `instruction`, a request or note sent to you. `hasMore` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (`assigned`; claim_goal starts it) and your work gone quiet (`stalled`).";
|
|
1552
1565
|
var CreateGoalSchema = z3.object({
|
|
1553
1566
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1554
1567
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1568,10 +1581,6 @@ var CreateGoalSchema = z3.object({
|
|
|
1568
1581
|
* repo name. Delegated work inherits this from its parent Goal when omitted. */
|
|
1569
1582
|
repo: z3.string().trim().min(1).max(200).optional()
|
|
1570
1583
|
});
|
|
1571
|
-
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
1572
|
-
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
1573
|
-
}).strict();
|
|
1574
|
-
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.';
|
|
1575
1584
|
var UpdateGoalSchema = z3.object({
|
|
1576
1585
|
revision: z3.number().int().positive(),
|
|
1577
1586
|
changes: z3.object({
|
|
@@ -1595,7 +1604,65 @@ var UpdateGoalSchema = z3.object({
|
|
|
1595
1604
|
reason: z3.string().trim().min(1).max(2e3),
|
|
1596
1605
|
operationId: z3.string().uuid().optional()
|
|
1597
1606
|
}).strict();
|
|
1598
|
-
var UpdateGoalToolSchema =
|
|
1607
|
+
var UpdateGoalToolSchema = z3.object({
|
|
1608
|
+
goalId: z3.string().uuid(),
|
|
1609
|
+
revision: UpdateGoalSchema.shape.revision,
|
|
1610
|
+
changes: UpdateGoalSchema.shape.changes.innerType().pick({ progress: true, reviewed: true, dueAt: true, withdraw: true }).strict().refine((v) => Object.keys(v).length > 0),
|
|
1611
|
+
reason: UpdateGoalSchema.shape.reason
|
|
1612
|
+
}).strict();
|
|
1613
|
+
var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
1614
|
+
var goalTitle = z3.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
|
|
1615
|
+
var goalOutcome = z3.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
|
|
1616
|
+
var CreateChange = z3.object({
|
|
1617
|
+
kind: z3.literal("create"),
|
|
1618
|
+
title: goalTitle,
|
|
1619
|
+
outcome: goalOutcome,
|
|
1620
|
+
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."),
|
|
1621
|
+
parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
1622
|
+
sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
1623
|
+
}).strict();
|
|
1624
|
+
var EditChange = z3.object({ kind: z3.literal("edit"), goalId: changedGoal, title: goalTitle.optional(), outcome: goalOutcome.optional() }).strict();
|
|
1625
|
+
var StateChange = z3.object({ kind: z3.literal("state"), goalId: changedGoal, state: z3.enum(["open", "completed", "canceled"]).describe(
|
|
1626
|
+
"completed: the Goal's own work is done now (refused while required children are open); open: reopen it; canceled: it will not be done."
|
|
1627
|
+
) }).strict();
|
|
1628
|
+
var AssignChange = z3.object({ kind: z3.literal("assign"), goalId: changedGoal, ownerId: z3.string().trim().min(1).describe("The new owner's participant.") }).strict();
|
|
1629
|
+
var DeferChange = z3.object({ kind: z3.literal("defer"), goalId: changedGoal, until: z3.string().datetime({ offset: true }).nullable().describe(
|
|
1630
|
+
"Postpone it until this instant (its owner is woken then), or null to take the postponement back."
|
|
1631
|
+
) }).strict();
|
|
1632
|
+
var MoveChange = z3.object({ kind: z3.literal("move"), goalId: changedGoal, parentGoalId: z3.string().uuid().nullable().describe(
|
|
1633
|
+
"Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
|
|
1634
|
+
) }).strict();
|
|
1635
|
+
var DependencyChange = z3.object({
|
|
1636
|
+
kind: z3.literal("dependency"),
|
|
1637
|
+
change: z3.enum(["add", "remove"]),
|
|
1638
|
+
goalId: changedGoal,
|
|
1639
|
+
dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
|
|
1640
|
+
action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
|
|
1641
|
+
reason: z3.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
|
|
1642
|
+
}).strict();
|
|
1643
|
+
var read = { revision: z3.number().int().positive().optional() };
|
|
1644
|
+
var GoalChangeSchema = z3.discriminatedUnion("kind", [
|
|
1645
|
+
CreateChange,
|
|
1646
|
+
EditChange.extend(read),
|
|
1647
|
+
StateChange.extend(read),
|
|
1648
|
+
AssignChange.extend(read),
|
|
1649
|
+
DeferChange.extend(read),
|
|
1650
|
+
MoveChange.extend(read),
|
|
1651
|
+
DependencyChange
|
|
1652
|
+
]);
|
|
1653
|
+
var editNames = (changes, ctx) => changes.forEach((c, i) => {
|
|
1654
|
+
if (c.kind === "edit" && c.title === void 0 && c.outcome === void 0) {
|
|
1655
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["changes", i], message: "an edit supplies title, outcome, or both" });
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
var ManageGoalsSchema = z3.object({
|
|
1659
|
+
requestId: z3.string().uuid().optional(),
|
|
1660
|
+
changes: z3.array(GoalChangeSchema).min(1).max(50)
|
|
1661
|
+
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
1662
|
+
var ManageGoalsToolSchema = z3.object({
|
|
1663
|
+
changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
1664
|
+
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
1665
|
+
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_joined, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until, or null); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change a Goal you own or have written on. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
1599
1666
|
var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
|
|
1600
1667
|
var GetGoalSchema = z3.object({
|
|
1601
1668
|
goalId: z3.string().uuid(),
|
|
@@ -1608,20 +1675,162 @@ var GetGoalSchema = z3.object({
|
|
|
1608
1675
|
diagnose: z3.boolean().optional()
|
|
1609
1676
|
}).strict();
|
|
1610
1677
|
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.";
|
|
1611
|
-
var UPDATE_GOAL_DESCRIPTION =
|
|
1612
|
-
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
|
|
1613
|
-
var
|
|
1614
|
-
|
|
1678
|
+
var UPDATE_GOAL_DESCRIPTION = "Report work on a Goal you are on, at an exact revision: progress, review acknowledgement, when to wake for it, and withdrawing a question of yours. What the Goal is (its title, outcome, state, owner, parent, dependencies) changes with manage_goals. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). Stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. progress says where the work stands. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision (contact({ackEventIds}) does the same for an `update` event). dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. Returns the Goal as get_goal reads it, at its new revision.";
|
|
1679
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
1680
|
+
var SearchToolSchema = z3.object({
|
|
1681
|
+
query: z3.string().trim().min(1).max(500),
|
|
1682
|
+
types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
1683
|
+
goalId: z3.string().uuid().optional(),
|
|
1684
|
+
limit: z3.number().int().min(1).max(20).optional()
|
|
1685
|
+
}).strict();
|
|
1686
|
+
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and Answers (found by their Question or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, its Questions' Answers, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (an Answer carries its Question, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
1687
|
+
var WhoIsWorkingSchema = z3.object({}).strict();
|
|
1615
1688
|
var AGENT_TOOLS = [
|
|
1616
|
-
{ 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(
|
|
1689
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(WhoIsWorkingSchema) },
|
|
1617
1690
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
1618
|
-
{ name: "
|
|
1619
|
-
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
1691
|
+
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
1620
1692
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
1621
1693
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
1622
|
-
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
|
|
1694
|
+
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) },
|
|
1695
|
+
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) }
|
|
1623
1696
|
];
|
|
1624
1697
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
1698
|
+
var id = z4.string().uuid();
|
|
1699
|
+
var key = z4.string().min(1).max(40);
|
|
1700
|
+
var participant = z4.string().regex(/^(human|agent):.+$/, "a participant ID such as agent:<uuid>");
|
|
1701
|
+
var SourceSchema = z4.object({ entryId: id }).strict();
|
|
1702
|
+
var ResultRefSchema = z4.union([z4.object({ id }).strict(), z4.object({ local: key }).strict()]);
|
|
1703
|
+
var NodeRefSchema = z4.object({ type: z4.enum(["goal", "question"]), id }).strict();
|
|
1704
|
+
var DependencySchema = z4.object({
|
|
1705
|
+
blocker: NodeRefSchema,
|
|
1706
|
+
blocked: NodeRefSchema,
|
|
1707
|
+
action: z4.enum(["start", "complete", "answer"]),
|
|
1708
|
+
reason: z4.string().max(500).optional()
|
|
1709
|
+
}).strict();
|
|
1710
|
+
var goalState = z4.enum(["open", "completed", "canceled"]);
|
|
1711
|
+
var existingGoalChanges = [
|
|
1712
|
+
z4.object({ kind: z4.literal("edit"), goalId: id, title: z4.string().min(1).max(120).optional(), outcome: z4.string().min(1).optional() }).strict().refine((c) => c.title !== void 0 || c.outcome !== void 0, "an edit changes the title or the outcome"),
|
|
1713
|
+
z4.object({ kind: z4.literal("state"), goalId: id, state: goalState }).strict(),
|
|
1714
|
+
z4.object({ kind: z4.literal("assign"), goalId: id, ownerId: participant }).strict(),
|
|
1715
|
+
z4.object({ kind: z4.literal("defer"), goalId: id, until: z4.string().datetime().nullable() }).strict(),
|
|
1716
|
+
z4.object({ kind: z4.literal("move"), goalId: id, parentGoalId: id.nullable() }).strict(),
|
|
1717
|
+
z4.object({
|
|
1718
|
+
kind: z4.literal("dependency"),
|
|
1719
|
+
change: z4.enum(["add", "remove"]),
|
|
1720
|
+
goalId: id,
|
|
1721
|
+
dependsOnGoalId: id,
|
|
1722
|
+
action: z4.enum(["start", "complete"]),
|
|
1723
|
+
reason: z4.string().max(500).optional()
|
|
1724
|
+
}).strict()
|
|
1725
|
+
];
|
|
1726
|
+
var BrainGoalChangeSchema = z4.union([
|
|
1727
|
+
// newSession: Paigy starts a session to own it (owner, 2026-10-06); until it has, the person owns it.
|
|
1728
|
+
z4.object({
|
|
1729
|
+
kind: z4.literal("create"),
|
|
1730
|
+
outcome: z4.string().min(1),
|
|
1731
|
+
title: z4.string().min(1).max(120),
|
|
1732
|
+
ownerId: participant,
|
|
1733
|
+
parentGoal: ResultRefSchema.nullable(),
|
|
1734
|
+
newSession: z4.literal(true).optional()
|
|
1735
|
+
}).strict(),
|
|
1736
|
+
...existingGoalChanges
|
|
1737
|
+
]);
|
|
1738
|
+
var option = z4.object({ id: z4.string().min(1).max(40), label: z4.string().min(1), hint: z4.string().optional() }).strict();
|
|
1739
|
+
var QuestionChangeSchema = z4.union([
|
|
1740
|
+
z4.object({
|
|
1741
|
+
kind: z4.literal("create"),
|
|
1742
|
+
text: z4.string().min(1),
|
|
1743
|
+
answererId: participant,
|
|
1744
|
+
goal: ResultRefSchema.optional(),
|
|
1745
|
+
blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
|
|
1746
|
+
options: z4.array(option).min(2).optional(),
|
|
1747
|
+
pickMode: z4.enum(["one", "many", "rank"]).optional()
|
|
1748
|
+
}).strict().refine((c) => c.options === void 0 === (c.pickMode === void 0), "options and pickMode come together"),
|
|
1749
|
+
z4.object({ kind: z4.literal("edit"), questionId: id, text: z4.string().min(1) }).strict(),
|
|
1750
|
+
z4.object({ kind: z4.literal("assign"), questionId: id, answererId: participant }).strict(),
|
|
1751
|
+
z4.object({ kind: z4.literal("withdraw"), questionId: id }).strict(),
|
|
1752
|
+
z4.object({ kind: z4.literal("dependency"), change: z4.enum(["add", "remove"]), edge: DependencySchema }).strict()
|
|
1753
|
+
]);
|
|
1754
|
+
var lessonScope = z4.union([
|
|
1755
|
+
z4.object({ kind: z4.literal("user") }).strict(),
|
|
1756
|
+
z4.object({ kind: z4.literal("goal"), goal: ResultRefSchema }).strict()
|
|
1757
|
+
]);
|
|
1758
|
+
var LessonChangeSchema = z4.union([
|
|
1759
|
+
z4.object({ kind: z4.literal("remember"), text: z4.string().min(1), scope: lessonScope }).strict(),
|
|
1760
|
+
z4.object({ kind: z4.literal("revise"), lessonId: id, text: z4.string().min(1), scope: lessonScope }).strict(),
|
|
1761
|
+
z4.object({ kind: z4.literal("withdraw"), lessonId: id }).strict()
|
|
1762
|
+
]);
|
|
1763
|
+
var BrainNextSchema = z4.object({
|
|
1764
|
+
instructions: z4.array(z4.object({ entryKeys: z4.array(key).min(1) }).strict()),
|
|
1765
|
+
say: z4.array(key),
|
|
1766
|
+
then: z4.enum(["listen", "hold", "end", "none"]),
|
|
1767
|
+
waitFor: z4.array(ResultRefSchema),
|
|
1768
|
+
reason: z4.string()
|
|
1769
|
+
}).strict();
|
|
1770
|
+
var messageBase = {
|
|
1771
|
+
key,
|
|
1772
|
+
text: z4.string().min(1),
|
|
1773
|
+
goals: z4.array(ResultRefSchema),
|
|
1774
|
+
questions: z4.array(ResultRefSchema),
|
|
1775
|
+
entryKeys: z4.array(key),
|
|
1776
|
+
obligationIds: z4.array(id)
|
|
1777
|
+
};
|
|
1778
|
+
var BrainMessageSchema = z4.union([
|
|
1779
|
+
z4.object({ ...messageBase, to: z4.object({ kind: z4.literal("user") }).strict() }).strict(),
|
|
1780
|
+
z4.object({
|
|
1781
|
+
...messageBase,
|
|
1782
|
+
to: z4.object({ kind: z4.literal("agent"), agentId: z4.string().regex(/^agent:(?!paigy$)\S+$/) }).strict(),
|
|
1783
|
+
call: z4.object({ callId: id, listen: z4.literal(true) }).strict().optional()
|
|
1784
|
+
}).strict()
|
|
1785
|
+
]);
|
|
1786
|
+
var BrainSearchSchema = z4.object({
|
|
1787
|
+
query: z4.string().min(1).max(500),
|
|
1788
|
+
within: z4.array(z4.enum(["entries", "goals", "answers"])).min(1),
|
|
1789
|
+
goalId: id.optional()
|
|
1790
|
+
}).strict();
|
|
1791
|
+
var BrainResultSchema = z4.object({
|
|
1792
|
+
messages: z4.array(BrainMessageSchema),
|
|
1793
|
+
entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
|
|
1794
|
+
questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
|
|
1795
|
+
answers: z4.array(z4.object({
|
|
1796
|
+
question: ResultRefSchema,
|
|
1797
|
+
entryKeys: z4.array(key).min(1),
|
|
1798
|
+
summary: z4.string().min(1),
|
|
1799
|
+
selectedOptionIds: z4.array(z4.string().min(1)).optional()
|
|
1800
|
+
}).strict()),
|
|
1801
|
+
goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
|
|
1802
|
+
feedback: z4.array(z4.object({ entryKeys: z4.array(key).min(1) }).strict()),
|
|
1803
|
+
lessons: z4.array(z4.object({ entryKeys: z4.array(key).min(1), change: LessonChangeSchema }).strict()),
|
|
1804
|
+
next: BrainNextSchema,
|
|
1805
|
+
search: BrainSearchSchema.optional()
|
|
1806
|
+
}).strict();
|
|
1807
|
+
var str = z4.string();
|
|
1808
|
+
var strs = z4.array(z4.string());
|
|
1809
|
+
var CompactResultSchema = z4.object({
|
|
1810
|
+
next: z4.object({ say: strs, then: z4.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason: str, instructions: strs }).strict(),
|
|
1811
|
+
messages: z4.array(z4.object({ key: str, to: str, text: str, about: strs, entries: strs, owed: strs, inviteToCall: z4.boolean() }).strict()),
|
|
1812
|
+
entries: z4.array(z4.object({ key: str, from: str, on: strs }).strict()),
|
|
1813
|
+
answers: z4.array(z4.object({ question: str, entries: strs, summary: str, picked: strs }).strict()),
|
|
1814
|
+
changes: z4.array(z4.object({
|
|
1815
|
+
key: str,
|
|
1816
|
+
what: z4.enum(["goal", "question", "lesson"]),
|
|
1817
|
+
op: z4.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
|
|
1818
|
+
id: str,
|
|
1819
|
+
text: str,
|
|
1820
|
+
title: str,
|
|
1821
|
+
who: str,
|
|
1822
|
+
under: str,
|
|
1823
|
+
gate: z4.enum(["start", "complete", "answer", "none"]),
|
|
1824
|
+
options: z4.array(z4.object({ id: str, label: str }).strict()),
|
|
1825
|
+
pick: z4.enum(["one", "many", "rank", "words"]),
|
|
1826
|
+
entries: strs
|
|
1827
|
+
}).strict()),
|
|
1828
|
+
feedback: z4.array(strs),
|
|
1829
|
+
search: z4.object({ query: str, within: strs, goal: str }).strict()
|
|
1830
|
+
}).strict();
|
|
1831
|
+
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
1832
|
+
zodToJsonSchema(CompactResultSchema, { $refStrategy: "none" })
|
|
1833
|
+
);
|
|
1625
1834
|
function entryWords(entry) {
|
|
1626
1835
|
const content = entry.content;
|
|
1627
1836
|
if (content && "sealed" in content) return "";
|
|
@@ -1634,22 +1843,24 @@ function entryWords(entry) {
|
|
|
1634
1843
|
const description = plain.description;
|
|
1635
1844
|
const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
|
|
1636
1845
|
if (parts.length) return parts.join("\n\n");
|
|
1846
|
+
const line = plain.line;
|
|
1847
|
+
if (typeof line === "string" && line.trim()) return line;
|
|
1637
1848
|
}
|
|
1638
1849
|
return entry.sources.map((source) => source.text).join("\n");
|
|
1639
1850
|
}
|
|
1640
1851
|
var LIVE_MS = 3 * 6e4;
|
|
1641
1852
|
var WORKING_MS = 60 * 6e4;
|
|
1642
|
-
var ContextSchema =
|
|
1643
|
-
title:
|
|
1644
|
-
description:
|
|
1853
|
+
var ContextSchema = z5.object({
|
|
1854
|
+
title: z5.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
1855
|
+
description: z5.array(z5.string().min(1)).describe(
|
|
1645
1856
|
"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."
|
|
1646
1857
|
)
|
|
1647
1858
|
});
|
|
1648
|
-
var ParticipantSchema =
|
|
1649
|
-
kind:
|
|
1650
|
-
id:
|
|
1859
|
+
var ParticipantSchema = z5.object({
|
|
1860
|
+
kind: z5.enum(["human", "agent"]),
|
|
1861
|
+
id: z5.string()
|
|
1651
1862
|
});
|
|
1652
|
-
var TransformSchema =
|
|
1863
|
+
var TransformSchema = z5.enum([
|
|
1653
1864
|
"structure",
|
|
1654
1865
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
1655
1866
|
"request_more",
|
|
@@ -1672,13 +1883,13 @@ var PAIGY_SELF = { kind: "agent", id: "paigy" };
|
|
|
1672
1883
|
function isPaigy(ref) {
|
|
1673
1884
|
return ref === participantRef(PAIGY_SELF);
|
|
1674
1885
|
}
|
|
1675
|
-
var VisualSchema =
|
|
1676
|
-
url:
|
|
1677
|
-
label:
|
|
1886
|
+
var VisualSchema = z5.object({
|
|
1887
|
+
url: z5.string().url(),
|
|
1888
|
+
label: z5.string().optional()
|
|
1678
1889
|
});
|
|
1679
|
-
var NotifyLevelSchema =
|
|
1680
|
-
var SelectShapeSchema =
|
|
1681
|
-
var ReceiptEventSchema =
|
|
1890
|
+
var NotifyLevelSchema = z5.enum(["inbox", "push", "banner", "call"]);
|
|
1891
|
+
var SelectShapeSchema = z5.enum(["one", "many", "rank", "confirm", "text"]);
|
|
1892
|
+
var ReceiptEventSchema = z5.enum([
|
|
1682
1893
|
"delivered",
|
|
1683
1894
|
// the bundle reached the recipient at some level
|
|
1684
1895
|
"seen",
|
|
@@ -1708,47 +1919,47 @@ var ReceiptEventSchema = z4.enum([
|
|
|
1708
1919
|
// be rewound by a writer that forgot to advance it.
|
|
1709
1920
|
"restarted"
|
|
1710
1921
|
]);
|
|
1711
|
-
var AttentionSchema =
|
|
1922
|
+
var AttentionSchema = z5.object({
|
|
1712
1923
|
urgency: NotifyLevelSchema,
|
|
1713
1924
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
1714
1925
|
select: SelectShapeSchema.nullable(),
|
|
1715
1926
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
1716
|
-
points:
|
|
1927
|
+
points: z5.array(z5.string()).nullable(),
|
|
1717
1928
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
1718
|
-
blocking:
|
|
1929
|
+
blocking: z5.boolean(),
|
|
1719
1930
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
1720
1931
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
1721
|
-
deadline:
|
|
1932
|
+
deadline: z5.string().datetime().nullable().optional()
|
|
1722
1933
|
});
|
|
1723
|
-
var NotifyRequestFields =
|
|
1934
|
+
var NotifyRequestFields = z5.object({
|
|
1724
1935
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
1725
1936
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
1726
1937
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
1727
1938
|
context: ContextSchema.optional(),
|
|
1728
|
-
options:
|
|
1939
|
+
options: z5.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
1729
1940
|
"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)."
|
|
1730
1941
|
),
|
|
1731
|
-
points:
|
|
1942
|
+
points: z5.array(z5.string().min(1)).optional().describe(
|
|
1732
1943
|
"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."
|
|
1733
1944
|
),
|
|
1734
|
-
visuals:
|
|
1945
|
+
visuals: z5.array(VisualSchema).optional().describe(
|
|
1735
1946
|
"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."
|
|
1736
1947
|
),
|
|
1737
1948
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
1738
|
-
repo:
|
|
1949
|
+
repo: z5.string().optional(),
|
|
1739
1950
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
1740
|
-
branch:
|
|
1951
|
+
branch: z5.string().optional(),
|
|
1741
1952
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
1742
1953
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
1743
1954
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
1744
1955
|
* still accepts the old name from older clients. */
|
|
1745
|
-
parentId:
|
|
1956
|
+
parentId: z5.string().uuid().optional(),
|
|
1746
1957
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
1747
1958
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
1748
|
-
workId:
|
|
1959
|
+
workId: z5.string().uuid().optional(),
|
|
1749
1960
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
1750
1961
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
1751
|
-
goalId:
|
|
1962
|
+
goalId: z5.string().uuid().optional(),
|
|
1752
1963
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
1753
1964
|
"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."
|
|
1754
1965
|
),
|
|
@@ -1756,7 +1967,7 @@ var NotifyRequestFields = z4.object({
|
|
|
1756
1967
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
1757
1968
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
1758
1969
|
* says HOW. */
|
|
1759
|
-
clarifies:
|
|
1970
|
+
clarifies: z5.string().optional(),
|
|
1760
1971
|
select: SelectShapeSchema.optional().describe(
|
|
1761
1972
|
"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."
|
|
1762
1973
|
),
|
|
@@ -1770,20 +1981,20 @@ var NotifyRequestFields = z4.object({
|
|
|
1770
1981
|
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
1771
1982
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
1772
1983
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
1773
|
-
ask:
|
|
1984
|
+
ask: z5.string().min(1).max(1e4).optional().describe(
|
|
1774
1985
|
'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.'
|
|
1775
1986
|
),
|
|
1776
|
-
needs:
|
|
1987
|
+
needs: z5.array(z5.string().min(1)).optional().describe(
|
|
1777
1988
|
"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."
|
|
1778
1989
|
),
|
|
1779
|
-
urgencyHint:
|
|
1990
|
+
urgencyHint: z5.enum(["whenever", "soon", "now"]).optional().describe(
|
|
1780
1991
|
"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."
|
|
1781
1992
|
),
|
|
1782
1993
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
1783
1994
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
1784
1995
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
1785
1996
|
* urgencyHint/blocking win when both are sent. */
|
|
1786
|
-
waiting:
|
|
1997
|
+
waiting: z5.enum(["none", "soft", "hard"]).optional().describe(
|
|
1787
1998
|
"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."
|
|
1788
1999
|
),
|
|
1789
2000
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -1791,98 +2002,98 @@ var NotifyRequestFields = z4.object({
|
|
|
1791
2002
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
1792
2003
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
1793
2004
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
1794
|
-
confirm:
|
|
2005
|
+
confirm: z5.boolean().optional().describe(
|
|
1795
2006
|
"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'."
|
|
1796
2007
|
),
|
|
1797
2008
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
1798
2009
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
1799
2010
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
1800
2011
|
* waiting:'hard'. */
|
|
1801
|
-
channel:
|
|
2012
|
+
channel: z5.enum(["call", "message"]).optional().describe(
|
|
1802
2013
|
"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."
|
|
1803
2014
|
),
|
|
1804
|
-
confirmStyle:
|
|
2015
|
+
confirmStyle: z5.enum(["yesno", "approve"]).default("yesno").describe(
|
|
1805
2016
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
1806
2017
|
),
|
|
1807
|
-
blocking:
|
|
2018
|
+
blocking: z5.boolean().default(false).describe(
|
|
1808
2019
|
"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."
|
|
1809
2020
|
)
|
|
1810
2021
|
});
|
|
1811
2022
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
1812
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
2023
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
1813
2024
|
if (r.ask !== void 0) {
|
|
1814
2025
|
for (const f of ["context", "select", "points"]) {
|
|
1815
2026
|
if (r[f] !== void 0)
|
|
1816
|
-
ctx.addIssue({ code:
|
|
2027
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
1817
2028
|
}
|
|
1818
2029
|
return;
|
|
1819
2030
|
}
|
|
1820
2031
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
1821
|
-
ctx.addIssue({ code:
|
|
2032
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
1822
2033
|
if (!r.context)
|
|
1823
|
-
ctx.addIssue({ code:
|
|
2034
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
1824
2035
|
if (!r.select)
|
|
1825
|
-
ctx.addIssue({ code:
|
|
2036
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
1826
2037
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
1827
2038
|
if (needsOptions && !r.options?.length)
|
|
1828
|
-
ctx.addIssue({ code:
|
|
2039
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
1829
2040
|
if (!needsOptions && r.options?.length)
|
|
1830
|
-
ctx.addIssue({ code:
|
|
2041
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
1831
2042
|
});
|
|
1832
|
-
var NotifyStatusSchema =
|
|
1833
|
-
var AgentStateSchema =
|
|
1834
|
-
var TurnSchema =
|
|
1835
|
-
prompt:
|
|
1836
|
-
reply:
|
|
2043
|
+
var NotifyStatusSchema = z5.enum(["pending", "answered", "ignored"]);
|
|
2044
|
+
var AgentStateSchema = z5.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
2045
|
+
var TurnSchema = z5.object({
|
|
2046
|
+
prompt: z5.string(),
|
|
2047
|
+
reply: z5.string()
|
|
1837
2048
|
});
|
|
1838
|
-
var UserAnswerSchema =
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2049
|
+
var UserAnswerSchema = z5.discriminatedUnion("kind", [
|
|
2050
|
+
z5.object({ kind: z5.literal("option"), optionId: z5.string(), label: z5.string().optional() }),
|
|
2051
|
+
z5.object({ kind: z5.literal("text"), text: z5.string() }),
|
|
2052
|
+
z5.object({ kind: z5.literal("ignored") }),
|
|
2053
|
+
z5.object({ kind: z5.literal("multi"), optionIds: z5.array(z5.string()), labels: z5.array(z5.string()).optional() }),
|
|
2054
|
+
z5.object({ kind: z5.literal("ranked"), optionIds: z5.array(z5.string()), labels: z5.array(z5.string()).optional() }),
|
|
2055
|
+
z5.object({ kind: z5.literal("clarify"), chunks: z5.array(z5.string()).min(1) }),
|
|
2056
|
+
z5.object({ kind: z5.literal("confirm"), approved: z5.boolean() }),
|
|
2057
|
+
z5.object({ kind: z5.literal("turns"), turns: z5.array(TurnSchema).min(1) }),
|
|
1847
2058
|
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
1848
2059
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
1849
2060
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
1850
2061
|
* card the user can reply to — the broker never overrides the user. */
|
|
1851
|
-
|
|
2062
|
+
z5.object({ kind: z5.literal("precedent"), answer: z5.string(), derivation: z5.string(), sources: z5.array(z5.string()).min(1) })
|
|
1852
2063
|
]);
|
|
1853
|
-
var IntentSchema =
|
|
2064
|
+
var IntentSchema = z5.object({
|
|
1854
2065
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
1855
2066
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
1856
2067
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
1857
2068
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
1858
|
-
kind:
|
|
1859
|
-
detail:
|
|
2069
|
+
kind: z5.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
2070
|
+
detail: z5.string(),
|
|
1860
2071
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
1861
2072
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
1862
2073
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
1863
2074
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
1864
|
-
dueInSeconds:
|
|
2075
|
+
dueInSeconds: z5.number().int().positive().optional(),
|
|
1865
2076
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
1866
|
-
* already read the utterance, so `
|
|
2077
|
+
* already read the utterance, so `signals.kind` stops defaulting to
|
|
1867
2078
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
1868
2079
|
* about what cannot answer "is the bot looping less this week?". */
|
|
1869
|
-
fault:
|
|
2080
|
+
fault: z5.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
1870
2081
|
});
|
|
1871
|
-
var RideAlongSchema =
|
|
2082
|
+
var RideAlongSchema = z5.object({
|
|
1872
2083
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
1873
|
-
noteId:
|
|
2084
|
+
noteId: z5.string(),
|
|
1874
2085
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
1875
|
-
text:
|
|
2086
|
+
text: z5.string(),
|
|
1876
2087
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
1877
|
-
parentId:
|
|
2088
|
+
parentId: z5.string().nullable()
|
|
1878
2089
|
});
|
|
1879
|
-
var AwaitItemSchema =
|
|
1880
|
-
|
|
1881
|
-
type:
|
|
1882
|
-
parentId:
|
|
1883
|
-
notificationId:
|
|
1884
|
-
workId:
|
|
1885
|
-
decisionId:
|
|
2090
|
+
var AwaitItemSchema = z5.discriminatedUnion("type", [
|
|
2091
|
+
z5.object({
|
|
2092
|
+
type: z5.literal("reply"),
|
|
2093
|
+
parentId: z5.string(),
|
|
2094
|
+
notificationId: z5.string(),
|
|
2095
|
+
workId: z5.string().uuid().optional(),
|
|
2096
|
+
decisionId: z5.string().uuid().optional(),
|
|
1886
2097
|
answer: UserAnswerSchema,
|
|
1887
2098
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
1888
2099
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -1892,103 +2103,103 @@ var AwaitItemSchema = z4.discriminatedUnion("type", [
|
|
|
1892
2103
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
1893
2104
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
1894
2105
|
* a sentence with a hole in it is worse than no sentence. */
|
|
1895
|
-
note:
|
|
2106
|
+
note: z5.string().optional(),
|
|
1896
2107
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
1897
2108
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
1898
2109
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
1899
2110
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
1900
|
-
transcript:
|
|
2111
|
+
transcript: z5.string().optional(),
|
|
1901
2112
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
1902
2113
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
1903
|
-
covered:
|
|
2114
|
+
covered: z5.array(z5.string()).optional(),
|
|
1904
2115
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
1905
2116
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
1906
2117
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
1907
2118
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
1908
2119
|
* dropped. Absent/empty = nothing owed. */
|
|
1909
|
-
also:
|
|
2120
|
+
also: z5.array(RideAlongSchema).optional()
|
|
1910
2121
|
}),
|
|
1911
|
-
|
|
1912
|
-
type:
|
|
1913
|
-
parentId:
|
|
1914
|
-
notificationId:
|
|
1915
|
-
remindAt:
|
|
2122
|
+
z5.object({
|
|
2123
|
+
type: z5.literal("remind"),
|
|
2124
|
+
parentId: z5.string(),
|
|
2125
|
+
notificationId: z5.string(),
|
|
2126
|
+
remindAt: z5.string().datetime({ offset: true }),
|
|
1916
2127
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
1917
|
-
remindInSeconds:
|
|
2128
|
+
remindInSeconds: z5.number()
|
|
1918
2129
|
}),
|
|
1919
2130
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
1920
2131
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
1921
2132
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
1922
|
-
* re-orient via
|
|
1923
|
-
|
|
1924
|
-
type:
|
|
1925
|
-
parentId:
|
|
1926
|
-
notificationId:
|
|
2133
|
+
* re-orient via contact({})). */
|
|
2134
|
+
z5.object({
|
|
2135
|
+
type: z5.literal("superseded"),
|
|
2136
|
+
parentId: z5.string(),
|
|
2137
|
+
notificationId: z5.string()
|
|
1927
2138
|
}),
|
|
1928
2139
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
1929
2140
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
1930
2141
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
1931
2142
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
1932
2143
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
1933
|
-
|
|
1934
|
-
type:
|
|
1935
|
-
notificationId:
|
|
1936
|
-
inFlight:
|
|
1937
|
-
turn:
|
|
1938
|
-
idx:
|
|
1939
|
-
prompt:
|
|
1940
|
-
reply:
|
|
1941
|
-
acts:
|
|
2144
|
+
z5.object({
|
|
2145
|
+
type: z5.literal("partial"),
|
|
2146
|
+
notificationId: z5.string(),
|
|
2147
|
+
inFlight: z5.literal(true),
|
|
2148
|
+
turn: z5.object({
|
|
2149
|
+
idx: z5.number(),
|
|
2150
|
+
prompt: z5.string(),
|
|
2151
|
+
reply: z5.string(),
|
|
2152
|
+
acts: z5.array(IntentSchema).nullable().optional()
|
|
1942
2153
|
})
|
|
1943
2154
|
}),
|
|
1944
|
-
|
|
1945
|
-
type:
|
|
1946
|
-
also:
|
|
2155
|
+
z5.object({
|
|
2156
|
+
type: z5.literal("idle"),
|
|
2157
|
+
also: z5.array(RideAlongSchema).optional(),
|
|
1947
2158
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
1948
2159
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
1949
2160
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
1950
2161
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
1951
2162
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
1952
|
-
inFlight:
|
|
2163
|
+
inFlight: z5.boolean().optional()
|
|
1953
2164
|
})
|
|
1954
2165
|
]);
|
|
1955
|
-
var VoiceKeySchema =
|
|
1956
|
-
var AgendaTurnSchema =
|
|
2166
|
+
var VoiceKeySchema = z5.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
2167
|
+
var AgendaTurnSchema = z5.object({
|
|
1957
2168
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
1958
2169
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
1959
2170
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
1960
2171
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
1961
2172
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
1962
2173
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
1963
|
-
id:
|
|
2174
|
+
id: z5.string().optional(),
|
|
1964
2175
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
1965
2176
|
* the planner declares duplicates instead of asking them twice. */
|
|
1966
|
-
coveredIds:
|
|
2177
|
+
coveredIds: z5.array(z5.string()).optional(),
|
|
1967
2178
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
1968
2179
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
1969
|
-
info:
|
|
1970
|
-
question:
|
|
2180
|
+
info: z5.array(z5.string().min(1)).default([]),
|
|
2181
|
+
question: z5.string().min(1).nullable(),
|
|
1971
2182
|
/** True on the one turn carrying the agent's own declared question. */
|
|
1972
|
-
asks:
|
|
2183
|
+
asks: z5.boolean().optional(),
|
|
1973
2184
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
1974
2185
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
1975
2186
|
* which route nothing. */
|
|
1976
|
-
claimId:
|
|
2187
|
+
claimId: z5.string().optional(),
|
|
1977
2188
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
1978
|
-
voice:
|
|
2189
|
+
voice: z5.string().optional(),
|
|
1979
2190
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
1980
2191
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
1981
2192
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
1982
2193
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
1983
|
-
agent:
|
|
2194
|
+
agent: z5.string().optional(),
|
|
1984
2195
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
1985
2196
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
1986
2197
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
1987
2198
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
1988
2199
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
1989
|
-
agentId:
|
|
2200
|
+
agentId: z5.string().optional(),
|
|
1990
2201
|
select: SelectShapeSchema.optional(),
|
|
1991
|
-
options:
|
|
2202
|
+
options: z5.array(OptionSchema.omit({ id: true })).optional(),
|
|
1992
2203
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
1993
2204
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
1994
2205
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -1997,33 +2208,33 @@ var AgendaTurnSchema = z4.object({
|
|
|
1997
2208
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
1998
2209
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
1999
2210
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
2000
|
-
blocking:
|
|
2211
|
+
blocking: z5.boolean().optional(),
|
|
2001
2212
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
2002
2213
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
2003
2214
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
2004
2215
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
2005
2216
|
* speak, it is dropped and their words are taken like any reply. */
|
|
2006
|
-
ifSilent:
|
|
2217
|
+
ifSilent: z5.boolean().optional()
|
|
2007
2218
|
});
|
|
2008
2219
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
2009
|
-
var InboxItemSchema =
|
|
2010
|
-
id:
|
|
2011
|
-
tokenId:
|
|
2220
|
+
var InboxItemSchema = z5.object({
|
|
2221
|
+
id: z5.string(),
|
|
2222
|
+
tokenId: z5.string().optional(),
|
|
2012
2223
|
status: NotifyStatusSchema,
|
|
2013
2224
|
context: ContextSchema,
|
|
2014
|
-
options:
|
|
2225
|
+
options: z5.array(OptionSchema).optional(),
|
|
2015
2226
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
2016
|
-
points:
|
|
2227
|
+
points: z5.array(z5.string()).optional(),
|
|
2017
2228
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
2018
2229
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
2019
2230
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
2020
2231
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
2021
|
-
asks:
|
|
2232
|
+
asks: z5.boolean().optional(),
|
|
2022
2233
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
2023
2234
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
2024
2235
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
2025
2236
|
* which must never CLAIM stalled. */
|
|
2026
|
-
lastSeenAt:
|
|
2237
|
+
lastSeenAt: z5.string().optional(),
|
|
2027
2238
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
2028
2239
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
2029
2240
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -2033,7 +2244,7 @@ var InboxItemSchema = z4.object({
|
|
|
2033
2244
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
2034
2245
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
2035
2246
|
* `createdAt`. */
|
|
2036
|
-
agentStateAt:
|
|
2247
|
+
agentStateAt: z5.string().datetime().optional(),
|
|
2037
2248
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
2038
2249
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
2039
2250
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -2047,54 +2258,54 @@ var InboxItemSchema = z4.object({
|
|
|
2047
2258
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
2048
2259
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
2049
2260
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
2050
|
-
questions:
|
|
2051
|
-
id:
|
|
2052
|
-
entryId:
|
|
2053
|
-
title:
|
|
2054
|
-
state:
|
|
2055
|
-
answer:
|
|
2261
|
+
questions: z5.array(z5.object({
|
|
2262
|
+
id: z5.string(),
|
|
2263
|
+
entryId: z5.string(),
|
|
2264
|
+
title: z5.string(),
|
|
2265
|
+
state: z5.enum(["open", "answered"]),
|
|
2266
|
+
answer: z5.string().nullable(),
|
|
2056
2267
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
2057
2268
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
2058
2269
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
2059
2270
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
2060
|
-
agent:
|
|
2271
|
+
agent: z5.object({ tokenId: z5.string(), name: z5.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
2061
2272
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
2062
2273
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
2063
2274
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
2064
2275
|
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
2065
2276
|
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
2066
|
-
options:
|
|
2277
|
+
options: z5.array(OptionSchema).optional()
|
|
2067
2278
|
})).optional(),
|
|
2068
|
-
visuals:
|
|
2279
|
+
visuals: z5.array(VisualSchema).optional(),
|
|
2069
2280
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
2070
2281
|
* agent's suggestion, or a default silly name). */
|
|
2071
|
-
name:
|
|
2282
|
+
name: z5.string(),
|
|
2072
2283
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
2073
2284
|
voice: VoiceKeySchema.optional(),
|
|
2074
|
-
repo:
|
|
2075
|
-
branch:
|
|
2076
|
-
createdAt:
|
|
2077
|
-
snoozedUntil:
|
|
2285
|
+
repo: z5.string().optional(),
|
|
2286
|
+
branch: z5.string().optional(),
|
|
2287
|
+
createdAt: z5.string().datetime(),
|
|
2288
|
+
snoozedUntil: z5.string().datetime().optional(),
|
|
2078
2289
|
agentState: AgentStateSchema.default("idle"),
|
|
2079
2290
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
2080
2291
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
2081
2292
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
2082
|
-
turn:
|
|
2293
|
+
turn: z5.enum(["you", "agent"]).default("you"),
|
|
2083
2294
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
2084
2295
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
2085
2296
|
* error badge + Retry. */
|
|
2086
|
-
error:
|
|
2297
|
+
error: z5.string().optional(),
|
|
2087
2298
|
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
2088
2299
|
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
2089
2300
|
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
2090
2301
|
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
2091
|
-
cold:
|
|
2092
|
-
clarifies:
|
|
2302
|
+
cold: z5.string().datetime().optional(),
|
|
2303
|
+
clarifies: z5.string().optional(),
|
|
2093
2304
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
2094
2305
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
2095
2306
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
2096
2307
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
2097
|
-
onCall:
|
|
2308
|
+
onCall: z5.literal(true).optional(),
|
|
2098
2309
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
2099
2310
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
2100
2311
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -2104,31 +2315,31 @@ var InboxItemSchema = z4.object({
|
|
|
2104
2315
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
2105
2316
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
2106
2317
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
2107
|
-
ring:
|
|
2108
|
-
declinedAt:
|
|
2109
|
-
anchorAt:
|
|
2110
|
-
nextRingAt:
|
|
2111
|
-
step:
|
|
2318
|
+
ring: z5.object({
|
|
2319
|
+
declinedAt: z5.string().datetime(),
|
|
2320
|
+
anchorAt: z5.string().datetime(),
|
|
2321
|
+
nextRingAt: z5.string().datetime().nullable(),
|
|
2322
|
+
step: z5.number().int()
|
|
2112
2323
|
}).optional(),
|
|
2113
2324
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
2114
2325
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
2115
2326
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
2116
2327
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
2117
2328
|
* feedback ABOUT. */
|
|
2118
|
-
why:
|
|
2329
|
+
why: z5.object({
|
|
2119
2330
|
asked: NotifyLevelSchema,
|
|
2120
2331
|
got: NotifyLevelSchema,
|
|
2121
|
-
because:
|
|
2122
|
-
line:
|
|
2332
|
+
because: z5.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
2333
|
+
line: z5.string()
|
|
2123
2334
|
}).optional(),
|
|
2124
|
-
select:
|
|
2125
|
-
confirmStyle:
|
|
2335
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
2336
|
+
confirmStyle: z5.enum(["yesno", "approve"]).default("yesno").describe(
|
|
2126
2337
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
2127
2338
|
),
|
|
2128
2339
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
2129
2340
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
2130
2341
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
2131
|
-
blocking:
|
|
2342
|
+
blocking: z5.boolean().default(false),
|
|
2132
2343
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
2133
2344
|
answer: UserAnswerSchema.optional(),
|
|
2134
2345
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -2136,38 +2347,49 @@ var InboxItemSchema = z4.object({
|
|
|
2136
2347
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
2137
2348
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
2138
2349
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
2139
|
-
communication:
|
|
2140
|
-
deliveryId:
|
|
2141
|
-
kind:
|
|
2142
|
-
entryId:
|
|
2143
|
-
goalIds:
|
|
2144
|
-
decisionNeedId:
|
|
2145
|
-
sealed:
|
|
2146
|
-
goalState:
|
|
2350
|
+
communication: z5.object({
|
|
2351
|
+
deliveryId: z5.string(),
|
|
2352
|
+
kind: z5.enum(["notification", "call"]),
|
|
2353
|
+
entryId: z5.string(),
|
|
2354
|
+
goalIds: z5.array(z5.string()),
|
|
2355
|
+
decisionNeedId: z5.string().optional(),
|
|
2356
|
+
sealed: z5.boolean(),
|
|
2357
|
+
goalState: z5.string().optional(),
|
|
2147
2358
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
2148
2359
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
2149
|
-
goalTitle:
|
|
2150
|
-
}).optional()
|
|
2360
|
+
goalTitle: z5.string().optional()
|
|
2361
|
+
}).optional(),
|
|
2362
|
+
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
2363
|
+
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
2364
|
+
* how the incremental read knows a card has not moved: the phone echoes back the revs it holds
|
|
2365
|
+
* (`POST /api/inbox/changes`) and is sent only the cards whose rev differs.
|
|
2366
|
+
*
|
|
2367
|
+
* THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK — the same mechanism as `QueueItem.rev`
|
|
2368
|
+
* (#2928) and an entry's (#3018), for the same reason: a card shows facts no `updated_at` of its
|
|
2369
|
+
* own moves (its Goal's state and name, how cold the work behind it has gone, whether a ring is
|
|
2370
|
+
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
2371
|
+
* with no rev is simply always re-sent. */
|
|
2372
|
+
rev: z5.string().optional()
|
|
2151
2373
|
});
|
|
2152
2374
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
2153
|
-
var PushTokenSchema =
|
|
2154
|
-
voipToken:
|
|
2155
|
-
alertToken:
|
|
2156
|
-
fcmToken:
|
|
2157
|
-
platform:
|
|
2375
|
+
var PushTokenSchema = z5.object({
|
|
2376
|
+
voipToken: z5.string().min(1).optional(),
|
|
2377
|
+
alertToken: z5.string().min(1).optional(),
|
|
2378
|
+
fcmToken: z5.string().min(1).optional(),
|
|
2379
|
+
platform: z5.enum(["ios", "android"])
|
|
2158
2380
|
}).superRefine((v, ctx) => {
|
|
2159
2381
|
if (v.platform !== "ios") return;
|
|
2160
2382
|
for (const field of ["voipToken", "alertToken"]) {
|
|
2161
2383
|
const token = v[field];
|
|
2162
2384
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
2163
2385
|
ctx.addIssue({
|
|
2164
|
-
code:
|
|
2386
|
+
code: z5.ZodIssueCode.custom,
|
|
2165
2387
|
path: [field],
|
|
2166
2388
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
2167
2389
|
});
|
|
2168
2390
|
}
|
|
2169
2391
|
});
|
|
2170
|
-
var MissedCallSchema =
|
|
2392
|
+
var MissedCallSchema = z5.enum([
|
|
2171
2393
|
"retry_10m",
|
|
2172
2394
|
"retry_30m",
|
|
2173
2395
|
"retry_60m",
|
|
@@ -2179,31 +2401,31 @@ var MissedCallSchema = z4.enum([
|
|
|
2179
2401
|
]);
|
|
2180
2402
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
2181
2403
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
2182
|
-
var BrokerTuningSchema =
|
|
2404
|
+
var BrokerTuningSchema = z5.object({
|
|
2183
2405
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
2184
|
-
ackVerbosity:
|
|
2406
|
+
ackVerbosity: z5.enum(["normal", "none"]).optional(),
|
|
2185
2407
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
2186
2408
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
2187
|
-
clarifyEagerness:
|
|
2409
|
+
clarifyEagerness: z5.enum(["low", "normal", "high"]).optional(),
|
|
2188
2410
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
2189
|
-
phrasebook:
|
|
2411
|
+
phrasebook: z5.array(z5.object({ say: z5.string().min(1).max(60), mean: z5.string().min(1).max(120) })).max(24).optional(),
|
|
2190
2412
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
2191
2413
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
2192
2414
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
2193
2415
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
2194
2416
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
2195
|
-
language:
|
|
2417
|
+
language: z5.enum(["en", "es"]).optional()
|
|
2196
2418
|
});
|
|
2197
|
-
var UserSettingsSchema =
|
|
2198
|
-
permissions:
|
|
2199
|
-
call:
|
|
2200
|
-
banner:
|
|
2201
|
-
push:
|
|
2419
|
+
var UserSettingsSchema = z5.object({
|
|
2420
|
+
permissions: z5.object({
|
|
2421
|
+
call: z5.boolean(),
|
|
2422
|
+
banner: z5.boolean(),
|
|
2423
|
+
push: z5.boolean()
|
|
2202
2424
|
}),
|
|
2203
2425
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
2204
2426
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
2205
2427
|
* what people read (owner, 2026-09-30). */
|
|
2206
|
-
sessionMode:
|
|
2428
|
+
sessionMode: z5.enum(["default", "all_calls", "silent"]),
|
|
2207
2429
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
2208
2430
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
2209
2431
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -2212,7 +2434,7 @@ var UserSettingsSchema = z4.object({
|
|
|
2212
2434
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
2213
2435
|
* next dead field for the same reason. */
|
|
2214
2436
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
2215
|
-
improveConsent:
|
|
2437
|
+
improveConsent: z5.boolean(),
|
|
2216
2438
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
2217
2439
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
2218
2440
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -2220,17 +2442,17 @@ var UserSettingsSchema = z4.object({
|
|
|
2220
2442
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
2221
2443
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
2222
2444
|
* write, 'hosted' on read (see store.ts). */
|
|
2223
|
-
voiceMode:
|
|
2445
|
+
voiceMode: z5.enum(["hosted", "on_device"]).optional(),
|
|
2224
2446
|
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
2225
2447
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
2226
2448
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
2227
2449
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
2228
2450
|
* read (see store.ts). */
|
|
2229
|
-
talk:
|
|
2451
|
+
talk: z5.boolean().optional(),
|
|
2230
2452
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
2231
2453
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
2232
2454
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
2233
|
-
callDiagnostics:
|
|
2455
|
+
callDiagnostics: z5.boolean().optional(),
|
|
2234
2456
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
2235
2457
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
2236
2458
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -2238,7 +2460,7 @@ var UserSettingsSchema = z4.object({
|
|
|
2238
2460
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
2239
2461
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
2240
2462
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
2241
|
-
voiceTuning:
|
|
2463
|
+
voiceTuning: z5.record(z5.string(), z5.union([z5.number(), z5.string()])).optional(),
|
|
2242
2464
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
2243
2465
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
2244
2466
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -2247,53 +2469,53 @@ var UserSettingsSchema = z4.object({
|
|
|
2247
2469
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
2248
2470
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
2249
2471
|
* call — honest about what we know. */
|
|
2250
|
-
timezone:
|
|
2472
|
+
timezone: z5.string().min(1).max(64).optional(),
|
|
2251
2473
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
2252
2474
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
2253
2475
|
broker: BrokerTuningSchema.optional()
|
|
2254
2476
|
});
|
|
2255
|
-
var HistoryWorkSchema =
|
|
2256
|
-
id:
|
|
2257
|
-
title:
|
|
2258
|
-
state:
|
|
2477
|
+
var HistoryWorkSchema = z5.object({
|
|
2478
|
+
id: z5.string(),
|
|
2479
|
+
title: z5.string(),
|
|
2480
|
+
state: z5.enum(["done", "cancelled"]),
|
|
2259
2481
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
2260
|
-
assignee:
|
|
2482
|
+
assignee: z5.string()
|
|
2261
2483
|
});
|
|
2262
|
-
var HistoryEntrySchema =
|
|
2263
|
-
|
|
2264
|
-
|
|
2484
|
+
var HistoryEntrySchema = z5.union([
|
|
2485
|
+
z5.object({ at: z5.string(), card: InboxItemSchema }),
|
|
2486
|
+
z5.object({ at: z5.string(), work: HistoryWorkSchema })
|
|
2265
2487
|
]);
|
|
2266
|
-
var HistoryPageSchema =
|
|
2267
|
-
entries:
|
|
2268
|
-
next:
|
|
2488
|
+
var HistoryPageSchema = z5.object({
|
|
2489
|
+
entries: z5.array(HistoryEntrySchema),
|
|
2490
|
+
next: z5.string().nullable()
|
|
2269
2491
|
});
|
|
2270
2492
|
var ACTIVITY_LINES = 2;
|
|
2271
2493
|
var ACTIVITY_LINE_MAX = 80;
|
|
2272
|
-
var AgentActivitySchema =
|
|
2494
|
+
var AgentActivitySchema = z5.object({
|
|
2273
2495
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
2274
|
-
lines:
|
|
2496
|
+
lines: z5.array(z5.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
2275
2497
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
2276
2498
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
2277
|
-
at:
|
|
2499
|
+
at: z5.string().datetime()
|
|
2278
2500
|
});
|
|
2279
|
-
var ConnectionSummarySchema =
|
|
2501
|
+
var ConnectionSummarySchema = z5.object({
|
|
2280
2502
|
/** The connection = the agent's token id (used to address a request). */
|
|
2281
|
-
id:
|
|
2503
|
+
id: z5.string(),
|
|
2282
2504
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
2283
2505
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
2284
2506
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
2285
2507
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
2286
|
-
kind:
|
|
2508
|
+
kind: z5.enum(["device", "agent"]).optional(),
|
|
2287
2509
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
2288
2510
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
2289
2511
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
2290
|
-
mintedByDevice:
|
|
2291
|
-
device:
|
|
2512
|
+
mintedByDevice: z5.string().nullable().optional(),
|
|
2513
|
+
device: z5.string().nullable(),
|
|
2292
2514
|
/** The agent's display name (the single pairing name). */
|
|
2293
|
-
name:
|
|
2515
|
+
name: z5.string(),
|
|
2294
2516
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
2295
2517
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
2296
|
-
provider:
|
|
2518
|
+
provider: z5.string().nullable(),
|
|
2297
2519
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
2298
2520
|
voice: VoiceKeySchema.nullable(),
|
|
2299
2521
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -2304,34 +2526,34 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2304
2526
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
2305
2527
|
* set once must not overrule a rule they set about one agent. */
|
|
2306
2528
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
2307
|
-
createdAt:
|
|
2529
|
+
createdAt: z5.string().datetime(),
|
|
2308
2530
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
2309
2531
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
2310
|
-
lastContactAt:
|
|
2532
|
+
lastContactAt: z5.string().datetime().nullable(),
|
|
2311
2533
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
2312
2534
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
2313
|
-
lastSeenAt:
|
|
2535
|
+
lastSeenAt: z5.string().datetime().nullable().optional(),
|
|
2314
2536
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
2315
2537
|
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
2316
2538
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
2317
|
-
lastWorkedAt:
|
|
2539
|
+
lastWorkedAt: z5.string().datetime().nullable().optional(),
|
|
2318
2540
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
2319
2541
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
2320
|
-
oldestReadyAt:
|
|
2542
|
+
oldestReadyAt: z5.string().datetime().nullable().optional(),
|
|
2321
2543
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
2322
2544
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
2323
2545
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
2324
|
-
runtime:
|
|
2546
|
+
runtime: z5.object({
|
|
2325
2547
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
2326
2548
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
2327
|
-
version:
|
|
2328
|
-
harnesses:
|
|
2329
|
-
workspaces:
|
|
2549
|
+
version: z5.string().optional(),
|
|
2550
|
+
harnesses: z5.array(z5.object({ name: z5.string(), label: z5.string(), status: z5.string() })).optional(),
|
|
2551
|
+
workspaces: z5.array(z5.string()).optional(),
|
|
2330
2552
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
2331
2553
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
2332
2554
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
2333
2555
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
2334
|
-
repos:
|
|
2556
|
+
repos: z5.array(z5.object({ path: z5.string(), remote: z5.string() })).optional()
|
|
2335
2557
|
}).optional(),
|
|
2336
2558
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
2337
2559
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -2340,173 +2562,156 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2340
2562
|
activity: AgentActivitySchema.optional(),
|
|
2341
2563
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
2342
2564
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
2343
|
-
managed:
|
|
2565
|
+
managed: z5.boolean()
|
|
2344
2566
|
});
|
|
2345
|
-
var LedgerItemSchema =
|
|
2346
|
-
var AgentLedgerSchema =
|
|
2567
|
+
var LedgerItemSchema = z5.object({ id: z5.string(), parentId: z5.string(), title: z5.string(), createdAt: z5.string() });
|
|
2568
|
+
var AgentLedgerSchema = z5.object({
|
|
2347
2569
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
2348
|
-
agent:
|
|
2570
|
+
agent: z5.object({ id: z5.string(), name: z5.string().nullable(), revokedAt: z5.string().nullable() }),
|
|
2349
2571
|
/** Its own questions you have not answered. */
|
|
2350
|
-
asks:
|
|
2572
|
+
asks: z5.array(LedgerItemSchema),
|
|
2351
2573
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
2352
|
-
answered:
|
|
2574
|
+
answered: z5.array(LedgerItemSchema),
|
|
2353
2575
|
/** Requests you sent it that it never took. */
|
|
2354
|
-
requests:
|
|
2355
|
-
goals:
|
|
2356
|
-
callbacks:
|
|
2576
|
+
requests: z5.array(LedgerItemSchema),
|
|
2577
|
+
goals: z5.array(z5.object({ id: z5.string(), outcome: z5.string(), state: z5.string() })),
|
|
2578
|
+
callbacks: z5.array(z5.object({ id: z5.string(), parentId: z5.string(), trigger: z5.string(), note: z5.string(), dueAt: z5.string().nullable() }))
|
|
2357
2579
|
});
|
|
2358
|
-
var ReassignResultSchema =
|
|
2359
|
-
moved:
|
|
2360
|
-
parentId:
|
|
2580
|
+
var ReassignResultSchema = z5.object({
|
|
2581
|
+
moved: z5.object({ asks: z5.number(), answered: z5.number(), requests: z5.number(), goals: z5.number(), callbacks: z5.number() }),
|
|
2582
|
+
parentId: z5.string().nullable()
|
|
2361
2583
|
});
|
|
2362
|
-
var
|
|
2363
|
-
var
|
|
2364
|
-
id:
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
/** The
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
/**
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
* break is visible instead of a pin silently disappearing. */
|
|
2379
|
-
pinBroken: z4.boolean(),
|
|
2380
|
-
/** When the ruling was distilled. */
|
|
2381
|
-
learnedAt: z4.string(),
|
|
2382
|
-
/** Last time it answered an ask. Null = never fired. */
|
|
2383
|
-
lastUsedAt: z4.string().nullable(),
|
|
2384
|
-
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
2385
|
-
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
2386
|
-
usedCount: z4.number(),
|
|
2387
|
-
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
2388
|
-
confirms: z4.number(),
|
|
2389
|
-
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
2390
|
-
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
2391
|
-
counters: z4.number(),
|
|
2392
|
-
/** The agent that asked the question this move came from, when known. Null for a move
|
|
2393
|
-
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
2394
|
-
learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
|
|
2584
|
+
var LessonStateSchema = z5.enum(["active", "proposed", "retired"]);
|
|
2585
|
+
var LessonViewSchema = z5.object({
|
|
2586
|
+
id: z5.string(),
|
|
2587
|
+
text: z5.string(),
|
|
2588
|
+
state: LessonStateSchema,
|
|
2589
|
+
/** The Goal it is scoped to; null = the whole account. */
|
|
2590
|
+
scopeGoalId: z5.string().nullable(),
|
|
2591
|
+
goalTitle: z5.string().nullable(),
|
|
2592
|
+
version: z5.number(),
|
|
2593
|
+
pinned: z5.boolean(),
|
|
2594
|
+
/** When the person last wrote its text themselves. */
|
|
2595
|
+
editedAt: z5.string().nullable(),
|
|
2596
|
+
createdAt: z5.string(),
|
|
2597
|
+
updatedAt: z5.string(),
|
|
2598
|
+
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
2599
|
+
sources: z5.array(z5.object({ entryId: z5.string(), words: z5.string().nullable(), at: z5.string() }))
|
|
2395
2600
|
});
|
|
2396
|
-
var QueueQuestionSchema =
|
|
2601
|
+
var QueueQuestionSchema = z5.object({
|
|
2397
2602
|
/** The decision need's id — what an answer is accepted against. */
|
|
2398
|
-
id:
|
|
2603
|
+
id: z5.string(),
|
|
2399
2604
|
/** The words that were asked, from the request Entry that asked them. */
|
|
2400
|
-
question:
|
|
2605
|
+
question: z5.string(),
|
|
2401
2606
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
2402
2607
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
2403
2608
|
* can answer. */
|
|
2404
|
-
deliveryId:
|
|
2609
|
+
deliveryId: z5.string().nullable().default(null),
|
|
2405
2610
|
/** The Entry the ruling is about. */
|
|
2406
|
-
aboutId:
|
|
2611
|
+
aboutId: z5.string().nullable().default(null),
|
|
2407
2612
|
/** Empty for a free-text question. */
|
|
2408
|
-
options:
|
|
2409
|
-
select:
|
|
2410
|
-
askedAt:
|
|
2613
|
+
options: z5.array(OptionSchema).default([]),
|
|
2614
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
2615
|
+
askedAt: z5.string(),
|
|
2411
2616
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
2412
|
-
answeredAt:
|
|
2617
|
+
answeredAt: z5.string().nullable().default(null),
|
|
2413
2618
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
2414
2619
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
2415
2620
|
* for a settled question whose reply carried nothing readable. */
|
|
2416
|
-
answer:
|
|
2621
|
+
answer: z5.string().nullable().default(null),
|
|
2417
2622
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
2418
2623
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
2419
2624
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
2420
2625
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
2421
2626
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
2422
2627
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
2423
|
-
goalId:
|
|
2628
|
+
goalId: z5.string(),
|
|
2424
2629
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
2425
2630
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
2426
2631
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
2427
2632
|
* gate at all. */
|
|
2428
|
-
blocked:
|
|
2633
|
+
blocked: z5.boolean().default(false)
|
|
2429
2634
|
});
|
|
2430
|
-
var QueueReplySchema =
|
|
2635
|
+
var QueueReplySchema = z5.object({
|
|
2431
2636
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
2432
2637
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
2433
2638
|
* walk can name it in its zoom. */
|
|
2434
|
-
id:
|
|
2639
|
+
id: z5.string(),
|
|
2435
2640
|
/** The Goal the note is on. */
|
|
2436
|
-
goalId:
|
|
2641
|
+
goalId: z5.string(),
|
|
2437
2642
|
/** What the note said. */
|
|
2438
|
-
note:
|
|
2643
|
+
note: z5.string(),
|
|
2439
2644
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
2440
|
-
deliveryId:
|
|
2441
|
-
requestEntryId:
|
|
2442
|
-
askedAt:
|
|
2645
|
+
deliveryId: z5.string(),
|
|
2646
|
+
requestEntryId: z5.string(),
|
|
2647
|
+
askedAt: z5.string(),
|
|
2443
2648
|
/** When the person last replied — the window's start. */
|
|
2444
|
-
repliedAt:
|
|
2649
|
+
repliedAt: z5.string(),
|
|
2445
2650
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
2446
|
-
reply:
|
|
2651
|
+
reply: z5.string().nullable()
|
|
2447
2652
|
});
|
|
2448
|
-
var QueueItemSchema =
|
|
2449
|
-
id:
|
|
2653
|
+
var QueueItemSchema = z5.object({
|
|
2654
|
+
id: z5.string(),
|
|
2450
2655
|
/** One-line headline — the first sentence of the outcome. */
|
|
2451
|
-
title:
|
|
2656
|
+
title: z5.string(),
|
|
2452
2657
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
2453
|
-
intent:
|
|
2658
|
+
intent: z5.string(),
|
|
2454
2659
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
2455
|
-
state:
|
|
2660
|
+
state: z5.string(),
|
|
2456
2661
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
2457
|
-
assignee:
|
|
2662
|
+
assignee: z5.string().nullable().default(null),
|
|
2458
2663
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
2459
|
-
progress:
|
|
2664
|
+
progress: z5.string().nullable().default(null),
|
|
2460
2665
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
2461
2666
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
2462
|
-
progressLine:
|
|
2463
|
-
reviewPending:
|
|
2464
|
-
dueAt:
|
|
2667
|
+
progressLine: z5.string().nullable().optional(),
|
|
2668
|
+
reviewPending: z5.boolean().default(false),
|
|
2669
|
+
dueAt: z5.string().nullable().default(null),
|
|
2465
2670
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
2466
2671
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
2467
|
-
finishedAt:
|
|
2672
|
+
finishedAt: z5.string().nullable().optional(),
|
|
2468
2673
|
/** The Goal this one was opened under; null at the root. */
|
|
2469
|
-
parentGoalId:
|
|
2674
|
+
parentGoalId: z5.string().nullable().default(null),
|
|
2470
2675
|
/** Goals opened under this one — only those the same list holds. */
|
|
2471
|
-
childGoalIds:
|
|
2676
|
+
childGoalIds: z5.array(z5.string()).default([]),
|
|
2472
2677
|
/** Goals this one waits on (start or finish gates). */
|
|
2473
|
-
dependencyGoalIds:
|
|
2678
|
+
dependencyGoalIds: z5.array(z5.string()).default([]),
|
|
2474
2679
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
2475
|
-
blocked:
|
|
2680
|
+
blocked: z5.boolean().default(false),
|
|
2476
2681
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
2477
2682
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
2478
2683
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
2479
2684
|
* question is opened. */
|
|
2480
|
-
questions:
|
|
2685
|
+
questions: z5.array(QueueQuestionSchema).default([]),
|
|
2481
2686
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
2482
2687
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
2483
2688
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
2484
2689
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
2485
|
-
asked:
|
|
2486
|
-
answered:
|
|
2690
|
+
asked: z5.number().optional(),
|
|
2691
|
+
answered: z5.number().optional(),
|
|
2487
2692
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
2488
2693
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
2489
2694
|
* not spell an empty list. */
|
|
2490
|
-
replies:
|
|
2695
|
+
replies: z5.array(QueueReplySchema).optional(),
|
|
2491
2696
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
2492
|
-
repo:
|
|
2493
|
-
createdAt:
|
|
2494
|
-
updatedAt:
|
|
2697
|
+
repo: z5.string().nullable().optional(),
|
|
2698
|
+
createdAt: z5.string(),
|
|
2699
|
+
updatedAt: z5.string().nullable().default(null),
|
|
2495
2700
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
2496
2701
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
2497
2702
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
2498
|
-
lastProgressAt:
|
|
2703
|
+
lastProgressAt: z5.string().nullable().optional(),
|
|
2499
2704
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
2500
2705
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
2501
2706
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
2502
2707
|
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
2503
2708
|
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
2504
2709
|
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
2505
|
-
latest:
|
|
2506
|
-
from:
|
|
2507
|
-
said:
|
|
2508
|
-
at:
|
|
2509
|
-
entryId:
|
|
2710
|
+
latest: z5.object({
|
|
2711
|
+
from: z5.enum(["person", "agent"]),
|
|
2712
|
+
said: z5.string(),
|
|
2713
|
+
at: z5.string(),
|
|
2714
|
+
entryId: z5.string()
|
|
2510
2715
|
}).nullable().optional(),
|
|
2511
2716
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
2512
2717
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -2519,36 +2724,51 @@ var QueueItemSchema = z4.object({
|
|
|
2519
2724
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
2520
2725
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
2521
2726
|
* Entry with no `about_id` is in neither. */
|
|
2522
|
-
lastPersonAt:
|
|
2727
|
+
lastPersonAt: z5.string().nullable().optional(),
|
|
2728
|
+
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
2729
|
+
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
2730
|
+
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
2731
|
+
* (`POST /api/goals/changes`) and is sent only the rows whose rev differs.
|
|
2732
|
+
*
|
|
2733
|
+
* IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
|
|
2734
|
+
* writes change which field — the comparison is over the bytes the phone is holding, so a fact
|
|
2735
|
+
* the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
|
|
2736
|
+
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
2737
|
+
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
2738
|
+
rev: z5.string().optional()
|
|
2739
|
+
});
|
|
2740
|
+
var QueueDeltaSchema = z5.object({
|
|
2741
|
+
ids: z5.array(z5.string()),
|
|
2742
|
+
items: z5.array(QueueItemSchema)
|
|
2523
2743
|
});
|
|
2524
2744
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
2525
|
-
var NoteSourceSchema =
|
|
2526
|
-
var NoteStatusSchema =
|
|
2527
|
-
var NoteRepeatSchema =
|
|
2528
|
-
var DecisionSchema =
|
|
2529
|
-
id:
|
|
2745
|
+
var NoteSourceSchema = z5.enum(["app", "call"]);
|
|
2746
|
+
var NoteStatusSchema = z5.enum(["open", "assigned", "in_progress", "done"]);
|
|
2747
|
+
var NoteRepeatSchema = z5.enum(["once", "until_done"]);
|
|
2748
|
+
var DecisionSchema = z5.object({
|
|
2749
|
+
id: z5.string(),
|
|
2530
2750
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
2531
2751
|
* extensibility seam — any conversation can accrue decisions). */
|
|
2532
|
-
noteId:
|
|
2752
|
+
noteId: z5.string().nullable(),
|
|
2533
2753
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
2534
|
-
question:
|
|
2754
|
+
question: z5.string(),
|
|
2535
2755
|
/** The user's ruling; null while the question is open. */
|
|
2536
|
-
answer:
|
|
2537
|
-
decidedAt:
|
|
2538
|
-
createdAt:
|
|
2756
|
+
answer: z5.string().nullable(),
|
|
2757
|
+
decidedAt: z5.string().nullable(),
|
|
2758
|
+
createdAt: z5.string()
|
|
2539
2759
|
});
|
|
2540
|
-
var NoteSchema =
|
|
2541
|
-
id:
|
|
2760
|
+
var NoteSchema = z5.object({
|
|
2761
|
+
id: z5.string(),
|
|
2542
2762
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
2543
|
-
title:
|
|
2763
|
+
title: z5.string(),
|
|
2544
2764
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
2545
|
-
intent:
|
|
2765
|
+
intent: z5.string(),
|
|
2546
2766
|
source: NoteSourceSchema,
|
|
2547
2767
|
status: NoteStatusSchema,
|
|
2548
2768
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
2549
|
-
assignee:
|
|
2769
|
+
assignee: z5.string().nullable(),
|
|
2550
2770
|
/** The request thread minted at assignment; null until assigned. */
|
|
2551
|
-
parentId:
|
|
2771
|
+
parentId: z5.string().nullable(),
|
|
2552
2772
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
2553
2773
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
2554
2774
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -2557,149 +2777,154 @@ var NoteSchema = z4.object({
|
|
|
2557
2777
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
2558
2778
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
2559
2779
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
2560
|
-
dueAt:
|
|
2780
|
+
dueAt: z5.string().nullable().default(null),
|
|
2561
2781
|
repeat: NoteRepeatSchema.default("once"),
|
|
2562
2782
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
2563
|
-
rides:
|
|
2564
|
-
lastRideAt:
|
|
2565
|
-
createdAt:
|
|
2783
|
+
rides: z5.number().int().default(0),
|
|
2784
|
+
lastRideAt: z5.string().nullable().default(null),
|
|
2785
|
+
createdAt: z5.string()
|
|
2566
2786
|
});
|
|
2567
|
-
var TriageItemSchema =
|
|
2568
|
-
noteId:
|
|
2787
|
+
var TriageItemSchema = z5.object({
|
|
2788
|
+
noteId: z5.string(),
|
|
2569
2789
|
/** The note's headline at run time. */
|
|
2570
|
-
title:
|
|
2790
|
+
title: z5.string(),
|
|
2571
2791
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
2572
2792
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
2573
2793
|
* Never a model's reasoning transcript, never an id. */
|
|
2574
|
-
why:
|
|
2794
|
+
why: z5.string()
|
|
2575
2795
|
});
|
|
2576
|
-
var TriageAssignmentSchema =
|
|
2796
|
+
var TriageAssignmentSchema = z5.object({
|
|
2577
2797
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
2578
|
-
agent:
|
|
2798
|
+
agent: z5.string(),
|
|
2579
2799
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
2580
2800
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
2581
|
-
agentName:
|
|
2582
|
-
notes:
|
|
2801
|
+
agentName: z5.string(),
|
|
2802
|
+
notes: z5.array(TriageItemSchema)
|
|
2583
2803
|
});
|
|
2584
|
-
var TriageStatusSchema =
|
|
2585
|
-
var SubmitTriageSchema =
|
|
2804
|
+
var TriageStatusSchema = z5.enum(["open", "superseded", "dismissed"]);
|
|
2805
|
+
var SubmitTriageSchema = z5.object({
|
|
2586
2806
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
2587
2807
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
2588
2808
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
2589
2809
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
2590
|
-
provider:
|
|
2810
|
+
provider: z5.string().min(1).max(60),
|
|
2591
2811
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
2592
|
-
model:
|
|
2812
|
+
model: z5.string().max(200).nullable().optional(),
|
|
2593
2813
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
2594
2814
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
2595
|
-
reviewed:
|
|
2596
|
-
close:
|
|
2597
|
-
stale:
|
|
2598
|
-
assign:
|
|
2815
|
+
reviewed: z5.number().int().min(0).max(1e4).default(0),
|
|
2816
|
+
close: z5.array(TriageItemSchema).max(200).default([]),
|
|
2817
|
+
stale: z5.array(TriageItemSchema).max(200).default([]),
|
|
2818
|
+
assign: z5.array(TriageAssignmentSchema).max(50).default([])
|
|
2599
2819
|
});
|
|
2600
2820
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
2601
|
-
id:
|
|
2602
|
-
runAt:
|
|
2821
|
+
id: z5.string(),
|
|
2822
|
+
runAt: z5.string(),
|
|
2603
2823
|
status: TriageStatusSchema,
|
|
2604
|
-
model:
|
|
2824
|
+
model: z5.string().nullable().default(null)
|
|
2605
2825
|
});
|
|
2606
|
-
var AcceptTriageSchema =
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
group:
|
|
2611
|
-
agent:
|
|
2612
|
-
noteIds:
|
|
2826
|
+
var AcceptTriageSchema = z5.discriminatedUnion("group", [
|
|
2827
|
+
z5.object({ group: z5.literal("close"), noteIds: z5.array(z5.string()).max(200).optional() }),
|
|
2828
|
+
z5.object({ group: z5.literal("stale"), noteIds: z5.array(z5.string()).max(200).optional() }),
|
|
2829
|
+
z5.object({
|
|
2830
|
+
group: z5.literal("assign"),
|
|
2831
|
+
agent: z5.string().min(1),
|
|
2832
|
+
noteIds: z5.array(z5.string()).max(200).optional()
|
|
2613
2833
|
})
|
|
2614
2834
|
]);
|
|
2615
|
-
var AcceptTriageResultSchema =
|
|
2616
|
-
accepted:
|
|
2617
|
-
failed:
|
|
2835
|
+
var AcceptTriageResultSchema = z5.object({
|
|
2836
|
+
accepted: z5.array(z5.string()),
|
|
2837
|
+
failed: z5.array(z5.object({ noteId: z5.string(), reason: z5.string() }))
|
|
2618
2838
|
});
|
|
2619
|
-
var DeliveryModeSchema =
|
|
2839
|
+
var DeliveryModeSchema = z5.enum(["poll", "self_hosted"]);
|
|
2620
2840
|
var WAKE_EVENT = "wake";
|
|
2621
2841
|
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
2622
|
-
var RegisterDeliverySchema =
|
|
2623
|
-
var OAuthStartSchema =
|
|
2624
|
-
provider:
|
|
2625
|
-
returnTo:
|
|
2842
|
+
var RegisterDeliverySchema = z5.object({ mode: DeliveryModeSchema });
|
|
2843
|
+
var OAuthStartSchema = z5.object({
|
|
2844
|
+
provider: z5.enum(["cma"]),
|
|
2845
|
+
returnTo: z5.string().min(1)
|
|
2626
2846
|
});
|
|
2627
|
-
var DeliveryConfigSchema =
|
|
2628
|
-
tokenId:
|
|
2847
|
+
var DeliveryConfigSchema = z5.object({
|
|
2848
|
+
tokenId: z5.string(),
|
|
2629
2849
|
mode: DeliveryModeSchema,
|
|
2630
2850
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
2631
2851
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
2632
2852
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
2633
|
-
realtime:
|
|
2853
|
+
realtime: z5.object({ url: z5.string(), anonKey: z5.string() }).nullable()
|
|
2634
2854
|
});
|
|
2635
|
-
var HostDecisionSchema =
|
|
2855
|
+
var HostDecisionSchema = z5.object({
|
|
2636
2856
|
/** The agent's token id: the row's `recipient`. */
|
|
2637
|
-
agent:
|
|
2638
|
-
decision:
|
|
2857
|
+
agent: z5.string().uuid(),
|
|
2858
|
+
decision: z5.enum(["stood_back", "took_over"]),
|
|
2639
2859
|
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
2640
|
-
goalId:
|
|
2860
|
+
goalId: z5.string().uuid().nullable().optional(),
|
|
2641
2861
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
2642
|
-
seenAt:
|
|
2643
|
-
/** When that work last moved (`claimable.since` on `
|
|
2644
|
-
since:
|
|
2862
|
+
seenAt: z5.string().datetime().nullable().optional(),
|
|
2863
|
+
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
2864
|
+
since: z5.string().datetime().nullable().optional(),
|
|
2645
2865
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
2646
|
-
said:
|
|
2866
|
+
said: z5.string().max(300).optional()
|
|
2647
2867
|
});
|
|
2648
|
-
var WakeNudgeSchema =
|
|
2649
|
-
kind:
|
|
2650
|
-
notificationId:
|
|
2651
|
-
parentId:
|
|
2868
|
+
var WakeNudgeSchema = z5.object({
|
|
2869
|
+
kind: z5.enum(["reply", "request", "callback"]),
|
|
2870
|
+
notificationId: z5.string().optional(),
|
|
2871
|
+
parentId: z5.string()
|
|
2652
2872
|
});
|
|
2653
|
-
var PairingStatusSchema =
|
|
2654
|
-
var DeviceCodeSchema =
|
|
2655
|
-
device_code:
|
|
2656
|
-
user_code:
|
|
2657
|
-
verification_uri:
|
|
2658
|
-
verification_uri_complete:
|
|
2659
|
-
interval:
|
|
2660
|
-
expires_in:
|
|
2873
|
+
var PairingStatusSchema = z5.enum(["pending", "approved", "denied", "expired"]);
|
|
2874
|
+
var DeviceCodeSchema = z5.object({
|
|
2875
|
+
device_code: z5.string(),
|
|
2876
|
+
user_code: z5.string(),
|
|
2877
|
+
verification_uri: z5.string().url(),
|
|
2878
|
+
verification_uri_complete: z5.string().url(),
|
|
2879
|
+
interval: z5.number(),
|
|
2880
|
+
expires_in: z5.number()
|
|
2661
2881
|
});
|
|
2662
|
-
var DeviceInfoSchema =
|
|
2663
|
-
code:
|
|
2882
|
+
var DeviceInfoSchema = z5.object({
|
|
2883
|
+
code: z5.string(),
|
|
2664
2884
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
2665
2885
|
* pre-filling the name field the human can edit. */
|
|
2666
|
-
name:
|
|
2886
|
+
name: z5.string(),
|
|
2667
2887
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
2668
2888
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
2669
2889
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
2670
2890
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
2671
|
-
agent:
|
|
2672
|
-
device:
|
|
2891
|
+
agent: z5.string().optional(),
|
|
2892
|
+
device: z5.string().nullable(),
|
|
2673
2893
|
status: PairingStatusSchema
|
|
2674
2894
|
});
|
|
2675
|
-
var DeviceTokenSchema =
|
|
2676
|
-
access_token:
|
|
2895
|
+
var DeviceTokenSchema = z5.object({
|
|
2896
|
+
access_token: z5.string(),
|
|
2677
2897
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
2678
2898
|
* a default silly name). */
|
|
2679
|
-
name:
|
|
2680
|
-
device:
|
|
2899
|
+
name: z5.string(),
|
|
2900
|
+
device: z5.string().nullable(),
|
|
2681
2901
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
2682
2902
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
2683
|
-
voice:
|
|
2903
|
+
voice: z5.string().nullable().optional(),
|
|
2684
2904
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
2685
2905
|
* that survives a rename. Cached by the host's identity beat. */
|
|
2686
|
-
token_id:
|
|
2906
|
+
token_id: z5.string().nullable().optional(),
|
|
2687
2907
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
2688
2908
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
2689
2909
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
2690
2910
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
2691
|
-
workspace:
|
|
2911
|
+
workspace: z5.string().nullable().optional(),
|
|
2692
2912
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
2693
|
-
harness:
|
|
2694
|
-
session_id:
|
|
2695
|
-
|
|
2913
|
+
harness: z5.enum(["claude", "codex", "agy"]).optional(),
|
|
2914
|
+
session_id: z5.string().uuid().optional(),
|
|
2915
|
+
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
2916
|
+
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
2917
|
+
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
2918
|
+
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
2919
|
+
full_session: z5.string().optional(),
|
|
2920
|
+
uik_pub: z5.string().nullable().optional()
|
|
2696
2921
|
});
|
|
2697
|
-
var SupportRequestSchema =
|
|
2698
|
-
email:
|
|
2699
|
-
message:
|
|
2700
|
-
name:
|
|
2922
|
+
var SupportRequestSchema = z5.object({
|
|
2923
|
+
email: z5.string().email().max(320),
|
|
2924
|
+
message: z5.string().trim().min(1).max(5e3),
|
|
2925
|
+
name: z5.string().trim().max(120).optional()
|
|
2701
2926
|
});
|
|
2702
|
-
var NotificationFeedbackKindSchema =
|
|
2927
|
+
var NotificationFeedbackKindSchema = z5.enum([
|
|
2703
2928
|
"break_down",
|
|
2704
2929
|
// "This should be more than one ask — break it down."
|
|
2705
2930
|
"regenerate_options",
|
|
@@ -2714,116 +2939,116 @@ var NotificationFeedbackKindSchema = z4.enum([
|
|
|
2714
2939
|
// anything else — the note carries it.
|
|
2715
2940
|
]);
|
|
2716
2941
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
2717
|
-
var QuestionRowSchema =
|
|
2942
|
+
var QuestionRowSchema = z5.object({
|
|
2718
2943
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
2719
|
-
id:
|
|
2720
|
-
deliveryId:
|
|
2721
|
-
entryId:
|
|
2944
|
+
id: z5.string(),
|
|
2945
|
+
deliveryId: z5.string(),
|
|
2946
|
+
entryId: z5.string(),
|
|
2722
2947
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
2723
|
-
needId:
|
|
2724
|
-
goalIds:
|
|
2948
|
+
needId: z5.string().nullable(),
|
|
2949
|
+
goalIds: z5.array(z5.string()),
|
|
2725
2950
|
/** The name of the work it is about, when the read could word it. */
|
|
2726
|
-
goalTitle:
|
|
2727
|
-
tokenId:
|
|
2728
|
-
name:
|
|
2729
|
-
title:
|
|
2730
|
-
body:
|
|
2731
|
-
select:
|
|
2732
|
-
options:
|
|
2733
|
-
hasPreview:
|
|
2734
|
-
blocking:
|
|
2735
|
-
askedAt:
|
|
2951
|
+
goalTitle: z5.string().optional(),
|
|
2952
|
+
tokenId: z5.string().optional(),
|
|
2953
|
+
name: z5.string(),
|
|
2954
|
+
title: z5.string(),
|
|
2955
|
+
body: z5.string(),
|
|
2956
|
+
select: z5.enum(["one", "many", "rank", "confirm", "text"]),
|
|
2957
|
+
options: z5.array(SlimOptionSchema),
|
|
2958
|
+
hasPreview: z5.boolean(),
|
|
2959
|
+
blocking: z5.boolean(),
|
|
2960
|
+
askedAt: z5.string().datetime(),
|
|
2736
2961
|
ring: InboxItemSchema.shape.ring,
|
|
2737
|
-
onCall:
|
|
2738
|
-
sealed:
|
|
2962
|
+
onCall: z5.literal(true).optional(),
|
|
2963
|
+
sealed: z5.boolean()
|
|
2739
2964
|
});
|
|
2740
|
-
var WorkStateSchema =
|
|
2741
|
-
var WorkRowSchema =
|
|
2742
|
-
id:
|
|
2743
|
-
parentId:
|
|
2744
|
-
title:
|
|
2965
|
+
var WorkStateSchema = z5.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
2966
|
+
var WorkRowSchema = z5.object({
|
|
2967
|
+
id: z5.string(),
|
|
2968
|
+
parentId: z5.string().nullable(),
|
|
2969
|
+
title: z5.string(),
|
|
2745
2970
|
/** Straight off the Goal. */
|
|
2746
2971
|
state: WorkStateSchema,
|
|
2747
|
-
owner:
|
|
2748
|
-
revision:
|
|
2972
|
+
owner: z5.string().nullable(),
|
|
2973
|
+
revision: z5.number().int(),
|
|
2749
2974
|
/** Open questions on it, counted to 100. */
|
|
2750
|
-
waiting:
|
|
2975
|
+
waiting: z5.number().int(),
|
|
2751
2976
|
/** Held by a gate on work that is not done. */
|
|
2752
|
-
blocked:
|
|
2753
|
-
lastProgressAt:
|
|
2977
|
+
blocked: z5.boolean(),
|
|
2978
|
+
lastProgressAt: z5.string().datetime().nullable(),
|
|
2754
2979
|
/** The line written for its newest progress note, else that note's first words. */
|
|
2755
|
-
line:
|
|
2980
|
+
line: z5.string().nullable(),
|
|
2756
2981
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
2757
|
-
children:
|
|
2758
|
-
createdAt:
|
|
2759
|
-
updatedAt:
|
|
2982
|
+
children: z5.number().int(),
|
|
2983
|
+
createdAt: z5.string().datetime(),
|
|
2984
|
+
updatedAt: z5.string().datetime(),
|
|
2760
2985
|
/** When anything at or under it last moved — the order the list is in. */
|
|
2761
|
-
activeAt:
|
|
2986
|
+
activeAt: z5.string().datetime(),
|
|
2762
2987
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
2763
|
-
sealed:
|
|
2988
|
+
sealed: z5.boolean()
|
|
2764
2989
|
});
|
|
2765
2990
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
2766
2991
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
2767
2992
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
2768
|
-
asking:
|
|
2769
|
-
oldestAskAt:
|
|
2993
|
+
asking: z5.number().int().nullable(),
|
|
2994
|
+
oldestAskAt: z5.string().datetime().nullable(),
|
|
2770
2995
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
2771
2996
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
2772
2997
|
* (`agent_holds`); null when that read failed. */
|
|
2773
|
-
holds:
|
|
2774
|
-
held:
|
|
2998
|
+
holds: z5.array(z5.object({ id: z5.string(), title: z5.string() })).nullable(),
|
|
2999
|
+
held: z5.number().int().nullable(),
|
|
2775
3000
|
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
2776
3001
|
* while none has, or when that read failed. */
|
|
2777
|
-
cold:
|
|
3002
|
+
cold: z5.string().datetime().nullable(),
|
|
2778
3003
|
/** The newest line of its working log, and when the harness saw it. */
|
|
2779
|
-
line:
|
|
2780
|
-
lineAt:
|
|
3004
|
+
line: z5.string().nullable(),
|
|
3005
|
+
lineAt: z5.string().datetime().nullable()
|
|
2781
3006
|
});
|
|
2782
|
-
var SnapshotSchema =
|
|
3007
|
+
var SnapshotSchema = z5.object({
|
|
2783
3008
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
2784
|
-
at:
|
|
2785
|
-
questions:
|
|
3009
|
+
at: z5.string().datetime(),
|
|
3010
|
+
questions: z5.object({
|
|
2786
3011
|
/** The newest 30 open cards, questions before updates. */
|
|
2787
|
-
items:
|
|
3012
|
+
items: z5.array(QuestionRowSchema),
|
|
2788
3013
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
2789
|
-
total:
|
|
2790
|
-
updates:
|
|
2791
|
-
putOff:
|
|
3014
|
+
total: z5.number().int(),
|
|
3015
|
+
updates: z5.number().int(),
|
|
3016
|
+
putOff: z5.number().int()
|
|
2792
3017
|
}).nullable(),
|
|
2793
|
-
agents:
|
|
3018
|
+
agents: z5.object({
|
|
2794
3019
|
/** Up to 60, most recently seen first. */
|
|
2795
|
-
items:
|
|
2796
|
-
more:
|
|
3020
|
+
items: z5.array(AgentRowSchema),
|
|
3021
|
+
more: z5.boolean()
|
|
2797
3022
|
}).nullable(),
|
|
2798
|
-
work:
|
|
3023
|
+
work: z5.object({
|
|
2799
3024
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
2800
|
-
items:
|
|
3025
|
+
items: z5.array(WorkRowSchema),
|
|
2801
3026
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
2802
3027
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
2803
3028
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
2804
3029
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
2805
|
-
counts:
|
|
3030
|
+
counts: z5.object({ live: z5.number().int(), waiting: z5.number().int(), notStarted: z5.number().int(), done: z5.number().int() })
|
|
2806
3031
|
}).nullable(),
|
|
2807
|
-
you:
|
|
3032
|
+
you: z5.object({
|
|
2808
3033
|
settings: UserSettingsSchema,
|
|
2809
|
-
callable:
|
|
3034
|
+
callable: z5.boolean(),
|
|
2810
3035
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
2811
|
-
computers:
|
|
3036
|
+
computers: z5.array(ComputerRowSchema).nullable()
|
|
2812
3037
|
}).nullable()
|
|
2813
3038
|
});
|
|
2814
|
-
var CallRecapSchema =
|
|
2815
|
-
call:
|
|
2816
|
-
status:
|
|
2817
|
-
startedAt:
|
|
2818
|
-
durationMs:
|
|
2819
|
-
agents:
|
|
3039
|
+
var CallRecapSchema = z5.object({
|
|
3040
|
+
call: z5.object({
|
|
3041
|
+
status: z5.string(),
|
|
3042
|
+
startedAt: z5.string(),
|
|
3043
|
+
durationMs: z5.number().nullable(),
|
|
3044
|
+
agents: z5.array(z5.object({ id: z5.string(), name: z5.string().nullable() }))
|
|
2820
3045
|
}),
|
|
2821
|
-
topics:
|
|
2822
|
-
goalId:
|
|
2823
|
-
title:
|
|
2824
|
-
owner:
|
|
2825
|
-
state:
|
|
2826
|
-
questions:
|
|
3046
|
+
topics: z5.array(z5.object({
|
|
3047
|
+
goalId: z5.string().uuid(),
|
|
3048
|
+
title: z5.string(),
|
|
3049
|
+
owner: z5.string(),
|
|
3050
|
+
state: z5.string(),
|
|
3051
|
+
questions: z5.array(z5.object({ id: z5.string().uuid(), state: z5.string(), title: z5.string() })),
|
|
2827
3052
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
2828
3053
|
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
2829
3054
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
@@ -2831,14 +3056,14 @@ var CallRecapSchema = z4.object({
|
|
|
2831
3056
|
* anything that is not an answer. */
|
|
2832
3057
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
2833
3058
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
2834
|
-
lines:
|
|
3059
|
+
lines: z5.array(z5.object({ entryId: z5.string().uuid(), words: z5.string(), headline: z5.string().optional(), about: z5.string().nullable().optional() }))
|
|
2835
3060
|
})),
|
|
2836
|
-
unfiled:
|
|
2837
|
-
more:
|
|
3061
|
+
unfiled: z5.array(z5.object({ lineId: z5.string().uuid(), words: z5.string(), atMs: z5.number() })),
|
|
3062
|
+
more: z5.object({ lines: z5.number(), entries: z5.number(), topics: z5.number() })
|
|
2838
3063
|
});
|
|
2839
3064
|
function sessionSlot(sessionId2) {
|
|
2840
|
-
const
|
|
2841
|
-
return `session:${
|
|
3065
|
+
const id2 = sessionId2 ?? sessionId();
|
|
3066
|
+
return `session:${id2.slice(0, 8)}`;
|
|
2842
3067
|
}
|
|
2843
3068
|
function agentName() {
|
|
2844
3069
|
return process.env.PAIGY_AGENT || sessionSlot();
|
|
@@ -2896,7 +3121,7 @@ function updateSlot(agent2, patch) {
|
|
|
2896
3121
|
}
|
|
2897
3122
|
function slotIdentity(agent2) {
|
|
2898
3123
|
const t = readTokenFile()[agent2];
|
|
2899
|
-
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null };
|
|
3124
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null, fullSession: t?.full_session ?? null };
|
|
2900
3125
|
}
|
|
2901
3126
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2902
3127
|
function readToken(agent2 = agentName()) {
|
|
@@ -3002,28 +3227,28 @@ var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
|
3002
3227
|
async function fail(what, res) {
|
|
3003
3228
|
throw new ApiError(what, res.status, await res.text());
|
|
3004
3229
|
}
|
|
3005
|
-
async function
|
|
3230
|
+
async function manageGoals(input, opts = {}) {
|
|
3006
3231
|
const token = authToken(opts.token) ?? "";
|
|
3007
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
3232
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/manage`, {
|
|
3008
3233
|
method: "POST",
|
|
3009
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}
|
|
3234
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3010
3235
|
body: JSON.stringify(input)
|
|
3011
3236
|
}));
|
|
3012
|
-
if (!res.ok) await fail("
|
|
3237
|
+
if (!res.ok) await fail("manage_goals", res);
|
|
3013
3238
|
return await res.json();
|
|
3014
3239
|
}
|
|
3015
3240
|
async function claimGoal(goalId, opts = {}) {
|
|
3016
3241
|
const token = authToken(opts.token) ?? "";
|
|
3017
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}
|
|
3242
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
3018
3243
|
if (!res.ok) await fail("claim_goal", res);
|
|
3019
3244
|
return await res.json();
|
|
3020
3245
|
}
|
|
3021
|
-
async function getGoal(goalId, opts = {},
|
|
3246
|
+
async function getGoal(goalId, opts = {}, read3 = {}) {
|
|
3022
3247
|
const token = authToken(opts.token) ?? "";
|
|
3023
|
-
const query = [
|
|
3248
|
+
const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
3024
3249
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
|
|
3025
3250
|
method: "GET",
|
|
3026
|
-
headers: { authorization: `Bearer ${token}
|
|
3251
|
+
headers: { authorization: `Bearer ${token}` }
|
|
3027
3252
|
}));
|
|
3028
3253
|
if (!res.ok) await fail("get_goal", res);
|
|
3029
3254
|
return await res.json();
|
|
@@ -3031,7 +3256,7 @@ async function getGoal(goalId, opts = {}, read = {}) {
|
|
|
3031
3256
|
async function updateGoal(goalId, input, opts = {}) {
|
|
3032
3257
|
const token = authToken(opts.token) ?? "";
|
|
3033
3258
|
const operationId = input.operationId ?? randomUUID2();
|
|
3034
|
-
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}
|
|
3259
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ ...input, operationId }) }));
|
|
3035
3260
|
if (!res.ok) await fail("update_goal", res);
|
|
3036
3261
|
return await res.json();
|
|
3037
3262
|
}
|
|
@@ -3118,7 +3343,9 @@ async function recordDecision(decision, opts = {}) {
|
|
|
3118
3343
|
async function heartbeat(runtime, opts = {}) {
|
|
3119
3344
|
const body = {
|
|
3120
3345
|
...runtime !== void 0 ? { runtime } : {},
|
|
3121
|
-
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
3346
|
+
...opts.activity !== void 0 ? { activity: opts.activity } : {},
|
|
3347
|
+
// A listener's beat (`paigy-listen`): stamps when it last swept, and is not presence.
|
|
3348
|
+
...opts.listening ? { listening: true } : {}
|
|
3122
3349
|
};
|
|
3123
3350
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
3124
3351
|
method: "POST",
|
|
@@ -3145,7 +3372,7 @@ async function listNotes(opts = {}) {
|
|
|
3145
3372
|
}
|
|
3146
3373
|
async function listConnections(opts = {}) {
|
|
3147
3374
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
3148
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}
|
|
3375
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
3149
3376
|
}));
|
|
3150
3377
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
3151
3378
|
return await res.json();
|
|
@@ -3183,9 +3410,18 @@ async function dismissTriage(proposalId, opts = {}) {
|
|
|
3183
3410
|
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
3184
3411
|
return await res.json();
|
|
3185
3412
|
}
|
|
3413
|
+
async function searchRecords(input, opts = {}) {
|
|
3414
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/records/search`, {
|
|
3415
|
+
method: "POST",
|
|
3416
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3417
|
+
body: JSON.stringify(input)
|
|
3418
|
+
}));
|
|
3419
|
+
if (!res.ok) await fail("search", res);
|
|
3420
|
+
return await res.json();
|
|
3421
|
+
}
|
|
3186
3422
|
async function whoIsWorking(opts = {}) {
|
|
3187
3423
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
|
|
3188
|
-
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}
|
|
3424
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3189
3425
|
}));
|
|
3190
3426
|
if (!res.ok) await fail("who_is_working", res);
|
|
3191
3427
|
return await res.json();
|
|
@@ -3216,15 +3452,18 @@ function currentRepo(cwd = process.cwd()) {
|
|
|
3216
3452
|
}
|
|
3217
3453
|
return asked;
|
|
3218
3454
|
}
|
|
3219
|
-
var NOT_SENT = "Recorded as progress, not sent: nobody was notified.
|
|
3455
|
+
var NOT_SENT = "Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, mark the Goal done first, then send the one message saying what is done and anything they need to do or check.";
|
|
3456
|
+
var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again, with or without a Goal.";
|
|
3220
3457
|
async function contact(input, opts = {}) {
|
|
3221
3458
|
const parsed = ContactSchema.parse(input);
|
|
3459
|
+
if (!("asks" in parsed) && !("deliveryId" in parsed)) throw new Error("contact with nothing to send receives: call receiveContact");
|
|
3222
3460
|
opts.signal?.throwIfAborted();
|
|
3223
3461
|
const send2 = opts.reach ?? reach;
|
|
3224
|
-
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}
|
|
3462
|
+
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` };
|
|
3225
3463
|
let deliveryId;
|
|
3226
3464
|
let deliveries = [];
|
|
3227
3465
|
let recorded = [];
|
|
3466
|
+
let demoted;
|
|
3228
3467
|
if ("deliveryId" in parsed) {
|
|
3229
3468
|
deliveryId = parsed.deliveryId;
|
|
3230
3469
|
} else {
|
|
@@ -3255,16 +3494,17 @@ async function contact(input, opts = {}) {
|
|
|
3255
3494
|
if (!res.ok) await fail("batch contact", res);
|
|
3256
3495
|
const receipt = await res.json();
|
|
3257
3496
|
deliveries = receipt.deliveries ?? [];
|
|
3497
|
+
if (parsed.channel === "call" && receipt.channel && receipt.channel !== "call") demoted = DEMOTED;
|
|
3258
3498
|
recorded = receipt.recorded ?? [];
|
|
3259
3499
|
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
3260
3500
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
3261
3501
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
3262
3502
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
3263
3503
|
}
|
|
3264
|
-
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...
|
|
3504
|
+
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id: id2, deliveryId: deliveryId2, goalId }) => ({ ...id2 ? { id: id2 } : {}, deliveryId: deliveryId2, goalId }));
|
|
3265
3505
|
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
3266
3506
|
const joinedCall = deliveries[0]?.joinedCall === true;
|
|
3267
|
-
const
|
|
3507
|
+
const read3 = async (signal2) => {
|
|
3268
3508
|
const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
3269
3509
|
if (!res.ok) await fail("read_delivery", res);
|
|
3270
3510
|
const one = await res.json();
|
|
@@ -3273,18 +3513,19 @@ async function contact(input, opts = {}) {
|
|
|
3273
3513
|
...all.length > 1 ? { deliveries: all } : {},
|
|
3274
3514
|
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
3275
3515
|
...joinedCard ? { joinedCard: true } : {},
|
|
3276
|
-
...joinedCall ? { joinedCall: true } : {}
|
|
3516
|
+
...joinedCall ? { joinedCall: true } : {},
|
|
3517
|
+
...demoted ? { demoted } : {}
|
|
3277
3518
|
};
|
|
3278
3519
|
};
|
|
3279
3520
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
3280
|
-
if (opts.waits === false) return
|
|
3521
|
+
if (opts.waits === false) return read3(opts.signal);
|
|
3281
3522
|
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
3282
3523
|
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
3283
3524
|
let latest;
|
|
3284
3525
|
try {
|
|
3285
3526
|
while (true) {
|
|
3286
3527
|
signal.throwIfAborted();
|
|
3287
|
-
latest = await
|
|
3528
|
+
latest = await read3(signal);
|
|
3288
3529
|
if (settled(latest)) return latest;
|
|
3289
3530
|
await sleep2(5e3, void 0, { signal });
|
|
3290
3531
|
}
|
|
@@ -3294,17 +3535,73 @@ async function contact(input, opts = {}) {
|
|
|
3294
3535
|
throw error;
|
|
3295
3536
|
}
|
|
3296
3537
|
}
|
|
3297
|
-
async function
|
|
3538
|
+
async function receive(opts = {}) {
|
|
3298
3539
|
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
3299
|
-
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}
|
|
3540
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
|
|
3300
3541
|
}));
|
|
3301
|
-
if (!res.ok) await fail("
|
|
3542
|
+
if (!res.ok) await fail("receive", res);
|
|
3302
3543
|
return await res.json();
|
|
3303
3544
|
}
|
|
3545
|
+
async function acknowledge(eventIds, opts = {}) {
|
|
3546
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries/ack`, {
|
|
3547
|
+
method: "POST",
|
|
3548
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
|
|
3549
|
+
body: JSON.stringify({ eventIds })
|
|
3550
|
+
}));
|
|
3551
|
+
if (!res.ok) await fail("acknowledge", res);
|
|
3552
|
+
return (await res.json()).results ?? [];
|
|
3553
|
+
}
|
|
3554
|
+
var EVENT_BATCH = 20;
|
|
3555
|
+
function pendingEvents(w) {
|
|
3556
|
+
const fromDelivery = (d) => ({
|
|
3557
|
+
eventId: d.deliveryId,
|
|
3558
|
+
kind: d.questions?.length ? "question" : "instruction",
|
|
3559
|
+
...d.goalIds.length ? { goalIds: d.goalIds } : {},
|
|
3560
|
+
...d.about ? { text: d.about } : {},
|
|
3561
|
+
...d.questions?.length ? { questions: d.questions } : {}
|
|
3562
|
+
});
|
|
3563
|
+
const deliveries = (w.deliveries ?? []).map(fromDelivery);
|
|
3564
|
+
return [
|
|
3565
|
+
...deliveries.filter((e) => e.kind === "question"),
|
|
3566
|
+
...(w.review ?? []).map((r) => ({
|
|
3567
|
+
eventId: r.eventId ?? r.goalId,
|
|
3568
|
+
kind: "update",
|
|
3569
|
+
goalIds: [r.goalId],
|
|
3570
|
+
text: r.said ? `${r.title}: ${r.said}` : r.title,
|
|
3571
|
+
...r.saidAt ? { at: r.saidAt } : {}
|
|
3572
|
+
})),
|
|
3573
|
+
...deliveries.filter((e) => e.kind !== "question")
|
|
3574
|
+
];
|
|
3575
|
+
}
|
|
3576
|
+
async function receiveContact(input, opts = {}) {
|
|
3577
|
+
opts.signal?.throwIfAborted();
|
|
3578
|
+
const acknowledged = input.ackEventIds?.length ? await acknowledge(input.ackEventIds, opts) : void 0;
|
|
3579
|
+
const shaped = (waiting, waitOutcome) => {
|
|
3580
|
+
const all = pendingEvents(waiting);
|
|
3581
|
+
return { waiting, events: all.slice(0, EVENT_BATCH), hasMore: all.length > EVENT_BATCH, waitOutcome, ...acknowledged ? { acknowledged } : {} };
|
|
3582
|
+
};
|
|
3583
|
+
const wait = (input.wait ?? !input.ackEventIds?.length) && opts.waits !== false;
|
|
3584
|
+
if (!wait) return shaped(await receive(opts), "not_waited");
|
|
3585
|
+
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
3586
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
3587
|
+
let latest;
|
|
3588
|
+
try {
|
|
3589
|
+
while (true) {
|
|
3590
|
+
signal.throwIfAborted();
|
|
3591
|
+
latest = await receive({ ...opts });
|
|
3592
|
+
if (pendingEvents(latest).length) return shaped(latest, "available");
|
|
3593
|
+
await sleep2(5e3, void 0, { signal });
|
|
3594
|
+
}
|
|
3595
|
+
} catch (error) {
|
|
3596
|
+
opts.signal?.throwIfAborted();
|
|
3597
|
+
if (window.aborted && latest) return shaped(latest, "expired");
|
|
3598
|
+
throw error;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3304
3601
|
function compact(o) {
|
|
3305
3602
|
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== null && v !== "" && !(Array.isArray(v) && v.length === 0)));
|
|
3306
3603
|
}
|
|
3307
|
-
var who = (
|
|
3604
|
+
var who = (participant2) => participant2.startsWith("human:") ? "person" : isPaigy(participant2) ? "paigy" : participant2.startsWith("agent:") ? "agent" : participant2;
|
|
3308
3605
|
var at = (iso) => {
|
|
3309
3606
|
const t = Date.parse(iso);
|
|
3310
3607
|
return Number.isFinite(t) ? new Date(t).toISOString().replace(/\.\d{3}Z$/, "Z") : iso;
|
|
@@ -3357,14 +3654,14 @@ function more(g) {
|
|
|
3357
3654
|
earlier,
|
|
3358
3655
|
g.subtree?.length ? "A Goal below, in full: get_goal({ goalId }) with its goalId above." : null,
|
|
3359
3656
|
g.parentGoalId ? `The Goal above: get_goal({ goalId: "${g.parentGoalId}" }).` : null,
|
|
3360
|
-
"Anything else
|
|
3657
|
+
"Anything else addressed to you (requests, answers, questions you owe): contact({ wait: false })."
|
|
3361
3658
|
].filter((line) => !!line);
|
|
3362
3659
|
}
|
|
3363
3660
|
function goalView(g) {
|
|
3364
3661
|
if (!g.goalId) return compact({ state: g.state, next: g.message });
|
|
3365
3662
|
const lines2 = conversation(g);
|
|
3366
3663
|
const owed = lines2.filter((l) => l.from === "person" && l.decision?.state === "open" && l.id);
|
|
3367
|
-
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({ asks: [{ ask: <your answer>,
|
|
3664
|
+
const owes = owed.length ? `The person asked you ${owed.length === 1 ? "a question" : `${owed.length} questions`} (${owed.map((l) => `id ${l.id}: "${l.said.slice(0, 80)}"`).join("; ")}) \u2014 answer with contact({ asks: [{ ask: <your answer>, answers: "<id>" }] }). ` : "";
|
|
3368
3665
|
const newest = [...lines2].reverse().find((l) => l.from === "agent");
|
|
3369
3666
|
return compact({
|
|
3370
3667
|
goalId: g.goalId,
|
|
@@ -3411,93 +3708,106 @@ function deliveryView(d) {
|
|
|
3411
3708
|
notSent: d.notSent,
|
|
3412
3709
|
joinedCard: d.joinedCard,
|
|
3413
3710
|
joinedCall: d.joinedCall,
|
|
3414
|
-
next: d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers : d.state === "open" && d.decisionNeeds.some((n) => n.state === "open") ? `Decision pending. Call contact(${JSON.stringify({ deliveryId: d.deliveryId })}) to continue this same Call \u2014 each call is ONE bounded window, and never resend the ask. When a window comes back with nothing new said, they are not typing: stop rereading, leave the question open, and collect the answer with
|
|
3711
|
+
next: 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}` : d.state === "open" && d.decisionNeeds.some((n) => n.state === "open") ? `Decision pending. Call contact(${JSON.stringify({ deliveryId: d.deliveryId })}) to continue this same Call \u2014 each call is ONE bounded window, and never resend the ask. When a window comes back with nothing new said, they are not typing: stop rereading, leave the question open, and collect the answer with contact({ wait: false }) or claim_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3415
3712
|
});
|
|
3416
3713
|
}
|
|
3417
|
-
function
|
|
3418
|
-
const
|
|
3419
|
-
const assigned =
|
|
3420
|
-
const
|
|
3421
|
-
const
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
//
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
//
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
// NEW ON YOUR GOALS (owner, 2026-09-23): a reply is an Entry on the Goal, not a Delivery, so the
|
|
3439
|
-
// Goals flagged for your review are listed here until you acknowledge them.
|
|
3440
|
-
...review.length ? { review } : {},
|
|
3441
|
-
// YOUR STALLED WORK (#2257): Goals you own with no progress for three days. Named here so an
|
|
3442
|
-
// agent that reads its replies also sees what it left, and says what to do about each.
|
|
3443
|
-
...stalled.length ? { stalled } : {},
|
|
3444
|
-
// OTHER AGENTS' STALLED WORK (owner, 2026-09-23): any agent may take it over and close it.
|
|
3445
|
-
...others.length ? { stalledOthers: others } : {},
|
|
3714
|
+
function receivedView(r) {
|
|
3715
|
+
const w = r.waiting;
|
|
3716
|
+
const assigned = w.assigned ?? [];
|
|
3717
|
+
const stalled = w.stalled ?? [];
|
|
3718
|
+
const others = w.stalledOthers ?? [];
|
|
3719
|
+
const owed = r.events.filter((e) => e.kind === "question");
|
|
3720
|
+
const refused = (r.acknowledged ?? []).filter((a) => a.refused);
|
|
3721
|
+
return compact({
|
|
3722
|
+
events: r.events.map((e) => compact({ ...e })),
|
|
3723
|
+
hasMore: r.hasMore || void 0,
|
|
3724
|
+
waitOutcome: r.waitOutcome,
|
|
3725
|
+
acknowledged: r.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
|
|
3726
|
+
notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
|
|
3727
|
+
// WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
|
|
3728
|
+
assigned,
|
|
3729
|
+
// WHAT claim_goal HANDS YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
|
|
3730
|
+
// sleeping agent, so "nothing is waiting" is never said while a claim would hand you work.
|
|
3731
|
+
claimable: w.claimable,
|
|
3732
|
+
// YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
|
|
3733
|
+
stalled,
|
|
3734
|
+
stalledOthers: others,
|
|
3446
3735
|
next: [
|
|
3447
|
-
|
|
3736
|
+
r.events.length ? `Handle each event, then acknowledge the ones you handled: contact({ ackEventIds: [${r.events.filter((e) => e.kind !== "question").map((e) => `"${e.eventId}"`).join(", ")}] }).` : w.claimable || assigned.length ? "No messages." : "Nothing is waiting.",
|
|
3737
|
+
...owed.length ? [`You owe ${owed.length === 1 ? "an answer" : `${owed.length} answers`}: contact({ asks: [{ ask: <your answer>, answers: "<questionId>" }] }) for each question; a question is answered, never acknowledged.`] : [],
|
|
3738
|
+
...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
|
|
3739
|
+
...r.waitOutcome === "expired" ? ["Nothing arrived in the window; receive again to keep waiting, without sending again."] : [],
|
|
3448
3740
|
...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): claim_goal({goalId}) to start one.`] : [],
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
// sentence names the person and quotes the freshest one, because a count is not a message.
|
|
3453
|
-
...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.`] : [],
|
|
3454
|
-
...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
|
|
3455
|
-
...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.`] : [],
|
|
3741
|
+
...w.claimable && !assigned.length ? [`claim_goal() hands you ${w.claimable.title ?? w.claimable.goalId} next.`] : [],
|
|
3742
|
+
...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, or finish or cancel it with manage_goals.`] : [],
|
|
3743
|
+
...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) joins one without changing its owner.`] : [],
|
|
3456
3744
|
// No name of its own (#2525): the API's ask, verbatim.
|
|
3457
|
-
...
|
|
3745
|
+
...w.unnamed ? [w.unnamed] : []
|
|
3458
3746
|
].join(" ")
|
|
3459
|
-
};
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
function managedView(m, changes) {
|
|
3750
|
+
const results = m.results ?? [];
|
|
3751
|
+
const failed = results.filter((r) => r.status !== "applied");
|
|
3752
|
+
return compact({
|
|
3753
|
+
ok: m.ok,
|
|
3754
|
+
results: results.map((r) => compact({ index: r.index, kind: changes[r.index]?.kind, status: r.status, goalId: r.goalId, error: r.error })),
|
|
3755
|
+
goals: m.goals,
|
|
3756
|
+
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
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
var lastRead = /* @__PURE__ */ new Map();
|
|
3760
|
+
var readKey = (token, goalId) => `${token ?? ""}:${goalId}`;
|
|
3761
|
+
function remember(token, goalId, revision) {
|
|
3762
|
+
if (revision === void 0) return;
|
|
3763
|
+
if (lastRead.size >= 5e3) lastRead.clear();
|
|
3764
|
+
lastRead.set(readKey(token, goalId), revision);
|
|
3460
3765
|
}
|
|
3461
|
-
function
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
const newest = withWords[0];
|
|
3465
|
-
const rest = review.length - 1;
|
|
3466
|
-
return `The person has spoken on ${withWords.length === 1 ? "one of your Goals" : `${withWords.length} of your Goals`} \u2014 newest, on ${newest.title ?? newest.goalId}: "${newest.said}". Read it with claim_goal or get_goal, answer what it asks, then update_goal with reviewed: true.${rest > 0 ? ` (${rest} more flagged; see review.)` : ""}`;
|
|
3766
|
+
function read2(token, goal) {
|
|
3767
|
+
if (goal.goalId) remember(token, goal.goalId, goal.revision);
|
|
3768
|
+
return goal;
|
|
3467
3769
|
}
|
|
3468
3770
|
async function runTool(name, args, opts) {
|
|
3469
3771
|
const { waits, signal, ...client } = opts;
|
|
3470
3772
|
const input = args ?? {};
|
|
3471
3773
|
switch (name) {
|
|
3472
3774
|
case "contact": {
|
|
3473
|
-
const
|
|
3775
|
+
const parsed = ContactSchema.parse(input);
|
|
3776
|
+
if (!("asks" in parsed) && !("deliveryId" in parsed)) return receivedView(await receiveContact(parsed, { ...client, signal, waits }));
|
|
3777
|
+
const sent = await contact(parsed, { ...client, signal, waits });
|
|
3474
3778
|
return "sent" in sent ? sent : deliveryView(sent);
|
|
3475
3779
|
}
|
|
3476
3780
|
case "who_is_working":
|
|
3477
|
-
|
|
3781
|
+
WhoIsWorkingSchema.parse(input);
|
|
3478
3782
|
return whoIsWorking(client);
|
|
3479
|
-
case "
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3783
|
+
case "manage_goals": {
|
|
3784
|
+
const { changes } = ManageGoalsToolSchema.parse(input);
|
|
3785
|
+
const versioned = changes.map((c) => {
|
|
3786
|
+
if (c.kind === "create" || c.kind === "dependency") return c;
|
|
3787
|
+
const revision = lastRead.get(readKey(client.token, c.goalId));
|
|
3788
|
+
return revision === void 0 ? c : { ...c, revision };
|
|
3789
|
+
});
|
|
3790
|
+
const managed = await manageGoals({ requestId: randomUUID4(), changes: versioned }, client);
|
|
3791
|
+
for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
|
|
3792
|
+
return managedView(managed, changes);
|
|
3485
3793
|
}
|
|
3486
3794
|
case "claim_goal": {
|
|
3487
3795
|
const { goalId } = ClaimGoalSchema.parse(input);
|
|
3488
|
-
return goalView(await claimGoal(goalId, client));
|
|
3796
|
+
return goalView(read2(client.token, await claimGoal(goalId, client)));
|
|
3489
3797
|
}
|
|
3490
3798
|
case "get_goal": {
|
|
3491
3799
|
const { goalId, history, diagnose } = GetGoalSchema.parse(input);
|
|
3492
|
-
const goal = await getGoal(goalId, client, { history, diagnose });
|
|
3800
|
+
const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
|
|
3493
3801
|
const view = goalView(goal);
|
|
3494
3802
|
const diagnosis = goal.diagnosis;
|
|
3495
3803
|
return diagnosis === void 0 ? view : { ...view, diagnosis };
|
|
3496
3804
|
}
|
|
3497
3805
|
case "update_goal": {
|
|
3498
3806
|
const { goalId, ...body } = UpdateGoalToolSchema.parse(input);
|
|
3499
|
-
return goalView(await updateGoal(goalId, body, client));
|
|
3807
|
+
return goalView(read2(client.token, await updateGoal(goalId, body, client)));
|
|
3500
3808
|
}
|
|
3809
|
+
case "search":
|
|
3810
|
+
return searchRecords(SearchToolSchema.parse(input), client);
|
|
3501
3811
|
default:
|
|
3502
3812
|
throw new Error(`Unknown tool: ${name}`);
|
|
3503
3813
|
}
|
|
@@ -3611,9 +3921,9 @@ function connect(line) {
|
|
|
3611
3921
|
ws.addEventListener("error", again);
|
|
3612
3922
|
}
|
|
3613
3923
|
function subscribeRealtime(args) {
|
|
3614
|
-
const
|
|
3615
|
-
let line = lines.get(
|
|
3616
|
-
if (!line) lines.set(
|
|
3924
|
+
const key2 = `${args.url} ${args.anonKey}`;
|
|
3925
|
+
let line = lines.get(key2);
|
|
3926
|
+
if (!line) lines.set(key2, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
3617
3927
|
let t = line.topics.get(args.topic);
|
|
3618
3928
|
if (!t) {
|
|
3619
3929
|
line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
|
|
@@ -3635,7 +3945,7 @@ function subscribeRealtime(args) {
|
|
|
3635
3945
|
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
3636
3946
|
return;
|
|
3637
3947
|
}
|
|
3638
|
-
lines.delete(
|
|
3948
|
+
lines.delete(key2);
|
|
3639
3949
|
stop(line);
|
|
3640
3950
|
try {
|
|
3641
3951
|
line.ws?.close();
|
|
@@ -3679,6 +3989,28 @@ function machineZone() {
|
|
|
3679
3989
|
return null;
|
|
3680
3990
|
}
|
|
3681
3991
|
}
|
|
3992
|
+
function withCodexEnv(text) {
|
|
3993
|
+
const head = /^\[mcp_servers\.paigy\][ \t]*(?:#[^\n]*)?\r?\n/m.exec(text);
|
|
3994
|
+
if (!head) return null;
|
|
3995
|
+
const start = head.index + head[0].length;
|
|
3996
|
+
const next = /^\[/m.exec(text.slice(start));
|
|
3997
|
+
const end = next ? start + next.index : text.length;
|
|
3998
|
+
const block = text.slice(start, end);
|
|
3999
|
+
const assignment = /^[ \t]*env_vars[ \t]*=/m.exec(block);
|
|
4000
|
+
let updated;
|
|
4001
|
+
if (!assignment) updated = `env_vars = [${CODEX_ENV.map((v) => JSON.stringify(v)).join(", ")}]
|
|
4002
|
+
${block}`;
|
|
4003
|
+
else {
|
|
4004
|
+
const array = /^[ \t]*env_vars[ \t]*=[ \t]*\[((?:#[^\n]*|[^\]"'#]|"[^"\n]*"|'[^'\n]*')*)\]/m.exec(block);
|
|
4005
|
+
if (!array) return null;
|
|
4006
|
+
const existing = [...array[1].matchAll(/"([^"\n]*)"|'([^'\n]*)'|#[^\n]*/g)].flatMap((m) => m[0].startsWith("#") ? [] : [m[1] ?? m[2]]);
|
|
4007
|
+
const missing = CODEX_ENV.filter((v) => !existing.includes(v));
|
|
4008
|
+
if (!missing.length) return text;
|
|
4009
|
+
const opening = array.index + array[0].indexOf("[") + 1;
|
|
4010
|
+
updated = block.slice(0, opening) + missing.map((v) => JSON.stringify(v)).join(", ") + (existing.length ? ", " : "") + block.slice(opening);
|
|
4011
|
+
}
|
|
4012
|
+
return text.slice(0, start) + updated + text.slice(end);
|
|
4013
|
+
}
|
|
3682
4014
|
|
|
3683
4015
|
export {
|
|
3684
4016
|
CODEX_ENV,
|
|
@@ -3688,6 +4020,7 @@ export {
|
|
|
3688
4020
|
isNetworkError,
|
|
3689
4021
|
reach,
|
|
3690
4022
|
reachAs,
|
|
4023
|
+
setClient,
|
|
3691
4024
|
AGENT_TOOLS,
|
|
3692
4025
|
AGENT_TOOL_NAMES,
|
|
3693
4026
|
NotifyRequestSchema,
|
|
@@ -3713,7 +4046,7 @@ export {
|
|
|
3713
4046
|
ApiError,
|
|
3714
4047
|
overrideToken,
|
|
3715
4048
|
authToken,
|
|
3716
|
-
|
|
4049
|
+
manageGoals,
|
|
3717
4050
|
claimGoal,
|
|
3718
4051
|
getGoal,
|
|
3719
4052
|
updateGoal,
|
|
@@ -3734,12 +4067,17 @@ export {
|
|
|
3734
4067
|
getTriage,
|
|
3735
4068
|
acceptTriage,
|
|
3736
4069
|
dismissTriage,
|
|
4070
|
+
searchRecords,
|
|
3737
4071
|
whoIsWorking,
|
|
3738
4072
|
repoFromRemote,
|
|
3739
4073
|
currentRepo,
|
|
3740
4074
|
contact,
|
|
3741
|
-
|
|
4075
|
+
receive,
|
|
4076
|
+
acknowledge,
|
|
4077
|
+
pendingEvents,
|
|
4078
|
+
receiveContact,
|
|
3742
4079
|
runTool,
|
|
3743
4080
|
subscribeWake,
|
|
3744
|
-
parseDue
|
|
4081
|
+
parseDue,
|
|
4082
|
+
withCodexEnv
|
|
3745
4083
|
};
|