@paigy/mcp 0.40.1 → 0.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -145,42 +145,48 @@ not a compatibility fallback for target MCP tools.
145
145
  no extra setup. Two edges: with `HTTP_PROXY` set, an `http://localhost` backend
146
146
  proxies too unless `NO_PROXY=localhost` (hostname entries work; CIDR ranges are
147
147
  ignored), and `paigy-listen`'s realtime wake channel is a websocket that doesn't
148
- proxy — its REST sweeps do.
148
+ proxy — its REST reads do.
149
149
 
150
150
  ## Wake any harness (`paigy-listen`)
151
151
 
152
152
  `paigy-listen` is the self-hosted push daemon: it subscribes to this connection's
153
- wake channel and sweeps on every nudge. Keep it alive past the terminal with
154
- `paigy-listen --install` (launchd on macOS, systemd user unit on Linux; `--uninstall`
155
- removes it).
153
+ wake channel and reads what is waiting on every nudge. Keep it alive past the terminal
154
+ with `paigy-listen --install` (launchd on macOS, systemd user unit on Linux;
155
+ `--uninstall` removes it).
156
+
157
+ The read is `check_replies` — the open Deliveries addressed to this agent — and it
158
+ **consumes nothing**: no lease, no acknowledgement, and the same read twice returns the
159
+ same list. So the daemon hands the launcher what it found and stops there; claiming is
160
+ the agent's own first act (`claim_goal`), because the daemon shares this machine's
161
+ token with the agent it launches and a second claimer on one identity eats the first
162
+ one's claim.
156
163
 
157
164
  To launch an agent — any agent, not just Claude — when work arrives, set
158
- `PAIGY_ON_WAKE` to a command before `--install`. The sweep has already claimed the
159
- work, so the launcher reads it from the environment rather than calling
160
- `check_replies` again:
165
+ `PAIGY_ON_WAKE` to a command before `--install`:
161
166
 
162
167
  | Variable | What it holds |
163
168
  | --- | --- |
164
- | `PAIGY_WORK` | the whole swept payload as JSON (`replies`, `requests`, `owedCallbacks`, `stalled`, `threads`) |
165
- | `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `cron:callback`… |
166
- | `PAIGY_THREAD_ID` | the thread to continue on |
167
- | `PAIGY_NOTIFICATION_ID` | the notification being answered or acted on (unset for an owed callback) |
168
- | `PAIGY_CONTEXT_THREAD_ID` | a past conversation the user seeded this with — `get_thread` it **first** |
169
- | `PAIGY_TEXT` | what the user said (or the callback note you owe them), in prose |
169
+ | `PAIGY_WORK` | everything the wake found, as JSON (`deliveries`, `goal`) |
170
+ | `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `wake:request`… |
171
+ | `PAIGY_PARENT_ID` | the thread to continue on (`PAIGY_THREAD_ID` is the same value, for existing scripts) |
172
+ | `PAIGY_DELIVERY_ID` | the Delivery being acted on — `contact({deliveryId})` rereads it |
173
+ | `PAIGY_GOAL_ID` | the Goal it belongs to — `claim_goal` it **first** |
174
+ | `PAIGY_CONTEXT_THREAD_ID` | a past conversation this Goal names — `get_thread` it **first** |
175
+ | `PAIGY_TEXT` | what the person said, in prose |
170
176
 
171
177
  Hand `$PAIGY_WORK` to a harness that can read JSON and decide for itself; use the
172
178
  scalars for a plain shell launcher that shouldn't need `jq`. They describe **one**
173
- item — the oldest thread without a turn already in flight — because the queue rail's
174
- contract is one thread at a time. An absent fact is unset rather than empty, so
175
- `${PAIGY_CONTEXT_THREAD_ID:-}` distinguishes "no seed" from "seeded with nothing".
179
+ item — the oldest open Delivery — because the contract is one thread at a time. An
180
+ absent fact is unset rather than empty, so `${PAIGY_CONTEXT_THREAD_ID:-}`
181
+ distinguishes "no seed" from "seeded with nothing".
176
182
 
177
183
  ```sh
178
184
  # Claude Code — hand it everything and let it plan:
179
- PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — rehydrate threads you do not recognize via get_thread first."' \
185
+ PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — claim_goal first, and rehydrate threads you do not recognize via get_thread."' \
180
186
  npx -y -p @paigy/mcp paigy-listen --install
181
187
 
182
188
  # Codex (or any CLI harness) — the scalars are enough for a one-liner:
183
- PAIGY_ON_WAKE='codex exec "Continue Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Call get_thread on it first if you do not recognize it, then reply with contact."' \
189
+ PAIGY_ON_WAKE='codex exec "Claim Paigy goal $PAIGY_GOAL_ID and continue thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. Reply with contact."' \
184
190
  npx -y -p @paigy/mcp paigy-listen --install
185
191
  ```
186
192
 
@@ -194,10 +200,7 @@ script and branch on `$PAIGY_EVENT` there:
194
200
  seed=""
195
201
  [ -n "${PAIGY_CONTEXT_THREAD_ID:-}" ] && seed="It continues thread $PAIGY_CONTEXT_THREAD_ID — get_thread that first."
196
202
 
