@paigy/mcp 0.40.20 → 0.40.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-6MEUSSRH.js → chunk-2ED4GL4N.js} +2 -2
- package/dist/{chunk-YNKTKLQD.js → chunk-BFN2GJ42.js} +1 -1
- package/dist/{chunk-6C6CO7H3.js → chunk-KSXJCZ2L.js} +144 -37
- package/dist/{chunk-RLN2B5IZ.js → chunk-WDZ67U4G.js} +109 -31
- package/dist/{dist-33DA5X2G.js → dist-YAKVSYVQ.js} +5 -1
- package/dist/enable.js +3 -3
- package/dist/index.js +40 -4
- package/dist/listen.js +2 -2
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
serverInstructions,
|
|
5
5
|
sessionStartHook,
|
|
6
6
|
withSessionStartHook
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WDZ67U4G.js";
|
|
8
8
|
import {
|
|
9
9
|
agentName
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-KSXJCZ2L.js";
|
|
11
11
|
|
|
12
12
|
// src/toolset.ts
|
|
13
13
|
var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
|
|
@@ -1527,7 +1527,20 @@ var AskInputSchema = z2.object({
|
|
|
1527
1527
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
1528
1528
|
"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 reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. 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."
|
|
1529
1529
|
),
|
|
1530
|
-
options: z2.array(OptionInputSchema).min(1).max(6).optional()
|
|
1530
|
+
options: z2.array(OptionInputSchema).min(1).max(6).optional(),
|
|
1531
|
+
// A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
|
|
1532
|
+
// checklist mode of answering"). The answer shape was taken off this surface with the wire form
|
|
1533
|
+
// (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
|
|
1534
|
+
// only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
|
|
1535
|
+
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
1536
|
+
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
1537
|
+
// not: whether its own options exclude each other.
|
|
1538
|
+
select: z2.enum(["one", "many"]).optional().describe(
|
|
1539
|
+
'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
|
|
1540
|
+
),
|
|
1541
|
+
answers: z2.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
1542
|
+
"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. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
1543
|
+
)
|
|
1531
1544
|
}).strict();
|
|
1532
1545
|
var StartContactSchema = z2.object({
|
|
1533
1546
|
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; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
@@ -1536,7 +1549,7 @@ var StartContactSchema = z2.object({
|
|
|
1536
1549
|
}).strict();
|
|
1537
1550
|
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
|
|
1538
1551
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
1539
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', '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 is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact 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.";
|
|
1552
|
+
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 is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). 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'); when the options are independent and they may want several, pass select:'many' and they get a checklist. If the person is already on a call, your contact 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.";
|
|
1540
1553
|
var CreateGoalSchema = z3.object({
|
|
1541
1554
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
1542
1555
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -1592,11 +1605,11 @@ var GetGoalSchema = z3.object({
|
|
|
1592
1605
|
history: z3.boolean().optional(),
|
|
1593
1606
|
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
1594
1607
|
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
1595
|
-
* between two of them named. Off by default; `
|
|
1608
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
1596
1609
|
diagnose: z3.boolean().optional()
|
|
1597
1610
|
}).strict();
|
|
1598
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. 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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
|
|
1599
|
-
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. 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. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
1611
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
|
|
1612
|
+
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; 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. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. 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. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
1600
1613
|
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
1601
1614
|
var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
1602
1615
|
var CheckRepliesSchema = z3.object({}).strict();
|
|
@@ -1625,6 +1638,7 @@ function entryWords(entry) {
|
|
|
1625
1638
|
return entry.sources.map((source) => source.text).join("\n");
|
|
1626
1639
|
}
|
|
1627
1640
|
var LIVE_MS = 3 * 6e4;
|
|
1641
|
+
var WORKING_MS = 30 * 6e4;
|
|
1628
1642
|
var ContextSchema = z4.object({
|
|
1629
1643
|
title: z4.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
1630
1644
|
description: z4.array(z4.string().min(1)).describe(
|
|
@@ -1651,6 +1665,13 @@ var TransformSchema = z4.enum([
|
|
|
1651
1665
|
"summarize"
|
|
1652
1666
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
1653
1667
|
]);
|
|
1668
|
+
function participantRef(p) {
|
|
1669
|
+
return `${p.kind}:${p.id}`;
|
|
1670
|
+
}
|
|
1671
|
+
var PAIGY_SELF = { kind: "agent", id: "paigy" };
|
|
1672
|
+
function isPaigy(ref) {
|
|
1673
|
+
return ref === participantRef(PAIGY_SELF);
|
|
1674
|
+
}
|
|
1654
1675
|
var VisualSchema = z4.object({
|
|
1655
1676
|
url: z4.string().url(),
|
|
1656
1677
|
label: z4.string().optional()
|
|
@@ -1695,7 +1716,7 @@ var AttentionSchema = z4.object({
|
|
|
1695
1716
|
points: z4.array(z4.string()).nullable(),
|
|
1696
1717
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
1697
1718
|
blocking: z4.boolean(),
|
|
1698
|
-
/** Reserved (
|
|
1719
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
1699
1720
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
1700
1721
|
deadline: z4.string().datetime().nullable().optional()
|
|
1701
1722
|
});
|
|
@@ -1746,7 +1767,7 @@ var NotifyRequestFields = z4.object({
|
|
|
1746
1767
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
1747
1768
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
1748
1769
|
// the ask into topics and gives each one at most three sentences and one question
|
|
1749
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
1770
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
1750
1771
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
1751
1772
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
1752
1773
|
ask: z4.string().min(1).max(1e4).optional().describe(
|
|
@@ -1823,7 +1844,7 @@ var UserAnswerSchema = z4.discriminatedUnion("kind", [
|
|
|
1823
1844
|
z4.object({ kind: z4.literal("clarify"), chunks: z4.array(z4.string()).min(1) }),
|
|
1824
1845
|
z4.object({ kind: z4.literal("confirm"), approved: z4.boolean() }),
|
|
1825
1846
|
z4.object({ kind: z4.literal("turns"), turns: z4.array(TurnSchema).min(1) }),
|
|
1826
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
1847
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
1827
1848
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
1828
1849
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
1829
1850
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -1872,7 +1893,7 @@ var AwaitItemSchema = z4.discriminatedUnion("type", [
|
|
|
1872
1893
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
1873
1894
|
* a sentence with a hole in it is worse than no sentence. */
|
|
1874
1895
|
note: z4.string().optional(),
|
|
1875
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
1896
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
1876
1897
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
1877
1898
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
1878
1899
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -1968,10 +1989,11 @@ var AgendaTurnSchema = z4.object({
|
|
|
1968
1989
|
agentId: z4.string().optional(),
|
|
1969
1990
|
select: SelectShapeSchema.optional(),
|
|
1970
1991
|
options: z4.array(OptionSchema.omit({ id: true })).optional(),
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1992
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
1993
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
1994
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
1995
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
1996
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
1975
1997
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
1976
1998
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
1977
1999
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -2006,7 +2028,7 @@ var InboxItemSchema = z4.object({
|
|
|
2006
2028
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
2007
2029
|
* `createdAt`. */
|
|
2008
2030
|
agentStateAt: z4.string().datetime().optional(),
|
|
2009
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
2031
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
2010
2032
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
2011
2033
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
2012
2034
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -2067,7 +2089,7 @@ var InboxItemSchema = z4.object({
|
|
|
2067
2089
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
2068
2090
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
2069
2091
|
onCall: z4.literal(true).optional(),
|
|
2070
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
2092
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
2071
2093
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
2072
2094
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
2073
2095
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -2098,7 +2120,7 @@ var InboxItemSchema = z4.object({
|
|
|
2098
2120
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
2099
2121
|
),
|
|
2100
2122
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
2101
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
2123
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
2102
2124
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
2103
2125
|
blocking: z4.boolean().default(false),
|
|
2104
2126
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -2172,9 +2194,17 @@ var UserSettingsSchema = z4.object({
|
|
|
2172
2194
|
banner: z4.boolean(),
|
|
2173
2195
|
push: z4.boolean()
|
|
2174
2196
|
}),
|
|
2197
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
2198
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
2199
|
+
* what people read (owner, 2026-09-30). */
|
|
2175
2200
|
sessionMode: z4.enum(["default", "all_calls", "silent"]),
|
|
2176
|
-
silentPush
|
|
2177
|
-
|
|
2201
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
2202
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
2203
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
2204
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
2205
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
2206
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
2207
|
+
* next dead field for the same reason. */
|
|
2178
2208
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
2179
2209
|
improveConsent: z4.boolean(),
|
|
2180
2210
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -2185,12 +2215,16 @@ var UserSettingsSchema = z4.object({
|
|
|
2185
2215
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
2186
2216
|
* write, 'hosted' on read (see store.ts). */
|
|
2187
2217
|
voiceMode: z4.enum(["hosted", "on_device"]).optional(),
|
|
2188
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
2218
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
2189
2219
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
2190
2220
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
2191
2221
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
2192
2222
|
* read (see store.ts). */
|
|
2193
2223
|
talk: z4.boolean().optional(),
|
|
2224
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
2225
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
2226
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
2227
|
+
callDiagnostics: z4.boolean().optional(),
|
|
2194
2228
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
2195
2229
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
2196
2230
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -2199,9 +2233,6 @@ var UserSettingsSchema = z4.object({
|
|
|
2199
2233
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
2200
2234
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
2201
2235
|
voiceTuning: z4.record(z4.string(), z4.union([z4.number(), z4.string()])).optional(),
|
|
2202
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
2203
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
2204
|
-
pstnCalls: z4.boolean().optional(),
|
|
2205
2236
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
2206
2237
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
2207
2238
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -2245,7 +2276,7 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2245
2276
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
2246
2277
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
2247
2278
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
2248
|
-
*
|
|
2279
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
2249
2280
|
kind: z4.enum(["device", "agent"]).optional(),
|
|
2250
2281
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
2251
2282
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -2263,7 +2294,7 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2263
2294
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
2264
2295
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
2265
2296
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
2266
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
2297
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
2267
2298
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
2268
2299
|
* set once must not overrule a rule they set about one agent. */
|
|
2269
2300
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
@@ -2274,7 +2305,14 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2274
2305
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
2275
2306
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
2276
2307
|
lastSeenAt: z4.string().datetime().nullable().optional(),
|
|
2277
|
-
/**
|
|
2308
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
2309
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
2310
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
2311
|
+
lastWorkedAt: z4.string().datetime().nullable().optional(),
|
|
2312
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
2313
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
2314
|
+
oldestReadyAt: z4.string().datetime().nullable().optional(),
|
|
2315
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
2278
2316
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
2279
2317
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
2280
2318
|
runtime: z4.object({
|
|
@@ -2282,7 +2320,12 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2282
2320
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
2283
2321
|
version: z4.string().optional(),
|
|
2284
2322
|
harnesses: z4.array(z4.object({ name: z4.string(), label: z4.string(), status: z4.string() })).optional(),
|
|
2285
|
-
workspaces: z4.array(z4.string()).optional()
|
|
2323
|
+
workspaces: z4.array(z4.string()).optional(),
|
|
2324
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
2325
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
2326
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
2327
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
2328
|
+
repos: z4.array(z4.object({ path: z4.string(), remote: z4.string() })).optional()
|
|
2286
2329
|
}).optional(),
|
|
2287
2330
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
2288
2331
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -2295,7 +2338,8 @@ var ConnectionSummarySchema = z4.object({
|
|
|
2295
2338
|
});
|
|
2296
2339
|
var LedgerItemSchema = z4.object({ id: z4.string(), parentId: z4.string(), title: z4.string(), createdAt: z4.string() });
|
|
2297
2340
|
var AgentLedgerSchema = z4.object({
|
|
2298
|
-
agent
|
|
2341
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
2342
|
+
agent: z4.object({ id: z4.string(), name: z4.string().nullable(), revokedAt: z4.string().nullable() }),
|
|
2299
2343
|
/** Its own questions you have not answered. */
|
|
2300
2344
|
asks: z4.array(LedgerItemSchema),
|
|
2301
2345
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -2365,7 +2409,7 @@ var QueueQuestionSchema = z4.object({
|
|
|
2365
2409
|
* for a settled question whose reply carried nothing readable. */
|
|
2366
2410
|
answer: z4.string().nullable().default(null),
|
|
2367
2411
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
2368
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
2412
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
2369
2413
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
2370
2414
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
2371
2415
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -2457,7 +2501,19 @@ var QueueItemSchema = z4.object({
|
|
|
2457
2501
|
said: z4.string(),
|
|
2458
2502
|
at: z4.string(),
|
|
2459
2503
|
entryId: z4.string()
|
|
2460
|
-
}).nullable().optional()
|
|
2504
|
+
}).nullable().optional(),
|
|
2505
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
2506
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
2507
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
2508
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
2509
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
2510
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
2511
|
+
*
|
|
2512
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
2513
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
2514
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
2515
|
+
* Entry with no `about_id` is in neither. */
|
|
2516
|
+
lastPersonAt: z4.string().nullable().optional()
|
|
2461
2517
|
});
|
|
2462
2518
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
2463
2519
|
var NoteSourceSchema = z4.enum(["app", "call"]);
|
|
@@ -2487,7 +2543,7 @@ var NoteSchema = z4.object({
|
|
|
2487
2543
|
assignee: z4.string().nullable(),
|
|
2488
2544
|
/** The request thread minted at assignment; null until assigned. */
|
|
2489
2545
|
parentId: z4.string().nullable(),
|
|
2490
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
2546
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
2491
2547
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
2492
2548
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
2493
2549
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -2749,6 +2805,31 @@ var SnapshotSchema = z4.object({
|
|
|
2749
2805
|
computers: z4.array(ComputerRowSchema).nullable()
|
|
2750
2806
|
}).nullable()
|
|
2751
2807
|
});
|
|
2808
|
+
var CallRecapSchema = z4.object({
|
|
2809
|
+
call: z4.object({
|
|
2810
|
+
status: z4.string(),
|
|
2811
|
+
startedAt: z4.string(),
|
|
2812
|
+
durationMs: z4.number().nullable(),
|
|
2813
|
+
agents: z4.array(z4.object({ id: z4.string(), name: z4.string().nullable() }))
|
|
2814
|
+
}),
|
|
2815
|
+
topics: z4.array(z4.object({
|
|
2816
|
+
goalId: z4.string().uuid(),
|
|
2817
|
+
title: z4.string(),
|
|
2818
|
+
owner: z4.string(),
|
|
2819
|
+
state: z4.string(),
|
|
2820
|
+
questions: z4.array(z4.object({ id: z4.string().uuid(), state: z4.string(), title: z4.string() })),
|
|
2821
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
2822
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
2823
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
2824
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
2825
|
+
* anything that is not an answer. */
|
|
2826
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
2827
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
2828
|
+
lines: z4.array(z4.object({ entryId: z4.string().uuid(), words: z4.string(), headline: z4.string().optional(), about: z4.string().nullable().optional() }))
|
|
2829
|
+
})),
|
|
2830
|
+
unfiled: z4.array(z4.object({ lineId: z4.string().uuid(), words: z4.string(), atMs: z4.number() })),
|
|
2831
|
+
more: z4.object({ lines: z4.number(), entries: z4.number(), topics: z4.number() })
|
|
2832
|
+
});
|
|
2752
2833
|
function sessionSlot(sessionId2) {
|
|
2753
2834
|
const id = sessionId2 ?? sessionId();
|
|
2754
2835
|
return `session:${id.slice(0, 8)}`;
|
|
@@ -2996,6 +3077,22 @@ async function claimSessions(opts = {}) {
|
|
|
2996
3077
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
2997
3078
|
return (await res.json()).sessions;
|
|
2998
3079
|
}
|
|
3080
|
+
async function claimSessionEnds(opts = {}) {
|
|
3081
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
|
|
3082
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
3083
|
+
// the HOST's identity
|
|
3084
|
+
}));
|
|
3085
|
+
if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
|
|
3086
|
+
return (await res.json()).ends ?? [];
|
|
3087
|
+
}
|
|
3088
|
+
async function finishSessionEnd(endId, result, opts = {}) {
|
|
3089
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
|
|
3090
|
+
method: "POST",
|
|
3091
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
3092
|
+
body: JSON.stringify({ result })
|
|
3093
|
+
}));
|
|
3094
|
+
if (!res.ok) await fail("finish_session_end", res);
|
|
3095
|
+
}
|
|
2999
3096
|
async function recordDecision(decision, opts = {}) {
|
|
3000
3097
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
3001
3098
|
method: "POST",
|
|
@@ -3119,7 +3216,9 @@ async function contact(input, opts = {}) {
|
|
|
3119
3216
|
// this cannot see.
|
|
3120
3217
|
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
3121
3218
|
ask: a.ask,
|
|
3122
|
-
options: a.options ?? []
|
|
3219
|
+
options: a.options ?? [],
|
|
3220
|
+
...a.select ? { select: a.select } : {},
|
|
3221
|
+
...a.answers ? { answers: a.answers } : {}
|
|
3123
3222
|
}));
|
|
3124
3223
|
const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/goals/contact`, {
|
|
3125
3224
|
method: "POST",
|
|
@@ -3184,7 +3283,7 @@ async function checkReplies(opts = {}) {
|
|
|
3184
3283
|
function compact(o) {
|
|
3185
3284
|
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== null && v !== "" && !(Array.isArray(v) && v.length === 0)));
|
|
3186
3285
|
}
|
|
3187
|
-
var who = (participant) => participant.startsWith("human:") ? "person" : participant.startsWith("agent:") ? "agent" : participant;
|
|
3286
|
+
var who = (participant) => participant.startsWith("human:") ? "person" : isPaigy(participant) ? "paigy" : participant.startsWith("agent:") ? "agent" : participant;
|
|
3188
3287
|
var at = (iso) => {
|
|
3189
3288
|
const t = Date.parse(iso);
|
|
3190
3289
|
return Number.isFinite(t) ? new Date(t).toISOString().replace(/\.\d{3}Z$/, "Z") : iso;
|
|
@@ -3204,6 +3303,9 @@ function decided(result) {
|
|
|
3204
3303
|
return void 0;
|
|
3205
3304
|
}
|
|
3206
3305
|
}
|
|
3306
|
+
function stateOf(state, answer, decided2) {
|
|
3307
|
+
return state === "answered" && answer && decided2 === void 0 ? "replied" : state;
|
|
3308
|
+
}
|
|
3207
3309
|
var handle = (entryId) => entryId.slice(0, 8);
|
|
3208
3310
|
function conversation(e) {
|
|
3209
3311
|
const answers = new Map((e.answers ?? []).map((a) => [a.decisionNeedId, a]));
|
|
@@ -3216,14 +3318,15 @@ function conversation(e) {
|
|
|
3216
3318
|
const need = (e.decisionNeeds ?? []).find((n) => n.requestEntryId === entry.entryId);
|
|
3217
3319
|
const answer = need ? answers.get(need.decisionNeedId) : void 0;
|
|
3218
3320
|
const decision = answer ? decided(answer.result) : void 0;
|
|
3321
|
+
const owed = need?.state === "open" && entry.authorParticipant.startsWith("human:");
|
|
3219
3322
|
return compact({
|
|
3220
|
-
id: repliedTo.has(entry.entryId) ? handle(entry.entryId) : void 0,
|
|
3323
|
+
id: repliedTo.has(entry.entryId) || owed ? handle(entry.entryId) : void 0,
|
|
3221
3324
|
from: who(entry.authorParticipant),
|
|
3222
3325
|
at: at(entry.createdAt),
|
|
3223
3326
|
said: sealed ? "[encrypted]" : entryWords(entry).trim(),
|
|
3224
3327
|
re: entry.aboutId && shown.has(entry.aboutId) ? handle(entry.aboutId) : void 0,
|
|
3225
3328
|
options,
|
|
3226
|
-
decision: need ? compact({ state: need.state, answer: decision }) : void 0
|
|
3329
|
+
decision: need ? compact({ state: stateOf(need.state, answer, decision), answer: decision }) : void 0
|
|
3227
3330
|
});
|
|
3228
3331
|
});
|
|
3229
3332
|
}
|
|
@@ -3239,7 +3342,9 @@ function more(g) {
|
|
|
3239
3342
|
function goalView(g) {
|
|
3240
3343
|
if (!g.goalId) return compact({ state: g.state, next: g.message });
|
|
3241
3344
|
const lines2 = conversation(g);
|
|
3242
|
-
const
|
|
3345
|
+
const owed = lines2.filter((l) => l.from === "person" && l.decision?.state === "open" && l.id);
|
|
3346
|
+
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>, parentId: "${g.goalId}", answers: "<id>" }] }). ` : "";
|
|
3347
|
+
const newest = [...lines2].reverse().find((l) => l.from === "agent");
|
|
3243
3348
|
return compact({
|
|
3244
3349
|
goalId: g.goalId,
|
|
3245
3350
|
revision: g.revision,
|
|
@@ -3263,7 +3368,7 @@ function goalView(g) {
|
|
|
3263
3368
|
dueAt: g.dueAt,
|
|
3264
3369
|
leaseExpiresAt: g.leaseExpiresAt,
|
|
3265
3370
|
conversation: lines2,
|
|
3266
|
-
next: g.message,
|
|
3371
|
+
next: owes ? `${owes}${g.message ?? ""}`.trim() : g.message,
|
|
3267
3372
|
more: more(g)
|
|
3268
3373
|
});
|
|
3269
3374
|
}
|
|
@@ -3283,7 +3388,7 @@ function deliveryView(d) {
|
|
|
3283
3388
|
notSent: d.notSent,
|
|
3284
3389
|
joinedCard: d.joinedCard,
|
|
3285
3390
|
joinedCall: d.joinedCall,
|
|
3286
|
-
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 })})
|
|
3391
|
+
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 check_replies or claim_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
|
|
3287
3392
|
});
|
|
3288
3393
|
}
|
|
3289
3394
|
function repliesView(r) {
|
|
@@ -3590,6 +3695,8 @@ export {
|
|
|
3590
3695
|
NameTakenError,
|
|
3591
3696
|
setIdentity,
|
|
3592
3697
|
claimSessions,
|
|
3698
|
+
claimSessionEnds,
|
|
3699
|
+
finishSessionEnd,
|
|
3593
3700
|
recordDecision,
|
|
3594
3701
|
heartbeat,
|
|
3595
3702
|
registerDelivery,
|
|
@@ -59,7 +59,20 @@ var AskInputSchema = z2.object({
|
|
|
59
59
|
ask: z2.string().trim().min(1).max(1e4).describe(
|
|
60
60
|
"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 reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. 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."
|
|
61
61
|
),
|
|
62
|
-
options: z2.array(OptionInputSchema).min(1).max(6).optional()
|
|
62
|
+
options: z2.array(OptionInputSchema).min(1).max(6).optional(),
|
|
63
|
+
// A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
|
|
64
|
+
// checklist mode of answering"). The answer shape was taken off this surface with the wire form
|
|
65
|
+
// (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
|
|
66
|
+
// only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
|
|
67
|
+
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
68
|
+
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
69
|
+
// not: whether its own options exclude each other.
|
|
70
|
+
select: z2.enum(["one", "many"]).optional().describe(
|
|
71
|
+
'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
|
|
72
|
+
),
|
|
73
|
+
answers: z2.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
74
|
+
"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. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
75
|
+
)
|
|
63
76
|
}).strict();
|
|
64
77
|
var StartContactSchema = z2.object({
|
|
65
78
|
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; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
@@ -68,7 +81,7 @@ var StartContactSchema = z2.object({
|
|
|
68
81
|
}).strict();
|
|
69
82
|
var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
|
|
70
83
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
71
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', '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 is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact 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.";
|
|
84
|
+
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 is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). 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'); when the options are independent and they may want several, pass select:'many' and they get a checklist. If the person is already on a call, your contact 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.";
|
|
72
85
|
var CreateGoalSchema = z3.object({
|
|
73
86
|
outcome: z3.string().trim().min(1).max(1e4),
|
|
74
87
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -124,11 +137,11 @@ var GetGoalSchema = z3.object({
|
|
|
124
137
|
history: z3.boolean().optional(),
|
|
125
138
|
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
126
139
|
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
127
|
-
* between two of them named. Off by default; `
|
|
140
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
128
141
|
diagnose: z3.boolean().optional()
|
|
129
142
|
}).strict();
|
|
130
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. 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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
|
|
131
|
-
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. 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. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
143
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
|
|
144
|
+
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; 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. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. 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. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
132
145
|
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
133
146
|
var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
134
147
|
var CheckRepliesSchema = z3.object({}).strict();
|
|
@@ -149,7 +162,11 @@ HOW TO ASK:
|
|
|
149
162
|
1. One question per ask. Five questions are five objects in \`asks\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
150
163
|
2. Name the Goal you are working on as the ask's \`parentId\`: the question goes onto that Goal and its answer comes back there. Without one, Paigy places it.
|
|
151
164
|
3. \`waiting: hard\` only for a decision you are blocked on; \`waiting: none\` for a question you can keep working around.
|
|
152
|
-
4. Never hold the process open with while-loops. For an open Call with a pending decision, follow its \`next\` with another bounded \`contact({deliveryId})\` tool call; a returned wait window is not a completed conversation. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`check_replies\` or \`claim_goal\` on that wake
|
|
165
|
+
4. Never hold the process open with while-loops. For an open Call with a pending decision, follow its \`next\` with another bounded \`contact({deliveryId})\` tool call; a returned wait window is not a completed conversation. STOP REREADING when a window comes back with nothing new said \u2014 they are not typing, the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`check_replies\` or \`claim_goal\` on that wake.
|
|
166
|
+
5. One ask, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open.
|
|
167
|
+
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
168
|
+
7. A question carries its options. Without them it reaches the person as a bare title nobody can answer. Each option names its choice in full -- never \`All three\` or \`Option 2\` -- and options they may want several of are \`select: "many"\`, a checklist.
|
|
169
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.`;
|
|
153
170
|
}
|
|
154
171
|
function entryWords(entry) {
|
|
155
172
|
const content = entry.content;
|
|
@@ -167,6 +184,7 @@ function entryWords(entry) {
|
|
|
167
184
|
return entry.sources.map((source) => source.text).join("\n");
|
|
168
185
|
}
|
|
169
186
|
var LIVE_MS = 3 * 6e4;
|
|
187
|
+
var WORKING_MS = 30 * 6e4;
|
|
170
188
|
var ContextSchema = z4.object({
|
|
171
189
|
title: z4.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
172
190
|
description: z4.array(z4.string().min(1)).describe(
|
|
@@ -237,7 +255,7 @@ var AttentionSchema = z4.object({
|
|
|
237
255
|
points: z4.array(z4.string()).nullable(),
|
|
238
256
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
239
257
|
blocking: z4.boolean(),
|
|
240
|
-
/** Reserved (
|
|
258
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
241
259
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
242
260
|
deadline: z4.string().datetime().nullable().optional()
|
|
243
261
|
});
|
|
@@ -288,7 +306,7 @@ var NotifyRequestFields = z4.object({
|
|
|
288
306
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
289
307
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
290
308
|
// the ask into topics and gives each one at most three sentences and one question
|
|
291
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
309
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
292
310
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
293
311
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
294
312
|
ask: z4.string().min(1).max(1e4).optional().describe(
|
|
@@ -365,7 +383,7 @@ var UserAnswerSchema = z4.discriminatedUnion("kind", [
|
|
|
365
383
|
z4.object({ kind: z4.literal("clarify"), chunks: z4.array(z4.string()).min(1) }),
|
|
366
384
|
z4.object({ kind: z4.literal("confirm"), approved: z4.boolean() }),
|
|
367
385
|
z4.object({ kind: z4.literal("turns"), turns: z4.array(TurnSchema).min(1) }),
|
|
368
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
386
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
369
387
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
370
388
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
371
389
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -414,7 +432,7 @@ var AwaitItemSchema = z4.discriminatedUnion("type", [
|
|
|
414
432
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
415
433
|
* a sentence with a hole in it is worse than no sentence. */
|
|
416
434
|
note: z4.string().optional(),
|
|
417
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
435
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
418
436
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
419
437
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
420
438
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -510,10 +528,11 @@ var AgendaTurnSchema = z4.object({
|
|
|
510
528
|
agentId: z4.string().optional(),
|
|
511
529
|
select: SelectShapeSchema.optional(),
|
|
512
530
|
options: z4.array(OptionSchema.omit({ id: true })).optional(),
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
531
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
532
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
533
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
534
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
535
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
517
536
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
518
537
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
519
538
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -548,7 +567,7 @@ var InboxItemSchema = z4.object({
|
|
|
548
567
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
549
568
|
* `createdAt`. */
|
|
550
569
|
agentStateAt: z4.string().datetime().optional(),
|
|
551
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
570
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
552
571
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
553
572
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
554
573
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -609,7 +628,7 @@ var InboxItemSchema = z4.object({
|
|
|
609
628
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
610
629
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
611
630
|
onCall: z4.literal(true).optional(),
|
|
612
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
631
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
613
632
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
614
633
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
615
634
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -640,7 +659,7 @@ var InboxItemSchema = z4.object({
|
|
|
640
659
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
641
660
|
),
|
|
642
661
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
643
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
662
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
644
663
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
645
664
|
blocking: z4.boolean().default(false),
|
|
646
665
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -714,9 +733,17 @@ var UserSettingsSchema = z4.object({
|
|
|
714
733
|
banner: z4.boolean(),
|
|
715
734
|
push: z4.boolean()
|
|
716
735
|
}),
|
|
736
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
737
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
738
|
+
* what people read (owner, 2026-09-30). */
|
|
717
739
|
sessionMode: z4.enum(["default", "all_calls", "silent"]),
|
|
718
|
-
silentPush
|
|
719
|
-
|
|
740
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
741
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
742
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
743
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
744
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
745
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
746
|
+
* next dead field for the same reason. */
|
|
720
747
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
721
748
|
improveConsent: z4.boolean(),
|
|
722
749
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -727,12 +754,16 @@ var UserSettingsSchema = z4.object({
|
|
|
727
754
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
728
755
|
* write, 'hosted' on read (see store.ts). */
|
|
729
756
|
voiceMode: z4.enum(["hosted", "on_device"]).optional(),
|
|
730
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
757
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
731
758
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
732
759
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
733
760
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
734
761
|
* read (see store.ts). */
|
|
735
762
|
talk: z4.boolean().optional(),
|
|
763
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
764
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
765
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
766
|
+
callDiagnostics: z4.boolean().optional(),
|
|
736
767
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
737
768
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
738
769
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -741,9 +772,6 @@ var UserSettingsSchema = z4.object({
|
|
|
741
772
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
742
773
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
743
774
|
voiceTuning: z4.record(z4.string(), z4.union([z4.number(), z4.string()])).optional(),
|
|
744
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
745
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
746
|
-
pstnCalls: z4.boolean().optional(),
|
|
747
775
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
748
776
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
749
777
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -787,7 +815,7 @@ var ConnectionSummarySchema = z4.object({
|
|
|
787
815
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
788
816
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
789
817
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
790
|
-
*
|
|
818
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
791
819
|
kind: z4.enum(["device", "agent"]).optional(),
|
|
792
820
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
793
821
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -805,7 +833,7 @@ var ConnectionSummarySchema = z4.object({
|
|
|
805
833
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
806
834
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
807
835
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
808
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
836
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
809
837
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
810
838
|
* set once must not overrule a rule they set about one agent. */
|
|
811
839
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
@@ -816,7 +844,14 @@ var ConnectionSummarySchema = z4.object({
|
|
|
816
844
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
817
845
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
818
846
|
lastSeenAt: z4.string().datetime().nullable().optional(),
|
|
819
|
-
/**
|
|
847
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
848
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
849
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
850
|
+
lastWorkedAt: z4.string().datetime().nullable().optional(),
|
|
851
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
852
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
853
|
+
oldestReadyAt: z4.string().datetime().nullable().optional(),
|
|
854
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
820
855
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
821
856
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
822
857
|
runtime: z4.object({
|
|
@@ -824,7 +859,12 @@ var ConnectionSummarySchema = z4.object({
|
|
|
824
859
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
825
860
|
version: z4.string().optional(),
|
|
826
861
|
harnesses: z4.array(z4.object({ name: z4.string(), label: z4.string(), status: z4.string() })).optional(),
|
|
827
|
-
workspaces: z4.array(z4.string()).optional()
|
|
862
|
+
workspaces: z4.array(z4.string()).optional(),
|
|
863
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
864
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
865
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
866
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
867
|
+
repos: z4.array(z4.object({ path: z4.string(), remote: z4.string() })).optional()
|
|
828
868
|
}).optional(),
|
|
829
869
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
830
870
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -837,7 +877,8 @@ var ConnectionSummarySchema = z4.object({
|
|
|
837
877
|
});
|
|
838
878
|
var LedgerItemSchema = z4.object({ id: z4.string(), parentId: z4.string(), title: z4.string(), createdAt: z4.string() });
|
|
839
879
|
var AgentLedgerSchema = z4.object({
|
|
840
|
-
agent
|
|
880
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
881
|
+
agent: z4.object({ id: z4.string(), name: z4.string().nullable(), revokedAt: z4.string().nullable() }),
|
|
841
882
|
/** Its own questions you have not answered. */
|
|
842
883
|
asks: z4.array(LedgerItemSchema),
|
|
843
884
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -907,7 +948,7 @@ var QueueQuestionSchema = z4.object({
|
|
|
907
948
|
* for a settled question whose reply carried nothing readable. */
|
|
908
949
|
answer: z4.string().nullable().default(null),
|
|
909
950
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
910
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
951
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
911
952
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
912
953
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
913
954
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -999,7 +1040,19 @@ var QueueItemSchema = z4.object({
|
|
|
999
1040
|
said: z4.string(),
|
|
1000
1041
|
at: z4.string(),
|
|
1001
1042
|
entryId: z4.string()
|
|
1002
|
-
}).nullable().optional()
|
|
1043
|
+
}).nullable().optional(),
|
|
1044
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
1045
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
1046
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
1047
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
1048
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
1049
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
1050
|
+
*
|
|
1051
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
1052
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
1053
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
1054
|
+
* Entry with no `about_id` is in neither. */
|
|
1055
|
+
lastPersonAt: z4.string().nullable().optional()
|
|
1003
1056
|
});
|
|
1004
1057
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
1005
1058
|
var NoteSourceSchema = z4.enum(["app", "call"]);
|
|
@@ -1029,7 +1082,7 @@ var NoteSchema = z4.object({
|
|
|
1029
1082
|
assignee: z4.string().nullable(),
|
|
1030
1083
|
/** The request thread minted at assignment; null until assigned. */
|
|
1031
1084
|
parentId: z4.string().nullable(),
|
|
1032
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1085
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1033
1086
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
1034
1087
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
1035
1088
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -1289,6 +1342,31 @@ var SnapshotSchema = z4.object({
|
|
|
1289
1342
|
computers: z4.array(ComputerRowSchema).nullable()
|
|
1290
1343
|
}).nullable()
|
|
1291
1344
|
});
|
|
1345
|
+
var CallRecapSchema = z4.object({
|
|
1346
|
+
call: z4.object({
|
|
1347
|
+
status: z4.string(),
|
|
1348
|
+
startedAt: z4.string(),
|
|
1349
|
+
durationMs: z4.number().nullable(),
|
|
1350
|
+
agents: z4.array(z4.object({ id: z4.string(), name: z4.string().nullable() }))
|
|
1351
|
+
}),
|
|
1352
|
+
topics: z4.array(z4.object({
|
|
1353
|
+
goalId: z4.string().uuid(),
|
|
1354
|
+
title: z4.string(),
|
|
1355
|
+
owner: z4.string(),
|
|
1356
|
+
state: z4.string(),
|
|
1357
|
+
questions: z4.array(z4.object({ id: z4.string().uuid(), state: z4.string(), title: z4.string() })),
|
|
1358
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
1359
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
1360
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
1361
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
1362
|
+
* anything that is not an answer. */
|
|
1363
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
1364
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
1365
|
+
lines: z4.array(z4.object({ entryId: z4.string().uuid(), words: z4.string(), headline: z4.string().optional(), about: z4.string().nullable().optional() }))
|
|
1366
|
+
})),
|
|
1367
|
+
unfiled: z4.array(z4.object({ lineId: z4.string().uuid(), words: z4.string(), atMs: z4.number() })),
|
|
1368
|
+
more: z4.object({ lines: z4.number(), entries: z4.number(), topics: z4.number() })
|
|
1369
|
+
});
|
|
1292
1370
|
|
|
1293
1371
|
// src/listening.ts
|
|
1294
1372
|
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -17,12 +17,14 @@ import {
|
|
|
17
17
|
authToken,
|
|
18
18
|
checkReplies,
|
|
19
19
|
claimGoal,
|
|
20
|
+
claimSessionEnds,
|
|
20
21
|
claimSessions,
|
|
21
22
|
contact,
|
|
22
23
|
createGoal,
|
|
23
24
|
currentRepo,
|
|
24
25
|
deleteToken,
|
|
25
26
|
dismissTriage,
|
|
27
|
+
finishSessionEnd,
|
|
26
28
|
getGoal,
|
|
27
29
|
getTriage,
|
|
28
30
|
hatch,
|
|
@@ -55,7 +57,7 @@ import {
|
|
|
55
57
|
updateGoal,
|
|
56
58
|
updateSlot,
|
|
57
59
|
whoAmI
|
|
58
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-KSXJCZ2L.js";
|
|
59
61
|
export {
|
|
60
62
|
AGENT_TOOLS,
|
|
61
63
|
AGENT_TOOL_NAMES,
|
|
@@ -75,12 +77,14 @@ export {
|
|
|
75
77
|
authToken,
|
|
76
78
|
checkReplies,
|
|
77
79
|
claimGoal,
|
|
80
|
+
claimSessionEnds,
|
|
78
81
|
claimSessions,
|
|
79
82
|
contact,
|
|
80
83
|
createGoal,
|
|
81
84
|
currentRepo,
|
|
82
85
|
deleteToken,
|
|
83
86
|
dismissTriage,
|
|
87
|
+
finishSessionEnd,
|
|
84
88
|
getGoal,
|
|
85
89
|
getTriage,
|
|
86
90
|
hatch,
|
package/dist/enable.js
CHANGED
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
PAIGY_TOOL_IDS,
|
|
4
4
|
enablePaigyTools,
|
|
5
5
|
installSessionListening
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-2ED4GL4N.js";
|
|
7
|
+
import "./chunk-WDZ67U4G.js";
|
|
8
|
+
import "./chunk-KSXJCZ2L.js";
|
|
9
9
|
|
|
10
10
|
// src/enable.ts
|
|
11
11
|
function main() {
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resolvePairing,
|
|
7
7
|
startPairing,
|
|
8
8
|
suggestedAgentName
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-BFN2GJ42.js";
|
|
10
10
|
import {
|
|
11
11
|
ENABLE_COMMAND,
|
|
12
12
|
OnboardSchema,
|
|
@@ -15,11 +15,11 @@ import {
|
|
|
15
15
|
SERVER_INSTRUCTIONS,
|
|
16
16
|
TOOLS,
|
|
17
17
|
paigyToolsAllowlisted
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-2ED4GL4N.js";
|
|
19
19
|
import {
|
|
20
20
|
decideListen,
|
|
21
21
|
listenerAlive
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-WDZ67U4G.js";
|
|
23
23
|
import {
|
|
24
24
|
clearSurface,
|
|
25
25
|
writeSurface
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
sessionId,
|
|
40
40
|
slotName,
|
|
41
41
|
whoAmI
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-KSXJCZ2L.js";
|
|
43
43
|
|
|
44
44
|
// src/index.ts
|
|
45
45
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -50,6 +50,40 @@ import {
|
|
|
50
50
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
51
51
|
import { fileURLToPath } from "url";
|
|
52
52
|
import qrcode from "qrcode-generator";
|
|
53
|
+
|
|
54
|
+
// src/survive.ts
|
|
55
|
+
function detail(cause) {
|
|
56
|
+
const stack = cause?.stack;
|
|
57
|
+
return stack ? stack : String(cause);
|
|
58
|
+
}
|
|
59
|
+
function guardTheProcess(proc, log) {
|
|
60
|
+
proc.on("unhandledRejection", (reason) => {
|
|
61
|
+
log(
|
|
62
|
+
`paigy: a promise failed with nobody waiting \u2014 the server stays up and its tools keep working (${detail(reason)})`
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
proc.on("uncaughtException", (error) => {
|
|
66
|
+
log(
|
|
67
|
+
`paigy: an error escaped every catch \u2014 the server stays up and its tools keep working (${detail(error)})`
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function traceTheTransport(transport2, log) {
|
|
72
|
+
const theirError = transport2.onerror;
|
|
73
|
+
transport2.onerror = (error) => {
|
|
74
|
+
log(`paigy: the stdio stream errored \u2014 the host may be dropping this server (${detail(error)})`);
|
|
75
|
+
theirError?.(error);
|
|
76
|
+
};
|
|
77
|
+
const theirClose = transport2.onclose;
|
|
78
|
+
transport2.onclose = () => {
|
|
79
|
+
log(
|
|
80
|
+
"paigy: the stdio stream closed \u2014 the host dropped this server, so its tools are gone from this session until the host restarts it (in Claude Code: `/mcp paigy reconnect`)"
|
|
81
|
+
);
|
|
82
|
+
theirClose?.();
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/index.ts
|
|
53
87
|
var JOIN_CAP_MS = 45e3;
|
|
54
88
|
function pairedResult(token, sas, note) {
|
|
55
89
|
clearSurface();
|
|
@@ -349,5 +383,7 @@ async function beat() {
|
|
|
349
383
|
}
|
|
350
384
|
void beat();
|
|
351
385
|
setInterval(() => void beat(), 6e4).unref();
|
|
386
|
+
guardTheProcess(process, (line) => console.error(line));
|
|
352
387
|
var transport = new StdioServerTransport();
|
|
388
|
+
traceTheTransport(transport, (line) => console.error(line));
|
|
353
389
|
await server.connect(transport);
|
package/dist/listen.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
entryWords,
|
|
4
4
|
removeListenMark,
|
|
5
5
|
writeListenMark
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-WDZ67U4G.js";
|
|
7
7
|
import {
|
|
8
8
|
agentName,
|
|
9
9
|
checkReplies,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
getGoal,
|
|
12
12
|
slotName,
|
|
13
13
|
subscribeWake
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KSXJCZ2L.js";
|
|
15
15
|
|
|
16
16
|
// src/listen.ts
|
|
17
17
|
import { spawn } from "child_process";
|
package/dist/onboard.js
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
resolvePairing,
|
|
4
4
|
startPairing,
|
|
5
5
|
suggestedAgentName
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BFN2GJ42.js";
|
|
7
7
|
import {
|
|
8
8
|
autoConfigureClients,
|
|
9
9
|
claudeInstallHint,
|
|
10
10
|
openBrowser
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-2ED4GL4N.js";
|
|
12
|
+
import "./chunk-WDZ67U4G.js";
|
|
13
13
|
import {
|
|
14
14
|
TOKEN_PATH,
|
|
15
15
|
agentName,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
saveToken,
|
|
21
21
|
setIdentity,
|
|
22
22
|
whoAmI
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-KSXJCZ2L.js";
|
|
24
24
|
|
|
25
25
|
// src/onboard.ts
|
|
26
26
|
function reportRegistered() {
|
package/dist/slot.js
CHANGED
package/dist/stalled.js
CHANGED
|
@@ -29,7 +29,7 @@ async function main() {
|
|
|
29
29
|
const dir = join(homedir(), ".paigy", "stalled-reminded");
|
|
30
30
|
const mark = join(dir, `${session || "session"}-${day}`);
|
|
31
31
|
if (existsSync(mark)) return;
|
|
32
|
-
const { checkReplies } = await import("./dist-
|
|
32
|
+
const { checkReplies } = await import("./dist-YAKVSYVQ.js");
|
|
33
33
|
const reply = stopReply((await checkReplies()).stalled ?? [], input.hook_event_name ?? "Stop");
|
|
34
34
|
if (!reply) return;
|
|
35
35
|
mkdirSync(dir, { recursive: true });
|
package/dist/statusline.js
CHANGED
package/package.json
CHANGED