@paigy/mcp 0.40.1 → 0.40.3

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.
@@ -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 { WebSocket } from "undici";
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, the server judges it against the caller's open Goals
3594
+ * (`apps/api/src/goal/intake.ts`). 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. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. 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({
@@ -3896,16 +3925,6 @@ function deriveAsk(req) {
3896
3925
  }
3897
3926
  var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
3898
3927
  var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
3899
- var SetTaskStateSchema = z3.object({
3900
- state: z3.enum(["in_progress", "completed", "needs_input"])
3901
- });
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
3928
  var TurnSchema = z3.object({
3910
3929
  prompt: z3.string(),
3911
3930
  reply: z3.string()
@@ -4035,108 +4054,6 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
4035
4054
  })
4036
4055
  ]);
4037
4056
  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
- var PendingRepliesSchema = z3.object({
4046
- replies: z3.array(
4047
- z3.object({
4048
- parentId: z3.string(),
4049
- notificationId: z3.string(),
4050
- workId: z3.string().uuid().optional(),
4051
- decisionId: z3.string().uuid().optional(),
4052
- answer: UserAnswerSchema,
4053
- /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
4054
- * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
4055
- * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
4056
- sealed: z3.lazy(() => SealedAnswerSchema).optional(),
4057
- /** The call record rendered for THIS agent: the raw words the shaped answer was
4058
- * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
4059
- transcript: z3.string().optional(),
4060
- /** Coverage report (#396): which declared `points` this answer addressed. */
4061
- covered: z3.array(z3.string()).optional()
4062
- })
4063
- ),
4064
- pending: z3.array(
4065
- z3.object({ parentId: z3.string(), notificationId: z3.string(), createdAt: z3.string() })
4066
- ),
4067
- /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
4068
- * sees for this session's identity. From inside a session there was no way to find out —
4069
- * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
4070
- * agent that cannot tell which agent it is cannot tell whether work addressed to
4071
- * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
4072
- you: z3.object({ name: z3.string(), device: z3.string().nullable(), tokenId: z3.string() }).optional(),
4073
- /** User-initiated requests addressed to this agent; act on them and reply via
4074
- * contact on the same parentId. Keeps reappearing until you call
4075
- * set_work_state on its workId (or notificationId once while bootstrapping). */
4076
- requests: z3.array(
4077
- z3.object({
4078
- parentId: z3.string(),
4079
- notificationId: z3.string(),
4080
- workId: z3.string().uuid().optional(),
4081
- text: z3.string(),
4082
- createdAt: z3.string(),
4083
- /** The user seeded this request with a past conversation — call get_thread on it
4084
- * FIRST and treat the transcript as prior context (#57/#251). */
4085
- contextParentId: z3.string().optional(),
4086
- /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
4087
- * the account — the name here — which has not been seen since it landed, so nobody
4088
- * came for it. Handed to you because you are the session that is here. Take it like
4089
- * any request (set_work_state claims it, reply with contact on its parentId), and say
4090
- * whose it was, because the user chose that agent on purpose. */
4091
- stranded: z3.string().optional()
4092
- })
4093
- ),
4094
- /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
4095
- * notification lists remain during migration so old clients keep their existing view. */
4096
- work: z3.array(z3.object({
4097
- workId: z3.string().uuid(),
4098
- parentId: z3.string().optional(),
4099
- objective: z3.string().nullable(),
4100
- state: z3.enum(["active", "blocked", "waiting_external"]),
4101
- blockedOn: z3.array(z3.string().uuid()),
4102
- updatedAt: z3.string()
4103
- })).optional(),
4104
- /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
4105
- * if blocked, or at a time that has passed). Re-surfaced every sweep until you
4106
- * fulfill one by calling contact on its parentId. */
4107
- /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
4108
- * reach. Same array the contact/await replies carry — one queue, every carrier. */
4109
- also: z3.array(RideAlongSchema).optional(),
4110
- owedCallbacks: z3.array(
4111
- z3.object({ parentId: z3.string(), trigger: CallbackTriggerSchema, note: z3.string() })
4112
- ),
4113
- /** Work (either direction) you reported in_progress a while ago and never reported
4114
- * completed — likely left half-done by this session or a prior one that crashed or
4115
- * went idle. Report a real state (set_work_state) or continue the work. */
4116
- stalled: z3.array(
4117
- z3.object({ parentId: z3.string(), notificationId: z3.string(), title: z3.string().nullable(), startedAt: z3.string() })
4118
- ),
4119
- /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
4120
- * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
4121
- * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
4122
- * thread already has a turn in progress (younger than the stall cutoff); let it finish and
4123
- * ride the next turn. `items` are that thread's replies/requests in arrival order; the
4124
- * full payload for each is in the flat `replies`/`requests` arrays (matched by
4125
- * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
4126
- threads: z3.array(
4127
- z3.object({
4128
- parentId: z3.string(),
4129
- busy: z3.boolean(),
4130
- items: z3.array(
4131
- z3.object({
4132
- kind: z3.enum(["reply", "request"]),
4133
- notificationId: z3.string(),
4134
- at: z3.string()
4135
- })
4136
- )
4137
- })
4138
- )
4139
- });
4140
4057
  var NotifyResponseSchema = z3.object({
4141
4058
  notificationId: z3.string(),
4142
4059
  workId: z3.string().uuid().optional(),
@@ -4364,7 +4281,21 @@ var InboxItemSchema = z3.object({
4364
4281
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
4365
4282
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
4366
4283
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
4367
- agentX25519: z3.string().optional()
4284
+ agentX25519: z3.string().optional(),
4285
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
4286
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
4287
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
4288
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
4289
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
4290
+ communication: z3.object({
4291
+ deliveryId: z3.string(),
4292
+ kind: z3.enum(["notification", "call"]),
4293
+ entryId: z3.string(),
4294
+ goalIds: z3.array(z3.string()),
4295
+ decisionNeedId: z3.string().optional(),
4296
+ sealed: z3.boolean(),
4297
+ goalState: z3.string().optional()
4298
+ }).optional()
4368
4299
  });
4369
4300
  var SnoozeRequestSchema = z3.object({
4370
4301
  requestId: z3.string(),
@@ -4600,26 +4531,54 @@ var CreateRequestSchema = z3.object({
4600
4531
  * the agent reads it via get_thread. Must belong to the requesting user. */
4601
4532
  contextParentId: z3.string().optional()
4602
4533
  });
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"]
4534
+ var QueueQuestionSchema = z3.object({
4535
+ /** The decision need's id — what an answer is accepted against. */
4536
+ id: z3.string(),
4537
+ /** The words that were asked, from the request Entry that asked them. */
4538
+ question: z3.string(),
4539
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
4540
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
4541
+ * can answer. */
4542
+ deliveryId: z3.string().nullable().default(null),
4543
+ /** The Entry the ruling is about. */
4544
+ aboutId: z3.string().nullable().default(null),
4545
+ /** Empty for a free-text question. */
4546
+ options: z3.array(OptionSchema).default([]),
4547
+ select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
4548
+ askedAt: z3.string(),
4549
+ /** Null while the question is open — which is how the page tells the two apart. */
4550
+ answeredAt: z3.string().nullable().default(null),
4551
+ /** The ruling in the person's own words, from the contribution that replied — not the
4552
+ * option id, which is not something anyone reads back. Null while it is open, and null
4553
+ * for a settled question whose reply carried nothing readable. */
4554
+ answer: z3.string().nullable().default(null)
4555
+ });
4556
+ var QueueItemSchema = z3.object({
4557
+ id: z3.string(),
4558
+ /** One-line headline — the first sentence of the outcome. */
4559
+ title: z3.string(),
4560
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
4561
+ intent: z3.string(),
4562
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
4563
+ state: z3.string(),
4564
+ /** Who holds it (a participant ref); null when nobody does yet. */
4565
+ assignee: z3.string().nullable().default(null),
4566
+ /** What the agent last said it was doing; null if it has said nothing. */
4567
+ progress: z3.string().nullable().default(null),
4568
+ reviewPending: z3.boolean().default(false),
4569
+ dueAt: z3.string().nullable().default(null),
4570
+ /** The Goal this one was opened under; null at the root. */
4571
+ parentGoalId: z3.string().nullable().default(null),
4572
+ /** Goals opened under this one — only those the same list holds. */
4573
+ childGoalIds: z3.array(z3.string()).default([]),
4574
+ /** Goals this one waits on (start or finish gates). */
4575
+ dependencyGoalIds: z3.array(z3.string()).default([]),
4576
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
4577
+ blocked: z3.boolean().default(false),
4578
+ /** Every decision need on it, open or settled — the page decides which to show. */
4579
+ questions: z3.array(QueueQuestionSchema).default([]),
4580
+ createdAt: z3.string(),
4581
+ updatedAt: z3.string().nullable().default(null)
4623
4582
  });
4624
4583
  var NoteSourceSchema = z3.enum(["app", "call"]);
4625
4584
  var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
@@ -4748,6 +4707,8 @@ var AcceptTriageResultSchema = z3.object({
4748
4707
  failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
4749
4708
  });
4750
4709
  var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
4710
+ var WAKE_EVENT = "wake";
4711
+ var wakeChannel = (tokenId) => `wake:${tokenId}`;
4751
4712
  var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
4752
4713
  var OAuthStartSchema = z3.object({
4753
4714
  provider: z3.enum(["cma"]),
@@ -4756,8 +4717,9 @@ var OAuthStartSchema = z3.object({
4756
4717
  var DeliveryConfigSchema = z3.object({
4757
4718
  tokenId: z3.string(),
4758
4719
  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. */
4720
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
4721
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
4722
+ * carries credentials; only a `poll` registration can come back with null here. */
4761
4723
  realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
4762
4724
  });
4763
4725
  var StatusSchema = z3.object({
@@ -4966,21 +4928,6 @@ function utf8ToBytes(s) {
4966
4928
  }
4967
4929
  return Uint8Array.from(out);
4968
4930
  }
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
4931
  var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4985
4932
  function toB64(bytes) {
4986
4933
  let out = "";
@@ -5026,15 +4973,9 @@ function concat(parts) {
5026
4973
  }
5027
4974
  return out;
5028
4975
  }
5029
- function u16be(n) {
5030
- return Uint8Array.of(n >>> 8 & 255, n & 255);
5031
- }
5032
4976
  function u32be(n) {
5033
4977
  return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
5034
4978
  }
5035
- function readU32be(b, o) {
5036
- return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
5037
- }
5038
4979
  function lp(b) {
5039
4980
  return concat([u32be(b.length), b]);
5040
4981
  }
@@ -5076,9 +5017,6 @@ function sharedSecret(peerX25519PubB64, mySecretB64) {
5076
5017
  if (raw.every((b) => b === 0)) throw new Error("low-order X25519 point \u2014 shared secret rejected");
5077
5018
  return toB64(raw);
5078
5019
  }
5079
- function keyId(publicKeyB64) {
5080
- return toB64(import_tweetnacl2.default.hash(fromB64(publicKeyB64)).slice(0, 8));
5081
- }
5082
5020
  function credentialBody(c) {
5083
5021
  return concat([
5084
5022
  lp(utf8ToBytes("paigy-device-credential-v1")),
@@ -5096,43 +5034,6 @@ function verifyDeviceCredential(cred, uikPublicB64) {
5096
5034
  return false;
5097
5035
  }
5098
5036
  }
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
5037
  var SAS_DOMAIN = "paigy-sas-v2";
5137
5038
  function deriveSAS(input, bits = 24) {
5138
5039
  const bundle = concat([
@@ -5360,21 +5261,6 @@ async function pairStep(deviceCode, keyFile) {
5360
5261
  return { kind: "e2ee_aborted", warning: e.message };
5361
5262
  }
5362
5263
  }
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
5264
  var UnpairedError = class extends Error {
5379
5265
  constructor() {
5380
5266
  super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
@@ -5435,6 +5321,7 @@ async function updateGoal(goalId, input, opts = {}) {
5435
5321
  if (!res.ok) await fail("update_goal", res);
5436
5322
  return await res.json();
5437
5323
  }
5324
+ var AWAIT_WINDOW_MS = 45e3;
5438
5325
  async function getThread(parentId, opts = {}) {
5439
5326
  const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/thread/${encodeURIComponent(parentId)}`, {
5440
5327
  headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
@@ -5449,17 +5336,6 @@ async function searchThreads(q, opts = {}) {
5449
5336
  if (!res.ok) await fail("search_threads", res);
5450
5337
  return await res.json();
5451
5338
  }
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
5339
  var tokenOverride = null;
5464
5340
  function overrideToken(secret) {
5465
5341
  tokenOverride = secret;
@@ -5505,10 +5381,10 @@ async function heartbeat(runtime, opts = {}) {
5505
5381
  }));
5506
5382
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
5507
5383
  }
5508
- async function registerDelivery(mode) {
5384
+ async function registerDelivery(mode, opts = {}) {
5509
5385
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
5510
5386
  method: "POST",
5511
- headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
5387
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
5512
5388
  body: JSON.stringify({ mode })
5513
5389
  }));
5514
5390
  if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
@@ -5544,7 +5420,7 @@ Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answe
5544
5420
  };
5545
5421
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
5546
5422
  if (opts.waits === false) return read(opts.signal);
5547
- const window = AbortSignal.timeout(45e3);
5423
+ const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
5548
5424
  const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
5549
5425
  let latest;
5550
5426
  try {
@@ -5552,7 +5428,7 @@ Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answe
5552
5428
  signal.throwIfAborted();
5553
5429
  latest = await read(signal);
5554
5430
  if (settled(latest)) return latest;
5555
- await sleep3(5e3, void 0, { signal });
5431
+ await sleep2(5e3, void 0, { signal });
5556
5432
  }
5557
5433
  } catch (error) {
5558
5434
  opts.signal?.throwIfAborted();
@@ -5604,106 +5480,102 @@ async function runTool(name, args, opts) {
5604
5480
  throw new Error(`Unknown tool: ${name}`);
5605
5481
  }
5606
5482
  }
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) {
5483
+ var HEARTBEAT_MS = 25e3;
5484
+ var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
5485
+ var JOIN = { config: { broadcast: { self: false } } };
5486
+ var lines = /* @__PURE__ */ new Map();
5487
+ function frame(line, topic, event, payload) {
5655
5488
  try {
5656
- return (await res.text()).slice(0, 300);
5489
+ line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
5657
5490
  } catch {
5658
- return "";
5659
5491
  }
5660
5492
  }
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) {
5493
+ function stop(line) {
5494
+ line.open = false;
5495
+ clearInterval(line.heart);
5496
+ clearTimeout(line.retry);
5497
+ }
5498
+ function connect(line) {
5499
+ const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
5500
+ const ws = line.ws = new WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
5501
+ ws.addEventListener("open", () => {
5502
+ line.open = true;
5503
+ line.attempt = 0;
5504
+ for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
5505
+ line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
5506
+ });
5507
+ ws.addEventListener("message", (e) => {
5508
+ let m;
5667
5509
  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;
5510
+ m = JSON.parse(String(e.data));
5511
+ } catch {
5512
+ return;
5513
+ }
5514
+ if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
5515
+ for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
5516
+ if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
5517
+ }
5518
+ });
5519
+ let gone = false;
5520
+ const again = () => {
5521
+ if (gone) return;
5522
+ gone = true;
5523
+ stop(line);
5524
+ if (!line.topics.size) return;
5525
+ line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
5526
+ };
5527
+ ws.addEventListener("close", again);
5528
+ ws.addEventListener("error", again);
5529
+ }
5530
+ function subscribeRealtime(args) {
5531
+ const key = `${args.url} ${args.anonKey}`;
5532
+ let line = lines.get(key);
5533
+ if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
5534
+ let heard = line.topics.get(args.topic);
5535
+ if (!heard) {
5536
+ line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
5537
+ if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
5538
+ }
5539
+ const me = { event: args.event, onMessage: args.onMessage };
5540
+ heard.add(me);
5541
+ if (!line.ws) connect(line);
5542
+ return {
5543
+ close: () => {
5544
+ if (!heard.delete(me) || heard.size) return;
5545
+ line.topics.delete(args.topic);
5546
+ if (line.topics.size) {
5547
+ if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
5548
+ return;
5687
5549
  }
5688
- lastError = error;
5689
- if (attempt + 1 < attempts) {
5690
- await doSleep(backoffMs * 2 ** attempt);
5550
+ lines.delete(key);
5551
+ stop(line);
5552
+ try {
5553
+ line.ws?.close();
5554
+ } catch {
5691
5555
  }
5692
5556
  }
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
- );
5557
+ };
5698
5558
  }
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
5559
+ async function subscribeWake(onNudge, opts = {}) {
5560
+ const cfg = await registerDelivery("self_hosted", opts);
5561
+ const { url, anonKey } = cfg.realtime;
5562
+ const channel = wakeChannel(cfg.tokenId);
5563
+ const ch = subscribeRealtime({
5564
+ url,
5565
+ anonKey,
5566
+ topic: channel,
5567
+ event: WAKE_EVENT,
5568
+ onMessage: (payload) => onNudge(payload ?? {})
5706
5569
  });
5570
+ return {
5571
+ tokenId: cfg.tokenId,
5572
+ channel,
5573
+ close: async () => {
5574
+ ch.close();
5575
+ await registerDelivery("poll", opts).catch(() => {
5576
+ });
5577
+ }
5578
+ };
5707
5579
  }
5708
5580
 
5709
5581
  export {
@@ -5729,15 +5601,13 @@ export {
5729
5601
  fetchCredential,
5730
5602
  pairStep,
5731
5603
  UnpairedError,
5732
- sweepLeased,
5604
+ getGoal,
5733
5605
  overrideToken,
5734
- authToken,
5735
5606
  hatch,
5736
5607
  whoAmI,
5737
5608
  setIdentity,
5738
5609
  heartbeat,
5739
- registerDelivery,
5610
+ checkReplies,
5740
5611
  runTool,
5741
- decodeReply,
5742
- ackReply
5612
+ subscribeWake
5743
5613
  };