197
- case "$PAIGY_EVENT" in
198
- cron:callback*) codex exec "You owe the user a callback on thread $PAIGY_THREAD_ID: $PAIGY_TEXT. Deliver it with contact." ;;
199
- *) codex exec "Paigy thread $PAIGY_THREAD_ID. The user said: $PAIGY_TEXT. $seed Reply with contact when done." ;;
200
- esac
203
+ codex exec "Paigy goal $PAIGY_GOAL_ID on thread $PAIGY_THREAD_ID. claim_goal it first. The user said: $PAIGY_TEXT. $seed Reply with contact when done."
201
204
  ```
202
205
 
203
206
  ## Statusline (Claude Code)
@@ -2,10 +2,10 @@ import {
2
2
  AGENT_TOOLS,
3
3
  mcpInputSchema,
4
4
  serverInstructions
5
- } from "./chunk-3QUIB42M.js";
5
+ } from "./chunk-FFI3AIZE.js";
6
6
  import {
7
7
  AGENT_NAME
8
- } from "./chunk-4UZAPBKK.js";
8
+ } from "./chunk-YJEJZT56.js";
9
9
 
10
10
  // src/toolset.ts
11
11
  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.";
@@ -10,7 +10,7 @@ import {
10
10
  saveToken,
11
11
  sleep,
12
12
  startE2ee
13
- } from "./chunk-4UZAPBKK.js";
13
+ } from "./chunk-YJEJZT56.js";
14
14
 
15
15
  // src/identity.ts
16
16
  var CLIENT_LABELS = {
@@ -36,7 +36,22 @@ function mcpInputSchema(s) {
36
36
  }
37
37
  var StartContactSchema = z.object({
38
38
  goalIds: z.tuple([z.string().uuid()]),
39
- ask: z.string().trim().min(1).max(1e4),
39
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
40
+ //
41
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
42
+ // findings and a decision into one, and the person answers whichever part engaged them —
43
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
44
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
45
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
46
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
47
+ // asks carrying a decision, 27 are over 600 characters.
48
+ //
49
+ // Sending them separately costs nothing, because a CALL contact joins the call already
50
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
51
+ // one call, planned as its turns, and each keeps its own settleable need.
52
+ ask: z.string().trim().min(1).max(1e4).describe(
53
+ "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
54
+ ),
40
55
  waiting: z.enum(["none", "hard"]).default("none"),
41
56
  channel: z.enum(["notification", "call"]).default("notification"),
42
57
  options: z.array(z.object({ label: z.string().trim().min(1).max(1e3), image: z.string().url().optional(), html: z.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
@@ -48,12 +63,25 @@ var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pas
48
63
  var CreateGoalSchema = z2.object({
49
64
  outcome: z2.string().trim().min(1).max(1e4),
50
65
  ownerParticipant: z2.string().trim().min(1).optional(),
51
- idempotencyKey: z2.string().trim().min(1).max(200)
66
+ idempotencyKey: z2.string().trim().min(1).max(200),
67
+ /** A past conversation this Goal should be read against — History's "new session from this"
68
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
69
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
70
+ * the writer refuses a thread belonging to another account. */
71
+ contextThreadId: z2.string().uuid().optional(),
72
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
73
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
74
+ * worked"). It could not have been: this door took no parent, so the only route was
75
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
76
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
77
+ * ZERO times in 2,031 of them. Absent still means a root, which is what every caller meant
78
+ * before this existed. The writer refuses a Goal belonging to another account. */
79
+ parentGoalId: z2.string().uuid().optional()
52
80
  });
53
81
  var CreateGoalToolSchema = CreateGoalSchema.extend({
54
82
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
55
83
  }).strict();
56
- var CREATE_GOAL_DESCRIPTION = "Create a durable root Goal for an outcome. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
84
+ var CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Pass parentGoalId to create it UNDER an existing Goal \u2014 do that whenever the work is part of something already being tracked, rather than minting a root and leaving the relationship unsaid; absent means a root. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
57
85
  var UpdateGoalSchema = z2.object({
58
86
  revision: z2.number().int().positive(),
59
87
  changes: z2.object({
@@ -99,6 +127,21 @@ var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
99
127
  function serverInstructions(_opts) {
100
128
  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 its durable Entries and accepted decisions. get_goal rereads the same evidence without consuming it. Contact only for an existing Goal. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. On stdio, Calls hold one bounded window; on hosted MCP, Calls return after one read; continue with contact({deliveryId}) to reread that exact Delivery. Evidence can repeat: reads do not acknowledge or hide it. Use update_goal to report progress and explicitly acknowledge review. Never infer ringing from an open Call Delivery. Soft waiting, multipart contact, and re-presentation are unsupported. In an interactive session, ask the user directly; use Paigy when they asked for it or are away.";
101
129
  }
130
+ function entryWords(entry) {
131
+ const content = entry.content;
132
+ if (content && "sealed" in content) return "";
133
+ const plain = content?.plain;
134
+ if (typeof plain === "string") return plain;
135
+ if (plain && typeof plain === "object") {
136
+ const text = plain.text;
137
+ if (typeof text === "string") return text;
138
+ const title = plain.title;
139
+ const description = plain.description;
140
+ const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
141
+ if (parts.length) return parts.join("\n\n");
142
+ }
143
+ return entry.sources.map((source) => source.text).join("\n");
144
+ }
102
145
  var ContextSchema = z3.object({
103
146
  title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
104
147
  description: z3.array(z3.string().min(1)).describe(
@@ -332,13 +375,6 @@ var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input
332
375
  var SetTaskStateSchema = z3.object({
333
376
  state: z3.enum(["in_progress", "completed", "needs_input"])
334
377
  });
335
- var SetWorkStateSchema = z3.object({
336
- workId: z3.string().uuid().optional(),
337
- notificationId: z3.string().optional(),
338
- state: SetTaskStateSchema.shape.state
339
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
340
- message: "exactly one of workId or notificationId is required"
341
- });
342
378
  var TurnSchema = z3.object({
343
379
  prompt: z3.string(),
344
380
  reply: z3.string()
@@ -468,13 +504,6 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
468
504
  })
469
505
  ]);
470
506
  var CallbackTriggerSchema = z3.enum(["on_done", "on_blocked", "scheduled"]);
471
- var ScheduleCallbackSchema = z3.object({
472
- parentId: z3.string().describe("The thread to call back on (from a prior contact / reply / request)."),
473
- goalId: z3.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
474
- trigger: CallbackTriggerSchema,
475
- dueInSeconds: z3.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
476
- note: z3.string().optional().describe("What to tell the user when you follow up.")
477
- });
478
507
  var PendingRepliesSchema = z3.object({
479
508
  replies: z3.array(
480
509
  z3.object({
@@ -797,7 +826,21 @@ var InboxItemSchema = z3.object({
797
826
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
798
827
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
799
828
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
800
- agentX25519: z3.string().optional()
829
+ agentX25519: z3.string().optional(),
830
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
831
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
832
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
833
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
834
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
835
+ communication: z3.object({
836
+ deliveryId: z3.string(),
837
+ kind: z3.enum(["notification", "call"]),
838
+ entryId: z3.string(),
839
+ goalIds: z3.array(z3.string()),
840
+ decisionNeedId: z3.string().optional(),
841
+ sealed: z3.boolean(),
842
+ goalState: z3.string().optional()
843
+ }).optional()
801
844
  });
802
845
  var SnoozeRequestSchema = z3.object({
803
846
  requestId: z3.string(),
@@ -1033,26 +1076,54 @@ var CreateRequestSchema = z3.object({
1033
1076
  * the agent reads it via get_thread. Must belong to the requesting user. */
1034
1077
  contextParentId: z3.string().optional()
1035
1078
  });
1036
- var HandoffSchema = z3.object({
1037
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
1038
- workId: z3.string().uuid().optional(),
1039
- /** Land the note on an existing thread; omitted mints a fresh one. */
1040
- parentId: z3.string().uuid().optional(),
1041
- /** One-line headline of the working context handed off. */
1042
- title: z3.string().min(1),
1043
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
1044
- notes: z3.array(z3.string().min(1)).min(1),
1045
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
1046
- * only; omit to leave the thread for the user to hand off in the app. */
1047
- target: z3.string().optional(),
1048
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
1049
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
1050
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
1051
- * same primitive; a recap is one whose audience includes you. */
1052
- recap: z3.boolean().optional()
1053
- }).refine((value) => !value.workId || Boolean(value.target), {
1054
- message: "target is required when handing off Work",
1055
- path: ["target"]
1079
+ var QueueQuestionSchema = z3.object({
1080
+ /** The decision need's id — what an answer is accepted against. */
1081
+ id: z3.string(),
1082
+ /** The words that were asked, from the request Entry that asked them. */
1083
+ question: z3.string(),
1084
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
1085
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
1086
+ * can answer. */
1087
+ deliveryId: z3.string().nullable().default(null),
1088
+ /** The Entry the ruling is about. */
1089
+ aboutId: z3.string().nullable().default(null),
1090
+ /** Empty for a free-text question. */
1091
+ options: z3.array(OptionSchema).default([]),
1092
+ select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
1093
+ askedAt: z3.string(),
1094
+ /** Null while the question is open — which is how the page tells the two apart. */
1095
+ answeredAt: z3.string().nullable().default(null),
1096
+ /** The ruling in the person's own words, from the contribution that replied — not the
1097
+ * option id, which is not something anyone reads back. Null while it is open, and null
1098
+ * for a settled question whose reply carried nothing readable. */
1099
+ answer: z3.string().nullable().default(null)
1100
+ });
1101
+ var QueueItemSchema = z3.object({
1102
+ id: z3.string(),
1103
+ /** One-line headline — the first sentence of the outcome. */
1104
+ title: z3.string(),
1105
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
1106
+ intent: z3.string(),
1107
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
1108
+ state: z3.string(),
1109
+ /** Who holds it (a participant ref); null when nobody does yet. */
1110
+ assignee: z3.string().nullable().default(null),
1111
+ /** What the agent last said it was doing; null if it has said nothing. */
1112
+ progress: z3.string().nullable().default(null),
1113
+ reviewPending: z3.boolean().default(false),
1114
+ dueAt: z3.string().nullable().default(null),
1115
+ /** The Goal this one was opened under; null at the root. */
1116
+ parentGoalId: z3.string().nullable().default(null),
1117
+ /** Goals opened under this one — only those the same list holds. */
1118
+ childGoalIds: z3.array(z3.string()).default([]),
1119
+ /** Goals this one waits on (start or finish gates). */
1120
+ dependencyGoalIds: z3.array(z3.string()).default([]),
1121
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
1122
+ blocked: z3.boolean().default(false),
1123
+ /** Every decision need on it, open or settled — the page decides which to show. */
1124
+ questions: z3.array(QueueQuestionSchema).default([]),
1125
+ createdAt: z3.string(),
1126
+ updatedAt: z3.string().nullable().default(null)
1056
1127
  });
1057
1128
  var NoteSourceSchema = z3.enum(["app", "call"]);
1058
1129
  var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
@@ -1181,8 +1252,6 @@ var AcceptTriageResultSchema = z3.object({
1181
1252
  failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
1182
1253
  });
1183
1254
  var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
1184
- var WAKE_EVENT = "wake";
1185
- var wakeChannel = (tokenId) => `wake:${tokenId}`;
1186
1255
  var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
1187
1256
  var OAuthStartSchema = z3.object({
1188
1257
  provider: z3.enum(["cma"]),
@@ -1191,8 +1260,9 @@ var OAuthStartSchema = z3.object({
1191
1260
  var DeliveryConfigSchema = z3.object({
1192
1261
  tokenId: z3.string(),
1193
1262
  mode: DeliveryModeSchema,
1194
- /** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
1195
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
1263
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
1264
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
1265
+ * carries credentials; only a `poll` registration can come back with null here. */
1196
1266
  realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
1197
1267
  });
1198
1268
  var StatusSchema = z3.object({
@@ -1383,6 +1453,5 @@ export {
1383
1453
  mcpInputSchema,
1384
1454
  AGENT_TOOLS,
1385
1455
  serverInstructions,
1386
- WAKE_EVENT,
1387
- wakeChannel
1456
+ entryWords
1388
1457
  };
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __sdkCreateRequire } from "module";
3
3
  import { createHash, randomUUID } from "crypto";
4
4
  import { randomUUID as randomUUID3 } from "crypto";
5
- import { setTimeout as sleep3 } from "timers/promises";
5
+ import { setTimeout as sleep2 } from "timers/promises";
6
6
  import { z as z3 } from "zod";
7
7
  import { z as z2 } from "zod";
8
8
  import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind3 } from "zod/v3";
@@ -14,6 +14,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statS
14
14
  import { homedir } from "os";
15
15
  import { join } from "path";
16
16
  import { randomUUID as randomUUID4 } from "crypto";
17
+ import { createClient } from "@supabase/supabase-js";
17
18
  var require2 = __sdkCreateRequire(import.meta.url);
18
19
  var __create = Object.create;
19
20
  var __defProp = Object.defineProperty;
@@ -3551,7 +3552,22 @@ function mcpInputSchema(s) {
3551
3552
  }
3552
3553
  var StartContactSchema = z.object({
3553
3554
  goalIds: z.tuple([z.string().uuid()]),
3554
- ask: z.string().trim().min(1).max(1e4),
3555
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
3556
+ //
3557
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
3558
+ // findings and a decision into one, and the person answers whichever part engaged them —
3559
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
3560
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
3561
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
3562
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
3563
+ // asks carrying a decision, 27 are over 600 characters.
3564
+ //
3565
+ // Sending them separately costs nothing, because a CALL contact joins the call already
3566
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
3567
+ // one call, planned as its turns, and each keeps its own settleable need.
3568
+ ask: z.string().trim().min(1).max(1e4).describe(
3569
+ "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
3570
+ ),
3555
3571
  waiting: z.enum(["none", "hard"]).default("none"),
3556
3572
  channel: z.enum(["notification", "call"]).default("notification"),
3557
3573
  options: z.array(z.object({ label: z.string().trim().min(1).max(1e3), image: z.string().url().optional(), html: z.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
@@ -3563,12 +3579,25 @@ var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pas
3563
3579
  var CreateGoalSchema = z2.object({
3564
3580
  outcome: z2.string().trim().min(1).max(1e4),
3565
3581
  ownerParticipant: z2.string().trim().min(1).optional(),
3566
- idempotencyKey: z2.string().trim().min(1).max(200)
3582
+ idempotencyKey: z2.string().trim().min(1).max(200),
3583
+ /** A past conversation this Goal should be read against — History's "new session from this"
3584
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
3585
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
3586
+ * the writer refuses a thread belonging to another account. */
3587
+ contextThreadId: z2.string().uuid().optional(),
3588
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
3589
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
3590
+ * worked"). It could not have been: this door took no parent, so the only route was
3591
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
3592
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
3593
+ * ZERO times in 2,031 of them. Absent still means a root, which is what every caller meant
3594
+ * before this existed. The writer refuses a Goal belonging to another account. */
3595
+ parentGoalId: z2.string().uuid().optional()
3567
3596
  });
3568
3597
  var CreateGoalToolSchema = CreateGoalSchema.extend({
3569
3598
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
3570
3599
  }).strict();
3571
- var CREATE_GOAL_DESCRIPTION = "Create a durable root Goal for an outcome. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
3600
+ var CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Pass parentGoalId to create it UNDER an existing Goal \u2014 do that whenever the work is part of something already being tracked, rather than minting a root and leaving the relationship unsaid; absent means a root. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
3572
3601
  var UpdateGoalSchema = z2.object({
3573
3602
  revision: z2.number().int().positive(),
3574
3603
  changes: z2.object({
@@ -3899,13 +3928,6 @@ var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input
3899
3928
  var SetTaskStateSchema = z3.object({
3900
3929
  state: z3.enum(["in_progress", "completed", "needs_input"])
3901
3930
  });
3902
- var SetWorkStateSchema = z3.object({
3903
- workId: z3.string().uuid().optional(),
3904
- notificationId: z3.string().optional(),
3905
- state: SetTaskStateSchema.shape.state
3906
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
3907
- message: "exactly one of workId or notificationId is required"
3908
- });
3909
3931
  var TurnSchema = z3.object({
3910
3932
  prompt: z3.string(),
3911
3933
  reply: z3.string()
@@ -4035,13 +4057,6 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
4035
4057
  })
4036
4058
  ]);
4037
4059
  var CallbackTriggerSchema = z3.enum(["on_done", "on_blocked", "scheduled"]);
4038
- var ScheduleCallbackSchema = z3.object({
4039
- parentId: z3.string().describe("The thread to call back on (from a prior contact / reply / request)."),
4040
- goalId: z3.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
4041
- trigger: CallbackTriggerSchema,
4042
- dueInSeconds: z3.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
4043
- note: z3.string().optional().describe("What to tell the user when you follow up.")
4044
- });
4045
4060
  var PendingRepliesSchema = z3.object({
4046
4061
  replies: z3.array(
4047
4062
  z3.object({
@@ -4364,7 +4379,21 @@ var InboxItemSchema = z3.object({
4364
4379
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
4365
4380
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
4366
4381
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
4367
- agentX25519: z3.string().optional()
4382
+ agentX25519: z3.string().optional(),
4383
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
4384
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
4385
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
4386
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
4387
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
4388
+ communication: z3.object({
4389
+ deliveryId: z3.string(),
4390
+ kind: z3.enum(["notification", "call"]),
4391
+ entryId: z3.string(),
4392
+ goalIds: z3.array(z3.string()),
4393
+ decisionNeedId: z3.string().optional(),
4394
+ sealed: z3.boolean(),
4395
+ goalState: z3.string().optional()
4396
+ }).optional()
4368
4397
  });
4369
4398
  var SnoozeRequestSchema = z3.object({
4370
4399
  requestId: z3.string(),
@@ -4600,26 +4629,54 @@ var CreateRequestSchema = z3.object({
4600
4629
  * the agent reads it via get_thread. Must belong to the requesting user. */
4601
4630
  contextParentId: z3.string().optional()
4602
4631
  });
4603
- var HandoffSchema = z3.object({
4604
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
4605
- workId: z3.string().uuid().optional(),
4606
- /** Land the note on an existing thread; omitted mints a fresh one. */
4607
- parentId: z3.string().uuid().optional(),
4608
- /** One-line headline of the working context handed off. */
4609
- title: z3.string().min(1),
4610
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
4611
- notes: z3.array(z3.string().min(1)).min(1),
4612
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
4613
- * only; omit to leave the thread for the user to hand off in the app. */
4614
- target: z3.string().optional(),
4615
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
4616
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
4617
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
4618
- * same primitive; a recap is one whose audience includes you. */
4619
- recap: z3.boolean().optional()
4620
- }).refine((value) => !value.workId || Boolean(value.target), {
4621
- message: "target is required when handing off Work",
4622
- path: ["target"]
4632
+ var QueueQuestionSchema = z3.object({
4633
+ /** The decision need's id — what an answer is accepted against. */
4634
+ id: z3.string(),
4635
+ /** The words that were asked, from the request Entry that asked them. */
4636
+ question: z3.string(),
4637
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
4638
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
4639
+ * can answer. */
4640
+ deliveryId: z3.string().nullable().default(null),
4641
+ /** The Entry the ruling is about. */
4642
+ aboutId: z3.string().nullable().default(null),
4643
+ /** Empty for a free-text question. */
4644
+ options: z3.array(OptionSchema).default([]),
4645
+ select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
4646
+ askedAt: z3.string(),
4647
+ /** Null while the question is open — which is how the page tells the two apart. */
4648
+ answeredAt: z3.string().nullable().default(null),
4649
+ /** The ruling in the person's own words, from the contribution that replied — not the
4650
+ * option id, which is not something anyone reads back. Null while it is open, and null
4651
+ * for a settled question whose reply carried nothing readable. */
4652
+ answer: z3.string().nullable().default(null)
4653
+ });
4654
+ var QueueItemSchema = z3.object({
4655
+ id: z3.string(),
4656
+ /** One-line headline — the first sentence of the outcome. */
4657
+ title: z3.string(),
4658
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
4659
+ intent: z3.string(),
4660
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
4661
+ state: z3.string(),
4662
+ /** Who holds it (a participant ref); null when nobody does yet. */
4663
+ assignee: z3.string().nullable().default(null),
4664
+ /** What the agent last said it was doing; null if it has said nothing. */
4665
+ progress: z3.string().nullable().default(null),
4666
+ reviewPending: z3.boolean().default(false),
4667
+ dueAt: z3.string().nullable().default(null),
4668
+ /** The Goal this one was opened under; null at the root. */
4669
+ parentGoalId: z3.string().nullable().default(null),
4670
+ /** Goals opened under this one — only those the same list holds. */
4671
+ childGoalIds: z3.array(z3.string()).default([]),
4672
+ /** Goals this one waits on (start or finish gates). */
4673
+ dependencyGoalIds: z3.array(z3.string()).default([]),
4674
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
4675
+ blocked: z3.boolean().default(false),
4676
+ /** Every decision need on it, open or settled — the page decides which to show. */
4677
+ questions: z3.array(QueueQuestionSchema).default([]),
4678
+ createdAt: z3.string(),
4679
+ updatedAt: z3.string().nullable().default(null)
4623
4680
  });
4624
4681
  var NoteSourceSchema = z3.enum(["app", "call"]);
4625
4682
  var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
@@ -4748,6 +4805,8 @@ var AcceptTriageResultSchema = z3.object({
4748
4805
  failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
4749
4806
  });
4750
4807
  var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
4808
+ var WAKE_EVENT = "wake";
4809
+ var wakeChannel = (tokenId) => `wake:${tokenId}`;
4751
4810
  var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
4752
4811
  var OAuthStartSchema = z3.object({
4753
4812
  provider: z3.enum(["cma"]),
@@ -4756,8 +4815,9 @@ var OAuthStartSchema = z3.object({
4756
4815
  var DeliveryConfigSchema = z3.object({
4757
4816
  tokenId: z3.string(),
4758
4817
  mode: DeliveryModeSchema,
4759
- /** null when the server has no SUPABASE_ANON_KEY set — the listener then falls
4760
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
4818
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
4819
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
4820
+ * carries credentials; only a `poll` registration can come back with null here. */
4761
4821
  realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
4762
4822
  });
4763
4823
  var StatusSchema = z3.object({
@@ -4966,21 +5026,6 @@ function utf8ToBytes(s) {
4966
5026
  }
4967
5027
  return Uint8Array.from(out);
4968
5028
  }
4969
- function bytesToUtf8(b) {
4970
- let out = "";
4971
- for (let i = 0; i < b.length; ) {
4972
- const c = b[i++];
4973
- if (c < 128) out += String.fromCharCode(c);
4974
- else if (c < 224) out += String.fromCharCode((c & 31) << 6 | b[i++] & 63);
4975
- else if (c < 240) out += String.fromCharCode((c & 15) << 12 | (b[i++] & 63) << 6 | b[i++] & 63);
4976
- else {
4977
- const cp = (c & 7) << 18 | (b[i++] & 63) << 12 | (b[i++] & 63) << 6 | b[i++] & 63;
4978
- const u = cp - 65536;
4979
- out += String.fromCharCode(55296 + (u >> 10), 56320 + (u & 1023));
4980
- }
4981
- }
4982
- return out;
4983
- }
4984
5029
  var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4985
5030
  function toB64(bytes) {
4986
5031
  let out = "";
@@ -5026,15 +5071,9 @@ function concat(parts) {
5026
5071
  }
5027
5072
  return out;
5028
5073
  }
5029
- function u16be(n) {
5030
- return Uint8Array.of(n >>> 8 & 255, n & 255);
5031
- }
5032
5074
  function u32be(n) {
5033
5075
  return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
5034
5076
  }
5035
- function readU32be(b, o) {
5036
- return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
5037
- }
5038
5077
  function lp(b) {
5039
5078
  return concat([u32be(b.length), b]);
5040
5079
  }
@@ -5076,9 +5115,6 @@ function sharedSecret(peerX25519PubB64, mySecretB64) {
5076
5115
  if (raw.every((b) => b === 0)) throw new Error("low-order X25519 point \u2014 shared secret rejected");
5077
5116
  return toB64(raw);
5078
5117
  }
5079
- function keyId(publicKeyB64) {
5080
- return toB64(import_tweetnacl2.default.hash(fromB64(publicKeyB64)).slice(0, 8));
5081
- }
5082
5118
  function credentialBody(c) {
5083
5119
  return concat([
5084
5120
  lp(utf8ToBytes("paigy-device-credential-v1")),
@@ -5096,43 +5132,6 @@ function verifyDeviceCredential(cred, uikPublicB64) {
5096
5132
  return false;
5097
5133
  }
5098
5134
  }
5099
- var ENVELOPE_VERSION = 1;
5100
- var ENVELOPE_ALG = "x25519-xsalsa20poly1305";
5101
- function canonicalHeader(msgId, hdr) {
5102
- const parts = [
5103
- u16be(ENVELOPE_VERSION),
5104
- lp(utf8ToBytes(ENVELOPE_ALG)),
5105
- lp(msgId),
5106
- lp(utf8ToBytes(hdr.field)),
5107
- lp(utf8ToBytes(hdr.kind)),
5108
- lp(utf8ToBytes(hdr.senderRole)),
5109
- u32be(hdr.seq),
5110
- u32be(hdr.recipientKeyIds.length),
5111
- ...hdr.recipientKeyIds.map((k) => lp(utf8ToBytes(k)))
5112
- ];
5113
- return concat(parts);
5114
- }
5115
- function open(envelope, myKeyId, mySecretKeyB64) {
5116
- if (envelope.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version ${String(envelope.v)}`);
5117
- if (envelope.alg !== ENVELOPE_ALG) throw new Error(`unsupported alg ${String(envelope.alg)}`);
5118
- const rec = envelope.recipients.find((r) => r.keyId === myKeyId);
5119
- if (!rec) throw new Error("not a recipient of this envelope");
5120
- const cek = import_tweetnacl3.default.box.open(fromB64(rec.wrap), fromB64(rec.wnonce), fromB64(rec.epk), fromB64(mySecretKeyB64));
5121
- if (!cek) throw new Error("CEK unwrap failed (wrong key or tampered)");
5122
- const padded = import_tweetnacl3.default.secretbox.open(fromB64(envelope.ct), fromB64(envelope.nonce), cek);
5123
- if (!padded) throw new Error("body authentication failed (tampered ciphertext)");
5124
- if (padded.length < 8) throw new Error("malformed sealed plaintext");
5125
- const canonLen = readU32be(padded, 0);
5126
- if (canonLen < 0 || 8 + canonLen > padded.length) throw new Error("malformed sealed plaintext");
5127
- const sealedCanon = padded.slice(4, 4 + canonLen);
5128
- const bodyLen = readU32be(padded, 4 + canonLen);
5129
- const bodyStart = 8 + canonLen;
5130
- if (bodyStart + bodyLen > padded.length) throw new Error("malformed sealed plaintext");
5131
- const body = padded.slice(bodyStart, bodyStart + bodyLen);
5132
- const expected = canonicalHeader(fromB64(envelope.msgId), envelope.hdr);
5133
- if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
5134
- return { header: envelope.hdr, body };
5135
- }
5136
5135
  var SAS_DOMAIN = "paigy-sas-v2";
