@paigy/mcp 0.40.19 → 0.40.21

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.
@@ -4,10 +4,10 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-HGXXGOGG.js";
7
+ } from "./chunk-OW65TWMX.js";
8
8
  import {
9
9
  agentName
10
- } from "./chunk-VYWIPFEY.js";
10
+ } from "./chunk-2UEIJRRC.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.";
@@ -6,7 +6,7 @@ import {
6
6
  saveToken,
7
7
  setIdentity,
8
8
  sleep
9
- } from "./chunk-VYWIPFEY.js";
9
+ } from "./chunk-2UEIJRRC.js";
10
10
 
11
11
  // src/identity.ts
12
12
  var CLIENT_LABELS = {
@@ -57,18 +57,18 @@ var AskInputSchema = z2.object({
57
57
  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 Paigy places the question in the tree itself."),
58
58
  repo: z2.string().optional().describe("Optional repository context."),
59
59
  ask: z2.string().trim().min(1).max(1e4).describe(
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 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
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(2).max(6).optional()
62
+ options: z2.array(OptionInputSchema).min(1).max(6).optional()
63
63
  }).strict();
64
64
  var StartContactSchema = z2.object({
65
65
  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."),
66
- waiting: z2.enum(["none", "hard"]).default("none"),
66
+ waiting: z2.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
67
67
  channel: z2.enum(["notification", "call"]).default("notification")
68
68
  }).strict();
69
69
  var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
70
70
  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 \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: 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).";
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.";
72
72
  var CreateGoalSchema = z3.object({
73
73
  outcome: z3.string().trim().min(1).max(1e4),
74
74
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -106,7 +106,11 @@ var UpdateGoalSchema = z3.object({
106
106
  state: z3.enum(["active", "done", "cancelled"]).optional(),
107
107
  progress: z3.string().trim().min(1).max(1e4).optional(),
108
108
  reviewed: z3.literal(true).optional(),
109
- dueAt: z3.string().datetime({ offset: true }).nullable().optional()
109
+ dueAt: z3.string().datetime({ offset: true }).nullable().optional(),
110
+ /** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
111
+ * (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
112
+ * by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
113
+ withdraw: z3.array(z3.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
110
114
  }).strict().refine((v) => Object.keys(v).length > 0),
111
115
  reason: z3.string().trim().min(1).max(2e3),
112
116
  operationId: z3.string().uuid().optional()
@@ -117,10 +121,14 @@ var GetGoalSchema = z3.object({
117
121
  goalId: z3.string().uuid(),
118
122
  /** Every entry in full. Without it the read carries the person's words, open questions and your
119
123
  * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
120
- history: z3.boolean().optional()
124
+ history: z3.boolean().optional(),
125
+ /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
126
+ * about this work, each answer attributed to the reader that gave it, and every disagreement
127
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
128
+ diagnose: z3.boolean().optional()
121
129
  }).strict();
122
- 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.";
123
- 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. 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. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
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.`;
124
132
  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.";
125
133
  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.";
126
134
  var CheckRepliesSchema = z3.object({}).strict();
@@ -133,16 +141,19 @@ var AGENT_TOOLS = [
133
141
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
134
142
  ];
135
143
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
136
- var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
137
144
  function serverInstructions(opts) {
138
145
  const calls = opts.waits ? "A Call holds one bounded window here; continue with contact({deliveryId}) to hold the next or reread that exact Delivery." : "A Call returns after one read; continue with contact({deliveryId}) to reread that exact Delivery.";
139
- return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. ${DONE_MESSAGE} Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
146
+ return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
140
147
 
141
148
  HOW TO ASK:
142
149
  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.
143
150
  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.
144
151
  3. \`waiting: hard\` only for a decision you are blocked on; \`waiting: none\` for a question you can keep working around.
145
- 4. Never hold the process open with while-loops. Let the current process finish or schedule a wakeup, and collect answers with \`check_replies\` or \`claim_goal\` on the next wake.`;
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.
153
+ 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.
154
+ 6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped.
155
+ 7. A question carries its options. Without them it reaches the person as a bare title nobody can answer.
156
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.`;
146
157
  }
147
158
  function entryWords(entry) {
148
159
  const content = entry.content;
@@ -160,6 +171,7 @@ function entryWords(entry) {
160
171
  return entry.sources.map((source) => source.text).join("\n");
161
172
  }
162
173
  var LIVE_MS = 3 * 6e4;
174
+ var WORKING_MS = 30 * 6e4;
163
175
  var ContextSchema = z4.object({
164
176
  title: z4.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
165
177
  description: z4.array(z4.string().min(1)).describe(
@@ -230,7 +242,7 @@ var AttentionSchema = z4.object({
230
242
  points: z4.array(z4.string()).nullable(),
231
243
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
232
244
  blocking: z4.boolean(),
233
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
245
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
234
246
  * slice wires it; optional so today's rows/callers project cleanly. */
235
247
  deadline: z4.string().datetime().nullable().optional()
236
248
  });
@@ -281,7 +293,7 @@ var NotifyRequestFields = z4.object({
281
293
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
282
294
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
283
295
  // the ask into topics and gives each one at most three sentences and one question
284
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
296
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
285
297
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
286
298
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
287
299
  ask: z4.string().min(1).max(1e4).optional().describe(
@@ -358,7 +370,7 @@ var UserAnswerSchema = z4.discriminatedUnion("kind", [
358
370
  z4.object({ kind: z4.literal("clarify"), chunks: z4.array(z4.string()).min(1) }),
359
371
  z4.object({ kind: z4.literal("confirm"), approved: z4.boolean() }),
360
372
  z4.object({ kind: z4.literal("turns"), turns: z4.array(TurnSchema).min(1) }),
361
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
373
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
362
374
  * delivered through the same settle/await path as a human answer, carrying the judge's
363
375
  * derivation and the precedent ids it grew from. Always paired with a visible trail
364
376
  * card the user can reply to — the broker never overrides the user. */
@@ -407,7 +419,7 @@ var AwaitItemSchema = z4.discriminatedUnion("type", [
407
419
  * and now knows exactly which call to make. Absent when either half is missing —
408
420
  * a sentence with a hole in it is worse than no sentence. */
409
421
  note: z4.string().optional(),
410
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
422
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
411
423
  * shaped answer was mapped from, filtered to its own claims. There is no second list
412
424
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
413
425
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -503,10 +515,11 @@ var AgendaTurnSchema = z4.object({
503
515
  agentId: z4.string().optional(),
504
516
  select: SelectShapeSchema.optional(),
505
517
  options: z4.array(OptionSchema.omit({ id: true })).optional(),
506
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
507
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
508
- * (the beat for context, the answer window for asks). Clamped bot-side. */
509
- pace: z4.number().positive().optional(),
518
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
519
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
520
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
521
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
522
+ anything. A caller's own `beat_s` tuning is what it used to override. */
510
523
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
511
524
  * (a question blocks, context flows). blocking:false on a question = ask and move
512
525
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -541,7 +554,7 @@ var InboxItemSchema = z4.object({
541
554
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
542
555
  * `createdAt`. */
543
556
  agentStateAt: z4.string().datetime().optional(),
544
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
557
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
545
558
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
546
559
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
547
560
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -602,7 +615,7 @@ var InboxItemSchema = z4.object({
602
615
  * off the same open list the card came from, so it clears when the Call does. A card is the
603
616
  * backup for a call not taken; while the call has it, the call is where it is answered. */
604
617
  onCall: z4.literal(true).optional(),
605
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
618
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
606
619
  * declined, and what the ladder will do next — read off the cron's own row, never computed
607
620
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
608
621
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -633,7 +646,7 @@ var InboxItemSchema = z4.object({
633
646
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
634
647
  ),
635
648
  /** Real downstream work is stuck behind this one — set by the agent, independent of
636
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
649
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
637
650
  * inbox's blocking badge and the extra confirm step before dismissing it. */
638
651
  blocking: z4.boolean().default(false),
639
652
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -707,9 +720,16 @@ var UserSettingsSchema = z4.object({
707
720
  banner: z4.boolean(),
708
721
  push: z4.boolean()
709
722
  }),
723
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
724
+ * they are an enum on a live column across every account, and the rename is a rename of
725
+ * what people read (owner, 2026-09-30). */
710
726
  sessionMode: z4.enum(["default", "all_calls", "silent"]),
711
- silentPush: z4.boolean(),
712
- autoCallback: z4.boolean(),
727
+ /** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
728
+ * reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
729
+ * PATCHes the whole settings object and would be refused for sending a key we stopped
730
+ * wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
731
+ * its column once no installed build still sends it. */
732
+ silentPush: z4.boolean().optional(),
713
733
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
714
734
  improveConsent: z4.boolean(),
715
735
  missedCall: MissedCallSchema.default("backoff_standard"),
@@ -720,12 +740,16 @@ var UserSettingsSchema = z4.object({
720
740
  * must not silently reset this privacy choice. Absent = leave unchanged on
721
741
  * write, 'hosted' on read (see store.ts). */
722
742
  voiceMode: z4.enum(["hosted", "on_device"]).optional(),
723
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
743
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
724
744
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
725
745
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
726
746
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
727
747
  * read (see store.ts). */
728
748
  talk: z4.boolean().optional(),
749
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
750
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
751
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
752
+ callDiagnostics: z4.boolean().optional(),
729
753
  /** Per-user ring budget (#603): calls per rolling day before further calls
730
754
  * degrade to banner. Absent = the global default (25). A number, never a
731
755
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -734,9 +758,6 @@ var UserSettingsSchema = z4.object({
734
758
  * slower speaker). No API-side semantics; the bot resolves each key with its
735
759
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
736
760
  voiceTuning: z4.record(z4.string(), z4.union([z4.number(), z4.string()])).optional(),
737
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
738
- * defaulted — an older client PATCHing the full object must not clobber it. */
739
- pstnCalls: z4.boolean().optional(),
740
761
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
741
762
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
742
763
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -750,18 +771,20 @@ var UserSettingsSchema = z4.object({
750
771
  * clobber guard as voiceMode: absent = leave unchanged on write. */
751
772
  broker: BrokerTuningSchema.optional()
752
773
  });
753
- var HistoryItemSchema = z4.object({
774
+ var HistoryWorkSchema = z4.object({
754
775
  id: z4.string(),
755
- /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
756
- initiator: z4.enum(["user", "agent"]),
757
776
  title: z4.string(),
758
- /** The agent on the other end (its name). */
759
- name: z4.string(),
760
- createdAt: z4.string(),
761
- /** When the agent fetched your request (user→agent only). */
762
- agentAckedAt: z4.string().nullable(),
763
- /** When you answered the agent's notification (agent→user only). */
764
- humanAckedAt: z4.string().nullable()
777
+ state: z4.enum(["done", "cancelled"]),
778
+ /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
779
+ assignee: z4.string()
780
+ });
781
+ var HistoryEntrySchema = z4.union([
782
+ z4.object({ at: z4.string(), card: InboxItemSchema }),
783
+ z4.object({ at: z4.string(), work: HistoryWorkSchema })
784
+ ]);
785
+ var HistoryPageSchema = z4.object({
786
+ entries: z4.array(HistoryEntrySchema),
787
+ next: z4.string().nullable()
765
788
  });
766
789
  var ACTIVITY_LINES = 2;
767
790
  var ACTIVITY_LINE_MAX = 80;
@@ -778,7 +801,7 @@ var ConnectionSummarySchema = z4.object({
778
801
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
779
802
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
780
803
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
781
- * apps/api/src/tokens/devices-vs-agents-design.md. */
804
+ * docs/server/tokens/devices-vs-agents-design.md. */
782
805
  kind: z4.enum(["device", "agent"]).optional(),
783
806
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
784
807
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -796,7 +819,7 @@ var ConnectionSummarySchema = z4.object({
796
819
  * user on the agent's own page. null = no ceiling (today's behaviour for every
797
820
  * connection). Android binds importance to a relationship rather than to each message,
798
821
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
799
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
822
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
800
823
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
801
824
  * set once must not overrule a rule they set about one agent. */
802
825
  reach: NotifyLevelSchema.nullable().optional(),
@@ -807,7 +830,14 @@ var ConnectionSummarySchema = z4.object({
807
830
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
808
831
  * desktop app while open. Null = never seen; stale = offline. */
809
832
  lastSeenAt: z4.string().datetime().nullable().optional(),
810
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
833
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
834
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
835
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
836
+ lastWorkedAt: z4.string().datetime().nullable().optional(),
837
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
838
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
839
+ oldestReadyAt: z4.string().datetime().nullable().optional(),
840
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
811
841
  * harness availabilities + granted workspaces — the option set the phone's
812
842
  * "new session" sheet offers. Absent for ordinary MCP agents. */
813
843
  runtime: z4.object({
@@ -815,7 +845,12 @@ var ConnectionSummarySchema = z4.object({
815
845
  * shows its age here (`apps/desktop/src/update.ts`). */
816
846
  version: z4.string().optional(),
817
847
  harnesses: z4.array(z4.object({ name: z4.string(), label: z4.string(), status: z4.string() })).optional(),
818
- workspaces: z4.array(z4.string()).optional()
848
+ workspaces: z4.array(z4.string()).optional(),
849
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
850
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
851
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
852
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
853
+ repos: z4.array(z4.object({ path: z4.string(), remote: z4.string() })).optional()
819
854
  }).optional(),
820
855
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
821
856
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -898,7 +933,7 @@ var QueueQuestionSchema = z4.object({
898
933
  * for a settled question whose reply carried nothing readable. */
899
934
  answer: z4.string().nullable().default(null),
900
935
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
901
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
936
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
902
937
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
903
938
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
904
939
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -945,6 +980,9 @@ var QueueItemSchema = z4.object({
945
980
  progressLine: z4.string().nullable().optional(),
946
981
  reviewPending: z4.boolean().default(false),
947
982
  dueAt: z4.string().nullable().default(null),
983
+ /** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
984
+ * with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
985
+ finishedAt: z4.string().nullable().optional(),
948
986
  /** The Goal this one was opened under; null at the root. */
949
987
  parentGoalId: z4.string().nullable().default(null),
950
988
  /** Goals opened under this one — only those the same list holds. */
@@ -953,8 +991,17 @@ var QueueItemSchema = z4.object({
953
991
  dependencyGoalIds: z4.array(z4.string()).default([]),
954
992
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
955
993
  blocked: z4.boolean().default(false),
956
- /** Every decision need on it, open or settled — the page decides which to show. */
994
+ /** Its questions: every OPEN one, and at most ten settled, newest settled first
995
+ * (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
996
+ * `answered` are, and a settled one's words are a line (280 characters), its body read when the
997
+ * question is opened. */
957
998
  questions: z4.array(QueueQuestionSchema).default([]),
999
+ /** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
1000
+ * bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
1001
+ * right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
1002
+ * by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
1003
+ asked: z4.number().optional(),
1004
+ answered: z4.number().optional(),
958
1005
  /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
959
1006
  * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
960
1007
  * not spell an empty list. */
@@ -978,7 +1025,19 @@ var QueueItemSchema = z4.object({
978
1025
  said: z4.string(),
979
1026
  at: z4.string(),
980
1027
  entryId: z4.string()
981
- }).nullable().optional()
1028
+ }).nullable().optional(),
1029
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
1030
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
1031
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
1032
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
1033
+ * 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
1034
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
1035
+ *
1036
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
1037
+ * minute after the person speaks erases their instant from it, and the durable traces the client
1038
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
1039
+ * Entry with no `about_id` is in neither. */
1040
+ lastPersonAt: z4.string().nullable().optional()
982
1041
  });
983
1042
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
984
1043
  var NoteSourceSchema = z4.enum(["app", "call"]);
@@ -1008,7 +1067,7 @@ var NoteSchema = z4.object({
1008
1067
  assignee: z4.string().nullable(),
1009
1068
  /** The request thread minted at assignment; null until assigned. */
1010
1069
  parentId: z4.string().nullable(),
1011
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
1070
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
1012
1071
  * call — never a deadline. It only ever comes from the user's own words, so when it
1013
1072
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
1014
1073
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -1089,6 +1148,19 @@ var DeliveryConfigSchema = z4.object({
1089
1148
  * carries credentials; only a `poll` registration can come back with null here. */
1090
1149
  realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
1091
1150
  });
1151
+ var HostDecisionSchema = z4.object({
1152
+ /** The agent's token id: the row's `recipient`. */
1153
+ agent: z4.string().uuid(),
1154
+ decision: z4.enum(["stood_back", "took_over"]),
1155
+ /** The work it was about: the Goal `claim_goal` would hand that agent next. */
1156
+ goalId: z4.string().uuid().nullable().optional(),
1157
+ /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
1158
+ seenAt: z4.string().datetime().nullable().optional(),
1159
+ /** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
1160
+ since: z4.string().datetime().nullable().optional(),
1161
+ /** What the host said, in its log's own words: why it stood back, or what the take-over did. */
1162
+ said: z4.string().max(300).optional()
1163
+ });
1092
1164
  var WakeNudgeSchema = z4.object({
1093
1165
  kind: z4.enum(["reply", "request", "callback"]),
1094
1166
  notificationId: z4.string().optional(),
@@ -1133,6 +1205,9 @@ var DeviceTokenSchema = z4.object({
1133
1205
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
1134
1206
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
1135
1207
  workspace: z4.string().nullable().optional(),
1208
+ /** Local host recovery must preserve the launch's runtime and Paigy identity. */
1209
+ harness: z4.enum(["claude", "codex", "agy"]).optional(),
1210
+ session_id: z4.string().uuid().optional(),
1136
1211
  uik_pub: z4.string().nullable().optional()
1137
1212
  });
1138
1213
  var SupportRequestSchema = z4.object({
@@ -1252,6 +1327,29 @@ var SnapshotSchema = z4.object({
1252
1327
  computers: z4.array(ComputerRowSchema).nullable()
1253
1328
  }).nullable()
1254
1329
  });
1330
+ var CallRecapSchema = z4.object({
1331
+ call: z4.object({
1332
+ status: z4.string(),
1333
+ startedAt: z4.string(),
1334
+ durationMs: z4.number().nullable(),
1335
+ agents: z4.array(z4.object({ id: z4.string(), name: z4.string().nullable() }))
1336
+ }),
1337
+ topics: z4.array(z4.object({
1338
+ goalId: z4.string().uuid(),
1339
+ title: z4.string(),
1340
+ owner: z4.string(),
1341
+ state: z4.string(),
1342
+ questions: z4.array(z4.object({ id: z4.string().uuid(), state: z4.string(), title: z4.string() })),
1343
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
1344
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
1345
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
1346
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
1347
+ * anything that is not an answer. */
1348
+ lines: z4.array(z4.object({ entryId: z4.string().uuid(), words: z4.string(), headline: z4.string().optional() }))
1349
+ })),
1350
+ unfiled: z4.array(z4.object({ lineId: z4.string().uuid(), words: z4.string(), atMs: z4.number() })),
1351
+ more: z4.object({ lines: z4.number(), entries: z4.number(), topics: z4.number() })
1352
+ });
1255
1353
 
1256
1354
  // src/listening.ts
1257
1355
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -9,6 +9,7 @@ import {
9
9
  NameTakenError,
10
10
  NotifyRequestSchema,
11
11
  PROTO,
12
+ RevokedError,
12
13
  TOKEN_PATH,
13
14
  UnpairedError,
14
15
  acceptTriage,
@@ -36,6 +37,7 @@ import {
36
37
  reach,
37
38
  reachAs,
38
39
  readToken,
40
+ recordDecision,
39
41
  registerDelivery,
40
42
  repoFromRemote,
41
43
  requestCode,
@@ -53,7 +55,7 @@ import {
53
55
  updateGoal,
54
56
  updateSlot,
55
57
  whoAmI
56
- } from "./chunk-VYWIPFEY.js";
58
+ } from "./chunk-2UEIJRRC.js";
57
59
  export {
58
60
  AGENT_TOOLS,
59
61
  AGENT_TOOL_NAMES,
@@ -65,6 +67,7 @@ export {
65
67
  NameTakenError,
66
68
  NotifyRequestSchema,
67
69
  PROTO,
70
+ RevokedError,
68
71
  TOKEN_PATH,
69
72
  UnpairedError,
70
73
  acceptTriage,
@@ -92,6 +95,7 @@ export {
92
95
  reach,
93
96
  reachAs,
94
97
  readToken,
98
+ recordDecision,
95
99
  registerDelivery,
96
100
  repoFromRemote,
97
101
  requestCode,
package/dist/enable.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools,
5
5
  installSessionListening
6
- } from "./chunk-76ILN3YV.js";
7
- import "./chunk-HGXXGOGG.js";
8
- import "./chunk-VYWIPFEY.js";
6
+ } from "./chunk-B7SEHASI.js";
7
+ import "./chunk-OW65TWMX.js";
8
+ import "./chunk-2UEIJRRC.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-TRMYDJ5H.js";
9
+ } from "./chunk-LRN27BMO.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-76ILN3YV.js";
18
+ } from "./chunk-B7SEHASI.js";
19
19
  import {
20
20
  decideListen,
21
21
  listenerAlive
22
- } from "./chunk-HGXXGOGG.js";
22
+ } from "./chunk-OW65TWMX.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-VYWIPFEY.js";
42
+ } from "./chunk-2UEIJRRC.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-HGXXGOGG.js";
6
+ } from "./chunk-OW65TWMX.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-VYWIPFEY.js";
14
+ } from "./chunk-2UEIJRRC.js";
15
15
 
16
16
  // src/listen.ts
17
17
  import { spawn } from "child_process";
@@ -124,6 +124,7 @@ var { brief: BRIEF, release: RELEASE } = listenOptions(process.argv);
124
124
  var line = (text) => void process.stdout.write(text + "\n");
125
125
  var emit = (obj) => line(JSON.stringify(obj));
126
126
  var listeningAs = () => slotName(agentName()) ?? agentName();
127
+ var SWEEP_EVERY_MS = Number(process.env.PAIGY_SWEEP_MS) || 9e4;
127
128
  function saidIn(delivery) {
128
129
  return ("entries" in delivery ? delivery.entries : []).filter((entry) => entry.kind === "contribution").map((entry) => entryWords(entry)).filter(Boolean).join("\n");
129
130
  }
@@ -192,7 +193,10 @@ async function main() {
192
193
  process.on("exit", () => removeListenMark(slot));
193
194
  if (!BRIEF) emit({ type: "listening", channel: sub.channel });
194
195
  await sweep("boot");
196
+ const heartbeat = setInterval(() => void sweep("timer"), SWEEP_EVERY_MS);
197
+ heartbeat.unref?.();
195
198
  const shutdown = async () => {
199
+ clearInterval(heartbeat);
196
200
  removeListenMark(slot);
197
201
  await sub.close();
198
202
  process.exit(0);
@@ -224,6 +228,7 @@ if (isEntry()) {
224
228
  }
225
229
  }
226
230
  export {
231
+ SWEEP_EVERY_MS,
227
232
  brief,
228
233
  catchUpReason,
229
234
  launchEnv,