@paigy/mcp 0.40.19 → 0.40.20

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.
@@ -1525,18 +1525,18 @@ var AskInputSchema = z2.object({
1525
1525
  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."),
1526
1526
  repo: z2.string().optional().describe("Optional repository context."),
1527
1527
  ask: z2.string().trim().min(1).max(1e4).describe(
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 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."
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(2).max(6).optional()
1530
+ options: z2.array(OptionInputSchema).min(1).max(6).optional()
1531
1531
  }).strict();
1532
1532
  var StartContactSchema = z2.object({
1533
1533
  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."),
1534
- waiting: z2.enum(["none", "hard"]).default("none"),
1534
+ waiting: z2.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
1535
1535
  channel: z2.enum(["notification", "call"]).default("notification")
1536
1536
  }).strict();
1537
1537
  var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
1538
1538
  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 \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).";
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.";
1540
1540
  var CreateGoalSchema = z3.object({
1541
1541
  outcome: z3.string().trim().min(1).max(1e4),
1542
1542
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -1574,7 +1574,11 @@ var UpdateGoalSchema = z3.object({
1574
1574
  state: z3.enum(["active", "done", "cancelled"]).optional(),
1575
1575
  progress: z3.string().trim().min(1).max(1e4).optional(),
1576
1576
  reviewed: z3.literal(true).optional(),
1577
- dueAt: z3.string().datetime({ offset: true }).nullable().optional()
1577
+ dueAt: z3.string().datetime({ offset: true }).nullable().optional(),
1578
+ /** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
1579
+ * (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
1580
+ * by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
1581
+ withdraw: z3.array(z3.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
1578
1582
  }).strict().refine((v) => Object.keys(v).length > 0),
1579
1583
  reason: z3.string().trim().min(1).max(2e3),
1580
1584
  operationId: z3.string().uuid().optional()
@@ -1585,10 +1589,14 @@ var GetGoalSchema = z3.object({
1585
1589
  goalId: z3.string().uuid(),
1586
1590
  /** Every entry in full. Without it the read carries the person's words, open questions and your
1587
1591
  * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
1588
- history: z3.boolean().optional()
1592
+ history: z3.boolean().optional(),
1593
+ /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
1594
+ * about this work, each answer attributed to the reader that gave it, and every disagreement
1595
+ * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
1596
+ diagnose: z3.boolean().optional()
1589
1597
  }).strict();
1590
- 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.";
1591
- 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.`;
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.`;
1592
1600
  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.";
1593
1601
  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.";
1594
1602
  var CheckRepliesSchema = z3.object({}).strict();
@@ -2207,18 +2215,20 @@ var UserSettingsSchema = z4.object({
2207
2215
  * clobber guard as voiceMode: absent = leave unchanged on write. */
2208
2216
  broker: BrokerTuningSchema.optional()
2209
2217
  });
2210
- var HistoryItemSchema = z4.object({
2218
+ var HistoryWorkSchema = z4.object({
2211
2219
  id: z4.string(),
2212
- /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
2213
- initiator: z4.enum(["user", "agent"]),
2214
2220
  title: z4.string(),
2215
- /** The agent on the other end (its name). */
2216
- name: z4.string(),
2217
- createdAt: z4.string(),
2218
- /** When the agent fetched your request (user→agent only). */
2219
- agentAckedAt: z4.string().nullable(),
2220
- /** When you answered the agent's notification (agent→user only). */
2221
- humanAckedAt: z4.string().nullable()
2221
+ state: z4.enum(["done", "cancelled"]),
2222
+ /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
2223
+ assignee: z4.string()
2224
+ });
2225
+ var HistoryEntrySchema = z4.union([
2226
+ z4.object({ at: z4.string(), card: InboxItemSchema }),
2227
+ z4.object({ at: z4.string(), work: HistoryWorkSchema })
2228
+ ]);
2229
+ var HistoryPageSchema = z4.object({
2230
+ entries: z4.array(HistoryEntrySchema),
2231
+ next: z4.string().nullable()
2222
2232
  });
2223
2233
  var ACTIVITY_LINES = 2;
2224
2234
  var ACTIVITY_LINE_MAX = 80;
@@ -2402,6 +2412,9 @@ var QueueItemSchema = z4.object({
2402
2412
  progressLine: z4.string().nullable().optional(),
2403
2413
  reviewPending: z4.boolean().default(false),
2404
2414
  dueAt: z4.string().nullable().default(null),
2415
+ /** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
2416
+ * with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
2417
+ finishedAt: z4.string().nullable().optional(),
2405
2418
  /** The Goal this one was opened under; null at the root. */
2406
2419
  parentGoalId: z4.string().nullable().default(null),
2407
2420
  /** Goals opened under this one — only those the same list holds. */
@@ -2410,8 +2423,17 @@ var QueueItemSchema = z4.object({
2410
2423
  dependencyGoalIds: z4.array(z4.string()).default([]),
2411
2424
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
2412
2425
  blocked: z4.boolean().default(false),
2413
- /** Every decision need on it, open or settled — the page decides which to show. */
2426
+ /** Its questions: every OPEN one, and at most ten settled, newest settled first
2427
+ * (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
2428
+ * `answered` are, and a settled one's words are a line (280 characters), its body read when the
2429
+ * question is opened. */
2414
2430
  questions: z4.array(QueueQuestionSchema).default([]),
2431
+ /** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
2432
+ * bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
2433
+ * right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
2434
+ * by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
2435
+ asked: z4.number().optional(),
2436
+ answered: z4.number().optional(),
2415
2437
  /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
2416
2438
  * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
2417
2439
  * not spell an empty list. */
@@ -2548,6 +2570,19 @@ var DeliveryConfigSchema = z4.object({
2548
2570
  * carries credentials; only a `poll` registration can come back with null here. */
2549
2571
  realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
2550
2572
  });
2573
+ var HostDecisionSchema = z4.object({
2574
+ /** The agent's token id: the row's `recipient`. */
2575
+ agent: z4.string().uuid(),
2576
+ decision: z4.enum(["stood_back", "took_over"]),
2577
+ /** The work it was about: the Goal `claim_goal` would hand that agent next. */
2578
+ goalId: z4.string().uuid().nullable().optional(),
2579
+ /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
2580
+ seenAt: z4.string().datetime().nullable().optional(),
2581
+ /** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
2582
+ since: z4.string().datetime().nullable().optional(),
2583
+ /** What the host said, in its log's own words: why it stood back, or what the take-over did. */
2584
+ said: z4.string().max(300).optional()
2585
+ });
2551
2586
  var WakeNudgeSchema = z4.object({
2552
2587
  kind: z4.enum(["reply", "request", "callback"]),
2553
2588
  notificationId: z4.string().optional(),
@@ -2592,6 +2627,9 @@ var DeviceTokenSchema = z4.object({
2592
2627
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
2593
2628
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
2594
2629
  workspace: z4.string().nullable().optional(),
2630
+ /** Local host recovery must preserve the launch's runtime and Paigy identity. */
2631
+ harness: z4.enum(["claude", "codex", "agy"]).optional(),
2632
+ session_id: z4.string().uuid().optional(),
2595
2633
  uik_pub: z4.string().nullable().optional()
2596
2634
  });
2597
2635
  var SupportRequestSchema = z4.object({
@@ -2771,7 +2809,7 @@ function updateSlot(agent2, patch) {
2771
2809
  }
2772
2810
  function slotIdentity(agent2) {
2773
2811
  const t = readTokenFile()[agent2];
2774
- return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
2812
+ return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null };
2775
2813
  }
2776
2814
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2777
2815
  function readToken(agent2 = agentName()) {
@@ -2835,9 +2873,21 @@ var UnpairedError = class extends Error {
2835
2873
  this.name = "UnpairedError";
2836
2874
  }
2837
2875
  };
2838
- function ensureAuthed(res) {
2839
- if (res.status === 401) throw new UnpairedError();
2840
- return res;
2876
+ var RevokedError = class extends UnpairedError {
2877
+ constructor() {
2878
+ super();
2879
+ this.message = "This identity was revoked (the API answered 401 revoked): its credential is gone for good. Pairing again makes a new identity.";
2880
+ this.name = "RevokedError";
2881
+ }
2882
+ };
2883
+ async function ensureAuthed(res) {
2884
+ if (res.status !== 401) return res;
2885
+ let body = null;
2886
+ try {
2887
+ body = await res.json();
2888
+ } catch {
2889
+ }
2890
+ throw body?.error === "revoked" ? new RevokedError() : new UnpairedError();
2841
2891
  }
2842
2892
  var ApiError = class extends Error {
2843
2893
  status;
@@ -2864,7 +2914,7 @@ async function fail(what, res) {
2864
2914
  }
2865
2915
  async function createGoal(input, opts = {}) {
2866
2916
  const token = authToken(opts.token) ?? "";
2867
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
2917
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
2868
2918
  method: "POST",
2869
2919
  headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
2870
2920
  body: JSON.stringify(input)
@@ -2874,13 +2924,14 @@ async function createGoal(input, opts = {}) {
2874
2924
  }
2875
2925
  async function claimGoal(goalId, opts = {}) {
2876
2926
  const token = authToken(opts.token) ?? "";
2877
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
2927
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
2878
2928
  if (!res.ok) await fail("claim_goal", res);
2879
2929
  return await res.json();
2880
2930
  }
2881
2931
  async function getGoal(goalId, opts = {}, read = {}) {
2882
2932
  const token = authToken(opts.token) ?? "";
2883
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
2933
+ const query = [read.history ? "history=1" : "", read.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
2934
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
2884
2935
  method: "GET",
2885
2936
  headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
2886
2937
  }));
@@ -2890,13 +2941,13 @@ async function getGoal(goalId, opts = {}, read = {}) {
2890
2941
  async function updateGoal(goalId, input, opts = {}) {
2891
2942
  const token = authToken(opts.token) ?? "";
2892
2943
  const operationId = input.operationId ?? randomUUID2();
2893
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
2944
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
2894
2945
  if (!res.ok) await fail("update_goal", res);
2895
2946
  return await res.json();
2896
2947
  }
2897
2948
  var AWAIT_WINDOW_MS = 45e3;
2898
2949
  async function hatch(name, voice = null) {
2899
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
2950
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
2900
2951
  method: "POST",
2901
2952
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
2902
2953
  body: JSON.stringify({ name, voice })
@@ -2918,7 +2969,7 @@ var NameTakenError = class extends Error {
2918
2969
  code = "agent_name_taken";
2919
2970
  };
2920
2971
  async function setIdentity(patch, opts = {}) {
2921
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/identity`, {
2972
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/identity`, {
2922
2973
  method: "PATCH",
2923
2974
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
2924
2975
  body: JSON.stringify(patch)
@@ -2938,19 +2989,27 @@ async function setIdentity(patch, opts = {}) {
2938
2989
  return await res.json();
2939
2990
  }
2940
2991
  async function claimSessions(opts = {}) {
2941
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
2992
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
2942
2993
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
2943
2994
  // the HOST's identity
2944
2995
  }));
2945
2996
  if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
2946
2997
  return (await res.json()).sessions;
2947
2998
  }
2999
+ async function recordDecision(decision, opts = {}) {
3000
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
3001
+ method: "POST",
3002
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
3003
+ body: JSON.stringify(decision)
3004
+ }));
3005
+ if (!res.ok) await fail("record_decision", res);
3006
+ }
2948
3007
  async function heartbeat(runtime, opts = {}) {
2949
3008
  const body = {
2950
3009
  ...runtime !== void 0 ? { runtime } : {},
2951
3010
  ...opts.activity !== void 0 ? { activity: opts.activity } : {}
2952
3011
  };
2953
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
3012
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
2954
3013
  method: "POST",
2955
3014
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
2956
3015
  ...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
@@ -2958,7 +3017,7 @@ async function heartbeat(runtime, opts = {}) {
2958
3017
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
2959
3018
  }
2960
3019
  async function registerDelivery(mode, opts = {}) {
2961
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
3020
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
2962
3021
  method: "POST",
2963
3022
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
2964
3023
  body: JSON.stringify({ mode })
@@ -2967,21 +3026,21 @@ async function registerDelivery(mode, opts = {}) {
2967
3026
  return await res.json();
2968
3027
  }
2969
3028
  async function listNotes(opts = {}) {
2970
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
3029
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
2971
3030
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
2972
3031
  }));
2973
3032
  if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
2974
3033
  return await res.json();
2975
3034
  }
2976
3035
  async function listConnections(opts = {}) {
2977
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
3036
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
2978
3037
  headers: { authorization: `Bearer ${authToken(opts.token)}`, "x-paigy-model": "goal-entry-v1" }
2979
3038
  }));
2980
3039
  if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
2981
3040
  return await res.json();
2982
3041
  }
2983
3042
  async function submitTriage(run, opts = {}) {
2984
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
3043
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
2985
3044
  method: "POST",
2986
3045
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
2987
3046
  body: JSON.stringify(run)
@@ -2990,14 +3049,14 @@ async function submitTriage(run, opts = {}) {
2990
3049
  return await res.json();
2991
3050
  }
2992
3051
  async function getTriage(opts = {}) {
2993
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
3052
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
2994
3053
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
2995
3054
  }));
2996
3055
  if (!res.ok) throw new Error(`get_triage failed: ${res.status} ${await res.text()}`);
2997
3056
  return await res.json();
2998
3057
  }
2999
3058
  async function acceptTriage(proposalId, body, opts = {}) {
3000
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
3059
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
3001
3060
  method: "POST",
3002
3061
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
3003
3062
  body: JSON.stringify(body)
@@ -3006,7 +3065,7 @@ async function acceptTriage(proposalId, body, opts = {}) {
3006
3065
  return await res.json();
3007
3066
  }
3008
3067
  async function dismissTriage(proposalId, opts = {}) {
3009
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/dismiss`, {
3068
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/dismiss`, {
3010
3069
  method: "POST",
3011
3070
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
3012
3071
  }));
@@ -3062,7 +3121,7 @@ async function contact(input, opts = {}) {
3062
3121
  ask: a.ask,
3063
3122
  options: a.options ?? []
3064
3123
  }));
3065
- const res = ensureAuthed(await send2(`${BACKEND_URL}/api/goals/contact`, {
3124
+ const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/goals/contact`, {
3066
3125
  method: "POST",
3067
3126
  headers,
3068
3127
  signal: opts.signal,
@@ -3084,15 +3143,17 @@ async function contact(input, opts = {}) {
3084
3143
  }
3085
3144
  const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
3086
3145
  const joinedCard = deliveries[0]?.joinedCard === true;
3146
+ const joinedCall = deliveries[0]?.joinedCall === true;
3087
3147
  const read = async (signal2) => {
3088
- const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
3148
+ const res = await ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
3089
3149
  if (!res.ok) await fail("read_delivery", res);
3090
3150
  const one = await res.json();
3091
3151
  return {
3092
3152
  ...one,
3093
3153
  ...all.length > 1 ? { deliveries: all } : {},
3094
3154
  ...recorded.length ? { recorded, notSent: NOT_SENT } : {},
3095
- ...joinedCard ? { joinedCard: true } : {}
3155
+ ...joinedCard ? { joinedCard: true } : {},
3156
+ ...joinedCall ? { joinedCall: true } : {}
3096
3157
  };
3097
3158
  };
3098
3159
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
@@ -3114,7 +3175,7 @@ async function contact(input, opts = {}) {
3114
3175
  }
3115
3176
  }
3116
3177
  async function checkReplies(opts = {}) {
3117
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
3178
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
3118
3179
  headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
3119
3180
  }));
3120
3181
  if (!res.ok) await fail("check_replies", res);
@@ -3206,6 +3267,7 @@ function goalView(g) {
3206
3267
  more: more(g)
3207
3268
  });
3208
3269
  }
3270
+ var JOINED_CALL = "This ask joined the call the person is ALREADY ON \u2014 no ring, no card of its own. `conversation` above is that whole call, so the answers already given on it before your ask arrived are in it. Yours is queued for the bot's next turn: call contact({deliveryId}) again to keep reading this same call until your answer lands \u2014 do not resend the ask. To add information or a further question to the same call, contact again while it is live; that joins it too.";
3209
3271
  function deliveryView(d) {
3210
3272
  const answers = "Answers land on the Goal: read them with claim_goal or get_goal. Do not poll this notification.";
3211
3273
  return compact({
@@ -3220,7 +3282,8 @@ function deliveryView(d) {
3220
3282
  recorded: d.recorded,
3221
3283
  notSent: d.notSent,
3222
3284
  joinedCard: d.joinedCard,
3223
- next: d.kind === "notification" ? d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers : d.message
3285
+ 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 })}) now to continue this same Call; do not resend the ask or end your turn merely because this wait window returned. ${d.message ?? ""}`.trim() : d.message
3224
3287
  });
3225
3288
  }
3226
3289
  function repliesView(r) {
@@ -3255,7 +3318,11 @@ function repliesView(r) {
3255
3318
  next: [
3256
3319
  deliveries.length || r.claimable || review.length || assigned.length ? "Act on a Goal with claim_goal; reread one Delivery in full with contact({deliveryId})." : "Nothing is waiting.",
3257
3320
  ...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): claim_goal({goalId}) to start one.`] : [],
3258
- ...review.length ? [`${review.length} of your Goals have something new (review): read each with claim_goal or get_goal, then update_goal with reviewed: true.`] : [],
3321
+ // THE PERSON SPOKE, AND THAT IS NOT "SOMETHING NEW" (2026-09-29). This line said only how
3322
+ // many Goals were flagged, so four things the owner said on a call read as four chores. The
3323
+ // rows carry their words now (`said`, `saidAt`, newest first, `delivery/read.ts`); the
3324
+ // sentence names the person and quotes the freshest one, because a count is not a message.
3325
+ ...review.length ? [spoke(review) ?? `${review.length} of your Goals have something new (review): read each with claim_goal or get_goal, then update_goal with reviewed: true.`] : [],
3259
3326
  ...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
3260
3327
  ...others.length ? [`${others.length} of other agents' Goals have gone quiet for 3 days: claim_goal({goalId}) takes one over, then finish or cancel it with update_goal.`] : [],
3261
3328
  // No name of its own (#2525): the API's ask, verbatim.
@@ -3263,6 +3330,13 @@ function repliesView(r) {
3263
3330
  ].join(" ")
3264
3331
  };
3265
3332
  }
3333
+ function spoke(review) {
3334
+ const withWords = review.filter((r) => r.said?.trim());
3335
+ if (!withWords.length) return null;
3336
+ const newest = withWords[0];
3337
+ const rest = review.length - 1;
3338
+ return `The person has spoken on ${withWords.length === 1 ? "one of your Goals" : `${withWords.length} of your Goals`} \u2014 newest, on ${newest.title ?? newest.goalId}: "${newest.said}". Read it with claim_goal or get_goal, answer what it asks, then update_goal with reviewed: true.${rest > 0 ? ` (${rest} more flagged; see review.)` : ""}`;
3339
+ }
3266
3340
  async function runTool(name, args, opts) {
3267
3341
  const { waits, signal, ...client } = opts;
3268
3342
  const input = args ?? {};
@@ -3283,8 +3357,11 @@ async function runTool(name, args, opts) {
3283
3357
  return goalView(await claimGoal(goalId, client));
3284
3358
  }
3285
3359
  case "get_goal": {
3286
- const { goalId, history } = GetGoalSchema.parse(input);
3287
- return goalView(await getGoal(goalId, client, { history }));
3360
+ const { goalId, history, diagnose } = GetGoalSchema.parse(input);
3361
+ const goal = await getGoal(goalId, client, { history, diagnose });
3362
+ const view = goalView(goal);
3363
+ const diagnosis = goal.diagnosis;
3364
+ return diagnosis === void 0 ? view : { ...view, diagnosis };
3288
3365
  }
3289
3366
  case "update_goal": {
3290
3367
  const { goalId, ...body } = UpdateGoalToolSchema.parse(input);
@@ -3499,6 +3576,7 @@ export {
3499
3576
  requestCode,
3500
3577
  pollToken,
3501
3578
  UnpairedError,
3579
+ RevokedError,
3502
3580
  ApiError,
3503
3581
  overrideToken,
3504
3582
  authToken,
@@ -3512,6 +3590,7 @@ export {
3512
3590
  NameTakenError,
3513
3591
  setIdentity,
3514
3592
  claimSessions,
3593
+ recordDecision,
3515
3594
  heartbeat,
3516
3595
  registerDelivery,
3517
3596
  listNotes,
@@ -4,10 +4,10 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-HGXXGOGG.js";
7
+ } from "./chunk-RLN2B5IZ.js";
8
8
  import {
9
9
  agentName
10
- } from "./chunk-VYWIPFEY.js";
10
+ } from "./chunk-6C6CO7H3.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.";
@@ -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; `apps/api/src/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,15 @@ 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.`;
146
153
  }
147
154
  function entryWords(entry) {
148
155
  const content = entry.content;
@@ -750,18 +757,20 @@ var UserSettingsSchema = z4.object({
750
757
  * clobber guard as voiceMode: absent = leave unchanged on write. */
751
758
  broker: BrokerTuningSchema.optional()
752
759
  });
753
- var HistoryItemSchema = z4.object({
760
+ var HistoryWorkSchema = z4.object({
754
761
  id: z4.string(),
755
- /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
756
- initiator: z4.enum(["user", "agent"]),
757
762
  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()
763
+ state: z4.enum(["done", "cancelled"]),
764
+ /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
765
+ assignee: z4.string()
766
+ });
767
+ var HistoryEntrySchema = z4.union([
768
+ z4.object({ at: z4.string(), card: InboxItemSchema }),
769
+ z4.object({ at: z4.string(), work: HistoryWorkSchema })
770
+ ]);
771
+ var HistoryPageSchema = z4.object({
772
+ entries: z4.array(HistoryEntrySchema),
773
+ next: z4.string().nullable()
765
774
  });
766
775
  var ACTIVITY_LINES = 2;
767
776
  var ACTIVITY_LINE_MAX = 80;
@@ -945,6 +954,9 @@ var QueueItemSchema = z4.object({
945
954
  progressLine: z4.string().nullable().optional(),
946
955
  reviewPending: z4.boolean().default(false),
947
956
  dueAt: z4.string().nullable().default(null),
957
+ /** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
958
+ * with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
959
+ finishedAt: z4.string().nullable().optional(),
948
960
  /** The Goal this one was opened under; null at the root. */
949
961
  parentGoalId: z4.string().nullable().default(null),
950
962
  /** Goals opened under this one — only those the same list holds. */
@@ -953,8 +965,17 @@ var QueueItemSchema = z4.object({
953
965
  dependencyGoalIds: z4.array(z4.string()).default([]),
954
966
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
955
967
  blocked: z4.boolean().default(false),
956
- /** Every decision need on it, open or settled — the page decides which to show. */
968
+ /** Its questions: every OPEN one, and at most ten settled, newest settled first
969
+ * (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
970
+ * `answered` are, and a settled one's words are a line (280 characters), its body read when the
971
+ * question is opened. */
957
972
  questions: z4.array(QueueQuestionSchema).default([]),
973
+ /** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
974
+ * bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
975
+ * right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
976
+ * by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
977
+ asked: z4.number().optional(),
978
+ answered: z4.number().optional(),
958
979
  /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
959
980
  * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
960
981
  * not spell an empty list. */
@@ -1089,6 +1110,19 @@ var DeliveryConfigSchema = z4.object({
1089
1110
  * carries credentials; only a `poll` registration can come back with null here. */
1090
1111
  realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
1091
1112
  });
1113
+ var HostDecisionSchema = z4.object({
1114
+ /** The agent's token id: the row's `recipient`. */
1115
+ agent: z4.string().uuid(),
1116
+ decision: z4.enum(["stood_back", "took_over"]),
1117
+ /** The work it was about: the Goal `claim_goal` would hand that agent next. */
1118
+ goalId: z4.string().uuid().nullable().optional(),
1119
+ /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
1120
+ seenAt: z4.string().datetime().nullable().optional(),
1121
+ /** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
1122
+ since: z4.string().datetime().nullable().optional(),
1123
+ /** What the host said, in its log's own words: why it stood back, or what the take-over did. */
1124
+ said: z4.string().max(300).optional()
1125
+ });
1092
1126
  var WakeNudgeSchema = z4.object({
1093
1127
  kind: z4.enum(["reply", "request", "callback"]),
1094
1128
  notificationId: z4.string().optional(),
@@ -1133,6 +1167,9 @@ var DeviceTokenSchema = z4.object({
1133
1167
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
1134
1168
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
1135
1169
  workspace: z4.string().nullable().optional(),
1170
+ /** Local host recovery must preserve the launch's runtime and Paigy identity. */
1171
+ harness: z4.enum(["claude", "codex", "agy"]).optional(),
1172
+ session_id: z4.string().uuid().optional(),
1136
1173
  uik_pub: z4.string().nullable().optional()
1137
1174
  });
1138
1175
  var SupportRequestSchema = z4.object({
@@ -6,7 +6,7 @@ import {
6
6
  saveToken,
7
7
  setIdentity,
8
8
  sleep
9
- } from "./chunk-VYWIPFEY.js";
9
+ } from "./chunk-6C6CO7H3.js";
10
10
 
11
11
  // src/identity.ts
12
12
  var CLIENT_LABELS = {
@@ -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-6C6CO7H3.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-6MEUSSRH.js";
7
+ import "./chunk-RLN2B5IZ.js";
8
+ import "./chunk-6C6CO7H3.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-YNKTKLQD.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-6MEUSSRH.js";
19
19
  import {
20
20
  decideListen,
21
21
  listenerAlive
22
- } from "./chunk-HGXXGOGG.js";
22
+ } from "./chunk-RLN2B5IZ.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-6C6CO7H3.js";
43
43
 
44
44
  // src/index.ts
45
45
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
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-RLN2B5IZ.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-6C6CO7H3.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,
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-TRMYDJ5H.js";
6
+ } from "./chunk-YNKTKLQD.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
10
  openBrowser
11
- } from "./chunk-76ILN3YV.js";
12
- import "./chunk-HGXXGOGG.js";
11
+ } from "./chunk-6MEUSSRH.js";
12
+ import "./chunk-RLN2B5IZ.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-VYWIPFEY.js";
23
+ } from "./chunk-6C6CO7H3.js";
24
24
 
25
25
  // src/onboard.ts
26
26
  function reportRegistered() {
package/dist/slot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  agentName
4
- } from "./chunk-VYWIPFEY.js";
4
+ } from "./chunk-6C6CO7H3.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(agentName());
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-GJOOJ4FR.js");
32
+ const { checkReplies } = await import("./dist-33DA5X2G.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 });
@@ -7,7 +7,7 @@ import {
7
7
  readToken,
8
8
  sessionSlot,
9
9
  slotName
10
- } from "./chunk-VYWIPFEY.js";
10
+ } from "./chunk-6C6CO7H3.js";
11
11
 
12
12
  // src/statusline.ts
13
13
  import { realpathSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.40.19",
3
+ "version": "0.40.20",
4
4
  "description": "Paigy MCP server — the AI agent harness that calls you. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",