5137
5136
  function deriveSAS(input, bits = 24) {
5138
5137
  const bundle = concat([
@@ -5360,21 +5359,6 @@ async function pairStep(deviceCode, keyFile) {
5360
5359
  return { kind: "e2ee_aborted", warning: e.message };
5361
5360
  }
5362
5361
  }
5363
- function openSealedAnswer(sealed, keypair) {
5364
- const myKeyId = keyId(keypair.x25519.publicKey);
5365
- const { header, body } = open(sealed.envelope, myKeyId, keypair.x25519.secretKey);
5366
- if (header.field !== "answer" || header.senderRole !== "user") {
5367
- throw new Error("sealed answer has the wrong envelope field/role \u2014 refusing (possible replay/misattribution)");
5368
- }
5369
- const parsed = UserAnswerSchema.safeParse(JSON.parse(bytesToUtf8(body)));
5370
- if (!parsed.success) throw new Error("sealed answer body is not a valid UserAnswer");
5371
- const answer = parsed.data;
5372
- const bodyIsIgnored = answer.kind === "ignored";
5373
- if (sealed.ignored !== bodyIsIgnored) {
5374
- throw new Error("sealed answer tamper: `ignored` hint disagrees with the decrypted body");
5375
- }
5376
- return answer;
5377
- }
5378
5362
  var UnpairedError = class extends Error {
5379
5363
  constructor() {
5380
5364
  super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
@@ -5435,6 +5419,7 @@ async function updateGoal(goalId, input, opts = {}) {
5435
5419
  if (!res.ok) await fail("update_goal", res);
5436
5420
  return await res.json();
5437
5421
  }
5422
+ var AWAIT_WINDOW_MS = 45e3;
5438
5423
  async function getThread(parentId, opts = {}) {
5439
5424
  const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/thread/${encodeURIComponent(parentId)}`, {
5440
5425
  headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
@@ -5449,17 +5434,6 @@ async function searchThreads(q, opts = {}) {
5449
5434
  if (!res.ok) await fail("search_threads", res);
5450
5435
  return await res.json();
5451
5436
  }
5452
- async function sweepLeased(opts = {}) {
5453
- const token = authToken(opts.token);
5454
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
5455
- headers: {
5456
- authorization: `Bearer ${token}`,
5457
- "x-paigy-await-ack": "1"
5458
- }
5459
- }));
5460
- if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
5461
- return await res.json();
5462
- }
5463
5437
  var tokenOverride = null;
5464
5438
  function overrideToken(secret) {
5465
5439
  tokenOverride = secret;
@@ -5505,10 +5479,10 @@ async function heartbeat(runtime, opts = {}) {
5505
5479
  }));
5506
5480
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
5507
5481
  }
5508
- async function registerDelivery(mode) {
5482
+ async function registerDelivery(mode, opts = {}) {
5509
5483
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
5510
5484
  method: "POST",
5511
- headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
5485
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
5512
5486
  body: JSON.stringify({ mode })
5513
5487
  }));
5514
5488
  if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
@@ -5544,7 +5518,7 @@ Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answe
5544
5518
  };
5545
5519
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
5546
5520
  if (opts.waits === false) return read(opts.signal);
5547
- const window = AbortSignal.timeout(45e3);
5521
+ const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
5548
5522
  const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
5549
5523
  let latest;
5550
5524
  try {
@@ -5552,7 +5526,7 @@ Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answe
5552
5526
  signal.throwIfAborted();
5553
5527
  latest = await read(signal);
5554
5528
  if (settled(latest)) return latest;
5555
- await sleep3(5e3, void 0, { signal });
5529
+ await sleep2(5e3, void 0, { signal });
5556
5530
  }
5557
5531
  } catch (error) {
5558
5532
  opts.signal?.throwIfAborted();
@@ -5604,106 +5578,23 @@ async function runTool(name, args, opts) {
5604
5578
  throw new Error(`Unknown tool: ${name}`);
5605
5579
  }
5606
5580
  }
5607
- var sleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5608
- var ReplyLeaseExpiredError = class extends Error {
5609
- constructor() {
5610
- super(
5611
- "The Paigy reply lease expired before acknowledgement. The answer remains durable; wait on it again (contact with only { wait: notificationId }) to reclaim it before acting."
5612
- );
5613
- this.name = "ReplyLeaseExpiredError";
5614
- }
5615
- };
5616
- var ReplyAckError = class extends Error {
5617
- constructor(message, retryable) {
5618
- super(message);
5619
- this.retryable = retryable;
5620
- this.name = "ReplyAckError";
5621
- }
5622
- retryable;
5623
- };
5624
- function decryptAnswer(reply, deps = {}) {
5625
- if (!reply.sealed) return reply.answer;
5626
- const keyFile = (deps.readKeyFile ?? readKeyFile)();
5627
- if (!keyFile?.keypair) {
5628
- throw new Error(
5629
- "received a sealed answer but this agent has no device key to open it \u2014 re-pair."
5630
- );
5631
- }
5632
- return openSealedAnswer(reply.sealed, keyFile.keypair);
5633
- }
5634
- function decodeReply(item, deps = {}) {
5635
- const { delivery: _delivery, ...withoutDelivery } = item;
5636
- const enriched = { ...withoutDelivery };
5637
- if (!enriched.sealed) return enriched;
5638
- const answer = decryptAnswer(enriched, deps);
5639
- const { sealed: _sealed, ...plaintext } = enriched;
5640
- return { ...plaintext, answer };
5641
- }
5642
- function deliveryOf(item) {
5643
- if (item.type !== "reply" || !("delivery" in item) || item.delivery === void 0) {
5644
- return null;
5645
- }
5646
- const delivery = item.delivery;
5647
- if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
5648
- throw new Error(
5649
- "Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
5650
- );
5651
- }
5652
- return delivery;
5653
- }
5654
- async function responseDetail(res) {
5655
- try {
5656
- return (await res.text()).slice(0, 300);
5657
- } catch {
5658
- return "";
5659
- }
5660
- }
5661
- async function acknowledge(token, leaseId, opts = {}) {
5662
- const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
5663
- const backoffMs = Math.max(0, opts.backoffMs ?? 100);
5664
- const doSleep = opts.doSleep ?? sleep4;
5665
- let lastError;
5666
- for (let attempt = 0; attempt < attempts; attempt += 1) {
5667
- try {
5668
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await/ack`, {
5669
- method: "POST",
5670
- headers: {
5671
- authorization: `Bearer ${token}`,
5672
- "content-type": "application/json"
5673
- },
5674
- body: JSON.stringify({ leaseId })
5675
- }));
5676
- if (res.ok) return;
5677
- const detail2 = await responseDetail(res);
5678
- if (res.status === 409) throw new ReplyLeaseExpiredError();
5679
- const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
5680
- throw new ReplyAckError(
5681
- `Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
5682
- retryable
5683
- );
5684
- } catch (error) {
5685
- if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
5686
- throw error;
5687
- }
5688
- lastError = error;
5689
- if (attempt + 1 < attempts) {
5690
- await doSleep(backoffMs * 2 ** attempt);
5691
- }
5581
+ async function subscribeWake(onNudge, opts = {}) {
5582
+ const cfg = await registerDelivery("self_hosted", opts);
5583
+ const { url, anonKey } = cfg.realtime;
5584
+ const channel = wakeChannel(cfg.tokenId);
5585
+ const sb = createClient(url, anonKey, { auth: { persistSession: false } });
5586
+ const ch = sb.channel(channel).on("broadcast", { event: WAKE_EVENT }, (msg) => onNudge(msg.payload ?? {}));
5587
+ ch.subscribe();
5588
+ return {
5589
+ tokenId: cfg.tokenId,
5590
+ channel,
5591
+ close: async () => {
5592
+ await sb.removeChannel(ch).catch(() => {
5593
+ });
5594
+ await registerDelivery("poll", opts).catch(() => {
5595
+ });
5692
5596
  }
5693
- }
5694
- const detail = lastError instanceof Error ? lastError.message : String(lastError);
5695
- throw new Error(
5696
- `Could not acknowledge the Paigy reply after ${attempts} attempts (${detail}). The answer was not returned to the agent and remains recoverable after the lease expires.`
5697
- );
5698
- }
5699
- async function ackReply(item, token, opts = {}) {
5700
- const delivery = deliveryOf(item);
5701
- if (!delivery) throw new Error("Paigy returned a reply without a delivery lease; nothing to acknowledge.");
5702
- await acknowledge(token, delivery.leaseId, {
5703
- attempts: opts.ackAttempts,
5704
- backoffMs: opts.ackBackoffMs,
5705
- doSleep: opts.ackSleep
5706
- });
5597
+ };
5707
5598
  }
5708
5599
 
5709
5600
  export {
@@ -5729,15 +5620,13 @@ export {
5729
5620
  fetchCredential,
5730
5621
  pairStep,
5731
5622
  UnpairedError,
5732
- sweepLeased,
5623
+ getGoal,
5733
5624
  overrideToken,
5734
- authToken,
5735
5625
  hatch,
5736
5626
  whoAmI,
5737
5627
  setIdentity,
5738
5628
  heartbeat,
5739
- registerDelivery,
5629
+ checkReplies,
5740
5630
  runTool,
5741
- decodeReply,
5742
- ackReply
5631
+ subscribeWake
5743
5632
  };
package/dist/enable.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools
5
- } from "./chunk-TLASZUJD.js";
6
- import "./chunk-3QUIB42M.js";
7
- import "./chunk-4UZAPBKK.js";
5
+ } from "./chunk-647HS22P.js";
6
+ import "./chunk-FFI3AIZE.js";
7
+ import "./chunk-YJEJZT56.js";
8
8
 
9
9
  // src/enable.ts
10
10
  function main() {
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  resolvePairing,
6
6
  startPairing,
7
7
  suggestedAgentName
8
- } from "./chunk-CXABA27N.js";
8
+ } from "./chunk-BUTCP2ED.js";
9
9
  import {
10
10
  ENABLE_COMMAND,
11
11
  OnboardSchema,
@@ -16,8 +16,8 @@ import {
16
16
  autoConfigureClients,
17
17
  claudeInstallHint,
18
18
  paigyToolsAllowlisted
19
- } from "./chunk-TLASZUJD.js";
20
- import "./chunk-3QUIB42M.js";
19
+ } from "./chunk-647HS22P.js";
20
+ import "./chunk-FFI3AIZE.js";
21
21
  import {
22
22
  clearSurface,
23
23
  writeSurface
@@ -38,7 +38,7 @@ import {
38
38
  setIdentity,
39
39
  slotName,
40
40
  whoAmI
41
- } from "./chunk-4UZAPBKK.js";
41
+ } from "./chunk-YJEJZT56.js";
42
42
 
43
43
  // src/index.ts
44
44
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
package/dist/listen.js CHANGED
@@ -1,18 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- WAKE_EVENT,
4
- wakeChannel
5
- } from "./chunk-3QUIB42M.js";
3
+ entryWords
4
+ } from "./chunk-FFI3AIZE.js";
6
5
  import {
7
- ackReply,
8
- authToken,
9
- decodeReply,
10
- registerDelivery,
11
- sweepLeased
12
- } from "./chunk-4UZAPBKK.js";
6
+ checkReplies,
7
+ getGoal,
8
+ subscribeWake
9
+ } from "./chunk-YJEJZT56.js";
13
10
 
14
11
  // src/listen.ts
15
- import { createClient } from "@supabase/supabase-js";
16
12
  import { spawn } from "child_process";
17
13
  import { realpathSync } from "fs";
18
14
  import { fileURLToPath } from "url";
@@ -24,7 +20,7 @@ import { homedir } from "os";
24
20
  import { join } from "path";
25
21
  var LABEL = "ai.paigy.listen";
26
22
  function frozenEnv() {
27
- const keys = ["PAIGY_BACKEND_URL", "PAIGY_ON_WAKE", "PAIGY_SUPABASE_URL", "PAIGY_SUPABASE_ANON_KEY"];
23
+ const keys = ["PAIGY_BACKEND_URL", "PAIGY_ON_WAKE"];
28
24
  return Object.fromEntries(keys.flatMap((k) => process.env[k] ? [[k, process.env[k]]] : []));
29
25
  }
30
26
  function renderLaunchd(nodePath, scriptPath, env) {
@@ -114,114 +110,49 @@ function uninstallService() {
114
110
  return unit;
115
111
  }
116
112
 
117
- // src/message.ts
118
- function answerText(answer) {
119
- switch (answer.kind) {
120
- case "text":
121
- return answer.text;
122
- case "option":
123
- return answer.label ?? answer.optionId;
124
- case "multi":
125
- case "ranked":
126
- return (answer.labels ?? answer.optionIds).join(", ");
127
- case "confirm":
128
- return answer.approved ? "approved" : "declined";
129
- case "clarify":
130
- return answer.chunks.join(" ");
131
- case "turns":
132
- return answer.turns.map((turn) => turn.reply).join(" ");
133
- case "precedent":
134
- return answer.answer;
135
- case "ignored":
136
- return "";
137
- }
138
- }
139
-
140
113
  // src/listen.ts
141
114
  var emit = (obj) => void process.stdout.write(JSON.stringify(obj) + "\n");
142
- function launchEnv(work, reason) {
143
- const env = { PAIGY_WORK: JSON.stringify(work), PAIGY_EVENT: reason };
115
+ function saidIn(delivery) {
116
+ return delivery.entries.filter((entry) => entry.kind === "contribution").map((entry) => entryWords(entry)).filter(Boolean).join("\n");
117
+ }
118
+ function launchEnv(waiting, reason, goalId) {
119
+ const env = { PAIGY_WORK: JSON.stringify(waiting), PAIGY_EVENT: reason };
144
120
  const put = (key, value) => {
145
121
  if (value) env[key] = value;
146
122
  };
147
- const lead = work.threads?.find((t) => !t.busy)?.items[0]?.notificationId;
148
- const reply = lead ? work.replies.find((r) => r.notificationId === lead) : work.replies[0];
149
- const request = lead ? work.requests.find((r) => r.notificationId === lead) : work.requests[0];
150
- if (reply) {
151
- put("PAIGY_THREAD_ID", reply.parentId);
152
- put("PAIGY_PARENT_ID", reply.parentId);
153
- put("PAIGY_NOTIFICATION_ID", reply.notificationId);
154
- put("PAIGY_TEXT", answerText(reply.answer));
155
- return env;
156
- }
157
- if (request) {
158
- put("PAIGY_THREAD_ID", request.parentId);
159
- put("PAIGY_PARENT_ID", request.parentId);
160
- put("PAIGY_NOTIFICATION_ID", request.notificationId);
161
- put("PAIGY_CONTEXT_THREAD_ID", request.contextParentId);
162
- put("PAIGY_TEXT", request.text);
163
- return env;
164
- }
165
- const callback = work.owedCallbacks[0];
166
- if (callback) {
167
- put("PAIGY_THREAD_ID", callback.parentId);
168
- put("PAIGY_PARENT_ID", callback.parentId);
169
- put("PAIGY_TEXT", callback.note);
170
- }
123
+ const named = goalId ? waiting.deliveries.find((d) => d.goalIds.includes(goalId)) : void 0;
124
+ const lead = named ?? waiting.deliveries[0];
125
+ if (!lead) return env;
126
+ put("PAIGY_THREAD_ID", lead.threadId);
127
+ put("PAIGY_PARENT_ID", lead.threadId);
128
+ put("PAIGY_DELIVERY_ID", lead.deliveryId);
129
+ put("PAIGY_GOAL_ID", waiting.goal?.goalId ?? lead.goalIds[0]);
130
+ put("PAIGY_CONTEXT_THREAD_ID", waiting.goal?.contextThreadId);
131
+ put("PAIGY_TEXT", saidIn(lead) || lead.message);
171
132
  return env;
172
133
  }
173
- var SWEEP_PASSES = 10;
174
- async function sweep(reason) {
134
+ async function sweep(reason, goalId) {
175
135
  try {
176
136
  const cmd = process.env.PAIGY_ON_WAKE;
177
- const token = authToken() ?? "";
178
- for (let pass = 0; pass < SWEEP_PASSES; pass += 1) {
179
- const raw = await sweepLeased();
180
- const leased = raw.replies;
181
- const work = {
182
- ...raw,
183
- work: raw.work ?? [],
184
- replies: leased.map((r) => {
185
- const { type: _type, ...reply } = decodeReply({ ...r, type: "reply" });
186
- return reply;
187
- })
188
- };
189
- emit({ type: "sweep", reason, ...work });
190
- const inbound = work.replies.length > 0 || work.requests.length > 0;
191
- const reengage = work.owedCallbacks.length > 0 && (reason === "boot" || reason.includes("callback"));
192
- if (cmd && (inbound || reengage)) {
193
- spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(work, reason) } });
194
- }
195
- if (cmd) for (const r of leased) await ackReply({ ...r, type: "reply" }, token);
196
- if (leased.length === 0) return;
137
+ const { deliveries } = await checkReplies();
138
+ const lead = (goalId ? deliveries.find((d) => d.goalIds.includes(goalId)) : void 0) ?? deliveries[0];
139
+ const leadGoalId = lead?.goalIds[0];
140
+ const goal = leadGoalId ? await getGoal(leadGoalId).then((g) => ({ goalId: leadGoalId, contextThreadId: g.contextThreadId, progress: g.progress })).catch(() => null) : null;
141
+ const waiting = { deliveries, goal };
142
+ emit({ type: "sweep", reason, ...goalId ? { about: goalId } : {}, ...waiting });
143
+ if (cmd && deliveries.length) {
144
+ spawn(cmd, { shell: true, stdio: "inherit", env: { ...process.env, ...launchEnv(waiting, reason, goalId) } });
197
145
  }
198
146
  } catch (e) {
199
147
  emit({ type: "error", reason, message: e.message });
200
148
  }
201
149
  }
202
150
  async function main() {
203
- const cfg = await registerDelivery("self_hosted");
204
- const url = cfg.realtime?.url ?? process.env.PAIGY_SUPABASE_URL;
205
- const anonKey = cfg.realtime?.anonKey ?? process.env.PAIGY_SUPABASE_ANON_KEY;
206
- if (!url || !anonKey) {
207
- throw new Error(
208
- "No Realtime credentials \u2014 set SUPABASE_ANON_KEY on the server, or PAIGY_SUPABASE_URL + PAIGY_SUPABASE_ANON_KEY locally."
209
- );
210
- }
211
- const channel = wakeChannel(cfg.tokenId);
212
- emit({ type: "listening", channel });
151
+ const sub = await subscribeWake((nudge) => void sweep(`wake:${nudge.kind ?? "?"}`, nudge.parentId));
152
+ emit({ type: "listening", channel: sub.channel });
213
153
  await sweep("boot");
214
- const sb = createClient(url, anonKey, { auth: { persistSession: false } });
215
- sb.channel(channel).on(
216
- "broadcast",
217
- { event: WAKE_EVENT },
218
- (msg) => void sweep(`wake:${msg.payload?.kind ?? "?"}`)
219
- ).subscribe();
220
154
  const shutdown = async () => {
221
- try {
222
- await registerDelivery("poll");
223
- } catch {
224
- }
155
+ await sub.close();
225
156
  process.exit(0);
226
157
  };
227
158
  process.on("SIGINT", () => void shutdown());
@@ -251,5 +182,6 @@ if (isEntry()) {
251
182
  }
252
183
  export {
253
184
  launchEnv,
185
+ saidIn,
254
186
  sweep
255
187
  };
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-CXABA27N.js";
6
+ } from "./chunk-BUTCP2ED.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
10
  openBrowser
11
- } from "./chunk-TLASZUJD.js";
12
- import "./chunk-3QUIB42M.js";
11
+ } from "./chunk-647HS22P.js";
12
+ import "./chunk-FFI3AIZE.js";
13
13
  import {
14
14
  AGENT_NAME,
15
15
  TOKEN_PATH,
@@ -20,7 +20,7 @@ import {
20
20
  saveToken,
21
21
  setIdentity,
22
22
  whoAmI
23
- } from "./chunk-4UZAPBKK.js";
23
+ } from "./chunk-YJEJZT56.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
  AGENT_NAME
4
- } from "./chunk-4UZAPBKK.js";
4
+ } from "./chunk-YJEJZT56.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(AGENT_NAME);
@@ -9,7 +9,7 @@ import {
9
9
  readToken,
10
10
  sessionSlot,
11
11
  slotName
12
- } from "./chunk-4UZAPBKK.js";
12
+ } from "./chunk-YJEJZT56.js";
13
13
 
14
14
  // src/statusline.ts
15
15
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.40.1",
3
+ "version": "0.40.2",
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",
@@ -39,8 +39,8 @@
39
39
  "typescript": "^5.7.2",
40
40
  "vitest": "^2.1.8",
41
41
  "@paigy/crypto": "0.0.0",
42
- "@paigy/schema": "0.0.0",
43
- "@paigy/sdk": "0.2.1"
42
+ "@paigy/sdk": "0.2.1",
43
+ "@paigy/schema": "0.0.0"
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",