@paigy/mcp 0.40.16 → 0.40.17

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-SFGGZLVO.js";
7
+ } from "./chunk-MWHYYLCE.js";
8
8
  import {
9
9
  agentName
10
- } from "./chunk-6OBUH2FB.js";
10
+ } from "./chunk-V5AQASR7.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.";
@@ -68,7 +68,7 @@ var StartContactSchema = z2.object({
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.";
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).";
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
@@ -115,9 +115,9 @@ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({
115
115
  var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
116
116
  var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
117
117
  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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
118
- 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. Returns the Goal as get_goal reads it, at its new revision.";
118
+ 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.`;
119
119
  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.";
120
- 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; 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.";
120
+ 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.";
121
121
  var CheckRepliesSchema = z3.object({}).strict();
122
122
  var AGENT_TOOLS = [
123
123
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
@@ -472,9 +472,9 @@ var AgendaTurnSchema = z4.object({
472
472
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
473
473
  * the planner declares duplicates instead of asking them twice. */
474
474
  coveredIds: z4.array(z4.string()).optional(),
475
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
476
- * line went out on 2026-07-28 and the caller could not answer it at all. */
477
- info: z4.array(z4.string().min(1)).max(3).default([]),
475
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
476
+ * (owner, 2026-09-25), and a count here refused whole plans. */
477
+ info: z4.array(z4.string().min(1)).default([]),
478
478
  question: z4.string().min(1).nullable(),
479
479
  /** True on the one turn carrying the agent's own declared question. */
480
480
  asks: z4.boolean().optional(),
@@ -585,6 +585,11 @@ var InboxItemSchema = z4.object({
585
585
  * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
586
586
  cold: z4.string().datetime().optional(),
587
587
  clarifies: z4.string().optional(),
588
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
589
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
590
+ * off the same open list the card came from, so it clears when the Call does. A card is the
591
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
592
+ onCall: z4.literal(true).optional(),
588
593
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
589
594
  * declined, and what the ladder will do next — read off the cron's own row, never computed
590
595
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -1149,7 +1154,8 @@ next check_replies. Start your own listener now, as a BACKGROUND task whose outp
1149
1154
  npx -y -p @paigy/mcp@latest paigy-listen
1150
1155
 
1151
1156
  Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
1152
- the answer needs a decision. Start it once; if it is already running, do nothing.
1157
+ the answer needs a decision. Start it once; if it is already running, do nothing. Then call
1158
+ claim_goal once: work handed to you before this session started waits there.
1153
1159
  PAIGY
1154
1160
  exit 0
1155
1161
  `;
@@ -4,7 +4,7 @@ import {
4
4
  requestCode,
5
5
  saveToken,
6
6
  sleep
7
- } from "./chunk-6OBUH2FB.js";
7
+ } from "./chunk-V5AQASR7.js";
8
8
 
9
9
  // src/identity.ts
10
10
  var CLIENT_LABELS = {
@@ -1536,7 +1536,7 @@ var StartContactSchema = z2.object({
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.";
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).";
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
@@ -1583,9 +1583,9 @@ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({
1583
1583
  var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
1584
1584
  var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
1585
1585
  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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
1586
- 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. Returns the Goal as get_goal reads it, at its new revision.";
1586
+ 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.`;
1587
1587
  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.";
1588
- 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; 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.";
1588
+ 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.";
1589
1589
  var CheckRepliesSchema = z3.object({}).strict();
1590
1590
  var AGENT_TOOLS = [
1591
1591
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
@@ -1930,9 +1930,9 @@ var AgendaTurnSchema = z4.object({
1930
1930
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
1931
1931
  * the planner declares duplicates instead of asking them twice. */
1932
1932
  coveredIds: z4.array(z4.string()).optional(),
1933
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
1934
- * line went out on 2026-07-28 and the caller could not answer it at all. */
1935
- info: z4.array(z4.string().min(1)).max(3).default([]),
1933
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
1934
+ * (owner, 2026-09-25), and a count here refused whole plans. */
1935
+ info: z4.array(z4.string().min(1)).default([]),
1936
1936
  question: z4.string().min(1).nullable(),
1937
1937
  /** True on the one turn carrying the agent's own declared question. */
1938
1938
  asks: z4.boolean().optional(),
@@ -2043,6 +2043,11 @@ var InboxItemSchema = z4.object({
2043
2043
  * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
2044
2044
  cold: z4.string().datetime().optional(),
2045
2045
  clarifies: z4.string().optional(),
2046
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
2047
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
2048
+ * off the same open list the card came from, so it clears when the Call does. A card is the
2049
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
2050
+ onCall: z4.literal(true).optional(),
2046
2051
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
2047
2052
  * declined, and what the ladder will do next — read off the cron's own row, never computed
2048
2053
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -2899,6 +2904,7 @@ function currentRepo(cwd = process.cwd()) {
2899
2904
  }
2900
2905
  return asked;
2901
2906
  }
2907
+ var NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
2902
2908
  async function contact(input, opts = {}) {
2903
2909
  const parsed = ContactSchema.parse(input);
2904
2910
  opts.signal?.throwIfAborted();
@@ -2906,6 +2912,7 @@ async function contact(input, opts = {}) {
2906
2912
  const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
2907
2913
  let deliveryId;
2908
2914
  let deliveries = [];
2915
+ let recorded = [];
2909
2916
  if ("deliveryId" in parsed) {
2910
2917
  deliveryId = parsed.deliveryId;
2911
2918
  } else {
@@ -2934,19 +2941,24 @@ async function contact(input, opts = {}) {
2934
2941
  if (!res.ok) await fail("batch contact", res);
2935
2942
  const receipt = await res.json();
2936
2943
  deliveries = receipt.deliveries ?? [];
2937
- if (deliveries.length === 0 && receipt.recorded?.length) {
2938
- return { recorded: receipt.recorded, message: receipt.message ?? "Recorded as progress on its Goal; nothing was sent to the person." };
2939
- }
2944
+ recorded = receipt.recorded ?? [];
2945
+ if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
2940
2946
  if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
2941
2947
  deliveryId = deliveries[0]?.deliveryId ?? "";
2942
2948
  if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
2943
2949
  }
2944
- const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
2950
+ const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
2951
+ const joinedCard = deliveries[0]?.joinedCard === true;
2945
2952
  const read = async (signal2) => {
2946
2953
  const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
2947
2954
  if (!res.ok) await fail("read_delivery", res);
2948
2955
  const one = await res.json();
2949
- return all.length > 1 ? { ...one, deliveries: all } : one;
2956
+ return {
2957
+ ...one,
2958
+ ...all.length > 1 ? { deliveries: all } : {},
2959
+ ...recorded.length ? { recorded, notSent: NOT_SENT } : {},
2960
+ ...joinedCard ? { joinedCard: true } : {}
2961
+ };
2950
2962
  };
2951
2963
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
2952
2964
  if (opts.waits === false) return read(opts.signal);
@@ -3044,6 +3056,7 @@ function goalView(g) {
3044
3056
  });
3045
3057
  }
3046
3058
  function deliveryView(d) {
3059
+ const answers = "Answers land on the Goal: read them with claim_goal or get_goal. Do not poll this notification.";
3047
3060
  return compact({
3048
3061
  deliveryId: d.deliveryId,
3049
3062
  kind: d.kind,
@@ -3052,11 +3065,16 @@ function deliveryView(d) {
3052
3065
  conversation: conversation(d),
3053
3066
  // Each ask's own Delivery when a contact carried several — the only handle on the others.
3054
3067
  deliveries: d.deliveries,
3055
- next: d.kind === "notification" ? "Answers land on the Goal: read them with claim_goal or get_goal. Do not poll this notification." : d.message
3068
+ // WHAT THIS CONTACT DID NOT SEND (#2450): asks the intake read kept as progress, by name.
3069
+ recorded: d.recorded,
3070
+ notSent: d.notSent,
3071
+ joinedCard: d.joinedCard,
3072
+ next: d.kind === "notification" ? d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers : d.message
3056
3073
  });
3057
3074
  }
3058
3075
  function repliesView(r) {
3059
3076
  const deliveries = r.deliveries ?? [];
3077
+ const assigned = r.assigned ?? [];
3060
3078
  const review = r.review ?? [];
3061
3079
  const stalled = r.stalled ?? [];
3062
3080
  const others = r.stalledOthers ?? [];
@@ -3068,6 +3086,9 @@ function repliesView(r) {
3068
3086
  openDecisions: d.decisionNeeds.filter((n) => n.state === "open").length || void 0,
3069
3087
  latest: "about" in d ? d.about : void 0
3070
3088
  })),
3089
+ // WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they
3090
+ // became yours — a Goal handed to you is not a Delivery, and must not wait for a claim to be seen.
3091
+ ...assigned.length ? { assigned } : {},
3071
3092
  // NEW ON YOUR GOALS (owner, 2026-09-23): a reply is an Entry on the Goal, not a Delivery, so the
3072
3093
  // Goals flagged for your review are listed here until you acknowledge them.
3073
3094
  ...review.length ? { review } : {},
@@ -3077,7 +3098,8 @@ function repliesView(r) {
3077
3098
  // OTHER AGENTS' STALLED WORK (owner, 2026-09-23): any agent may take it over and close it.
3078
3099
  ...others.length ? { stalledOthers: others } : {},
3079
3100
  next: [
3080
- deliveries.length || review.length ? "Act on a Goal with claim_goal; reread one Delivery in full with contact({deliveryId})." : "Nothing is waiting.",
3101
+ deliveries.length || review.length || assigned.length ? "Act on a Goal with claim_goal; reread one Delivery in full with contact({deliveryId})." : "Nothing is waiting.",
3102
+ ...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): claim_goal({goalId}) to start one.`] : [],
3081
3103
  ...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.`] : [],
3082
3104
  ...stalled.length ? [`${stalled.length} of your Goals have had no progress for 3 days: update_goal each with progress, finish it, or cancel it.`] : [],
3083
3105
  ...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.`] : []
@@ -3090,7 +3112,7 @@ async function runTool(name, args, opts) {
3090
3112
  switch (name) {
3091
3113
  case "contact": {
3092
3114
  const sent = await contact(input, { ...client, signal, waits });
3093
- return "recorded" in sent ? sent : deliveryView(sent);
3115
+ return "sent" in sent ? sent : deliveryView(sent);
3094
3116
  }
3095
3117
  case "check_replies":
3096
3118
  CheckRepliesSchema.parse(input);
@@ -3118,47 +3140,108 @@ async function runTool(name, args, opts) {
3118
3140
  var HEARTBEAT_MS = 25e3;
3119
3141
  var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
3120
3142
  var JOIN = { config: { broadcast: { self: false } } };
3143
+ var JOIN_TIMEOUT_MS = 1e4;
3121
3144
  var lines = /* @__PURE__ */ new Map();
3122
3145
  function frame(line, topic, event, payload) {
3146
+ const ref = String(++line.ref);
3123
3147
  try {
3124
- line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
3148
+ line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
3125
3149
  } catch {
3126
3150
  }
3151
+ return ref;
3127
3152
  }
3128
3153
  function stop(line) {
3129
3154
  line.open = false;
3155
+ line.beat = void 0;
3130
3156
  clearInterval(line.heart);
3131
3157
  clearTimeout(line.retry);
3158
+ for (const t of line.topics.values()) {
3159
+ clearTimeout(t.timer);
3160
+ t.joined = false;
3161
+ t.ref = void 0;
3162
+ }
3163
+ }
3164
+ function join3(line, name, t) {
3165
+ clearTimeout(t.timer);
3166
+ t.joined = false;
3167
+ t.ref = frame(line, `realtime:${name}`, "phx_join", JOIN);
3168
+ t.timer = setTimeout(() => rejoin(line, name, t), JOIN_TIMEOUT_MS);
3169
+ }
3170
+ function rejoin(line, name, t) {
3171
+ clearTimeout(t.timer);
3172
+ t.joined = false;
3173
+ t.ref = void 0;
3174
+ if (!line.open || line.topics.get(name) !== t) return;
3175
+ t.timer = setTimeout(() => {
3176
+ if (line.open && line.topics.get(name) === t) join3(line, name, t);
3177
+ }, RETRY_MS[Math.min(t.attempt++, RETRY_MS.length - 1)]);
3178
+ }
3179
+ function joined(t) {
3180
+ clearTimeout(t.timer);
3181
+ t.joined = true;
3182
+ t.attempt = 0;
3183
+ for (const l of t.listeners) l.onJoined?.(++l.joins);
3132
3184
  }
3133
3185
  function connect(line) {
3134
3186
  const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
3135
3187
  const ws = line.ws = new WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
3188
+ let gone = false;
3189
+ const again = () => {
3190
+ if (gone) return;
3191
+ gone = true;
3192
+ stop(line);
3193
+ if (!line.topics.size) return;
3194
+ line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
3195
+ };
3136
3196
  ws.addEventListener("open", () => {
3197
+ if (gone) return;
3137
3198
  line.open = true;
3138
3199
  line.attempt = 0;
3139
- for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
3140
- line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
3200
+ for (const [name, t] of line.topics) join3(line, name, t);
3201
+ line.heart = setInterval(() => {
3202
+ if (line.beat) {
3203
+ try {
3204
+ ws.close();
3205
+ } catch {
3206
+ }
3207
+ again();
3208
+ return;
3209
+ }
3210
+ line.beat = frame(line, "phoenix", "heartbeat", {});
3211
+ }, HEARTBEAT_MS);
3141
3212
  });
3142
3213
  ws.addEventListener("message", (e) => {
3214
+ if (gone) return;
3143
3215
  let m;
3144
3216
  try {
3145
3217
  m = JSON.parse(String(e.data));
3146
3218
  } catch {
3147
3219
  return;
3148
3220
  }
3149
- if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
3150
- for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
3221
+ if (m.topic === "phoenix") {
3222
+ if (m.event === "phx_reply" && m.ref === line.beat) line.beat = void 0;
3223
+ return;
3224
+ }
3225
+ if (!m.topic?.startsWith("realtime:")) return;
3226
+ const name = m.topic.slice("realtime:".length);
3227
+ const t = line.topics.get(name);
3228
+ if (!t) return;
3229
+ if (m.event === "phx_reply") {
3230
+ if (!t.ref || m.ref !== t.ref) return;
3231
+ if (m.payload?.status === "ok") joined(t);
3232
+ else rejoin(line, name, t);
3233
+ return;
3234
+ }
3235
+ if (m.event === "phx_error" || m.event === "phx_close") {
3236
+ if (m.ref && m.ref !== t.ref) return;
3237
+ rejoin(line, name, t);
3238
+ return;
3239
+ }
3240
+ if (m.event !== "broadcast") return;
3241
+ for (const l of t.listeners) {
3151
3242
  if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
3152
3243
  }
3153
3244
  });
3154
- let gone = false;
3155
- const again = () => {
3156
- if (gone) return;
3157
- gone = true;
3158
- stop(line);
3159
- if (!line.topics.size) return;
3160
- line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
3161
- };
3162
3245
  ws.addEventListener("close", again);
3163
3246
  ws.addEventListener("error", again);
3164
3247
  }
@@ -3166,17 +3249,22 @@ function subscribeRealtime(args) {
3166
3249
  const key = `${args.url} ${args.anonKey}`;
3167
3250
  let line = lines.get(key);
3168
3251
  if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
3169
- let heard = line.topics.get(args.topic);
3170
- if (!heard) {
3171
- line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
3172
- if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
3173
- }
3174
- const me = { event: args.event, onMessage: args.onMessage };
3175
- heard.add(me);
3252
+ let t = line.topics.get(args.topic);
3253
+ if (!t) {
3254
+ line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
3255
+ if (line.open) join3(line, args.topic, t);
3256
+ }
3257
+ const topic = t;
3258
+ const me = { event: args.event, onMessage: args.onMessage, onJoined: args.onJoined, joins: 0 };
3259
+ topic.listeners.add(me);
3260
+ if (topic.joined && me.onJoined) queueMicrotask(() => {
3261
+ if (topic.joined && topic.listeners.has(me)) me.onJoined?.(++me.joins);
3262
+ });
3176
3263
  if (!line.ws) connect(line);
3177
3264
  return {
3178
3265
  close: () => {
3179
- if (!heard.delete(me) || heard.size) return;
3266
+ if (!topic.listeners.delete(me) || topic.listeners.size) return;
3267
+ clearTimeout(topic.timer);
3180
3268
  line.topics.delete(args.topic);
3181
3269
  if (line.topics.size) {
3182
3270
  if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
@@ -3192,7 +3280,7 @@ function subscribeRealtime(args) {
3192
3280
  };
3193
3281
  }
3194
3282
  async function subscribeWake(onNudge, opts = {}) {
3195
- const { release = true, ...client } = opts;
3283
+ const { release = true, onJoined, ...client } = opts;
3196
3284
  const cfg = await registerDelivery("self_hosted", client);
3197
3285
  const { url, anonKey } = cfg.realtime;
3198
3286
  const channel = wakeChannel(cfg.tokenId);
@@ -3201,7 +3289,8 @@ async function subscribeWake(onNudge, opts = {}) {
3201
3289
  anonKey,
3202
3290
  topic: channel,
3203
3291
  event: WAKE_EVENT,
3204
- onMessage: (payload) => onNudge(payload ?? {})
3292
+ onMessage: (payload) => onNudge(payload ?? {}),
3293
+ onJoined
3205
3294
  });
3206
3295
  return {
3207
3296
  tokenId: cfg.tokenId,
@@ -52,7 +52,7 @@ import {
52
52
  updateGoal,
53
53
  updateSlot,
54
54
  whoAmI
55
- } from "./chunk-6OBUH2FB.js";
55
+ } from "./chunk-V5AQASR7.js";
56
56
  export {
57
57
  AGENT_TOOLS,
58
58
  AGENT_TOOL_NAMES,
package/dist/enable.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools,
5
5
  installSessionListening
6
- } from "./chunk-JWMR6K6D.js";
7
- import "./chunk-SFGGZLVO.js";
8
- import "./chunk-6OBUH2FB.js";
6
+ } from "./chunk-7VLZIOLF.js";
7
+ import "./chunk-MWHYYLCE.js";
8
+ import "./chunk-V5AQASR7.js";
9
9
 
10
10
  // src/enable.ts
11
11
  function main() {
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  resolvePairing,
6
6
  startPairing,
7
7
  suggestedAgentName
8
- } from "./chunk-OGTH55QH.js";
8
+ } from "./chunk-UIZLBPRG.js";
9
9
  import {
10
10
  ENABLE_COMMAND,
11
11
  OnboardSchema,
@@ -14,11 +14,11 @@ import {
14
14
  SERVER_INSTRUCTIONS,
15
15
  TOOLS,
16
16
  paigyToolsAllowlisted
17
- } from "./chunk-JWMR6K6D.js";
17
+ } from "./chunk-7VLZIOLF.js";
18
18
  import {
19
19
  decideListen,
20
20
  listenerAlive
21
- } from "./chunk-SFGGZLVO.js";
21
+ } from "./chunk-MWHYYLCE.js";
22
22
  import {
23
23
  clearSurface,
24
24
  writeSurface
@@ -39,7 +39,7 @@ import {
39
39
  setIdentity,
40
40
  slotName,
41
41
  whoAmI
42
- } from "./chunk-6OBUH2FB.js";
42
+ } from "./chunk-V5AQASR7.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-SFGGZLVO.js";
6
+ } from "./chunk-MWHYYLCE.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-6OBUH2FB.js";
14
+ } from "./chunk-V5AQASR7.js";
15
15
 
16
16
  // src/listen.ts
17
17
  import { spawn } from "child_process";
@@ -136,11 +136,13 @@ function brief(waiting, reason, as, goalId) {
136
136
  const review = waiting.review ?? [];
137
137
  const named = (goalId ? review.find((g) => g.goalId === goalId) : void 0) ?? review[0];
138
138
  const fresh = review.length ? ` \xB7 new on ${review.length === 1 ? "your Goal" : `${review.length} of your Goals`}, first ${named.goalId} ("${named.title}"): claim_goal to read it` : "";
139
- if (!lead) return `Paigy \xB7 ${reason === "boot" ? `listening as ${as}` : reason}${fresh || " \xB7 nothing waiting"}${stalled}`;
139
+ const assigned = waiting.assigned ?? [];
140
+ const mine = assigned.length ? ` \xB7 assigned to you: ${assigned.length === 1 ? "1 Goal" : `${assigned.length} Goals`}, first ${assigned[0].goalId} ("${assigned[0].title}"): claim_goal({goalId}) to start it` : "";
141
+ if (!lead) return `Paigy \xB7 ${reason === "boot" ? `listening as ${as}` : reason}${mine}${fresh || (mine ? "" : " \xB7 nothing waiting")}${stalled}`;
140
142
  const said = saidIn(lead) || ("about" in lead ? lead.about : "");
141
143
  const shown = said.length > 160 ? `${said.slice(0, 160)}\u2026` : said;
142
144
  const goal = waiting.goal?.goalId ?? lead.goalIds[0];
143
- return `Paigy \xB7 ${reason} \xB7 ${waiting.deliveries.length} waiting \xB7 lead ${lead.deliveryId} on Goal ${goal}: "${shown}"${fresh}${stalled}`;
145
+ return `Paigy \xB7 ${reason} \xB7 ${waiting.deliveries.length} waiting \xB7 lead ${lead.deliveryId} on Goal ${goal}: "${shown}"${mine}${fresh}${stalled}`;
144
146
  }
145
147
  function launchEnv(waiting, reason, goalId) {
146
148
  const env = { PAIGY_WORK: JSON.stringify(waiting), PAIGY_EVENT: reason };
@@ -157,16 +159,16 @@ function launchEnv(waiting, reason, goalId) {
157
159
  async function sweep(reason, goalId) {
158
160
  try {
159
161
  const cmd = process.env.PAIGY_ON_WAKE;
160
- const { deliveries: rows, stalled, review } = await checkReplies();
162
+ const { deliveries: rows, stalled, review, assigned } = await checkReplies();
161
163
  const lead = leadOf(rows, goalId);
162
- const full = lead ? await contact({ deliveryId: lead.deliveryId }, { waits: false }).then((d) => "recorded" in d ? null : d).catch(() => null) : null;
164
+ const full = lead ? await contact({ deliveryId: lead.deliveryId }, { waits: false }).then((d) => "sent" in d ? null : d).catch(() => null) : null;
163
165
  const deliveries = rows.map((d) => d === lead && full ? full : d);
164
166
  const leadGoalId = lead?.goalIds[0];
165
167
  const goal = leadGoalId ? await getGoal(leadGoalId).then((g) => ({ goalId: leadGoalId, progress: g.progress })).catch(() => null) : null;
166
- const waiting = { deliveries, goal, ...stalled?.length ? { stalled } : {}, ...review?.length ? { review } : {} };
168
+ const waiting = { deliveries, goal, ...stalled?.length ? { stalled } : {}, ...review?.length ? { review } : {}, ...assigned?.length ? { assigned } : {} };
167
169
  if (BRIEF) line(brief(waiting, reason, listeningAs(), goalId));
168
170
  else emit({ type: "sweep", reason, ...goalId ? { about: goalId } : {}, ...waiting });
169
- if (cmd && (deliveries.length || waiting.review?.length || reason === "boot" && waiting.stalled?.length)) {
171
+ if (cmd && (deliveries.length || waiting.review?.length || waiting.assigned?.length || reason === "boot" && waiting.stalled?.length)) {
170
172
  spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(waiting, reason, goalId) } });
171
173
  }
172
174
  } catch (e) {
@@ -174,8 +176,17 @@ async function sweep(reason, goalId) {
174
176
  else emit({ type: "error", reason, message: e.message });
175
177
  }
176
178
  }
179
+ function catchUpReason(joins) {
180
+ return joins > 1 ? "resubscribed" : null;
181
+ }
177
182
  async function main() {
178
- const sub = await subscribeWake((nudge) => void sweep(`wake:${nudge.kind ?? "?"}`, nudge.parentId), { release: RELEASE });
183
+ const sub = await subscribeWake((nudge) => void sweep(`wake:${nudge.kind ?? "?"}`, nudge.parentId), {
184
+ release: RELEASE,
185
+ onJoined: (joins) => {
186
+ const reason = catchUpReason(joins);
187
+ if (reason) void sweep(reason);
188
+ }
189
+ });
179
190
  const slot = agentName();
180
191
  writeListenMark(slot, process.pid);
181
192
  process.on("exit", () => removeListenMark(slot));
@@ -214,6 +225,7 @@ if (isEntry()) {
214
225
  }
215
226
  export {
216
227
  brief,
228
+ catchUpReason,
217
229
  launchEnv,
218
230
  leadOf,
219
231
  listenOptions,
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-OGTH55QH.js";
6
+ } from "./chunk-UIZLBPRG.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
10
  openBrowser
11
- } from "./chunk-JWMR6K6D.js";
12
- import "./chunk-SFGGZLVO.js";
11
+ } from "./chunk-7VLZIOLF.js";
12
+ import "./chunk-MWHYYLCE.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-6OBUH2FB.js";
23
+ } from "./chunk-V5AQASR7.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-6OBUH2FB.js";
4
+ } from "./chunk-V5AQASR7.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(agentName());
package/dist/stalled.js CHANGED
@@ -22,7 +22,7 @@ async function main() {
22
22
  const dir = join(homedir(), ".paigy", "stalled-reminded");
23
23
  const mark = join(dir, `${session || "session"}-${day}`);
24
24
  if (existsSync(mark)) return;
25
- const { checkReplies } = await import("./dist-6F22VALF.js");
25
+ const { checkReplies } = await import("./dist-PJLF3B27.js");
26
26
  const reply = stopReply((await checkReplies()).stalled ?? []);
27
27
  if (!reply) return;
28
28
  mkdirSync(dir, { recursive: true });
@@ -7,7 +7,7 @@ import {
7
7
  readToken,
8
8
  sessionSlot,
9
9
  slotName
10
- } from "./chunk-6OBUH2FB.js";
10
+ } from "./chunk-V5AQASR7.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.16",
3
+ "version": "0.40.17",
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",