@paigy/mcp 0.33.0 → 0.35.0

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AGENT_NAME
3
- } from "./chunk-E3OWSUGJ.js";
3
+ } from "./chunk-U6SLIORK.js";
4
4
 
5
5
  // src/clients.ts
6
6
  import { execFile } from "child_process";
@@ -2645,10 +2645,10 @@ var IntentSchema = z.object({
2645
2645
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
2646
2646
  kind: z.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
2647
2647
  detail: z.string(),
2648
- /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
2649
- * "after lunch") against the agent machine's clock — the user's — and attaches
2650
- * the seconds, ready to pass straight to schedule_callback. Absent when the
2651
- * detail didn't parse (the agent interprets it) or the kind isn't defer. */
2648
+ /** Defer only: seconds until the callback the caller asked for, when something upstream
2649
+ * already read the time. Nothing sets it today (#397 documented an MCP parser that was
2650
+ * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
2651
+ * (`parseDelay`, #1292), and a value here simply wins over that reading. */
2652
2652
  dueInSeconds: z.number().int().positive().optional(),
2653
2653
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
2654
2654
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
@@ -2756,6 +2756,12 @@ var PendingRepliesSchema = z.object({
2756
2756
  pending: z.array(
2757
2757
  z.object({ parentId: z.string(), notificationId: z.string(), createdAt: z.string() })
2758
2758
  ),
2759
+ /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
2760
+ * sees for this session's identity. From inside a session there was no way to find out —
2761
+ * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
2762
+ * agent that cannot tell which agent it is cannot tell whether work addressed to
2763
+ * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
2764
+ you: z.object({ name: z.string(), device: z.string().nullable(), tokenId: z.string() }).optional(),
2759
2765
  /** User-initiated requests addressed to this agent; act on them and reply via
2760
2766
  * contact on the same parentId. Keeps reappearing until you call
2761
2767
  * set_task_state on its notificationId. */
@@ -2767,7 +2773,13 @@ var PendingRepliesSchema = z.object({
2767
2773
  createdAt: z.string(),
2768
2774
  /** The user seeded this request with a past conversation — call get_thread on it
2769
2775
  * FIRST and treat the transcript as prior context (#57/#251). */
2770
- contextParentId: z.string().optional()
2776
+ contextParentId: z.string().optional(),
2777
+ /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
2778
+ * the account — the name here — which has not been seen since it landed, so nobody
2779
+ * came for it. Handed to you because you are the session that is here. Take it like
2780
+ * any request (set_task_state claims it, reply with contact on its parentId), and say
2781
+ * whose it was, because the user chose that agent on purpose. */
2782
+ stranded: z.string().optional()
2771
2783
  })
2772
2784
  ),
2773
2785
  /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
@@ -3068,7 +3080,13 @@ var BrokerTuningSchema = z.object({
3068
3080
  * uninterpretable, 'high' = whenever not fully certain. */
3069
3081
  clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
3070
3082
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
3071
- phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
3083
+ phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional(),
3084
+ /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
3085
+ * which is every account today — means the agent's own words decide, per ask: a call
3086
+ * about an English ask opens in English. This is the only thing that overrides that,
3087
+ * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
3088
+ * Set per user (no UI yet), like `voiceTuning`. */
3089
+ language: z.enum(["en", "es"]).optional()
3072
3090
  });
3073
3091
  var UserSettingsSchema = z.object({
3074
3092
  permissions: z.object({
@@ -3276,7 +3294,9 @@ var NoteSchema = z.object({
3276
3294
  /** The request thread minted at assignment; null until assigned. */
3277
3295
  parentId: z.string().nullable(),
3278
3296
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
3279
- * call — never a deadline, and nothing rings when it passes. Null = "the very next
3297
+ * call — never a deadline. It only ever comes from the user's own words, so when it
3298
+ * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
3299
+ * a ring at that time); after that it rides like any other. Null = "the very next
3280
3300
  * call", the right reading of "remind me to…" with no time attached. */
3281
3301
  // Defaulted, not required: a Note from an API deploy older than the reminders
3282
3302
  // migration has none of these, and the defaults ARE what it means — no not-before,
@@ -3319,6 +3339,59 @@ var AssignNoteSchema = z.object({
3319
3339
  workspace: z.string().min(1)
3320
3340
  }).optional()
3321
3341
  }).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
3342
+ var TriageVerdictSchema = z.enum(["keep", "stale", "close", "assign"]);
3343
+ var TriageItemSchema = z.object({
3344
+ noteId: z.string(),
3345
+ /** The note's headline at run time. */
3346
+ title: z.string(),
3347
+ /** WHY, in one short human line, evidence first — this is read on a phone underneath
3348
+ * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
3349
+ * Never a model's reasoning transcript, never an id. */
3350
+ why: z.string()
3351
+ });
3352
+ var TriageAssignmentSchema = z.object({
3353
+ /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
3354
+ agent: z.string(),
3355
+ /** Its display name at run time (the name on the hatchling's card). Denormalized for the
3356
+ * same reason as `title`: the card must render from the proposal alone. */
3357
+ agentName: z.string(),
3358
+ notes: z.array(TriageItemSchema)
3359
+ });
3360
+ var TriageStatusSchema = z.enum(["open", "superseded", "dismissed"]);
3361
+ var SubmitTriageSchema = z.object({
3362
+ /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
3363
+ * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
3364
+ * so the phone can say where the content went — an unattributed privacy claim is worth
3365
+ * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
3366
+ provider: z.string().min(1).max(60),
3367
+ /** The concrete model when the provider names one (an ollama tag); null otherwise. */
3368
+ model: z.string().max(200).nullable().optional(),
3369
+ /** How many open notes the run actually looked at — the denominator on the phone
3370
+ * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
3371
+ reviewed: z.number().int().min(0).max(1e4).default(0),
3372
+ close: z.array(TriageItemSchema).max(200).default([]),
3373
+ stale: z.array(TriageItemSchema).max(200).default([]),
3374
+ assign: z.array(TriageAssignmentSchema).max(50).default([])
3375
+ });
3376
+ var TriageProposalSchema = SubmitTriageSchema.extend({
3377
+ id: z.string(),
3378
+ runAt: z.string(),
3379
+ status: TriageStatusSchema,
3380
+ model: z.string().nullable().default(null)
3381
+ });
3382
+ var AcceptTriageSchema = z.discriminatedUnion("group", [
3383
+ z.object({ group: z.literal("close"), noteIds: z.array(z.string()).max(200).optional() }),
3384
+ z.object({ group: z.literal("stale"), noteIds: z.array(z.string()).max(200).optional() }),
3385
+ z.object({
3386
+ group: z.literal("assign"),
3387
+ agent: z.string().min(1),
3388
+ noteIds: z.array(z.string()).max(200).optional()
3389
+ })
3390
+ ]);
3391
+ var AcceptTriageResultSchema = z.object({
3392
+ accepted: z.array(z.string()),
3393
+ failed: z.array(z.object({ noteId: z.string(), reason: z.string() }))
3394
+ });
3322
3395
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
3323
3396
  var RegisterDeliverySchema = z.object({ mode: DeliveryModeSchema });
3324
3397
  var OAuthStartSchema = z.object({
@@ -4100,7 +4173,6 @@ async function submitNotification(req, opts = {}) {
4100
4173
  if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
4101
4174
  return await res.json();
4102
4175
  }
4103
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
4104
4176
  function decryptAnswer(reply, deps = {}) {
4105
4177
  if (!reply.sealed) return reply.answer;
4106
4178
  const keyFile = (deps.readKeyFile ?? readKeyFile)();
@@ -4109,15 +4181,6 @@ function decryptAnswer(reply, deps = {}) {
4109
4181
  }
4110
4182
  return openSealedAnswer(reply.sealed, keyFile.keypair);
4111
4183
  }
4112
- async function pollAnswer(notificationId) {
4113
- const token = readToken();
4114
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`, {
4115
- headers: { authorization: `Bearer ${token}` }
4116
- }));
4117
- if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
4118
- const item = await res.json();
4119
- return item.type === "idle" ? item : decryptItem(item);
4120
- }
4121
4184
  var AWAIT_WINDOW_MS = 45e3;
4122
4185
  var _partialSeen = /* @__PURE__ */ new Map();
4123
4186
  async function pollPartials(notificationId) {
@@ -4138,30 +4201,6 @@ async function pollPartials(notificationId) {
4138
4201
  return null;
4139
4202
  }
4140
4203
  }
4141
- async function awaitReply(notificationId, opts = {}) {
4142
- const intervalMs = opts.intervalMs ?? 5e3;
4143
- const windowMs = opts.windowMs ?? AWAIT_WINDOW_MS;
4144
- const doSleep = opts.sleep ?? sleep2;
4145
- const now = opts.now ?? Date.now;
4146
- const start = now();
4147
- while (true) {
4148
- if (opts.signal?.aborted) return { type: "idle" };
4149
- const item = await pollAnswer(notificationId);
4150
- if (item.type !== "idle") return item;
4151
- const partial = await pollPartials(notificationId);
4152
- if (partial) return partial;
4153
- if (now() - start >= windowMs) return { type: "idle" };
4154
- await doSleep(intervalMs);
4155
- }
4156
- }
4157
- function decryptItem(item) {
4158
- if (item.type !== "reply") return item;
4159
- const enriched = { ...item, intents: landIntents(item.intents) };
4160
- if (!enriched.sealed) return enriched;
4161
- const answer = decryptAnswer(enriched);
4162
- const { sealed: _s, ...rest } = enriched;
4163
- return { ...rest, answer };
4164
- }
4165
4204
  function landIntents(intents) {
4166
4205
  if (!intents?.length) return intents;
4167
4206
  return intents.map((i) => {
@@ -4187,10 +4226,14 @@ async function searchThreads(q) {
4187
4226
  async function checkReplies(opts = {}) {
4188
4227
  const token = authToken(opts.token);
4189
4228
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
4190
- headers: { authorization: `Bearer ${token}` }
4229
+ headers: {
4230
+ authorization: `Bearer ${token}`,
4231
+ ...opts.replyAck ? { "x-paigy-await-ack": "1" } : {}
4232
+ }
4191
4233
  }));
4192
4234
  if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
4193
4235
  const pending = await res.json();
4236
+ if (opts.replyAck) return pending;
4194
4237
  return {
4195
4238
  ...pending,
4196
4239
  replies: pending.replies.map((raw) => {
@@ -4255,15 +4298,6 @@ async function heartbeat(runtime, opts = {}) {
4255
4298
  }));
4256
4299
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
4257
4300
  }
4258
- async function setTaskState(notificationId, state, opts = {}) {
4259
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify/${notificationId}/state`, {
4260
- method: "PATCH",
4261
- headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
4262
- body: JSON.stringify({ state })
4263
- }));
4264
- if (!res.ok) throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
4265
- return await res.json();
4266
- }
4267
4301
  async function registerDelivery(mode) {
4268
4302
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
4269
4303
  method: "POST",
@@ -4293,6 +4327,204 @@ async function handoff(req) {
4293
4327
  if (!res.ok) throw new Error(`handoff failed: ${res.status} ${await res.text()}`);
4294
4328
  return await res.json();
4295
4329
  }
4330
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4331
+ var ReplyLeaseExpiredError = class extends Error {
4332
+ constructor() {
4333
+ super(
4334
+ "The Paigy reply lease expired before acknowledgement. The answer remains durable; call await_reply again to reclaim it before acting."
4335
+ );
4336
+ this.name = "ReplyLeaseExpiredError";
4337
+ }
4338
+ };
4339
+ var ReplyAckError = class extends Error {
4340
+ constructor(message, retryable) {
4341
+ super(message);
4342
+ this.retryable = retryable;
4343
+ this.name = "ReplyAckError";
4344
+ }
4345
+ retryable;
4346
+ };
4347
+ function ensureAuthed2(res) {
4348
+ if (res.status === 401) throw new UnpairedError();
4349
+ return res;
4350
+ }
4351
+ function decryptAnswer2(reply, deps = {}) {
4352
+ if (!reply.sealed) return reply.answer;
4353
+ const keyFile = (deps.readKeyFile ?? readKeyFile)();
4354
+ if (!keyFile?.keypair) {
4355
+ throw new Error(
4356
+ "received a sealed answer but this agent has no device key to open it \u2014 re-pair."
4357
+ );
4358
+ }
4359
+ return openSealedAnswer(reply.sealed, keyFile.keypair);
4360
+ }
4361
+ function landIntents2(intents) {
4362
+ if (!intents?.length) return intents;
4363
+ return intents.map((intent) => {
4364
+ if (intent.kind !== "defer") return intent;
4365
+ const due = parseDue(intent.detail);
4366
+ return due !== null ? { ...intent, dueInSeconds: due } : intent;
4367
+ });
4368
+ }
4369
+ function decodeReply(item, deps = {}) {
4370
+ const { delivery: _delivery, ...withoutDelivery } = item;
4371
+ const enriched = {
4372
+ ...withoutDelivery,
4373
+ intents: landIntents2(withoutDelivery.intents)
4374
+ };
4375
+ if (!enriched.sealed) return enriched;
4376
+ const answer = decryptAnswer2(enriched, deps);
4377
+ const { sealed: _sealed, ...plaintext } = enriched;
4378
+ return { ...plaintext, answer };
4379
+ }
4380
+ function deliveryOf(item) {
4381
+ if (item.type !== "reply" || !("delivery" in item) || item.delivery === void 0) {
4382
+ return null;
4383
+ }
4384
+ const delivery = item.delivery;
4385
+ if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
4386
+ throw new Error(
4387
+ "Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
4388
+ );
4389
+ }
4390
+ return delivery;
4391
+ }
4392
+ async function responseDetail(res) {
4393
+ try {
4394
+ return (await res.text()).slice(0, 300);
4395
+ } catch {
4396
+ return "";
4397
+ }
4398
+ }
4399
+ async function acknowledge(token, leaseId, opts = {}) {
4400
+ const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
4401
+ const backoffMs = Math.max(0, opts.backoffMs ?? 100);
4402
+ const doSleep = opts.doSleep ?? sleep2;
4403
+ let lastError;
4404
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
4405
+ try {
4406
+ const res = ensureAuthed2(await reach(`${BACKEND_URL}/api/await/ack`, {
4407
+ method: "POST",
4408
+ headers: {
4409
+ authorization: `Bearer ${token}`,
4410
+ "content-type": "application/json"
4411
+ },
4412
+ body: JSON.stringify({ leaseId })
4413
+ }));
4414
+ if (res.ok) return;
4415
+ const detail2 = await responseDetail(res);
4416
+ if (res.status === 409) throw new ReplyLeaseExpiredError();
4417
+ const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
4418
+ throw new ReplyAckError(
4419
+ `Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
4420
+ retryable
4421
+ );
4422
+ } catch (error) {
4423
+ if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
4424
+ throw error;
4425
+ }
4426
+ lastError = error;
4427
+ if (attempt + 1 < attempts) {
4428
+ await doSleep(backoffMs * 2 ** attempt);
4429
+ }
4430
+ }
4431
+ }
4432
+ const detail = lastError instanceof Error ? lastError.message : String(lastError);
4433
+ throw new Error(
4434
+ `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.`
4435
+ );
4436
+ }
4437
+ async function receiveReply(item, token, opts = {}) {
4438
+ const decoded = decodeReply(item, opts);
4439
+ const delivery = deliveryOf(item);
4440
+ if (!delivery) {
4441
+ if (opts.requireDelivery) {
4442
+ throw new Error("Paigy returned a catch-up reply without a delivery lease; the answer was not exposed.");
4443
+ }
4444
+ return decoded;
4445
+ }
4446
+ await acknowledge(token, delivery.leaseId, {
4447
+ attempts: opts.ackAttempts,
4448
+ backoffMs: opts.ackBackoffMs,
4449
+ doSleep: opts.ackSleep
4450
+ });
4451
+ return decoded;
4452
+ }
4453
+ async function pollAnswer(notificationId, opts = {}) {
4454
+ const token = authToken() ?? "";
4455
+ const res = ensureAuthed2(await reach(
4456
+ `${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`,
4457
+ {
4458
+ headers: {
4459
+ authorization: `Bearer ${token}`,
4460
+ "x-paigy-await-ack": "1"
4461
+ }
4462
+ }
4463
+ ));
4464
+ if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
4465
+ const wire = await res.json();
4466
+ if (wire.type !== "reply") return wire;
4467
+ return receiveReply(wire, token, opts);
4468
+ }
4469
+ async function awaitReply(notificationId, opts = {}) {
4470
+ const intervalMs = opts.intervalMs ?? 5e3;
4471
+ const windowMs = opts.windowMs ?? AWAIT_WINDOW_MS;
4472
+ const doSleep = opts.sleep ?? sleep2;
4473
+ const now = opts.now ?? Date.now;
4474
+ const start = now();
4475
+ while (true) {
4476
+ if (opts.signal?.aborted) return { type: "idle" };
4477
+ const item = await pollAnswer(notificationId);
4478
+ if (item.type !== "idle") return item;
4479
+ const partial = await pollPartials(notificationId);
4480
+ if (partial) return partial;
4481
+ if (now() - start >= windowMs) return { type: "idle" };
4482
+ await doSleep(intervalMs);
4483
+ }
4484
+ }
4485
+ async function checkReplies2(opts = {}) {
4486
+ const pending = await checkReplies({ ...opts, replyAck: true });
4487
+ const token = authToken(opts.token) ?? "";
4488
+ const replies = await Promise.all(pending.replies.map(async (reply) => {
4489
+ const received = await receiveReply(
4490
+ { type: "reply", ...reply },
4491
+ token,
4492
+ { requireDelivery: true }
4493
+ );
4494
+ const { type: _type, ...withoutType } = received;
4495
+ return withoutType;
4496
+ }));
4497
+ return {
4498
+ ...pending,
4499
+ replies
4500
+ };
4501
+ }
4502
+ function ensureAuthed3(res) {
4503
+ if (res.status === 401) throw new UnpairedError();
4504
+ return res;
4505
+ }
4506
+ async function setTaskState(notificationId, state, opts = {}) {
4507
+ const token = authToken(opts.token) ?? "";
4508
+ const res = ensureAuthed3(await reach(
4509
+ `${BACKEND_URL}/api/notify/${encodeURIComponent(notificationId)}/state`,
4510
+ {
4511
+ method: "PATCH",
4512
+ headers: {
4513
+ "content-type": "application/json",
4514
+ authorization: `Bearer ${token}`,
4515
+ "x-paigy-await-ack": "1"
4516
+ },
4517
+ body: JSON.stringify({ state })
4518
+ }
4519
+ ));
4520
+ if (!res.ok) {
4521
+ throw new Error(`set_task_state failed: ${res.status} ${await res.text()}`);
4522
+ }
4523
+ return await res.json();
4524
+ }
4525
+ function overrideToken2(secret) {
4526
+ overrideToken(secret);
4527
+ }
4296
4528
 
4297
4529
  export {
4298
4530
  BACKEND_URL,
@@ -4322,18 +4554,18 @@ export {
4322
4554
  pairStep,
4323
4555
  UnpairedError,
4324
4556
  submitNotification,
4325
- awaitReply,
4326
4557
  getThread,
4327
4558
  searchThreads,
4328
- checkReplies,
4329
4559
  answerCallerQuestion,
4330
- overrideToken,
4331
4560
  hatch,
4332
4561
  whoAmI,
4333
4562
  setIdentity,
4334
4563
  heartbeat,
4335
- setTaskState,
4336
4564
  registerDelivery,
4337
4565
  scheduleCallback,
4338
- handoff
4566
+ handoff,
4567
+ awaitReply,
4568
+ checkReplies2,
4569
+ setTaskState,
4570
+ overrideToken2
4339
4571
  };
@@ -251,10 +251,10 @@ var IntentSchema = z.object({
251
251
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
252
252
  kind: z.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
253
253
  detail: z.string(),
254
- /** Landed defer (#397): the MCP parses common spoken forms ("in 20 minutes",
255
- * "after lunch") against the agent machine's clock — the user's — and attaches
256
- * the seconds, ready to pass straight to schedule_callback. Absent when the
257
- * detail didn't parse (the agent interprets it) or the kind isn't defer. */
254
+ /** Defer only: seconds until the callback the caller asked for, when something upstream
255
+ * already read the time. Nothing sets it today (#397 documented an MCP parser that was
256
+ * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
257
+ * (`parseDelay`, #1292), and a value here simply wins over that reading. */
258
258
  dueInSeconds: z.number().int().positive().optional(),
259
259
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
260
260
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
@@ -362,6 +362,12 @@ var PendingRepliesSchema = z.object({
362
362
  pending: z.array(
363
363
  z.object({ parentId: z.string(), notificationId: z.string(), createdAt: z.string() })
364
364
  ),
365
+ /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
366
+ * sees for this session's identity. From inside a session there was no way to find out —
367
+ * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
368
+ * agent that cannot tell which agent it is cannot tell whether work addressed to
369
+ * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
370
+ you: z.object({ name: z.string(), device: z.string().nullable(), tokenId: z.string() }).optional(),
365
371
  /** User-initiated requests addressed to this agent; act on them and reply via
366
372
  * contact on the same parentId. Keeps reappearing until you call
367
373
  * set_task_state on its notificationId. */
@@ -373,7 +379,13 @@ var PendingRepliesSchema = z.object({
373
379
  createdAt: z.string(),
374
380
  /** The user seeded this request with a past conversation — call get_thread on it
375
381
  * FIRST and treat the transcript as prior context (#57/#251). */
376
- contextParentId: z.string().optional()
382
+ contextParentId: z.string().optional(),
383
+ /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
384
+ * the account — the name here — which has not been seen since it landed, so nobody
385
+ * came for it. Handed to you because you are the session that is here. Take it like
386
+ * any request (set_task_state claims it, reply with contact on its parentId), and say
387
+ * whose it was, because the user chose that agent on purpose. */
388
+ stranded: z.string().optional()
377
389
  })
378
390
  ),
379
391
  /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
@@ -684,7 +696,13 @@ var BrokerTuningSchema = z.object({
684
696
  * uninterpretable, 'high' = whenever not fully certain. */
685
697
  clarifyEagerness: z.enum(["low", "normal", "high"]).optional(),
686
698
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
687
- phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional()
699
+ phrasebook: z.array(z.object({ say: z.string().min(1).max(60), mean: z.string().min(1).max(120) })).max(24).optional(),
700
+ /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
701
+ * which is every account today — means the agent's own words decide, per ask: a call
702
+ * about an English ask opens in English. This is the only thing that overrides that,
703
+ * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
704
+ * Set per user (no UI yet), like `voiceTuning`. */
705
+ language: z.enum(["en", "es"]).optional()
688
706
  });
689
707
  var UserSettingsSchema = z.object({
690
708
  permissions: z.object({
@@ -892,7 +910,9 @@ var NoteSchema = z.object({
892
910
  /** The request thread minted at assignment; null until assigned. */
893
911
  parentId: z.string().nullable(),
894
912
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
895
- * call — never a deadline, and nothing rings when it passes. Null = "the very next
913
+ * call — never a deadline. It only ever comes from the user's own words, so when it
914
+ * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
915
+ * a ring at that time); after that it rides like any other. Null = "the very next
896
916
  * call", the right reading of "remind me to…" with no time attached. */
897
917
  // Defaulted, not required: a Note from an API deploy older than the reminders
898
918
  // migration has none of these, and the defaults ARE what it means — no not-before,
@@ -935,6 +955,59 @@ var AssignNoteSchema = z.object({
935
955
  workspace: z.string().min(1)
936
956
  }).optional()
937
957
  }).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
958
+ var TriageVerdictSchema = z.enum(["keep", "stale", "close", "assign"]);
959
+ var TriageItemSchema = z.object({
960
+ noteId: z.string(),
961
+ /** The note's headline at run time. */
962
+ title: z.string(),
963
+ /** WHY, in one short human line, evidence first — this is read on a phone underneath
964
+ * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
965
+ * Never a model's reasoning transcript, never an id. */
966
+ why: z.string()
967
+ });
968
+ var TriageAssignmentSchema = z.object({
969
+ /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
970
+ agent: z.string(),
971
+ /** Its display name at run time (the name on the hatchling's card). Denormalized for the
972
+ * same reason as `title`: the card must render from the proposal alone. */
973
+ agentName: z.string(),
974
+ notes: z.array(TriageItemSchema)
975
+ });
976
+ var TriageStatusSchema = z.enum(["open", "superseded", "dismissed"]);
977
+ var SubmitTriageSchema = z.object({
978
+ /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
979
+ * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
980
+ * so the phone can say where the content went — an unattributed privacy claim is worth
981
+ * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
982
+ provider: z.string().min(1).max(60),
983
+ /** The concrete model when the provider names one (an ollama tag); null otherwise. */
984
+ model: z.string().max(200).nullable().optional(),
985
+ /** How many open notes the run actually looked at — the denominator on the phone
986
+ * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
987
+ reviewed: z.number().int().min(0).max(1e4).default(0),
988
+ close: z.array(TriageItemSchema).max(200).default([]),
989
+ stale: z.array(TriageItemSchema).max(200).default([]),
990
+ assign: z.array(TriageAssignmentSchema).max(50).default([])
991
+ });
992
+ var TriageProposalSchema = SubmitTriageSchema.extend({
993
+ id: z.string(),
994
+ runAt: z.string(),
995
+ status: TriageStatusSchema,
996
+ model: z.string().nullable().default(null)
997
+ });
998
+ var AcceptTriageSchema = z.discriminatedUnion("group", [
999
+ z.object({ group: z.literal("close"), noteIds: z.array(z.string()).max(200).optional() }),
1000
+ z.object({ group: z.literal("stale"), noteIds: z.array(z.string()).max(200).optional() }),
1001
+ z.object({
1002
+ group: z.literal("assign"),
1003
+ agent: z.string().min(1),
1004
+ noteIds: z.array(z.string()).max(200).optional()
1005
+ })
1006
+ ]);
1007
+ var AcceptTriageResultSchema = z.object({
1008
+ accepted: z.array(z.string()),
1009
+ failed: z.array(z.object({ noteId: z.string(), reason: z.string() }))
1010
+ });
938
1011
  var DeliveryModeSchema = z.enum(["poll", "self_hosted"]);
939
1012
  var WAKE_EVENT = "wake";
940
1013
  var wakeChannel = (tokenId) => `wake:${tokenId}`;
package/dist/enable.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools
5
- } from "./chunk-SASREQWV.js";
6
- import "./chunk-E3OWSUGJ.js";
5
+ } from "./chunk-KNMI3CJ3.js";
6
+ import "./chunk-U6SLIORK.js";
7
7
 
8
8
  // src/enable.ts
9
9
  function main() {
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
  import {
3
3
  HandoffSchema,
4
4
  MISSED_CALL_PLAN
5
- } from "./chunk-JT7I3EGY.js";
5
+ } from "./chunk-YOGAG4YY.js";
6
6
  import {
7
7
  ENABLE_COMMAND,
8
8
  PAIGY_TOOL_IDS,
9
9
  autoConfigureClients,
10
10
  claudeInstallHint,
11
11
  paigyToolsAllowlisted
12
- } from "./chunk-SASREQWV.js";
12
+ } from "./chunk-KNMI3CJ3.js";
13
13
  import {
14
14
  clearSurface,
15
15
  writeSurface
@@ -22,7 +22,7 @@ import {
22
22
  UnpairedError,
23
23
  answerCallerQuestion,
24
24
  awaitReply,
25
- checkReplies,
25
+ checkReplies2,
26
26
  deleteKeyFile,
27
27
  deleteToken,
28
28
  fetchCredential,
@@ -34,7 +34,7 @@ import {
34
34
  lintNotify,
35
35
  listSlots,
36
36
  normalizeSpeech,
37
- overrideToken,
37
+ overrideToken2,
38
38
  pairStep,
39
39
  readKeyFile,
40
40
  readToken,
@@ -51,7 +51,7 @@ import {
51
51
  startE2ee,
52
52
  submitNotification,
53
53
  whoAmI
54
- } from "./chunk-E3OWSUGJ.js";
54
+ } from "./chunk-U6SLIORK.js";
55
55
 
56
56
  // src/index.ts
57
57
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -404,7 +404,7 @@ function prependNote(result, note) {
404
404
  }
405
405
  async function hatchUnderDevice(name, voice) {
406
406
  if (!listSlots().includes("Desktop")) return null;
407
- overrideToken(readToken("Desktop"));
407
+ overrideToken2(readToken("Desktop"));
408
408
  try {
409
409
  const minted = await hatch(name ?? suggestedAgentName() ?? "Agent", voice ?? null);
410
410
  const dt = { ok: true, access_token: minted.token, name: minted.name, device: null };
@@ -413,7 +413,7 @@ async function hatchUnderDevice(name, voice) {
413
413
  } catch {
414
414
  return null;
415
415
  } finally {
416
- overrideToken(null);
416
+ overrideToken2(null);
417
417
  }
418
418
  }
419
419
  async function runPair(device_code, name, voice, note) {
@@ -468,7 +468,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
468
468
  },
469
469
  {
470
470
  name: "check_replies",
471
- description: "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), your still-pending notifications, and `requests` \u2014 requests the user started toward you (each { notificationId, parentId, text }). Each keeps reappearing on every call until you actually engage with it: call set_task_state on its notificationId, which is what claims/acknowledges it \u2014 a human-initiated reply or request must never be silently dropped just because you read the list without acting. Also returns `threads` \u2014 the SAME replies + requests grouped by conversation, oldest thread first, each with a `busy` flag and its `items` in arrival order. WORK ONE THREAD AT A TIME: take the oldest thread whose `busy` is false, handle ALL of its items together in a single turn (one set_task_state), then go to the next \u2014 don't interleave threads item-by-item. A `busy` thread already has a turn in progress; leave it and let its new items ride the next turn. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. To wait on an answer to a contact call you just made, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with contact on its parentId. EVERY Paigy reply \u2014 this one, await_reply's, and contact's \u2014 may carry `also`: work assigned to you that no wake could reach, handed to you because you happened to be here. It is NOT what you asked about and it is never urgent: FINISH what you came for first, then take it up. Each entry has a `noteId` and the owner's own words; report on its `parentId` thread when it has one, and call set_task_state on that thread as you would for any assigned work. Ignoring it costs nothing \u2014 it rides your next reply too. Also returns `stalled`: work (either direction) you reported in_progress via set_task_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as await_reply's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.",
471
+ description: "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), your still-pending notifications, and `requests` \u2014 requests the user started toward you (each { notificationId, parentId, text }). Each keeps reappearing on every call until you actually engage with it: call set_task_state on its notificationId, which is what claims/acknowledges it \u2014 a human-initiated reply or request must never be silently dropped just because you read the list without acting. Also returns `threads` \u2014 the SAME replies + requests grouped by conversation, oldest thread first, each with a `busy` flag and its `items` in arrival order. WORK ONE THREAD AT A TIME: take the oldest thread whose `busy` is false, handle ALL of its items together in a single turn (one set_task_state), then go to the next \u2014 don't interleave threads item-by-item. A `busy` thread already has a turn in progress; leave it and let its new items ride the next turn. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. To wait on an answer to a contact call you just made, use await_reply instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with contact on its parentId. EVERY Paigy reply \u2014 this one, await_reply's, and contact's \u2014 may carry `also`: work assigned to you that no wake could reach, handed to you because you happened to be here. It is NOT what you asked about and it is never urgent: FINISH what you came for first, then take it up. Each entry has a `noteId` and the owner's own words; report on its `parentId` thread when it has one, and call set_task_state on that thread as you would for any assigned work. Ignoring it costs nothing \u2014 it rides your next reply too. Also returns `stalled`: work (either direction) you reported in_progress via set_task_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled. Also returns `you` \u2014 WHICH IDENTITY you are speaking as ({ name, device, tokenId }), the same name and device the user sees on their Agents screen. This is the only safe way to find out (calling `pair` can MINT a new identity instead of telling you about the current one). Use it when the user asks who you are, and to tell whether work addressed to a name is addressed to you. A request may also carry `stranded`: it was addressed to ANOTHER agent on this account (that name) which has not been seen since it landed, so nobody came for it and it is handed to you because you are the session that is here. Take it exactly like your own \u2014 set_task_state claims it, reply with contact on its parentId \u2014 and say whose it was, because the user picked that agent on purpose. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as await_reply's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.",
472
472
  inputSchema: json(z.object({}))
473
473
  },
474
474
  {
@@ -658,7 +658,7 @@ async function handleTool(request, signal) {
658
658
  return { content: [{ type: "text", text: JSON.stringify(item) }] };
659
659
  }
660
660
  case "check_replies": {
661
- const result = await checkReplies();
661
+ const result = await checkReplies2();
662
662
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
663
663
  }
664
664
  case "get_thread": {
package/dist/listen.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  WAKE_EVENT,
4
4
  wakeChannel
5
- } from "./chunk-JT7I3EGY.js";
5
+ } from "./chunk-YOGAG4YY.js";
6
6
  import {
7
- checkReplies,
7
+ checkReplies2,
8
8
  registerDelivery
9
- } from "./chunk-E3OWSUGJ.js";
9
+ } from "./chunk-U6SLIORK.js";
10
10
 
11
11
  // src/listen.ts
12
12
  import { createClient } from "@supabase/supabase-js";
@@ -170,7 +170,7 @@ function launchEnv(work, reason) {
170
170
  }
171
171
  async function sweep(reason) {
172
172
  try {
173
- const work = await checkReplies();
173
+ const work = await checkReplies2();
174
174
  emit({ type: "sweep", reason, ...work });
175
175
  const cmd = process.env.PAIGY_ON_WAKE;
176
176
  const inbound = work.replies.length > 0 || work.requests.length > 0;
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  autoConfigureClients,
4
4
  claudeInstallHint,
5
5
  openBrowser
6
- } from "./chunk-SASREQWV.js";
6
+ } from "./chunk-KNMI3CJ3.js";
7
7
  import {
8
8
  AGENT_NAME,
9
9
  TOKEN_PATH,
10
10
  hatch,
11
11
  listSlots,
12
- overrideToken,
12
+ overrideToken2,
13
13
  pollToken,
14
14
  readToken,
15
15
  requestCode,
@@ -17,7 +17,7 @@ import {
17
17
  setIdentity,
18
18
  sleep,
19
19
  whoAmI
20
- } from "./chunk-E3OWSUGJ.js";
20
+ } from "./chunk-U6SLIORK.js";
21
21
 
22
22
  // src/onboard.ts
23
23
  async function main() {
@@ -50,7 +50,7 @@ Paigy MCP onboarding (agent: "${AGENT_NAME}")
50
50
  console.log("The saved token is no longer valid \u2014 re-pairing.\n");
51
51
  }
52
52
  if (listSlots().includes("Desktop")) {
53
- overrideToken(readToken("Desktop"));
53
+ overrideToken2(readToken("Desktop"));
54
54
  try {
55
55
  const minted = await hatch(process.env.PAIGY_NAME ?? "Agent", null);
56
56
  saveToken({ ok: true, access_token: minted.token, name: minted.name, device: null });
@@ -67,7 +67,7 @@ Paigy MCP onboarding (agent: "${AGENT_NAME}")
67
67
  } catch {
68
68
  console.log("(This machine's saved credential didn't work \u2014 falling back to a code.)\n");
69
69
  } finally {
70
- overrideToken(null);
70
+ overrideToken2(null);
71
71
  }
72
72
  }
73
73
  const code = await requestCode();
@@ -6,7 +6,7 @@ import {
6
6
  BACKEND_URL,
7
7
  reach,
8
8
  readToken
9
- } from "./chunk-E3OWSUGJ.js";
9
+ } from "./chunk-U6SLIORK.js";
10
10
 
11
11
  // src/statusline.ts
12
12
  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.33.0",
3
+ "version": "0.35.0",
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",