@paigy/mcp 0.40.0 → 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 +43 -30
- package/dist/{chunk-FYH6FW2D.js → chunk-647HS22P.js} +6 -91
- package/dist/{chunk-BOLKVO6W.js → chunk-BUTCP2ED.js} +1 -1
- package/dist/{chunk-DM6YPBQH.js → chunk-FFI3AIZE.js} +703 -644
- package/dist/{chunk-CWGZVTPZ.js → chunk-YJEJZT56.js} +832 -1108
- package/dist/enable.js +3 -3
- package/dist/index.js +11 -153
- package/dist/listen.js +34 -82
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/statusline.js +4 -2
- package/package.json +12 -13
- package/dist/chunk-2SWUY5WK.js +0 -57
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// ../../packages/sdk/dist/index.js
|
|
2
2
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
3
3
|
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
6
|
+
import { z as z3 } from "zod";
|
|
4
7
|
import { z as z2 } from "zod";
|
|
5
|
-
import { z } from "zod";
|
|
6
8
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind3 } from "zod/v3";
|
|
7
9
|
import { ZodFirstPartyTypeKind } from "zod/v3";
|
|
8
10
|
import { ZodFirstPartyTypeKind as ZodFirstPartyTypeKind2 } from "zod/v3";
|
|
9
|
-
import {
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
10
13
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
11
14
|
import { homedir } from "os";
|
|
12
15
|
import { join } from "path";
|
|
16
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
17
|
+
import { createClient } from "@supabase/supabase-js";
|
|
13
18
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
14
19
|
var __create = Object.create;
|
|
15
20
|
var __defProp = Object.defineProperty;
|
|
@@ -340,17 +345,17 @@ var require_nacl_fast = __commonJS({
|
|
|
340
345
|
}
|
|
341
346
|
var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]);
|
|
342
347
|
function crypto_stream_salsa20_xor(c, cpos, m, mpos, b, n, k) {
|
|
343
|
-
var
|
|
348
|
+
var z4 = new Uint8Array(16), x = new Uint8Array(64);
|
|
344
349
|
var u, i;
|
|
345
|
-
for (i = 0; i < 16; i++)
|
|
346
|
-
for (i = 0; i < 8; i++)
|
|
350
|
+
for (i = 0; i < 16; i++) z4[i] = 0;
|
|
351
|
+
for (i = 0; i < 8; i++) z4[i] = n[i];
|
|
347
352
|
while (b >= 64) {
|
|
348
|
-
crypto_core_salsa20(x,
|
|
353
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
349
354
|
for (i = 0; i < 64; i++) c[cpos + i] = m[mpos + i] ^ x[i];
|
|
350
355
|
u = 1;
|
|
351
356
|
for (i = 8; i < 16; i++) {
|
|
352
|
-
u = u + (
|
|
353
|
-
|
|
357
|
+
u = u + (z4[i] & 255) | 0;
|
|
358
|
+
z4[i] = u & 255;
|
|
354
359
|
u >>>= 8;
|
|
355
360
|
}
|
|
356
361
|
b -= 64;
|
|
@@ -358,30 +363,30 @@ var require_nacl_fast = __commonJS({
|
|
|
358
363
|
mpos += 64;
|
|
359
364
|
}
|
|
360
365
|
if (b > 0) {
|
|
361
|
-
crypto_core_salsa20(x,
|
|
366
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
362
367
|
for (i = 0; i < b; i++) c[cpos + i] = m[mpos + i] ^ x[i];
|
|
363
368
|
}
|
|
364
369
|
return 0;
|
|
365
370
|
}
|
|
366
371
|
function crypto_stream_salsa20(c, cpos, b, n, k) {
|
|
367
|
-
var
|
|
372
|
+
var z4 = new Uint8Array(16), x = new Uint8Array(64);
|
|
368
373
|
var u, i;
|
|
369
|
-
for (i = 0; i < 16; i++)
|
|
370
|
-
for (i = 0; i < 8; i++)
|
|
374
|
+
for (i = 0; i < 16; i++) z4[i] = 0;
|
|
375
|
+
for (i = 0; i < 8; i++) z4[i] = n[i];
|
|
371
376
|
while (b >= 64) {
|
|
372
|
-
crypto_core_salsa20(x,
|
|
377
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
373
378
|
for (i = 0; i < 64; i++) c[cpos + i] = x[i];
|
|
374
379
|
u = 1;
|
|
375
380
|
for (i = 8; i < 16; i++) {
|
|
376
|
-
u = u + (
|
|
377
|
-
|
|
381
|
+
u = u + (z4[i] & 255) | 0;
|
|
382
|
+
z4[i] = u & 255;
|
|
378
383
|
u >>>= 8;
|
|
379
384
|
}
|
|
380
385
|
b -= 64;
|
|
381
386
|
cpos += 64;
|
|
382
387
|
}
|
|
383
388
|
if (b > 0) {
|
|
384
|
-
crypto_core_salsa20(x,
|
|
389
|
+
crypto_core_salsa20(x, z4, k, sigma);
|
|
385
390
|
for (i = 0; i < b; i++) c[cpos + i] = x[i];
|
|
386
391
|
}
|
|
387
392
|
return 0;
|
|
@@ -1261,12 +1266,12 @@ var require_nacl_fast = __commonJS({
|
|
|
1261
1266
|
for (a = 0; a < 16; a++) o[a] = c[a];
|
|
1262
1267
|
}
|
|
1263
1268
|
function crypto_scalarmult(q, n, p) {
|
|
1264
|
-
var
|
|
1269
|
+
var z4 = new Uint8Array(32);
|
|
1265
1270
|
var x = new Float64Array(80), r, i;
|
|
1266
1271
|
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf();
|
|
1267
|
-
for (i = 0; i < 31; i++)
|
|
1268
|
-
|
|
1269
|
-
|
|
1272
|
+
for (i = 0; i < 31; i++) z4[i] = n[i];
|
|
1273
|
+
z4[31] = n[31] & 127 | 64;
|
|
1274
|
+
z4[0] &= 248;
|
|
1270
1275
|
unpack25519(x, p);
|
|
1271
1276
|
for (i = 0; i < 16; i++) {
|
|
1272
1277
|
b[i] = x[i];
|
|
@@ -1274,7 +1279,7 @@ var require_nacl_fast = __commonJS({
|
|
|
1274
1279
|
}
|
|
1275
1280
|
a[0] = d[0] = 1;
|
|
1276
1281
|
for (i = 254; i >= 0; --i) {
|
|
1277
|
-
r =
|
|
1282
|
+
r = z4[i >>> 3] >>> (i & 7) & 1;
|
|
1278
1283
|
sel25519(a, b, r);
|
|
1279
1284
|
sel25519(c, d, r);
|
|
1280
1285
|
A(e, a, c);
|
|
@@ -3516,20 +3521,17 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
3516
3521
|
};
|
|
3517
3522
|
var OPTIONS_MIN = 2;
|
|
3518
3523
|
var OPTIONS_MAX = 6;
|
|
3519
|
-
var MISSED_CALL_PLAN = {
|
|
3520
|
-
retry_10m: { kind: "every", minutes: 10 },
|
|
3521
|
-
retry_30m: { kind: "every", minutes: 30 },
|
|
3522
|
-
retry_60m: { kind: "every", minutes: 60 },
|
|
3523
|
-
backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
|
|
3524
|
-
backoff_standard: { kind: "at", minutes: [10, 30, 120] },
|
|
3525
|
-
backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
|
|
3526
|
-
inbox: { kind: "once" },
|
|
3527
|
-
dismiss: { kind: "grace", minutes: 2 }
|
|
3528
|
-
};
|
|
3529
3524
|
function draft2020(node) {
|
|
3530
3525
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
3531
3526
|
if (node && typeof node === "object") {
|
|
3532
3527
|
const o = node;
|
|
3528
|
+
if (Array.isArray(o.items)) {
|
|
3529
|
+
o.prefixItems = o.items;
|
|
3530
|
+
if ("additionalItems" in o) {
|
|
3531
|
+
o.items = o.additionalItems;
|
|
3532
|
+
delete o.additionalItems;
|
|
3533
|
+
} else delete o.items;
|
|
3534
|
+
}
|
|
3533
3535
|
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
3534
3536
|
if (typeof o[excl] === "boolean") {
|
|
3535
3537
|
if (o[excl] === true && typeof o[lim] === "number") {
|
|
@@ -3548,73 +3550,107 @@ function mcpInputSchema(s) {
|
|
|
3548
3550
|
delete schema.$schema;
|
|
3549
3551
|
return draft2020(schema);
|
|
3550
3552
|
}
|
|
3551
|
-
var
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3553
|
+
var StartContactSchema = z.object({
|
|
3554
|
+
goalIds: z.tuple([z.string().uuid()]),
|
|
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
|
+
),
|
|
3571
|
+
waiting: z.enum(["none", "hard"]).default("none"),
|
|
3572
|
+
channel: z.enum(["notification", "call"]).default("notification"),
|
|
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(),
|
|
3574
|
+
threadId: z.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
|
|
3575
|
+
}).strict();
|
|
3576
|
+
var ContactSchema = z.union([StartContactSchema, z.object({ deliveryId: z.string().uuid() }).strict()]);
|
|
3577
|
+
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
3578
|
+
var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
|
|
3579
|
+
var CreateGoalSchema = z2.object({
|
|
3580
|
+
outcome: z2.string().trim().min(1).max(1e4),
|
|
3581
|
+
ownerParticipant: z2.string().trim().min(1).optional(),
|
|
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()
|
|
3565
3596
|
});
|
|
3566
|
-
var
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
),
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
description:
|
|
3597
|
+
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
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.")
|
|
3599
|
+
}).strict();
|
|
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.";
|
|
3601
|
+
var UpdateGoalSchema = z2.object({
|
|
3602
|
+
revision: z2.number().int().positive(),
|
|
3603
|
+
changes: z2.object({
|
|
3604
|
+
outcome: z2.string().trim().min(1).max(1e4).optional(),
|
|
3605
|
+
ownerParticipant: z2.string().trim().min(1).optional(),
|
|
3606
|
+
parentGoalId: z2.string().uuid().nullable().optional(),
|
|
3607
|
+
dependencies: z2.array(z2.object({ goalId: z2.string().uuid(), gate: z2.enum(["start", "finish"]) }).strict()).optional(),
|
|
3608
|
+
children: z2.array(z2.object({ outcome: z2.string().trim().min(1).max(1e4), ownerParticipant: z2.string().trim().min(1), gate: z2.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
3609
|
+
state: z2.enum(["active", "done", "cancelled"]).optional(),
|
|
3610
|
+
progress: z2.string().trim().min(1).max(1e4).optional(),
|
|
3611
|
+
reviewed: z2.literal(true).optional()
|
|
3612
|
+
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
3613
|
+
reason: z2.string().trim().min(1).max(2e3),
|
|
3614
|
+
operationId: z2.string().uuid().optional()
|
|
3615
|
+
}).strict();
|
|
3616
|
+
var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z2.string().uuid() }).strict();
|
|
3617
|
+
var ClaimGoalSchema = z2.object({ goalId: z2.string().uuid().optional() }).strict();
|
|
3618
|
+
var GetGoalSchema = z2.object({ goalId: z2.string().uuid() }).strict();
|
|
3619
|
+
var GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
|
|
3620
|
+
var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
|
|
3621
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
|
|
3622
|
+
var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
3623
|
+
var CheckRepliesSchema = z2.object({}).strict();
|
|
3624
|
+
var GetThreadSchema = z2.object({
|
|
3625
|
+
parentId: z2.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
3626
|
+
}).strict();
|
|
3627
|
+
var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
|
|
3628
|
+
var SearchThreadsSchema = z2.object({
|
|
3629
|
+
q: z2.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
3630
|
+
}).strict();
|
|
3631
|
+
var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
3632
|
+
var AGENT_TOOLS = [
|
|
3633
|
+
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
3634
|
+
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
3635
|
+
{ name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
|
|
3636
|
+
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
|
|
3637
|
+
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
3638
|
+
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
3639
|
+
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
3640
|
+
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
|
|
3641
|
+
];
|
|
3642
|
+
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
3643
|
+
var ContextSchema = z3.object({
|
|
3644
|
+
title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
3645
|
+
description: z3.array(z3.string().min(1)).describe(
|
|
3610
3646
|
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
3611
3647
|
)
|
|
3612
3648
|
});
|
|
3613
|
-
var ParticipantSchema =
|
|
3614
|
-
kind:
|
|
3615
|
-
id:
|
|
3649
|
+
var ParticipantSchema = z3.object({
|
|
3650
|
+
kind: z3.enum(["human", "agent"]),
|
|
3651
|
+
id: z3.string()
|
|
3616
3652
|
});
|
|
3617
|
-
var TransformSchema =
|
|
3653
|
+
var TransformSchema = z3.enum([
|
|
3618
3654
|
"structure",
|
|
3619
3655
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
3620
3656
|
"request_more",
|
|
@@ -3630,25 +3666,25 @@ var TransformSchema = z2.enum([
|
|
|
3630
3666
|
"summarize"
|
|
3631
3667
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
3632
3668
|
]);
|
|
3633
|
-
var OptionSchema =
|
|
3634
|
-
id:
|
|
3635
|
-
label:
|
|
3669
|
+
var OptionSchema = z3.object({
|
|
3670
|
+
id: z3.string(),
|
|
3671
|
+
label: z3.string(),
|
|
3636
3672
|
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
3637
3673
|
// the constraints below are what an agent reads when deciding to use these.
|
|
3638
|
-
html:
|
|
3674
|
+
html: z3.string().max(16384).describe(
|
|
3639
3675
|
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
3640
3676
|
).optional(),
|
|
3641
|
-
image:
|
|
3677
|
+
image: z3.string().url().describe(
|
|
3642
3678
|
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
3643
3679
|
).optional()
|
|
3644
3680
|
});
|
|
3645
|
-
var VisualSchema =
|
|
3646
|
-
url:
|
|
3647
|
-
label:
|
|
3681
|
+
var VisualSchema = z3.object({
|
|
3682
|
+
url: z3.string().url(),
|
|
3683
|
+
label: z3.string().optional()
|
|
3648
3684
|
});
|
|
3649
|
-
var NotifyLevelSchema =
|
|
3650
|
-
var SelectShapeSchema =
|
|
3651
|
-
var ReceiptEventSchema =
|
|
3685
|
+
var NotifyLevelSchema = z3.enum(["inbox", "push", "banner", "call"]);
|
|
3686
|
+
var SelectShapeSchema = z3.enum(["one", "many", "rank", "confirm", "text"]);
|
|
3687
|
+
var ReceiptEventSchema = z3.enum([
|
|
3652
3688
|
"delivered",
|
|
3653
3689
|
// the bundle reached the recipient at some level
|
|
3654
3690
|
"seen",
|
|
@@ -3678,44 +3714,47 @@ var ReceiptEventSchema = z2.enum([
|
|
|
3678
3714
|
// be rewound by a writer that forgot to advance it.
|
|
3679
3715
|
"restarted"
|
|
3680
3716
|
]);
|
|
3681
|
-
var AttentionSchema =
|
|
3717
|
+
var AttentionSchema = z3.object({
|
|
3682
3718
|
urgency: NotifyLevelSchema,
|
|
3683
3719
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
3684
3720
|
select: SelectShapeSchema.nullable(),
|
|
3685
3721
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
3686
|
-
points:
|
|
3722
|
+
points: z3.array(z3.string()).nullable(),
|
|
3687
3723
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
3688
|
-
blocking:
|
|
3724
|
+
blocking: z3.boolean(),
|
|
3689
3725
|
/** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
3690
3726
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
3691
|
-
deadline:
|
|
3727
|
+
deadline: z3.string().datetime().nullable().optional()
|
|
3692
3728
|
});
|
|
3693
|
-
var NotifyRequestFields =
|
|
3729
|
+
var NotifyRequestFields = z3.object({
|
|
3694
3730
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
3695
3731
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
3696
3732
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
3697
3733
|
context: ContextSchema.optional(),
|
|
3698
|
-
options:
|
|
3734
|
+
options: z3.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
3699
3735
|
"The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
|
|
3700
3736
|
),
|
|
3701
|
-
points:
|
|
3737
|
+
points: z3.array(z3.string().min(1)).optional().describe(
|
|
3702
3738
|
"The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
|
|
3703
3739
|
),
|
|
3704
|
-
visuals:
|
|
3740
|
+
visuals: z3.array(VisualSchema).optional().describe(
|
|
3705
3741
|
"Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
|
|
3706
3742
|
),
|
|
3707
3743
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
3708
|
-
repo:
|
|
3744
|
+
repo: z3.string().optional(),
|
|
3709
3745
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
3710
|
-
branch:
|
|
3746
|
+
branch: z3.string().optional(),
|
|
3711
3747
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
3712
3748
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
3713
3749
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
3714
3750
|
* still accepts the old name from older clients. */
|
|
3715
|
-
parentId:
|
|
3751
|
+
parentId: z3.string().uuid().optional(),
|
|
3716
3752
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
3717
3753
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
3718
|
-
workId:
|
|
3754
|
+
workId: z3.string().uuid().optional(),
|
|
3755
|
+
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
3756
|
+
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
3757
|
+
goalId: z3.string().uuid().optional(),
|
|
3719
3758
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
3720
3759
|
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
3721
3760
|
),
|
|
@@ -3723,7 +3762,7 @@ var NotifyRequestFields = z2.object({
|
|
|
3723
3762
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
3724
3763
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
3725
3764
|
* says HOW. */
|
|
3726
|
-
clarifies:
|
|
3765
|
+
clarifies: z3.string().optional(),
|
|
3727
3766
|
/** E2EE (text lane): when the pairing is E2EE, the sealed replacements for the
|
|
3728
3767
|
* plaintext content fields, keyed by field name. FINALIZED wire shape (was
|
|
3729
3768
|
* provisional in the storage PR): a per-field map `{ context?, options?,
|
|
@@ -3736,10 +3775,10 @@ var NotifyRequestFields = z2.object({
|
|
|
3736
3775
|
* notifications.envelope and relays it blindly; it never decrypts. Absent =
|
|
3737
3776
|
* today's plaintext path (context/options/visuals carry the cleartext).
|
|
3738
3777
|
* z.lazy because EnvelopeSchema is declared further down (E2EE section). */
|
|
3739
|
-
envelope:
|
|
3740
|
-
context:
|
|
3741
|
-
options:
|
|
3742
|
-
visuals:
|
|
3778
|
+
envelope: z3.object({
|
|
3779
|
+
context: z3.lazy(() => EnvelopeSchema).optional(),
|
|
3780
|
+
options: z3.lazy(() => EnvelopeSchema).optional(),
|
|
3781
|
+
visuals: z3.lazy(() => EnvelopeSchema).optional()
|
|
3743
3782
|
}).optional(),
|
|
3744
3783
|
select: SelectShapeSchema.optional().describe(
|
|
3745
3784
|
"How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
|
|
@@ -3754,20 +3793,20 @@ var NotifyRequestFields = z2.object({
|
|
|
3754
3793
|
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
3755
3794
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
3756
3795
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
3757
|
-
ask:
|
|
3796
|
+
ask: z3.string().min(1).max(1e4).optional().describe(
|
|
3758
3797
|
'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
|
|
3759
3798
|
),
|
|
3760
|
-
needs:
|
|
3799
|
+
needs: z3.array(z3.string().min(1)).optional().describe(
|
|
3761
3800
|
"With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
|
|
3762
3801
|
),
|
|
3763
|
-
urgencyHint:
|
|
3802
|
+
urgencyHint: z3.enum(["whenever", "soon", "now"]).optional().describe(
|
|
3764
3803
|
"With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
|
|
3765
3804
|
),
|
|
3766
3805
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
3767
3806
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
3768
3807
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
3769
3808
|
* urgencyHint/blocking win when both are sent. */
|
|
3770
|
-
waiting:
|
|
3809
|
+
waiting: z3.enum(["none", "soft", "hard"]).optional().describe(
|
|
3771
3810
|
"With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
|
|
3772
3811
|
),
|
|
3773
3812
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -3775,58 +3814,59 @@ var NotifyRequestFields = z2.object({
|
|
|
3775
3814
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
3776
3815
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
3777
3816
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
3778
|
-
confirm:
|
|
3817
|
+
confirm: z3.boolean().optional().describe(
|
|
3779
3818
|
"Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
|
|
3780
3819
|
),
|
|
3781
3820
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
3782
3821
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
3783
3822
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
3784
3823
|
* waiting:'hard'. */
|
|
3785
|
-
channel:
|
|
3824
|
+
channel: z3.enum(["call", "message"]).optional().describe(
|
|
3786
3825
|
"Only if the user explicitly said how to reach them \u2014 'call me' \u2192 'call', 'just message/text me' \u2192 'message'. Omit otherwise; Paigy picks."
|
|
3787
3826
|
),
|
|
3788
|
-
confirmStyle:
|
|
3827
|
+
confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
|
|
3789
3828
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
3790
3829
|
),
|
|
3791
|
-
blocking:
|
|
3830
|
+
blocking: z3.boolean().default(false).describe(
|
|
3792
3831
|
"Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
|
|
3793
3832
|
)
|
|
3794
3833
|
});
|
|
3795
3834
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
3835
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
3796
3836
|
const sealed = !!r.envelope;
|
|
3797
3837
|
if (sealed) {
|
|
3798
3838
|
if (!r.envelope?.context)
|
|
3799
|
-
ctx.addIssue({ code:
|
|
3839
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
|
|
3800
3840
|
for (const f of ["context", "options", "visuals"]) {
|
|
3801
3841
|
if (r[f] !== void 0)
|
|
3802
|
-
ctx.addIssue({ code:
|
|
3842
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
|
|
3803
3843
|
}
|
|
3804
3844
|
if (r.points !== void 0)
|
|
3805
|
-
ctx.addIssue({ code:
|
|
3845
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
|
|
3806
3846
|
if (r.ask !== void 0)
|
|
3807
|
-
ctx.addIssue({ code:
|
|
3847
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
|
|
3808
3848
|
if (r.needs !== void 0)
|
|
3809
|
-
ctx.addIssue({ code:
|
|
3849
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
|
|
3810
3850
|
return;
|
|
3811
3851
|
}
|
|
3812
3852
|
if (r.ask !== void 0) {
|
|
3813
3853
|
for (const f of ["context", "select", "points"]) {
|
|
3814
3854
|
if (r[f] !== void 0)
|
|
3815
|
-
ctx.addIssue({ code:
|
|
3855
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
3816
3856
|
}
|
|
3817
3857
|
return;
|
|
3818
3858
|
}
|
|
3819
3859
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
3820
|
-
ctx.addIssue({ code:
|
|
3860
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
3821
3861
|
if (!r.context)
|
|
3822
|
-
ctx.addIssue({ code:
|
|
3862
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
3823
3863
|
if (!r.select)
|
|
3824
|
-
ctx.addIssue({ code:
|
|
3864
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
3825
3865
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
3826
3866
|
if (needsOptions && !r.options?.length)
|
|
3827
|
-
ctx.addIssue({ code:
|
|
3867
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
3828
3868
|
if (!needsOptions && r.options?.length)
|
|
3829
|
-
ctx.addIssue({ code:
|
|
3869
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
3830
3870
|
});
|
|
3831
3871
|
function normalizeWaiting(req) {
|
|
3832
3872
|
if (!req.waiting) return req;
|
|
@@ -3883,70 +3923,63 @@ function deriveAsk(req) {
|
|
|
3883
3923
|
...needs?.length ? { points: needs } : {}
|
|
3884
3924
|
};
|
|
3885
3925
|
}
|
|
3886
|
-
var NotifyStatusSchema =
|
|
3887
|
-
var AgentStateSchema =
|
|
3888
|
-
var SetTaskStateSchema =
|
|
3889
|
-
state:
|
|
3890
|
-
});
|
|
3891
|
-
var SetWorkStateSchema = z2.object({
|
|
3892
|
-
workId: z2.string().uuid().optional(),
|
|
3893
|
-
notificationId: z2.string().optional(),
|
|
3894
|
-
state: SetTaskStateSchema.shape.state
|
|
3895
|
-
}).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
|
|
3896
|
-
message: "exactly one of workId or notificationId is required"
|
|
3926
|
+
var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
|
|
3927
|
+
var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
3928
|
+
var SetTaskStateSchema = z3.object({
|
|
3929
|
+
state: z3.enum(["in_progress", "completed", "needs_input"])
|
|
3897
3930
|
});
|
|
3898
|
-
var TurnSchema =
|
|
3899
|
-
prompt:
|
|
3900
|
-
reply:
|
|
3931
|
+
var TurnSchema = z3.object({
|
|
3932
|
+
prompt: z3.string(),
|
|
3933
|
+
reply: z3.string()
|
|
3901
3934
|
});
|
|
3902
|
-
var UserAnswerSchema =
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3935
|
+
var UserAnswerSchema = z3.discriminatedUnion("kind", [
|
|
3936
|
+
z3.object({ kind: z3.literal("option"), optionId: z3.string(), label: z3.string().optional() }),
|
|
3937
|
+
z3.object({ kind: z3.literal("text"), text: z3.string() }),
|
|
3938
|
+
z3.object({ kind: z3.literal("ignored") }),
|
|
3939
|
+
z3.object({ kind: z3.literal("multi"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
|
|
3940
|
+
z3.object({ kind: z3.literal("ranked"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
|
|
3941
|
+
z3.object({ kind: z3.literal("clarify"), chunks: z3.array(z3.string()).min(1) }),
|
|
3942
|
+
z3.object({ kind: z3.literal("confirm"), approved: z3.boolean() }),
|
|
3943
|
+
z3.object({ kind: z3.literal("turns"), turns: z3.array(TurnSchema).min(1) }),
|
|
3911
3944
|
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
3912
3945
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
3913
3946
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
3914
3947
|
* card the user can reply to — the broker never overrides the user. */
|
|
3915
|
-
|
|
3948
|
+
z3.object({ kind: z3.literal("precedent"), answer: z3.string(), derivation: z3.string(), sources: z3.array(z3.string()).min(1) })
|
|
3916
3949
|
]);
|
|
3917
|
-
var IntentSchema =
|
|
3950
|
+
var IntentSchema = z3.object({
|
|
3918
3951
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
3919
3952
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
3920
3953
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
3921
3954
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
3922
|
-
kind:
|
|
3923
|
-
detail:
|
|
3955
|
+
kind: z3.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
3956
|
+
detail: z3.string(),
|
|
3924
3957
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
3925
3958
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
3926
3959
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
3927
3960
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
3928
|
-
dueInSeconds:
|
|
3961
|
+
dueInSeconds: z3.number().int().positive().optional(),
|
|
3929
3962
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
3930
3963
|
* already read the utterance, so `feedback_from_call.kind` stops defaulting to
|
|
3931
3964
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
3932
3965
|
* about what cannot answer "is the bot looping less this week?". */
|
|
3933
|
-
fault:
|
|
3966
|
+
fault: z3.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
3934
3967
|
});
|
|
3935
|
-
var RideAlongSchema =
|
|
3968
|
+
var RideAlongSchema = z3.object({
|
|
3936
3969
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
3937
|
-
noteId:
|
|
3970
|
+
noteId: z3.string(),
|
|
3938
3971
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
3939
|
-
text:
|
|
3972
|
+
text: z3.string(),
|
|
3940
3973
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
3941
|
-
parentId:
|
|
3974
|
+
parentId: z3.string().nullable()
|
|
3942
3975
|
});
|
|
3943
|
-
var AwaitItemSchema =
|
|
3944
|
-
|
|
3945
|
-
type:
|
|
3946
|
-
parentId:
|
|
3947
|
-
notificationId:
|
|
3948
|
-
workId:
|
|
3949
|
-
decisionId:
|
|
3976
|
+
var AwaitItemSchema = z3.discriminatedUnion("type", [
|
|
3977
|
+
z3.object({
|
|
3978
|
+
type: z3.literal("reply"),
|
|
3979
|
+
parentId: z3.string(),
|
|
3980
|
+
notificationId: z3.string(),
|
|
3981
|
+
workId: z3.string().uuid().optional(),
|
|
3982
|
+
decisionId: z3.string().uuid().optional(),
|
|
3950
3983
|
answer: UserAnswerSchema,
|
|
3951
3984
|
/** E2EE: present when the answer is sealed. The server relays the opaque answer
|
|
3952
3985
|
* envelope + the plaintext `ignored` status hint; the receiving agent OPENS it
|
|
@@ -3954,7 +3987,7 @@ var AwaitItemSchema = z2.discriminatedUnion("type", [
|
|
|
3954
3987
|
* against `ignored` as tampering. Absent = today's plaintext answer (in `answer`).
|
|
3955
3988
|
* On a sealed reply the plaintext `answer` is a placeholder (kind reflects only
|
|
3956
3989
|
* the `ignored` bit) — never the real content, which stays sealed. */
|
|
3957
|
-
sealed:
|
|
3990
|
+
sealed: z3.lazy(() => SealedAnswerSchema).optional(),
|
|
3958
3991
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
3959
3992
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
3960
3993
|
* the notification they belong to, and the `contact` call that reaches the person
|
|
@@ -3963,147 +3996,140 @@ var AwaitItemSchema = z2.discriminatedUnion("type", [
|
|
|
3963
3996
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
3964
3997
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
3965
3998
|
* a sentence with a hole in it is worse than no sentence. */
|
|
3966
|
-
note:
|
|
3999
|
+
note: z3.string().optional(),
|
|
3967
4000
|
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
3968
4001
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
3969
4002
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
3970
4003
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
3971
|
-
transcript:
|
|
4004
|
+
transcript: z3.string().optional(),
|
|
3972
4005
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
3973
4006
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
3974
|
-
covered:
|
|
4007
|
+
covered: z3.array(z3.string()).optional(),
|
|
3975
4008
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
3976
4009
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
3977
4010
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
3978
4011
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
3979
4012
|
* dropped. Absent/empty = nothing owed. */
|
|
3980
|
-
also:
|
|
4013
|
+
also: z3.array(RideAlongSchema).optional()
|
|
3981
4014
|
}),
|
|
3982
|
-
|
|
3983
|
-
type:
|
|
3984
|
-
parentId:
|
|
3985
|
-
notificationId:
|
|
3986
|
-
remindAt:
|
|
4015
|
+
z3.object({
|
|
4016
|
+
type: z3.literal("remind"),
|
|
4017
|
+
parentId: z3.string(),
|
|
4018
|
+
notificationId: z3.string(),
|
|
4019
|
+
remindAt: z3.string().datetime({ offset: true }),
|
|
3987
4020
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
3988
|
-
remindInSeconds:
|
|
4021
|
+
remindInSeconds: z3.number()
|
|
3989
4022
|
}),
|
|
3990
4023
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
3991
4024
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
3992
4025
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
3993
4026
|
* re-orient via get_thread / check_replies). */
|
|
3994
|
-
|
|
3995
|
-
type:
|
|
3996
|
-
parentId:
|
|
3997
|
-
notificationId:
|
|
4027
|
+
z3.object({
|
|
4028
|
+
type: z3.literal("superseded"),
|
|
4029
|
+
parentId: z3.string(),
|
|
4030
|
+
notificationId: z3.string()
|
|
3998
4031
|
}),
|
|
3999
4032
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
4000
4033
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
4001
4034
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
4002
4035
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
4003
4036
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
4004
|
-
|
|
4005
|
-
type:
|
|
4006
|
-
notificationId:
|
|
4007
|
-
inFlight:
|
|
4008
|
-
turn:
|
|
4009
|
-
idx:
|
|
4010
|
-
prompt:
|
|
4011
|
-
reply:
|
|
4012
|
-
acts:
|
|
4037
|
+
z3.object({
|
|
4038
|
+
type: z3.literal("partial"),
|
|
4039
|
+
notificationId: z3.string(),
|
|
4040
|
+
inFlight: z3.literal(true),
|
|
4041
|
+
turn: z3.object({
|
|
4042
|
+
idx: z3.number(),
|
|
4043
|
+
prompt: z3.string(),
|
|
4044
|
+
reply: z3.string(),
|
|
4045
|
+
acts: z3.array(IntentSchema).nullable().optional()
|
|
4013
4046
|
})
|
|
4014
4047
|
}),
|
|
4015
|
-
|
|
4016
|
-
type:
|
|
4017
|
-
also:
|
|
4048
|
+
z3.object({
|
|
4049
|
+
type: z3.literal("idle"),
|
|
4050
|
+
also: z3.array(RideAlongSchema).optional(),
|
|
4018
4051
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
4019
4052
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
4020
4053
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
4021
4054
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
4022
4055
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
4023
|
-
inFlight:
|
|
4056
|
+
inFlight: z3.boolean().optional()
|
|
4024
4057
|
})
|
|
4025
4058
|
]);
|
|
4026
|
-
var CallbackTriggerSchema =
|
|
4027
|
-
var
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
var PendingRepliesSchema = z2.object({
|
|
4035
|
-
replies: z2.array(
|
|
4036
|
-
z2.object({
|
|
4037
|
-
parentId: z2.string(),
|
|
4038
|
-
notificationId: z2.string(),
|
|
4039
|
-
workId: z2.string().uuid().optional(),
|
|
4040
|
-
decisionId: z2.string().uuid().optional(),
|
|
4059
|
+
var CallbackTriggerSchema = z3.enum(["on_done", "on_blocked", "scheduled"]);
|
|
4060
|
+
var PendingRepliesSchema = z3.object({
|
|
4061
|
+
replies: z3.array(
|
|
4062
|
+
z3.object({
|
|
4063
|
+
parentId: z3.string(),
|
|
4064
|
+
notificationId: z3.string(),
|
|
4065
|
+
workId: z3.string().uuid().optional(),
|
|
4066
|
+
decisionId: z3.string().uuid().optional(),
|
|
4041
4067
|
answer: UserAnswerSchema,
|
|
4042
4068
|
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
|
|
4043
4069
|
* pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
|
|
4044
4070
|
* plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
|
|
4045
|
-
sealed:
|
|
4071
|
+
sealed: z3.lazy(() => SealedAnswerSchema).optional(),
|
|
4046
4072
|
/** The call record rendered for THIS agent: the raw words the shaped answer was
|
|
4047
4073
|
* mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
|
|
4048
|
-
transcript:
|
|
4074
|
+
transcript: z3.string().optional(),
|
|
4049
4075
|
/** Coverage report (#396): which declared `points` this answer addressed. */
|
|
4050
|
-
covered:
|
|
4076
|
+
covered: z3.array(z3.string()).optional()
|
|
4051
4077
|
})
|
|
4052
4078
|
),
|
|
4053
|
-
pending:
|
|
4054
|
-
|
|
4079
|
+
pending: z3.array(
|
|
4080
|
+
z3.object({ parentId: z3.string(), notificationId: z3.string(), createdAt: z3.string() })
|
|
4055
4081
|
),
|
|
4056
4082
|
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
4057
4083
|
* sees for this session's identity. From inside a session there was no way to find out —
|
|
4058
4084
|
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
4059
4085
|
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
4060
4086
|
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
4061
|
-
you:
|
|
4087
|
+
you: z3.object({ name: z3.string(), device: z3.string().nullable(), tokenId: z3.string() }).optional(),
|
|
4062
4088
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
4063
4089
|
* contact on the same parentId. Keeps reappearing until you call
|
|
4064
4090
|
* set_work_state on its workId (or notificationId once while bootstrapping). */
|
|
4065
|
-
requests:
|
|
4066
|
-
|
|
4067
|
-
parentId:
|
|
4068
|
-
notificationId:
|
|
4069
|
-
workId:
|
|
4070
|
-
text:
|
|
4071
|
-
createdAt:
|
|
4091
|
+
requests: z3.array(
|
|
4092
|
+
z3.object({
|
|
4093
|
+
parentId: z3.string(),
|
|
4094
|
+
notificationId: z3.string(),
|
|
4095
|
+
workId: z3.string().uuid().optional(),
|
|
4096
|
+
text: z3.string(),
|
|
4097
|
+
createdAt: z3.string(),
|
|
4072
4098
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
4073
4099
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
4074
|
-
contextParentId:
|
|
4100
|
+
contextParentId: z3.string().optional(),
|
|
4075
4101
|
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
4076
4102
|
* the account — the name here — which has not been seen since it landed, so nobody
|
|
4077
4103
|
* came for it. Handed to you because you are the session that is here. Take it like
|
|
4078
4104
|
* any request (set_work_state claims it, reply with contact on its parentId), and say
|
|
4079
4105
|
* whose it was, because the user chose that agent on purpose. */
|
|
4080
|
-
stranded:
|
|
4106
|
+
stranded: z3.string().optional()
|
|
4081
4107
|
})
|
|
4082
4108
|
),
|
|
4083
4109
|
/** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
|
|
4084
4110
|
* notification lists remain during migration so old clients keep their existing view. */
|
|
4085
|
-
work:
|
|
4086
|
-
workId:
|
|
4087
|
-
parentId:
|
|
4088
|
-
objective:
|
|
4089
|
-
state:
|
|
4090
|
-
blockedOn:
|
|
4091
|
-
updatedAt:
|
|
4111
|
+
work: z3.array(z3.object({
|
|
4112
|
+
workId: z3.string().uuid(),
|
|
4113
|
+
parentId: z3.string().optional(),
|
|
4114
|
+
objective: z3.string().nullable(),
|
|
4115
|
+
state: z3.enum(["active", "blocked", "waiting_external"]),
|
|
4116
|
+
blockedOn: z3.array(z3.string().uuid()),
|
|
4117
|
+
updatedAt: z3.string()
|
|
4092
4118
|
})).optional(),
|
|
4093
4119
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
4094
4120
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
4095
4121
|
* fulfill one by calling contact on its parentId. */
|
|
4096
4122
|
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
4097
4123
|
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
4098
|
-
also:
|
|
4099
|
-
owedCallbacks:
|
|
4100
|
-
|
|
4124
|
+
also: z3.array(RideAlongSchema).optional(),
|
|
4125
|
+
owedCallbacks: z3.array(
|
|
4126
|
+
z3.object({ parentId: z3.string(), trigger: CallbackTriggerSchema, note: z3.string() })
|
|
4101
4127
|
),
|
|
4102
4128
|
/** Work (either direction) you reported in_progress a while ago and never reported
|
|
4103
4129
|
* completed — likely left half-done by this session or a prior one that crashed or
|
|
4104
4130
|
* went idle. Report a real state (set_work_state) or continue the work. */
|
|
4105
|
-
stalled:
|
|
4106
|
-
|
|
4131
|
+
stalled: z3.array(
|
|
4132
|
+
z3.object({ parentId: z3.string(), notificationId: z3.string(), title: z3.string().nullable(), startedAt: z3.string() })
|
|
4107
4133
|
),
|
|
4108
4134
|
/** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
|
|
4109
4135
|
* thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
|
|
@@ -4112,125 +4138,132 @@ var PendingRepliesSchema = z2.object({
|
|
|
4112
4138
|
* ride the next turn. `items` are that thread's replies/requests in arrival order; the
|
|
4113
4139
|
* full payload for each is in the flat `replies`/`requests` arrays (matched by
|
|
4114
4140
|
* notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
|
|
4115
|
-
threads:
|
|
4116
|
-
|
|
4117
|
-
parentId:
|
|
4118
|
-
busy:
|
|
4119
|
-
items:
|
|
4120
|
-
|
|
4121
|
-
kind:
|
|
4122
|
-
notificationId:
|
|
4123
|
-
at:
|
|
4141
|
+
threads: z3.array(
|
|
4142
|
+
z3.object({
|
|
4143
|
+
parentId: z3.string(),
|
|
4144
|
+
busy: z3.boolean(),
|
|
4145
|
+
items: z3.array(
|
|
4146
|
+
z3.object({
|
|
4147
|
+
kind: z3.enum(["reply", "request"]),
|
|
4148
|
+
notificationId: z3.string(),
|
|
4149
|
+
at: z3.string()
|
|
4124
4150
|
})
|
|
4125
4151
|
)
|
|
4126
4152
|
})
|
|
4127
4153
|
)
|
|
4128
4154
|
});
|
|
4129
|
-
var NotifyResponseSchema =
|
|
4130
|
-
notificationId:
|
|
4131
|
-
workId:
|
|
4132
|
-
decisionId:
|
|
4155
|
+
var NotifyResponseSchema = z3.object({
|
|
4156
|
+
notificationId: z3.string(),
|
|
4157
|
+
workId: z3.string().uuid().optional(),
|
|
4158
|
+
decisionId: z3.string().uuid().optional(),
|
|
4133
4159
|
status: NotifyStatusSchema,
|
|
4134
|
-
createdAt:
|
|
4160
|
+
createdAt: z3.string().datetime(),
|
|
4135
4161
|
answer: UserAnswerSchema.optional(),
|
|
4136
|
-
answeredAt:
|
|
4162
|
+
answeredAt: z3.string().datetime().optional(),
|
|
4137
4163
|
/** Ride-alongs for THIS agent — pending work it should pick up when it's done with
|
|
4138
4164
|
* what it came for. Present on any reply, because an unwakeable agent's only
|
|
4139
4165
|
* reliable moment is one it initiated. Absent/empty = nothing owed. */
|
|
4140
|
-
also:
|
|
4166
|
+
also: z3.array(RideAlongSchema).optional()
|
|
4141
4167
|
});
|
|
4142
|
-
var NotifyPlanUnitSchema =
|
|
4143
|
-
notificationId:
|
|
4168
|
+
var NotifyPlanUnitSchema = z3.object({
|
|
4169
|
+
notificationId: z3.string(),
|
|
4144
4170
|
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
4145
|
-
title:
|
|
4171
|
+
title: z3.string(),
|
|
4146
4172
|
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
4147
4173
|
level: NotifyLevelSchema,
|
|
4148
4174
|
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
4149
4175
|
* says so. The agent should not wait on this one. */
|
|
4150
|
-
settled:
|
|
4176
|
+
settled: z3.literal(true).optional(),
|
|
4151
4177
|
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
4152
4178
|
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
4153
|
-
needs:
|
|
4179
|
+
needs: z3.array(z3.enum(["options", "visuals"])).optional(),
|
|
4154
4180
|
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
4155
4181
|
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
4156
4182
|
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
4157
4183
|
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
4158
4184
|
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
4159
|
-
proposal:
|
|
4185
|
+
proposal: z3.object({
|
|
4160
4186
|
select: SelectShapeSchema,
|
|
4161
|
-
options:
|
|
4187
|
+
options: z3.array(z3.object({ label: z3.string().min(1) })).optional()
|
|
4162
4188
|
}).optional(),
|
|
4163
4189
|
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
4164
4190
|
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
4165
4191
|
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
4166
4192
|
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
4167
4193
|
* agent declared (#731, #923). */
|
|
4168
|
-
wants:
|
|
4194
|
+
wants: z3.enum(["decision", "approval", "knowledge"]).optional()
|
|
4169
4195
|
});
|
|
4170
|
-
var NotifyPlanSchema =
|
|
4171
|
-
units:
|
|
4196
|
+
var NotifyPlanSchema = z3.object({
|
|
4197
|
+
units: z3.array(NotifyPlanUnitSchema),
|
|
4172
4198
|
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
4173
4199
|
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
4174
|
-
speaks:
|
|
4200
|
+
speaks: z3.string().optional()
|
|
4175
4201
|
});
|
|
4176
|
-
var UserResponseSchema =
|
|
4177
|
-
requestId:
|
|
4202
|
+
var UserResponseSchema = z3.object({
|
|
4203
|
+
requestId: z3.string(),
|
|
4178
4204
|
answer: UserAnswerSchema,
|
|
4179
|
-
answeredAt:
|
|
4180
|
-
transcript:
|
|
4205
|
+
answeredAt: z3.string().datetime(),
|
|
4206
|
+
transcript: z3.string().optional(),
|
|
4181
4207
|
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the item was
|
|
4182
4208
|
* E2EE-sealed. Present → the server persists it opaquely and branches status on `ignored`;
|
|
4183
4209
|
* the plaintext `answer` is a placeholder (`{ kind: "ignored" }`) the server ignores for a
|
|
4184
4210
|
* sealed row. Absent = today's plaintext answer, unchanged. */
|
|
4185
|
-
sealed:
|
|
4211
|
+
sealed: z3.lazy(() => SealedAnswerSchema).optional(),
|
|
4186
4212
|
/** Coverage report (#396): which of the ask's declared `points` were addressed. */
|
|
4187
|
-
covered:
|
|
4213
|
+
covered: z3.array(z3.string()).optional()
|
|
4188
4214
|
});
|
|
4189
|
-
var VoiceKeySchema =
|
|
4190
|
-
var AgendaTurnSchema =
|
|
4215
|
+
var VoiceKeySchema = z3.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
4216
|
+
var AgendaTurnSchema = z3.object({
|
|
4217
|
+
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
4218
|
+
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
4219
|
+
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
4220
|
+
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
4221
|
+
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
4222
|
+
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
4223
|
+
id: z3.string().optional(),
|
|
4191
4224
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
4192
4225
|
* the planner declares duplicates instead of asking them twice. */
|
|
4193
|
-
coveredIds:
|
|
4226
|
+
coveredIds: z3.array(z3.string()).optional(),
|
|
4194
4227
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
4195
4228
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
4196
|
-
info:
|
|
4197
|
-
question:
|
|
4229
|
+
info: z3.array(z3.string().min(1)).max(3).default([]),
|
|
4230
|
+
question: z3.string().min(1).nullable(),
|
|
4198
4231
|
/** True on the one turn carrying the agent's own declared question. */
|
|
4199
|
-
asks:
|
|
4232
|
+
asks: z3.boolean().optional(),
|
|
4200
4233
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
4201
4234
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
4202
4235
|
* which route nothing. */
|
|
4203
|
-
claimId:
|
|
4236
|
+
claimId: z3.string().optional(),
|
|
4204
4237
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
4205
|
-
voice:
|
|
4238
|
+
voice: z3.string().optional(),
|
|
4206
4239
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
4207
4240
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
4208
4241
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
4209
4242
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
4210
|
-
agent:
|
|
4243
|
+
agent: z3.string().optional(),
|
|
4211
4244
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
4212
4245
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
4213
4246
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
4214
4247
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
4215
4248
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
4216
|
-
agentId:
|
|
4249
|
+
agentId: z3.string().optional(),
|
|
4217
4250
|
select: SelectShapeSchema.optional(),
|
|
4218
|
-
options:
|
|
4251
|
+
options: z3.array(OptionSchema.omit({ id: true })).optional(),
|
|
4219
4252
|
/** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
|
|
4220
4253
|
* seconds the floor stays open after this turn speaks. Absent = the bot's defaults
|
|
4221
4254
|
* (the beat for context, the answer window for asks). Clamped bot-side. */
|
|
4222
|
-
pace:
|
|
4255
|
+
pace: z3.number().positive().optional(),
|
|
4223
4256
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
4224
4257
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
4225
4258
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
4226
|
-
blocking:
|
|
4259
|
+
blocking: z3.boolean().optional()
|
|
4227
4260
|
});
|
|
4228
4261
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
4229
|
-
var InboxItemSchema =
|
|
4230
|
-
id:
|
|
4262
|
+
var InboxItemSchema = z3.object({
|
|
4263
|
+
id: z3.string(),
|
|
4231
4264
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
4232
4265
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
4233
|
-
parentId:
|
|
4266
|
+
parentId: z3.string().optional(),
|
|
4234
4267
|
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
4235
4268
|
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
4236
4269
|
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
@@ -4238,13 +4271,13 @@ var InboxItemSchema = z2.object({
|
|
|
4238
4271
|
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
4239
4272
|
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
4240
4273
|
* thread, which is what the client did for all rows until now. */
|
|
4241
|
-
askId:
|
|
4274
|
+
askId: z3.string().optional(),
|
|
4242
4275
|
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
4243
4276
|
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
4244
4277
|
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
4245
4278
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
4246
4279
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
4247
|
-
seq:
|
|
4280
|
+
seq: z3.number().int().optional(),
|
|
4248
4281
|
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
4249
4282
|
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
4250
4283
|
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
@@ -4253,13 +4286,13 @@ var InboxItemSchema = z2.object({
|
|
|
4253
4286
|
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
4254
4287
|
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
4255
4288
|
* the one part of its own arrival a single row cannot answer. */
|
|
4256
|
-
units:
|
|
4257
|
-
tokenId:
|
|
4289
|
+
units: z3.number().int().positive().optional(),
|
|
4290
|
+
tokenId: z3.string().optional(),
|
|
4258
4291
|
status: NotifyStatusSchema,
|
|
4259
4292
|
context: ContextSchema,
|
|
4260
|
-
options:
|
|
4293
|
+
options: z3.array(OptionSchema).optional(),
|
|
4261
4294
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
4262
|
-
points:
|
|
4295
|
+
points: z3.array(z3.string()).optional(),
|
|
4263
4296
|
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
4264
4297
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
4265
4298
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
@@ -4268,12 +4301,12 @@ var InboxItemSchema = z2.object({
|
|
|
4268
4301
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
4269
4302
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
4270
4303
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
4271
|
-
asks:
|
|
4304
|
+
asks: z3.boolean().optional(),
|
|
4272
4305
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
4273
4306
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
4274
4307
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
4275
4308
|
* which must never CLAIM stalled. */
|
|
4276
|
-
lastSeenAt:
|
|
4309
|
+
lastSeenAt: z3.string().optional(),
|
|
4277
4310
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
4278
4311
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
4279
4312
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -4283,53 +4316,53 @@ var InboxItemSchema = z2.object({
|
|
|
4283
4316
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
4284
4317
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
4285
4318
|
* `createdAt`. */
|
|
4286
|
-
agentStateAt:
|
|
4287
|
-
agenda:
|
|
4288
|
-
visuals:
|
|
4319
|
+
agentStateAt: z3.string().datetime().optional(),
|
|
4320
|
+
agenda: z3.array(AgendaTurnSchema).optional(),
|
|
4321
|
+
visuals: z3.array(VisualSchema).optional(),
|
|
4289
4322
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
4290
4323
|
* agent's suggestion, or a default silly name). */
|
|
4291
|
-
name:
|
|
4324
|
+
name: z3.string(),
|
|
4292
4325
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
4293
4326
|
voice: VoiceKeySchema.optional(),
|
|
4294
|
-
repo:
|
|
4295
|
-
branch:
|
|
4296
|
-
createdAt:
|
|
4297
|
-
snoozedUntil:
|
|
4327
|
+
repo: z3.string().optional(),
|
|
4328
|
+
branch: z3.string().optional(),
|
|
4329
|
+
createdAt: z3.string().datetime(),
|
|
4330
|
+
snoozedUntil: z3.string().datetime().optional(),
|
|
4298
4331
|
agentState: AgentStateSchema.default("idle"),
|
|
4299
4332
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
4300
4333
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
4301
4334
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
4302
|
-
turn:
|
|
4335
|
+
turn: z3.enum(["you", "agent"]).default("you"),
|
|
4303
4336
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
4304
4337
|
* the agent (provider-agnostic; set server-side). Absent = no hard error, though the
|
|
4305
4338
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
4306
|
-
error:
|
|
4307
|
-
clarifies:
|
|
4339
|
+
error: z3.string().optional(),
|
|
4340
|
+
clarifies: z3.string().optional(),
|
|
4308
4341
|
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
4309
4342
|
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
4310
4343
|
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
4311
4344
|
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
4312
4345
|
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
4313
|
-
gaveUp:
|
|
4346
|
+
gaveUp: z3.boolean().default(false),
|
|
4314
4347
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
4315
4348
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
4316
4349
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
4317
4350
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
4318
4351
|
* feedback ABOUT. */
|
|
4319
|
-
why:
|
|
4352
|
+
why: z3.object({
|
|
4320
4353
|
asked: NotifyLevelSchema,
|
|
4321
4354
|
got: NotifyLevelSchema,
|
|
4322
|
-
because:
|
|
4323
|
-
line:
|
|
4355
|
+
because: z3.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
4356
|
+
line: z3.string()
|
|
4324
4357
|
}).optional(),
|
|
4325
|
-
select:
|
|
4326
|
-
confirmStyle:
|
|
4358
|
+
select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
4359
|
+
confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
|
|
4327
4360
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
4328
4361
|
),
|
|
4329
4362
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
4330
4363
|
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
4331
4364
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
4332
|
-
blocking:
|
|
4365
|
+
blocking: z3.boolean().default(false),
|
|
4333
4366
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
4334
4367
|
answer: UserAnswerSchema.optional(),
|
|
4335
4368
|
/** E2EE (text lane): the sealed content this item carries when the pairing is E2EE, in
|
|
@@ -4338,39 +4371,53 @@ var InboxItemSchema = z2.object({
|
|
|
4338
4371
|
* NotifyRequest.envelope. The native app decrypts it locally via openNotification;
|
|
4339
4372
|
* web / an un-enrolled device can't and renders a locked placeholder. Absent = today's
|
|
4340
4373
|
* plaintext item (context carries the cleartext), so plaintext items are unchanged. */
|
|
4341
|
-
envelope:
|
|
4342
|
-
context:
|
|
4343
|
-
options:
|
|
4344
|
-
visuals:
|
|
4374
|
+
envelope: z3.object({
|
|
4375
|
+
context: z3.lazy(() => EnvelopeSchema).optional(),
|
|
4376
|
+
options: z3.lazy(() => EnvelopeSchema).optional(),
|
|
4377
|
+
visuals: z3.lazy(() => EnvelopeSchema).optional()
|
|
4345
4378
|
}).optional(),
|
|
4346
4379
|
/** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
|
|
4347
4380
|
* sender the phone replies to). Sourced server-side from this item's pairing credential.
|
|
4348
4381
|
* Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
|
|
4349
|
-
agentX25519:
|
|
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()
|
|
4350
4397
|
});
|
|
4351
|
-
var SnoozeRequestSchema =
|
|
4352
|
-
requestId:
|
|
4353
|
-
until:
|
|
4398
|
+
var SnoozeRequestSchema = z3.object({
|
|
4399
|
+
requestId: z3.string(),
|
|
4400
|
+
until: z3.string().datetime()
|
|
4354
4401
|
});
|
|
4355
4402
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
4356
|
-
var PushTokenSchema =
|
|
4357
|
-
voipToken:
|
|
4358
|
-
alertToken:
|
|
4359
|
-
fcmToken:
|
|
4360
|
-
platform:
|
|
4403
|
+
var PushTokenSchema = z3.object({
|
|
4404
|
+
voipToken: z3.string().min(1).optional(),
|
|
4405
|
+
alertToken: z3.string().min(1).optional(),
|
|
4406
|
+
fcmToken: z3.string().min(1).optional(),
|
|
4407
|
+
platform: z3.enum(["ios", "android"])
|
|
4361
4408
|
}).superRefine((v, ctx) => {
|
|
4362
4409
|
if (v.platform !== "ios") return;
|
|
4363
4410
|
for (const field of ["voipToken", "alertToken"]) {
|
|
4364
4411
|
const token = v[field];
|
|
4365
4412
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
4366
4413
|
ctx.addIssue({
|
|
4367
|
-
code:
|
|
4414
|
+
code: z3.ZodIssueCode.custom,
|
|
4368
4415
|
path: [field],
|
|
4369
4416
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
4370
4417
|
});
|
|
4371
4418
|
}
|
|
4372
4419
|
});
|
|
4373
|
-
var MissedCallSchema =
|
|
4420
|
+
var MissedCallSchema = z3.enum([
|
|
4374
4421
|
"retry_10m",
|
|
4375
4422
|
"retry_30m",
|
|
4376
4423
|
"retry_60m",
|
|
@@ -4380,32 +4427,32 @@ var MissedCallSchema = z2.enum([
|
|
|
4380
4427
|
"inbox",
|
|
4381
4428
|
"dismiss"
|
|
4382
4429
|
]);
|
|
4383
|
-
var BrokerTuningSchema =
|
|
4430
|
+
var BrokerTuningSchema = z3.object({
|
|
4384
4431
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
4385
|
-
ackVerbosity:
|
|
4432
|
+
ackVerbosity: z3.enum(["normal", "none"]).optional(),
|
|
4386
4433
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
4387
4434
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
4388
|
-
clarifyEagerness:
|
|
4435
|
+
clarifyEagerness: z3.enum(["low", "normal", "high"]).optional(),
|
|
4389
4436
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
4390
|
-
phrasebook:
|
|
4437
|
+
phrasebook: z3.array(z3.object({ say: z3.string().min(1).max(60), mean: z3.string().min(1).max(120) })).max(24).optional(),
|
|
4391
4438
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
4392
4439
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
4393
4440
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
4394
4441
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
4395
4442
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
4396
|
-
language:
|
|
4443
|
+
language: z3.enum(["en", "es"]).optional()
|
|
4397
4444
|
});
|
|
4398
|
-
var UserSettingsSchema =
|
|
4399
|
-
permissions:
|
|
4400
|
-
call:
|
|
4401
|
-
banner:
|
|
4402
|
-
push:
|
|
4445
|
+
var UserSettingsSchema = z3.object({
|
|
4446
|
+
permissions: z3.object({
|
|
4447
|
+
call: z3.boolean(),
|
|
4448
|
+
banner: z3.boolean(),
|
|
4449
|
+
push: z3.boolean()
|
|
4403
4450
|
}),
|
|
4404
|
-
sessionMode:
|
|
4405
|
-
silentPush:
|
|
4406
|
-
autoCallback:
|
|
4451
|
+
sessionMode: z3.enum(["default", "all_calls", "silent"]),
|
|
4452
|
+
silentPush: z3.boolean(),
|
|
4453
|
+
autoCallback: z3.boolean(),
|
|
4407
4454
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
4408
|
-
improveConsent:
|
|
4455
|
+
improveConsent: z3.boolean(),
|
|
4409
4456
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
4410
4457
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
4411
4458
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -4413,7 +4460,7 @@ var UserSettingsSchema = z2.object({
|
|
|
4413
4460
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
4414
4461
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
4415
4462
|
* write, 'hosted' on read (see store.ts). */
|
|
4416
|
-
voiceMode:
|
|
4463
|
+
voiceMode: z3.enum(["hosted", "on_device"]).optional(),
|
|
4417
4464
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
4418
4465
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
4419
4466
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -4421,10 +4468,10 @@ var UserSettingsSchema = z2.object({
|
|
|
4421
4468
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
4422
4469
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
4423
4470
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
4424
|
-
voiceTuning:
|
|
4471
|
+
voiceTuning: z3.record(z3.string(), z3.union([z3.number(), z3.string()])).optional(),
|
|
4425
4472
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
4426
4473
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
4427
|
-
pstnCalls:
|
|
4474
|
+
pstnCalls: z3.boolean().optional(),
|
|
4428
4475
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
4429
4476
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
4430
4477
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -4433,59 +4480,59 @@ var UserSettingsSchema = z2.object({
|
|
|
4433
4480
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
4434
4481
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
4435
4482
|
* call — honest about what we know. */
|
|
4436
|
-
timezone:
|
|
4483
|
+
timezone: z3.string().min(1).max(64).optional(),
|
|
4437
4484
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
4438
4485
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
4439
4486
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
4440
4487
|
* settings object without it can't silently flip the account's E2EE state.
|
|
4441
4488
|
* Absent = leave unchanged on write, 'off' on read (see store.ts). The demo
|
|
4442
4489
|
* account is plaintext by construction and refuses any non-'off' value. */
|
|
4443
|
-
e2eeMode:
|
|
4490
|
+
e2eeMode: z3.enum(["off", "on"]).optional(),
|
|
4444
4491
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
4445
4492
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
4446
4493
|
broker: BrokerTuningSchema.optional()
|
|
4447
4494
|
});
|
|
4448
|
-
var HistoryItemSchema =
|
|
4449
|
-
id:
|
|
4450
|
-
parentId:
|
|
4495
|
+
var HistoryItemSchema = z3.object({
|
|
4496
|
+
id: z3.string(),
|
|
4497
|
+
parentId: z3.string(),
|
|
4451
4498
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
4452
|
-
initiator:
|
|
4453
|
-
title:
|
|
4499
|
+
initiator: z3.enum(["user", "agent"]),
|
|
4500
|
+
title: z3.string(),
|
|
4454
4501
|
/** The agent on the other end (its name). */
|
|
4455
|
-
name:
|
|
4456
|
-
createdAt:
|
|
4502
|
+
name: z3.string(),
|
|
4503
|
+
createdAt: z3.string(),
|
|
4457
4504
|
/** When the agent fetched your request (user→agent only). */
|
|
4458
|
-
agentAckedAt:
|
|
4505
|
+
agentAckedAt: z3.string().nullable(),
|
|
4459
4506
|
/** When you answered the agent's notification (agent→user only). */
|
|
4460
|
-
humanAckedAt:
|
|
4507
|
+
humanAckedAt: z3.string().nullable()
|
|
4461
4508
|
});
|
|
4462
4509
|
var ACTIVITY_LINES = 2;
|
|
4463
4510
|
var ACTIVITY_LINE_MAX = 80;
|
|
4464
|
-
var AgentActivitySchema =
|
|
4511
|
+
var AgentActivitySchema = z3.object({
|
|
4465
4512
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
4466
|
-
lines:
|
|
4513
|
+
lines: z3.array(z3.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
4467
4514
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
4468
4515
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
4469
|
-
at:
|
|
4516
|
+
at: z3.string().datetime()
|
|
4470
4517
|
});
|
|
4471
|
-
var ConnectionSummarySchema =
|
|
4518
|
+
var ConnectionSummarySchema = z3.object({
|
|
4472
4519
|
/** The connection = the agent's token id (used to address a request). */
|
|
4473
|
-
id:
|
|
4520
|
+
id: z3.string(),
|
|
4474
4521
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
4475
4522
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
4476
4523
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
4477
4524
|
* apps/api/src/tokens/devices-vs-agents-design.md. */
|
|
4478
|
-
kind:
|
|
4525
|
+
kind: z3.enum(["device", "agent"]).optional(),
|
|
4479
4526
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
4480
4527
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
4481
4528
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
4482
|
-
mintedByDevice:
|
|
4483
|
-
device:
|
|
4529
|
+
mintedByDevice: z3.string().nullable().optional(),
|
|
4530
|
+
device: z3.string().nullable(),
|
|
4484
4531
|
/** The agent's display name (the single pairing name). */
|
|
4485
|
-
name:
|
|
4532
|
+
name: z3.string(),
|
|
4486
4533
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
4487
4534
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
4488
|
-
provider:
|
|
4535
|
+
provider: z3.string().nullable(),
|
|
4489
4536
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
4490
4537
|
voice: VoiceKeySchema.nullable(),
|
|
4491
4538
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -4496,19 +4543,19 @@ var ConnectionSummarySchema = z2.object({
|
|
|
4496
4543
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
4497
4544
|
* set once must not overrule a rule they set about one agent. */
|
|
4498
4545
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
4499
|
-
createdAt:
|
|
4546
|
+
createdAt: z3.string().datetime(),
|
|
4500
4547
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
4501
4548
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
4502
|
-
lastContactAt:
|
|
4549
|
+
lastContactAt: z3.string().datetime().nullable(),
|
|
4503
4550
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
4504
4551
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
4505
|
-
lastSeenAt:
|
|
4552
|
+
lastSeenAt: z3.string().datetime().nullable().optional(),
|
|
4506
4553
|
/** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
|
|
4507
4554
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
4508
4555
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
4509
|
-
runtime:
|
|
4510
|
-
harnesses:
|
|
4511
|
-
workspaces:
|
|
4556
|
+
runtime: z3.object({
|
|
4557
|
+
harnesses: z3.array(z3.object({ name: z3.string(), label: z3.string(), status: z3.string() })).optional(),
|
|
4558
|
+
workspaces: z3.array(z3.string()).optional()
|
|
4512
4559
|
}).optional(),
|
|
4513
4560
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
4514
4561
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -4517,119 +4564,147 @@ var ConnectionSummarySchema = z2.object({
|
|
|
4517
4564
|
activity: AgentActivitySchema.optional(),
|
|
4518
4565
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
4519
4566
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
4520
|
-
managed:
|
|
4567
|
+
managed: z3.boolean()
|
|
4521
4568
|
});
|
|
4522
|
-
var LedgerItemSchema =
|
|
4523
|
-
var AgentLedgerSchema =
|
|
4524
|
-
agent:
|
|
4569
|
+
var LedgerItemSchema = z3.object({ id: z3.string(), parentId: z3.string(), title: z3.string(), createdAt: z3.string() });
|
|
4570
|
+
var AgentLedgerSchema = z3.object({
|
|
4571
|
+
agent: z3.object({ id: z3.string(), name: z3.string(), revokedAt: z3.string().nullable() }),
|
|
4525
4572
|
/** Its own questions you have not answered. */
|
|
4526
|
-
asks:
|
|
4573
|
+
asks: z3.array(LedgerItemSchema),
|
|
4527
4574
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
4528
|
-
answered:
|
|
4575
|
+
answered: z3.array(LedgerItemSchema),
|
|
4529
4576
|
/** Requests you sent it that it never took. */
|
|
4530
|
-
requests:
|
|
4531
|
-
goals:
|
|
4532
|
-
callbacks:
|
|
4577
|
+
requests: z3.array(LedgerItemSchema),
|
|
4578
|
+
goals: z3.array(z3.object({ id: z3.string(), outcome: z3.string(), state: z3.string() })),
|
|
4579
|
+
callbacks: z3.array(z3.object({ id: z3.string(), parentId: z3.string(), trigger: z3.string(), note: z3.string(), dueAt: z3.string().nullable() }))
|
|
4533
4580
|
});
|
|
4534
|
-
var ReassignResultSchema =
|
|
4535
|
-
moved:
|
|
4536
|
-
parentId:
|
|
4581
|
+
var ReassignResultSchema = z3.object({
|
|
4582
|
+
moved: z3.object({ asks: z3.number(), answered: z3.number(), requests: z3.number(), goals: z3.number(), callbacks: z3.number() }),
|
|
4583
|
+
parentId: z3.string().nullable()
|
|
4537
4584
|
});
|
|
4538
|
-
var MoveRingSchema =
|
|
4539
|
-
var MoveSchema =
|
|
4540
|
-
id:
|
|
4585
|
+
var MoveRingSchema = z3.enum(["home", "travels", "retired", "quarantined"]);
|
|
4586
|
+
var MoveSchema = z3.object({
|
|
4587
|
+
id: z3.string(),
|
|
4541
4588
|
/** The reusable question, as distill normalized it. */
|
|
4542
|
-
question:
|
|
4589
|
+
question: z3.string(),
|
|
4543
4590
|
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
4544
|
-
answer:
|
|
4591
|
+
answer: z3.string(),
|
|
4545
4592
|
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
4546
4593
|
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
4547
|
-
rationale:
|
|
4594
|
+
rationale: z3.string().nullable(),
|
|
4548
4595
|
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
4549
|
-
scope:
|
|
4596
|
+
scope: z3.string(),
|
|
4550
4597
|
ring: MoveRingSchema,
|
|
4551
4598
|
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
4552
|
-
pinned:
|
|
4599
|
+
pinned: z3.boolean(),
|
|
4553
4600
|
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
4554
4601
|
* break is visible instead of a pin silently disappearing. */
|
|
4555
|
-
pinBroken:
|
|
4602
|
+
pinBroken: z3.boolean(),
|
|
4556
4603
|
/** When the ruling was distilled. */
|
|
4557
|
-
learnedAt:
|
|
4604
|
+
learnedAt: z3.string(),
|
|
4558
4605
|
/** Last time it answered an ask. Null = never fired. */
|
|
4559
|
-
lastUsedAt:
|
|
4606
|
+
lastUsedAt: z3.string().nullable(),
|
|
4560
4607
|
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
4561
4608
|
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
4562
|
-
usedCount:
|
|
4609
|
+
usedCount: z3.number(),
|
|
4563
4610
|
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
4564
|
-
confirms:
|
|
4611
|
+
confirms: z3.number(),
|
|
4565
4612
|
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
4566
4613
|
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
4567
|
-
counters:
|
|
4614
|
+
counters: z3.number(),
|
|
4568
4615
|
/** The agent that asked the question this move came from, when known. Null for a move
|
|
4569
4616
|
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
4570
|
-
learnedFrom:
|
|
4617
|
+
learnedFrom: z3.object({ id: z3.string(), name: z3.string() }).nullable()
|
|
4571
4618
|
});
|
|
4572
|
-
var CreateRequestSchema =
|
|
4619
|
+
var CreateRequestSchema = z3.object({
|
|
4573
4620
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
4574
|
-
tokenId:
|
|
4621
|
+
tokenId: z3.string(),
|
|
4575
4622
|
/** The user's message to the agent. */
|
|
4576
|
-
text:
|
|
4623
|
+
text: z3.string().min(1),
|
|
4577
4624
|
/** Land the request on an existing conversation thread (History → "Continue")
|
|
4578
4625
|
* instead of minting a fresh one. Must belong to the requesting user. */
|
|
4579
|
-
parentId:
|
|
4626
|
+
parentId: z3.string().optional(),
|
|
4580
4627
|
/** Point the agent at a past conversation (possibly with a different agent) as
|
|
4581
4628
|
* starting context (History → "New session from this"). A reference, not a copy —
|
|
4582
4629
|
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
4583
|
-
contextParentId:
|
|
4630
|
+
contextParentId: z3.string().optional()
|
|
4631
|
+
});
|
|
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)
|
|
4584
4653
|
});
|
|
4585
|
-
var
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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)
|
|
4605
4680
|
});
|
|
4606
|
-
var NoteSourceSchema =
|
|
4607
|
-
var NoteStatusSchema =
|
|
4608
|
-
var NoteRepeatSchema =
|
|
4609
|
-
var DecisionSchema =
|
|
4610
|
-
id:
|
|
4681
|
+
var NoteSourceSchema = z3.enum(["app", "call"]);
|
|
4682
|
+
var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
|
|
4683
|
+
var NoteRepeatSchema = z3.enum(["once", "until_done"]);
|
|
4684
|
+
var DecisionSchema = z3.object({
|
|
4685
|
+
id: z3.string(),
|
|
4611
4686
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
4612
4687
|
* extensibility seam — any conversation can accrue decisions). */
|
|
4613
|
-
noteId:
|
|
4688
|
+
noteId: z3.string().nullable(),
|
|
4614
4689
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
4615
|
-
question:
|
|
4690
|
+
question: z3.string(),
|
|
4616
4691
|
/** The user's ruling; null while the question is open. */
|
|
4617
|
-
answer:
|
|
4618
|
-
decidedAt:
|
|
4619
|
-
createdAt:
|
|
4692
|
+
answer: z3.string().nullable(),
|
|
4693
|
+
decidedAt: z3.string().nullable(),
|
|
4694
|
+
createdAt: z3.string()
|
|
4620
4695
|
});
|
|
4621
|
-
var NoteSchema =
|
|
4622
|
-
id:
|
|
4696
|
+
var NoteSchema = z3.object({
|
|
4697
|
+
id: z3.string(),
|
|
4623
4698
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
4624
|
-
title:
|
|
4699
|
+
title: z3.string(),
|
|
4625
4700
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
4626
|
-
intent:
|
|
4701
|
+
intent: z3.string(),
|
|
4627
4702
|
source: NoteSourceSchema,
|
|
4628
4703
|
status: NoteStatusSchema,
|
|
4629
4704
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
4630
|
-
assignee:
|
|
4705
|
+
assignee: z3.string().nullable(),
|
|
4631
4706
|
/** The request thread minted at assignment; null until assigned. */
|
|
4632
|
-
parentId:
|
|
4707
|
+
parentId: z3.string().nullable(),
|
|
4633
4708
|
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
4634
4709
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
4635
4710
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -4638,200 +4713,216 @@ var NoteSchema = z2.object({
|
|
|
4638
4713
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
4639
4714
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
4640
4715
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
4641
|
-
dueAt:
|
|
4716
|
+
dueAt: z3.string().nullable().default(null),
|
|
4642
4717
|
repeat: NoteRepeatSchema.default("once"),
|
|
4643
4718
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
4644
|
-
rides:
|
|
4645
|
-
lastRideAt:
|
|
4646
|
-
createdAt:
|
|
4719
|
+
rides: z3.number().int().default(0),
|
|
4720
|
+
lastRideAt: z3.string().nullable().default(null),
|
|
4721
|
+
createdAt: z3.string()
|
|
4647
4722
|
});
|
|
4648
|
-
var CreateNoteSchema =
|
|
4723
|
+
var CreateNoteSchema = z3.object({
|
|
4649
4724
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
4650
|
-
text:
|
|
4725
|
+
text: z3.string().min(1).max(4e3),
|
|
4651
4726
|
/** Capture it as a REMINDER — a note assigned to the user themselves, which rides
|
|
4652
4727
|
* their next call instead of being handed to an agent. Everything else about the
|
|
4653
4728
|
* note is identical; this is the one parameter that separates the two. */
|
|
4654
|
-
forMe:
|
|
4729
|
+
forMe: z3.boolean().optional(),
|
|
4655
4730
|
/** The not-before, when the user already said one. Absent = the very next call. */
|
|
4656
|
-
dueAt:
|
|
4731
|
+
dueAt: z3.string().datetime().optional(),
|
|
4657
4732
|
repeat: NoteRepeatSchema.optional()
|
|
4658
4733
|
});
|
|
4659
|
-
var RecordDecisionSchema =
|
|
4734
|
+
var RecordDecisionSchema = z3.object({
|
|
4660
4735
|
/** An open decision (from /clarify) to answer. */
|
|
4661
|
-
decisionId:
|
|
4736
|
+
decisionId: z3.string().optional(),
|
|
4662
4737
|
/** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
|
|
4663
|
-
question:
|
|
4738
|
+
question: z3.string().min(1).max(500).optional(),
|
|
4664
4739
|
/** The ruling. */
|
|
4665
|
-
answer:
|
|
4740
|
+
answer: z3.string().min(1).max(2e3)
|
|
4666
4741
|
}).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
|
|
4667
|
-
var AssignNoteSchema =
|
|
4742
|
+
var AssignNoteSchema = z3.object({
|
|
4668
4743
|
/** An EXISTING agent: token id or nickname. Omit when spawning fresh. */
|
|
4669
|
-
target:
|
|
4744
|
+
target: z3.string().min(1).optional(),
|
|
4670
4745
|
/** Spawn a NEW session for this note (companion.md §2.2): the assignee doesn't
|
|
4671
4746
|
* exist yet — mint it on a live desktop that advertises the harness+workspace,
|
|
4672
4747
|
* named after the note. The brief arrives as its opening request. */
|
|
4673
|
-
spawn:
|
|
4674
|
-
hostTokenId:
|
|
4675
|
-
harness:
|
|
4676
|
-
workspace:
|
|
4748
|
+
spawn: z3.object({
|
|
4749
|
+
hostTokenId: z3.string().uuid(),
|
|
4750
|
+
harness: z3.string().min(1),
|
|
4751
|
+
workspace: z3.string().min(1)
|
|
4677
4752
|
}).optional()
|
|
4678
4753
|
}).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
|
|
4679
|
-
var TriageVerdictSchema =
|
|
4680
|
-
var TriageItemSchema =
|
|
4681
|
-
noteId:
|
|
4754
|
+
var TriageVerdictSchema = z3.enum(["keep", "stale", "close", "assign"]);
|
|
4755
|
+
var TriageItemSchema = z3.object({
|
|
4756
|
+
noteId: z3.string(),
|
|
4682
4757
|
/** The note's headline at run time. */
|
|
4683
|
-
title:
|
|
4758
|
+
title: z3.string(),
|
|
4684
4759
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
4685
4760
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
4686
4761
|
* Never a model's reasoning transcript, never an id. */
|
|
4687
|
-
why:
|
|
4762
|
+
why: z3.string()
|
|
4688
4763
|
});
|
|
4689
|
-
var TriageAssignmentSchema =
|
|
4764
|
+
var TriageAssignmentSchema = z3.object({
|
|
4690
4765
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
4691
|
-
agent:
|
|
4766
|
+
agent: z3.string(),
|
|
4692
4767
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
4693
4768
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
4694
|
-
agentName:
|
|
4695
|
-
notes:
|
|
4769
|
+
agentName: z3.string(),
|
|
4770
|
+
notes: z3.array(TriageItemSchema)
|
|
4696
4771
|
});
|
|
4697
|
-
var TriageStatusSchema =
|
|
4698
|
-
var SubmitTriageSchema =
|
|
4772
|
+
var TriageStatusSchema = z3.enum(["open", "superseded", "dismissed"]);
|
|
4773
|
+
var SubmitTriageSchema = z3.object({
|
|
4699
4774
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
4700
4775
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
4701
4776
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
4702
4777
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
4703
|
-
provider:
|
|
4778
|
+
provider: z3.string().min(1).max(60),
|
|
4704
4779
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
4705
|
-
model:
|
|
4780
|
+
model: z3.string().max(200).nullable().optional(),
|
|
4706
4781
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
4707
4782
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
4708
|
-
reviewed:
|
|
4709
|
-
close:
|
|
4710
|
-
stale:
|
|
4711
|
-
assign:
|
|
4783
|
+
reviewed: z3.number().int().min(0).max(1e4).default(0),
|
|
4784
|
+
close: z3.array(TriageItemSchema).max(200).default([]),
|
|
4785
|
+
stale: z3.array(TriageItemSchema).max(200).default([]),
|
|
4786
|
+
assign: z3.array(TriageAssignmentSchema).max(50).default([])
|
|
4712
4787
|
});
|
|
4713
4788
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
4714
|
-
id:
|
|
4715
|
-
runAt:
|
|
4789
|
+
id: z3.string(),
|
|
4790
|
+
runAt: z3.string(),
|
|
4716
4791
|
status: TriageStatusSchema,
|
|
4717
|
-
model:
|
|
4792
|
+
model: z3.string().nullable().default(null)
|
|
4718
4793
|
});
|
|
4719
|
-
var AcceptTriageSchema =
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
group:
|
|
4724
|
-
agent:
|
|
4725
|
-
noteIds:
|
|
4794
|
+
var AcceptTriageSchema = z3.discriminatedUnion("group", [
|
|
4795
|
+
z3.object({ group: z3.literal("close"), noteIds: z3.array(z3.string()).max(200).optional() }),
|
|
4796
|
+
z3.object({ group: z3.literal("stale"), noteIds: z3.array(z3.string()).max(200).optional() }),
|
|
4797
|
+
z3.object({
|
|
4798
|
+
group: z3.literal("assign"),
|
|
4799
|
+
agent: z3.string().min(1),
|
|
4800
|
+
noteIds: z3.array(z3.string()).max(200).optional()
|
|
4726
4801
|
})
|
|
4727
4802
|
]);
|
|
4728
|
-
var AcceptTriageResultSchema =
|
|
4729
|
-
accepted:
|
|
4730
|
-
failed:
|
|
4803
|
+
var AcceptTriageResultSchema = z3.object({
|
|
4804
|
+
accepted: z3.array(z3.string()),
|
|
4805
|
+
failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
|
|
4731
4806
|
});
|
|
4732
|
-
var DeliveryModeSchema =
|
|
4733
|
-
var
|
|
4734
|
-
var
|
|
4735
|
-
|
|
4736
|
-
|
|
4807
|
+
var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
|
|
4808
|
+
var WAKE_EVENT = "wake";
|
|
4809
|
+
var wakeChannel = (tokenId) => `wake:${tokenId}`;
|
|
4810
|
+
var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
|
|
4811
|
+
var OAuthStartSchema = z3.object({
|
|
4812
|
+
provider: z3.enum(["cma"]),
|
|
4813
|
+
returnTo: z3.string().min(1)
|
|
4737
4814
|
});
|
|
4738
|
-
var DeliveryConfigSchema =
|
|
4739
|
-
tokenId:
|
|
4815
|
+
var DeliveryConfigSchema = z3.object({
|
|
4816
|
+
tokenId: z3.string(),
|
|
4740
4817
|
mode: DeliveryModeSchema,
|
|
4741
|
-
/** null when the
|
|
4742
|
-
*
|
|
4743
|
-
|
|
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. */
|
|
4821
|
+
realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
|
|
4744
4822
|
});
|
|
4745
|
-
var StatusSchema =
|
|
4746
|
-
name:
|
|
4747
|
-
sessionMode:
|
|
4823
|
+
var StatusSchema = z3.object({
|
|
4824
|
+
name: z3.string(),
|
|
4825
|
+
sessionMode: z3.enum(["default", "all_calls", "silent"]),
|
|
4748
4826
|
/** A phone is registered for push/ring (any push token on the account). */
|
|
4749
|
-
phone:
|
|
4827
|
+
phone: z3.boolean(),
|
|
4828
|
+
/** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
|
|
4829
|
+
* it never picked up. THE SAME NUMBER the harness's wake gate reads
|
|
4830
|
+
* (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
|
|
4831
|
+
* zero while the sweep sees one is two ideas of "waiting".
|
|
4832
|
+
*
|
|
4833
|
+
* Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
|
|
4834
|
+
* that nobody spawned, so a terminal session only learns of work by asking. The harness
|
|
4835
|
+
* used to paper over that by spawning a SECOND process on the identity; now it stands
|
|
4836
|
+
* back, correctly, and the person sitting at the terminal is the one who can act. A
|
|
4837
|
+
* coffee-beans request sat unread for three days.
|
|
4838
|
+
*
|
|
4839
|
+
* Optional: an older API sends no field, and the statusline then renders exactly as before. */
|
|
4840
|
+
waiting: z3.number().int().nonnegative().optional()
|
|
4750
4841
|
});
|
|
4751
|
-
var EnvelopeRecipientSchema =
|
|
4752
|
-
keyId:
|
|
4753
|
-
epk:
|
|
4754
|
-
wnonce:
|
|
4755
|
-
wrap:
|
|
4842
|
+
var EnvelopeRecipientSchema = z3.object({
|
|
4843
|
+
keyId: z3.string(),
|
|
4844
|
+
epk: z3.string(),
|
|
4845
|
+
wnonce: z3.string(),
|
|
4846
|
+
wrap: z3.string()
|
|
4756
4847
|
});
|
|
4757
|
-
var EnvelopeHeaderSchema =
|
|
4758
|
-
field:
|
|
4759
|
-
kind:
|
|
4760
|
-
senderRole:
|
|
4761
|
-
recipientKeyIds:
|
|
4762
|
-
seq:
|
|
4848
|
+
var EnvelopeHeaderSchema = z3.object({
|
|
4849
|
+
field: z3.enum(["context", "options", "visuals", "answer"]),
|
|
4850
|
+
kind: z3.string(),
|
|
4851
|
+
senderRole: z3.enum(["agent", "user"]),
|
|
4852
|
+
recipientKeyIds: z3.array(z3.string()),
|
|
4853
|
+
seq: z3.number().int().nonnegative()
|
|
4763
4854
|
});
|
|
4764
|
-
var EnvelopeSchema =
|
|
4765
|
-
v:
|
|
4766
|
-
alg:
|
|
4767
|
-
msgId:
|
|
4855
|
+
var EnvelopeSchema = z3.object({
|
|
4856
|
+
v: z3.literal(1),
|
|
4857
|
+
alg: z3.literal("x25519-xsalsa20poly1305"),
|
|
4858
|
+
msgId: z3.string(),
|
|
4768
4859
|
hdr: EnvelopeHeaderSchema,
|
|
4769
|
-
recipients:
|
|
4770
|
-
nonce:
|
|
4771
|
-
ct:
|
|
4860
|
+
recipients: z3.array(EnvelopeRecipientSchema).min(1),
|
|
4861
|
+
nonce: z3.string(),
|
|
4862
|
+
ct: z3.string()
|
|
4772
4863
|
});
|
|
4773
|
-
var SealedAnswerSchema =
|
|
4774
|
-
ignored:
|
|
4864
|
+
var SealedAnswerSchema = z3.object({
|
|
4865
|
+
ignored: z3.boolean(),
|
|
4775
4866
|
envelope: EnvelopeSchema
|
|
4776
4867
|
});
|
|
4777
|
-
var DeviceCredentialSchema =
|
|
4778
|
-
deviceId:
|
|
4779
|
-
kind:
|
|
4780
|
-
x25519Pub:
|
|
4781
|
-
ed25519Pub:
|
|
4782
|
-
sig:
|
|
4868
|
+
var DeviceCredentialSchema = z3.object({
|
|
4869
|
+
deviceId: z3.string(),
|
|
4870
|
+
kind: z3.enum(["phone", "web", "agent"]),
|
|
4871
|
+
x25519Pub: z3.string(),
|
|
4872
|
+
ed25519Pub: z3.string(),
|
|
4873
|
+
sig: z3.string()
|
|
4783
4874
|
});
|
|
4784
|
-
var DeviceRosterSchema =
|
|
4785
|
-
uikPub:
|
|
4786
|
-
devices:
|
|
4875
|
+
var DeviceRosterSchema = z3.object({
|
|
4876
|
+
uikPub: z3.string(),
|
|
4877
|
+
devices: z3.array(DeviceCredentialSchema)
|
|
4787
4878
|
});
|
|
4788
|
-
var WakeNudgeSchema =
|
|
4789
|
-
kind:
|
|
4790
|
-
notificationId:
|
|
4791
|
-
parentId:
|
|
4879
|
+
var WakeNudgeSchema = z3.object({
|
|
4880
|
+
kind: z3.enum(["reply", "request", "callback"]),
|
|
4881
|
+
notificationId: z3.string().optional(),
|
|
4882
|
+
parentId: z3.string()
|
|
4792
4883
|
});
|
|
4793
|
-
var PairingStatusSchema =
|
|
4794
|
-
var PairingRevealSchema =
|
|
4795
|
-
x25519:
|
|
4796
|
-
ed25519:
|
|
4797
|
-
nonce:
|
|
4884
|
+
var PairingStatusSchema = z3.enum(["pending", "approved", "denied", "expired"]);
|
|
4885
|
+
var PairingRevealSchema = z3.object({
|
|
4886
|
+
x25519: z3.string(),
|
|
4887
|
+
ed25519: z3.string(),
|
|
4888
|
+
nonce: z3.string()
|
|
4798
4889
|
});
|
|
4799
|
-
var DeviceCodeRequestSchema =
|
|
4890
|
+
var DeviceCodeRequestSchema = z3.object({
|
|
4800
4891
|
/** The agent's suggested name for the pairing — the human sees it pre-filled at
|
|
4801
4892
|
* approval and can override. Optional; blank → a default silly name server-side. */
|
|
4802
|
-
suggestedName:
|
|
4893
|
+
suggestedName: z3.string().optional(),
|
|
4803
4894
|
/** Legacy alias for suggestedName (older MCPs sent `agent`). Accepted for
|
|
4804
4895
|
* back-compat; `suggestedName` wins when both are present. ponytail: drop once no
|
|
4805
4896
|
* pre-`name` MCP is in the wild. */
|
|
4806
|
-
agent:
|
|
4807
|
-
device:
|
|
4808
|
-
proto:
|
|
4897
|
+
agent: z3.union([z3.string(), z3.object({ name: z3.string().optional() })]).optional(),
|
|
4898
|
+
device: z3.string().optional(),
|
|
4899
|
+
proto: z3.string().optional(),
|
|
4809
4900
|
// compiled-in tag, e.g. "paigy-pair-v2|sas=24"
|
|
4810
|
-
commitment:
|
|
4901
|
+
commitment: z3.string().optional()
|
|
4811
4902
|
// agent's Ca
|
|
4812
4903
|
});
|
|
4813
|
-
var DeviceCodeSchema =
|
|
4814
|
-
device_code:
|
|
4815
|
-
user_code:
|
|
4816
|
-
verification_uri:
|
|
4817
|
-
verification_uri_complete:
|
|
4818
|
-
interval:
|
|
4819
|
-
expires_in:
|
|
4904
|
+
var DeviceCodeSchema = z3.object({
|
|
4905
|
+
device_code: z3.string(),
|
|
4906
|
+
user_code: z3.string(),
|
|
4907
|
+
verification_uri: z3.string().url(),
|
|
4908
|
+
verification_uri_complete: z3.string().url(),
|
|
4909
|
+
interval: z3.number(),
|
|
4910
|
+
expires_in: z3.number()
|
|
4820
4911
|
});
|
|
4821
|
-
var DeviceInfoSchema =
|
|
4822
|
-
code:
|
|
4912
|
+
var DeviceInfoSchema = z3.object({
|
|
4913
|
+
code: z3.string(),
|
|
4823
4914
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
4824
4915
|
* pre-filling the name field the human can edit. */
|
|
4825
|
-
name:
|
|
4916
|
+
name: z3.string(),
|
|
4826
4917
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
4827
4918
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
4828
4919
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
4829
4920
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
4830
|
-
agent:
|
|
4831
|
-
device:
|
|
4921
|
+
agent: z3.string().optional(),
|
|
4922
|
+
device: z3.string().nullable(),
|
|
4832
4923
|
status: PairingStatusSchema,
|
|
4833
|
-
proto:
|
|
4834
|
-
agent_commitment:
|
|
4924
|
+
proto: z3.string().nullable().optional(),
|
|
4925
|
+
agent_commitment: z3.string().nullable().optional(),
|
|
4835
4926
|
// Ca
|
|
4836
4927
|
agent_reveal: PairingRevealSchema.nullable().optional(),
|
|
4837
4928
|
// present once the agent reveals
|
|
@@ -4841,48 +4932,48 @@ var DeviceInfoSchema = z2.object({
|
|
|
4841
4932
|
// withheld until e2ee=true) can compute the SAS WITHOUT a token. Public keys — same safety
|
|
4842
4933
|
// class as agent_reveal above. Present only once the phone reveals; null otherwise.
|
|
4843
4934
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
4844
|
-
uik_pub:
|
|
4935
|
+
uik_pub: z3.string().nullable().optional()
|
|
4845
4936
|
});
|
|
4846
|
-
var DeviceTokenRequestSchema =
|
|
4847
|
-
device_code:
|
|
4937
|
+
var DeviceTokenRequestSchema = z3.object({
|
|
4938
|
+
device_code: z3.string(),
|
|
4848
4939
|
reveal: PairingRevealSchema.optional()
|
|
4849
4940
|
// agent's reveal {x25519, ed25519, nonce}
|
|
4850
4941
|
});
|
|
4851
|
-
var DeviceTokenSchema =
|
|
4852
|
-
access_token:
|
|
4942
|
+
var DeviceTokenSchema = z3.object({
|
|
4943
|
+
access_token: z3.string(),
|
|
4853
4944
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
4854
4945
|
* a default silly name). */
|
|
4855
|
-
name:
|
|
4856
|
-
device:
|
|
4946
|
+
name: z3.string(),
|
|
4947
|
+
device: z3.string().nullable(),
|
|
4857
4948
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
4858
4949
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
4859
|
-
voice:
|
|
4950
|
+
voice: z3.string().nullable().optional(),
|
|
4860
4951
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
4861
4952
|
* that survives a rename. Cached by the host's identity beat. */
|
|
4862
|
-
token_id:
|
|
4953
|
+
token_id: z3.string().nullable().optional(),
|
|
4863
4954
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
4864
4955
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
4865
4956
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
4866
4957
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
4867
|
-
workspace:
|
|
4958
|
+
workspace: z3.string().nullable().optional(),
|
|
4868
4959
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
4869
4960
|
// present once the phone reveals
|
|
4870
|
-
uik_pub:
|
|
4961
|
+
uik_pub: z3.string().nullable().optional()
|
|
4871
4962
|
});
|
|
4872
|
-
var DeviceCommitRequestSchema =
|
|
4873
|
-
code:
|
|
4874
|
-
commitment:
|
|
4963
|
+
var DeviceCommitRequestSchema = z3.object({
|
|
4964
|
+
code: z3.string(),
|
|
4965
|
+
commitment: z3.string()
|
|
4875
4966
|
// Cb
|
|
4876
4967
|
});
|
|
4877
|
-
var DevicePeerCommitSchema =
|
|
4878
|
-
commitment:
|
|
4968
|
+
var DevicePeerCommitSchema = z3.object({
|
|
4969
|
+
commitment: z3.string().nullable()
|
|
4879
4970
|
});
|
|
4880
|
-
var SupportRequestSchema =
|
|
4881
|
-
email:
|
|
4882
|
-
message:
|
|
4883
|
-
name:
|
|
4971
|
+
var SupportRequestSchema = z3.object({
|
|
4972
|
+
email: z3.string().email().max(320),
|
|
4973
|
+
message: z3.string().trim().min(1).max(5e3),
|
|
4974
|
+
name: z3.string().trim().max(120).optional()
|
|
4884
4975
|
});
|
|
4885
|
-
var NotificationFeedbackKindSchema =
|
|
4976
|
+
var NotificationFeedbackKindSchema = z3.enum([
|
|
4886
4977
|
"break_down",
|
|
4887
4978
|
// "This should be more than one ask — break it down."
|
|
4888
4979
|
"regenerate_options",
|
|
@@ -4896,29 +4987,21 @@ var NotificationFeedbackKindSchema = z2.enum([
|
|
|
4896
4987
|
"other"
|
|
4897
4988
|
// anything else — the note carries it.
|
|
4898
4989
|
]);
|
|
4899
|
-
var NotificationFeedbackSchema =
|
|
4900
|
-
notificationId:
|
|
4990
|
+
var NotificationFeedbackSchema = z3.object({
|
|
4991
|
+
notificationId: z3.string(),
|
|
4901
4992
|
kind: NotificationFeedbackKindSchema,
|
|
4902
4993
|
/** Optional free-text elaboration for a preset; required (non-empty) for 'other'. */
|
|
4903
|
-
note:
|
|
4994
|
+
note: z3.string().trim().max(2e3).optional()
|
|
4904
4995
|
}).superRefine((r, ctx) => {
|
|
4905
4996
|
if (r.kind === "other" && !r.note)
|
|
4906
|
-
ctx.addIssue({ code:
|
|
4997
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["note"], message: "note is required for 'other' feedback" });
|
|
4907
4998
|
});
|
|
4908
|
-
var FeedbackResolutionSchema =
|
|
4909
|
-
var FeedbackOutcomeSchema =
|
|
4999
|
+
var FeedbackResolutionSchema = z3.enum(["broker_fixed", "sent_to_agent"]);
|
|
5000
|
+
var FeedbackOutcomeSchema = z3.object({
|
|
4910
5001
|
resolution: FeedbackResolutionSchema,
|
|
4911
5002
|
transform: TransformSchema,
|
|
4912
|
-
message:
|
|
4913
|
-
childIds:
|
|
4914
|
-
});
|
|
4915
|
-
var CONTACT_SCHEMA = contactSchemaFrom({
|
|
4916
|
-
ask: NotifyRequestFields.shape.ask,
|
|
4917
|
-
waiting: NotifyRequestFields.shape.waiting,
|
|
4918
|
-
options: NotifyRequestFields.shape.options,
|
|
4919
|
-
channel: NotifyRequestFields.shape.channel,
|
|
4920
|
-
parentId: NotifyRequestFields.shape.parentId,
|
|
4921
|
-
workId: NotifyRequestFields.shape.workId
|
|
5003
|
+
message: z3.string(),
|
|
5004
|
+
childIds: z3.array(z3.string()).optional()
|
|
4922
5005
|
});
|
|
4923
5006
|
var import_tweetnacl = __toESM(require_nacl_fast(), 1);
|
|
4924
5007
|
var import_tweetnacl2 = __toESM(require_nacl_fast(), 1);
|
|
@@ -4943,21 +5026,6 @@ function utf8ToBytes(s) {
|
|
|
4943
5026
|
}
|
|
4944
5027
|
return Uint8Array.from(out);
|
|
4945
5028
|
}
|
|
4946
|
-
function bytesToUtf8(b) {
|
|
4947
|
-
let out = "";
|
|
4948
|
-
for (let i = 0; i < b.length; ) {
|
|
4949
|
-
const c = b[i++];
|
|
4950
|
-
if (c < 128) out += String.fromCharCode(c);
|
|
4951
|
-
else if (c < 224) out += String.fromCharCode((c & 31) << 6 | b[i++] & 63);
|
|
4952
|
-
else if (c < 240) out += String.fromCharCode((c & 15) << 12 | (b[i++] & 63) << 6 | b[i++] & 63);
|
|
4953
|
-
else {
|
|
4954
|
-
const cp = (c & 7) << 18 | (b[i++] & 63) << 12 | (b[i++] & 63) << 6 | b[i++] & 63;
|
|
4955
|
-
const u = cp - 65536;
|
|
4956
|
-
out += String.fromCharCode(55296 + (u >> 10), 56320 + (u & 1023));
|
|
4957
|
-
}
|
|
4958
|
-
}
|
|
4959
|
-
return out;
|
|
4960
|
-
}
|
|
4961
5029
|
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
4962
5030
|
function toB64(bytes) {
|
|
4963
5031
|
let out = "";
|
|
@@ -5003,15 +5071,9 @@ function concat(parts) {
|
|
|
5003
5071
|
}
|
|
5004
5072
|
return out;
|
|
5005
5073
|
}
|
|
5006
|
-
function u16be(n) {
|
|
5007
|
-
return Uint8Array.of(n >>> 8 & 255, n & 255);
|
|
5008
|
-
}
|
|
5009
5074
|
function u32be(n) {
|
|
5010
5075
|
return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
|
|
5011
5076
|
}
|
|
5012
|
-
function readU32be(b, o) {
|
|
5013
|
-
return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
|
|
5014
|
-
}
|
|
5015
5077
|
function lp(b) {
|
|
5016
5078
|
return concat([u32be(b.length), b]);
|
|
5017
5079
|
}
|
|
@@ -5053,9 +5115,6 @@ function sharedSecret(peerX25519PubB64, mySecretB64) {
|
|
|
5053
5115
|
if (raw.every((b) => b === 0)) throw new Error("low-order X25519 point \u2014 shared secret rejected");
|
|
5054
5116
|
return toB64(raw);
|
|
5055
5117
|
}
|
|
5056
|
-
function keyId(publicKeyB64) {
|
|
5057
|
-
return toB64(import_tweetnacl2.default.hash(fromB64(publicKeyB64)).slice(0, 8));
|
|
5058
|
-
}
|
|
5059
5118
|
function credentialBody(c) {
|
|
5060
5119
|
return concat([
|
|
5061
5120
|
lp(utf8ToBytes("paigy-device-credential-v1")),
|
|
@@ -5073,88 +5132,6 @@ function verifyDeviceCredential(cred, uikPublicB64) {
|
|
|
5073
5132
|
return false;
|
|
5074
5133
|
}
|
|
5075
5134
|
}
|
|
5076
|
-
var ENVELOPE_VERSION = 1;
|
|
5077
|
-
var ENVELOPE_ALG = "x25519-xsalsa20poly1305";
|
|
5078
|
-
function padme(L) {
|
|
5079
|
-
if (L < 2) return L;
|
|
5080
|
-
const E = Math.floor(Math.log2(L));
|
|
5081
|
-
const S = Math.floor(Math.log2(E)) + 1;
|
|
5082
|
-
const lastBits = E - S;
|
|
5083
|
-
if (lastBits <= 0) return L;
|
|
5084
|
-
const mask = (1 << lastBits) - 1;
|
|
5085
|
-
return L + mask & ~mask;
|
|
5086
|
-
}
|
|
5087
|
-
function canonicalHeader(msgId, hdr) {
|
|
5088
|
-
const parts = [
|
|
5089
|
-
u16be(ENVELOPE_VERSION),
|
|
5090
|
-
lp(utf8ToBytes(ENVELOPE_ALG)),
|
|
5091
|
-
lp(msgId),
|
|
5092
|
-
lp(utf8ToBytes(hdr.field)),
|
|
5093
|
-
lp(utf8ToBytes(hdr.kind)),
|
|
5094
|
-
lp(utf8ToBytes(hdr.senderRole)),
|
|
5095
|
-
u32be(hdr.seq),
|
|
5096
|
-
u32be(hdr.recipientKeyIds.length),
|
|
5097
|
-
...hdr.recipientKeyIds.map((k) => lp(utf8ToBytes(k)))
|
|
5098
|
-
];
|
|
5099
|
-
return concat(parts);
|
|
5100
|
-
}
|
|
5101
|
-
function seal(input) {
|
|
5102
|
-
ensureCsprng();
|
|
5103
|
-
if (input.recipients.length === 0) throw new Error("seal: no recipients");
|
|
5104
|
-
const kind = input.kind ?? "";
|
|
5105
|
-
const recs = input.recipients.map((pub) => ({ pub, id: keyId(pub) })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
5106
|
-
const hdr = {
|
|
5107
|
-
field: input.field,
|
|
5108
|
-
kind,
|
|
5109
|
-
senderRole: input.senderRole,
|
|
5110
|
-
recipientKeyIds: recs.map((r) => r.id),
|
|
5111
|
-
seq: input.seq
|
|
5112
|
-
};
|
|
5113
|
-
const msgIdBytes = input.msgId ? fromB64(input.msgId) : randomBytes(16);
|
|
5114
|
-
const cek = randomBytes(32);
|
|
5115
|
-
const canon = canonicalHeader(msgIdBytes, hdr);
|
|
5116
|
-
const plaintext = concat([u32be(canon.length), canon, u32be(input.body.length), input.body]);
|
|
5117
|
-
const padded = new Uint8Array(padme(plaintext.length));
|
|
5118
|
-
padded.set(plaintext);
|
|
5119
|
-
const nonce = randomBytes(24);
|
|
5120
|
-
const ct = import_tweetnacl3.default.secretbox(padded, nonce, cek);
|
|
5121
|
-
const recipients = recs.map((r) => {
|
|
5122
|
-
const eph = import_tweetnacl3.default.box.keyPair();
|
|
5123
|
-
const wnonce = randomBytes(24);
|
|
5124
|
-
const wrap = import_tweetnacl3.default.box(cek, wnonce, fromB64(r.pub), eph.secretKey);
|
|
5125
|
-
return { keyId: r.id, epk: toB64(eph.publicKey), wnonce: toB64(wnonce), wrap: toB64(wrap) };
|
|
5126
|
-
});
|
|
5127
|
-
return {
|
|
5128
|
-
v: ENVELOPE_VERSION,
|
|
5129
|
-
alg: ENVELOPE_ALG,
|
|
5130
|
-
msgId: toB64(msgIdBytes),
|
|
5131
|
-
hdr,
|
|
5132
|
-
recipients,
|
|
5133
|
-
nonce: toB64(nonce),
|
|
5134
|
-
ct: toB64(ct)
|
|
5135
|
-
};
|
|
5136
|
-
}
|
|
5137
|
-
function open(envelope, myKeyId, mySecretKeyB64) {
|
|
5138
|
-
if (envelope.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version ${String(envelope.v)}`);
|
|
5139
|
-
if (envelope.alg !== ENVELOPE_ALG) throw new Error(`unsupported alg ${String(envelope.alg)}`);
|
|
5140
|
-
const rec = envelope.recipients.find((r) => r.keyId === myKeyId);
|
|
5141
|
-
if (!rec) throw new Error("not a recipient of this envelope");
|
|
5142
|
-
const cek = import_tweetnacl3.default.box.open(fromB64(rec.wrap), fromB64(rec.wnonce), fromB64(rec.epk), fromB64(mySecretKeyB64));
|
|
5143
|
-
if (!cek) throw new Error("CEK unwrap failed (wrong key or tampered)");
|
|
5144
|
-
const padded = import_tweetnacl3.default.secretbox.open(fromB64(envelope.ct), fromB64(envelope.nonce), cek);
|
|
5145
|
-
if (!padded) throw new Error("body authentication failed (tampered ciphertext)");
|
|
5146
|
-
if (padded.length < 8) throw new Error("malformed sealed plaintext");
|
|
5147
|
-
const canonLen = readU32be(padded, 0);
|
|
5148
|
-
if (canonLen < 0 || 8 + canonLen > padded.length) throw new Error("malformed sealed plaintext");
|
|
5149
|
-
const sealedCanon = padded.slice(4, 4 + canonLen);
|
|
5150
|
-
const bodyLen = readU32be(padded, 4 + canonLen);
|
|
5151
|
-
const bodyStart = 8 + canonLen;
|
|
5152
|
-
if (bodyStart + bodyLen > padded.length) throw new Error("malformed sealed plaintext");
|
|
5153
|
-
const body = padded.slice(bodyStart, bodyStart + bodyLen);
|
|
5154
|
-
const expected = canonicalHeader(fromB64(envelope.msgId), envelope.hdr);
|
|
5155
|
-
if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
|
|
5156
|
-
return { header: envelope.hdr, body };
|
|
5157
|
-
}
|
|
5158
5135
|
var SAS_DOMAIN = "paigy-sas-v2";
|
|
5159
5136
|
function deriveSAS(input, bits = 24) {
|
|
5160
5137
|
const bundle = concat([
|
|
@@ -5192,28 +5169,6 @@ function sessionSlot(sessionId2) {
|
|
|
5192
5169
|
const id = sessionId2 ?? sessionId();
|
|
5193
5170
|
return `session:${id.slice(0, 8)}`;
|
|
5194
5171
|
}
|
|
5195
|
-
function subagentId(env = process.env) {
|
|
5196
|
-
if (!env.CLAUDE_CODE_CHILD_SESSION) return null;
|
|
5197
|
-
const pid = env.CLAUDE_PID ?? env.CLAUDE_CODE_MESSAGING_SOCKET?.match(/(\d+)\.sock$/)?.[1];
|
|
5198
|
-
return pid ?? null;
|
|
5199
|
-
}
|
|
5200
|
-
function subagentThread(env = process.env, sessionId2) {
|
|
5201
|
-
const base = sessionId2 ?? sessionId();
|
|
5202
|
-
const sub = subagentId(env);
|
|
5203
|
-
return sub ? `${base}:sub:${sub}` : base;
|
|
5204
|
-
}
|
|
5205
|
-
function uuidFromKey(key) {
|
|
5206
|
-
const h = createHash2("sha1").update(`paigy-thread:${key}`).digest();
|
|
5207
|
-
h[6] = h[6] & 15 | 80;
|
|
5208
|
-
h[8] = h[8] & 63 | 128;
|
|
5209
|
-
const hex = h.subarray(0, 16).toString("hex");
|
|
5210
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
5211
|
-
}
|
|
5212
|
-
function contactParentId(explicit, env = process.env) {
|
|
5213
|
-
if (explicit) return explicit;
|
|
5214
|
-
if (!subagentId(env)) return void 0;
|
|
5215
|
-
return uuidFromKey(subagentThread(env));
|
|
5216
|
-
}
|
|
5217
5172
|
var AGENT_NAME = process.env.PAIGY_AGENT || sessionSlot();
|
|
5218
5173
|
var PROTO = "paigy-pair-v2|sas=24";
|
|
5219
5174
|
var SAS_BITS = 24;
|
|
@@ -5404,45 +5359,6 @@ async function pairStep(deviceCode, keyFile) {
|
|
|
5404
5359
|
return { kind: "e2ee_aborted", warning: e.message };
|
|
5405
5360
|
}
|
|
5406
5361
|
}
|
|
5407
|
-
async function fetchRoster(token) {
|
|
5408
|
-
const res = await reach(`${BACKEND_URL}/api/roster`, { headers: { authorization: `Bearer ${token}` } });
|
|
5409
|
-
if (!res.ok) return null;
|
|
5410
|
-
return await res.json().catch(() => null);
|
|
5411
|
-
}
|
|
5412
|
-
function verifiedRecipients(roster, pinnedUikPub) {
|
|
5413
|
-
return roster.devices.filter((cred) => verifyDeviceCredential(cred, pinnedUikPub)).map((cred) => cred.x25519Pub);
|
|
5414
|
-
}
|
|
5415
|
-
function sealFields(content, recipients) {
|
|
5416
|
-
if (recipients.length === 0) throw new Error("sealFields: no verified recipients");
|
|
5417
|
-
const seq = Math.floor(Date.now() / 1e3);
|
|
5418
|
-
const out = {};
|
|
5419
|
-
for (const field of ["context", "options", "visuals"]) {
|
|
5420
|
-
if (content[field] === void 0) continue;
|
|
5421
|
-
out[field] = seal({
|
|
5422
|
-
body: utf8ToBytes(JSON.stringify(content[field])),
|
|
5423
|
-
field,
|
|
5424
|
-
senderRole: "agent",
|
|
5425
|
-
seq,
|
|
5426
|
-
recipients
|
|
5427
|
-
});
|
|
5428
|
-
}
|
|
5429
|
-
return out;
|
|
5430
|
-
}
|
|
5431
|
-
function openSealedAnswer(sealed, keypair) {
|
|
5432
|
-
const myKeyId = keyId(keypair.x25519.publicKey);
|
|
5433
|
-
const { header, body } = open(sealed.envelope, myKeyId, keypair.x25519.secretKey);
|
|
5434
|
-
if (header.field !== "answer" || header.senderRole !== "user") {
|
|
5435
|
-
throw new Error("sealed answer has the wrong envelope field/role \u2014 refusing (possible replay/misattribution)");
|
|
5436
|
-
}
|
|
5437
|
-
const parsed = UserAnswerSchema.safeParse(JSON.parse(bytesToUtf8(body)));
|
|
5438
|
-
if (!parsed.success) throw new Error("sealed answer body is not a valid UserAnswer");
|
|
5439
|
-
const answer = parsed.data;
|
|
5440
|
-
const bodyIsIgnored = answer.kind === "ignored";
|
|
5441
|
-
if (sealed.ignored !== bodyIsIgnored) {
|
|
5442
|
-
throw new Error("sealed answer tamper: `ignored` hint disagrees with the decrypted body");
|
|
5443
|
-
}
|
|
5444
|
-
return answer;
|
|
5445
|
-
}
|
|
5446
5362
|
var UnpairedError = class extends Error {
|
|
5447
5363
|
constructor() {
|
|
5448
5364
|
super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
|
|
@@ -5453,150 +5369,69 @@ function ensureAuthed(res) {
|
|
|
5453
5369
|
if (res.status === 401) throw new UnpairedError();
|
|
5454
5370
|
return res;
|
|
5455
5371
|
}
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
throw new Error("No roster device verified against the pinned identity key \u2014 refusing to send plaintext (a server-injected device is dropped).");
|
|
5372
|
+
var ApiError = class extends Error {
|
|
5373
|
+
status;
|
|
5374
|
+
body;
|
|
5375
|
+
constructor(what, status, text) {
|
|
5376
|
+
super(`${what} failed: ${status} ${text}`);
|
|
5377
|
+
this.name = "ApiError";
|
|
5378
|
+
this.status = status;
|
|
5379
|
+
let body = void 0;
|
|
5380
|
+
try {
|
|
5381
|
+
body = JSON.parse(text);
|
|
5382
|
+
} catch {
|
|
5383
|
+
}
|
|
5384
|
+
this.body = body;
|
|
5470
5385
|
}
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
...req.branch !== void 0 ? { branch: req.branch } : {}
|
|
5475
|
-
};
|
|
5476
|
-
const envelope = sealFields({ context: sealedContext, options: req.options, visuals: req.visuals }, recipients);
|
|
5477
|
-
const { context: _c, options: _o, visuals: _v, repo: _r, branch: _b, ...meta } = req;
|
|
5478
|
-
return { ...meta, envelope };
|
|
5386
|
+
};
|
|
5387
|
+
async function fail(what, res) {
|
|
5388
|
+
throw new ApiError(what, res.status, await res.text());
|
|
5479
5389
|
}
|
|
5480
5390
|
async function createGoal(input, opts = {}) {
|
|
5481
5391
|
const token = authToken(opts.token) ?? "";
|
|
5482
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/goals`, {
|
|
5392
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
5483
5393
|
method: "POST",
|
|
5484
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}
|
|
5394
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
|
|
5485
5395
|
body: JSON.stringify(input)
|
|
5486
5396
|
}));
|
|
5487
|
-
if (!res.ok)
|
|
5397
|
+
if (!res.ok) await fail("create_goal", res);
|
|
5488
5398
|
return await res.json();
|
|
5489
5399
|
}
|
|
5490
5400
|
async function claimGoal(goalId, opts = {}) {
|
|
5491
5401
|
const token = authToken(opts.token) ?? "";
|
|
5492
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/goals
|
|
5493
|
-
if (!res.ok)
|
|
5402
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
5403
|
+
if (!res.ok) await fail("claim_goal", res);
|
|
5494
5404
|
return await res.json();
|
|
5495
5405
|
}
|
|
5496
|
-
async function
|
|
5406
|
+
async function getGoal(goalId, opts = {}) {
|
|
5497
5407
|
const token = authToken(opts.token) ?? "";
|
|
5498
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
|
|
5499
|
-
|
|
5408
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
|
|
5409
|
+
method: "GET",
|
|
5410
|
+
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
5411
|
+
}));
|
|
5412
|
+
if (!res.ok) await fail("get_goal", res);
|
|
5500
5413
|
return await res.json();
|
|
5501
5414
|
}
|
|
5502
|
-
async function
|
|
5415
|
+
async function updateGoal(goalId, input, opts = {}) {
|
|
5503
5416
|
const token = authToken(opts.token) ?? "";
|
|
5504
|
-
const
|
|
5505
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/
|
|
5506
|
-
|
|
5507
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
5508
|
-
body: JSON.stringify(body)
|
|
5509
|
-
}));
|
|
5510
|
-
if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
|
|
5417
|
+
const operationId = input.operationId ?? randomUUID2();
|
|
5418
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
5419
|
+
if (!res.ok) await fail("update_goal", res);
|
|
5511
5420
|
return await res.json();
|
|
5512
5421
|
}
|
|
5513
|
-
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5514
5422
|
var AWAIT_WINDOW_MS = 45e3;
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
const token = authToken();
|
|
5519
|
-
const after = _partialSeen.get(notificationId) ?? 0;
|
|
5520
|
-
try {
|
|
5521
|
-
const res = await reach(
|
|
5522
|
-
`${BACKEND_URL}/api/await/partials?notificationId=${encodeURIComponent(notificationId)}&afterIdx=${after}`,
|
|
5523
|
-
{ headers: { authorization: `Bearer ${token}` } }
|
|
5524
|
-
);
|
|
5525
|
-
if (!res.ok) return null;
|
|
5526
|
-
const body = await res.json();
|
|
5527
|
-
const next = body.turns?.[0];
|
|
5528
|
-
if (!next) return null;
|
|
5529
|
-
_partialSeen.set(notificationId, next.idx);
|
|
5530
|
-
return { type: "partial", notificationId, inFlight: true, turn: next };
|
|
5531
|
-
} catch {
|
|
5532
|
-
return null;
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
|
-
async function awaitReply(notificationId, opts) {
|
|
5536
|
-
const intervalMs = opts.intervalMs ?? 5e3;
|
|
5537
|
-
const windowMs = opts.windowMs ?? AWAIT_WINDOW_MS;
|
|
5538
|
-
const doSleep = opts.sleep ?? sleep2;
|
|
5539
|
-
const now = opts.now ?? Date.now;
|
|
5540
|
-
const read = opts.read;
|
|
5541
|
-
const start = now();
|
|
5542
|
-
let sawLive = false;
|
|
5543
|
-
let quietSince = null;
|
|
5544
|
-
while (true) {
|
|
5545
|
-
if (opts.signal?.aborted) return { type: "idle" };
|
|
5546
|
-
try {
|
|
5547
|
-
const item = await read(notificationId);
|
|
5548
|
-
if (item.type !== "idle") return item;
|
|
5549
|
-
if (item.inFlight === true) {
|
|
5550
|
-
sawLive = true;
|
|
5551
|
-
quietSince = null;
|
|
5552
|
-
} else if (item.inFlight === false) {
|
|
5553
|
-
if (!sawLive) return { type: "idle", inFlight: false };
|
|
5554
|
-
quietSince ??= now();
|
|
5555
|
-
if (now() - quietSince >= NO_CALL_GRACE_MS) return { type: "idle", inFlight: false };
|
|
5556
|
-
}
|
|
5557
|
-
if (item.inFlight !== false) {
|
|
5558
|
-
const partial = await pollPartials(notificationId);
|
|
5559
|
-
if (partial) return partial;
|
|
5560
|
-
}
|
|
5561
|
-
} catch (e) {
|
|
5562
|
-
if (!isNetworkError(e)) throw e;
|
|
5563
|
-
}
|
|
5564
|
-
if (now() - start >= windowMs) return { type: "idle" };
|
|
5565
|
-
await doSleep(intervalMs);
|
|
5566
|
-
}
|
|
5567
|
-
}
|
|
5568
|
-
async function getThread(parentId) {
|
|
5569
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/thread/${encodeURIComponent(parentId)}`, {
|
|
5570
|
-
headers: { authorization: `Bearer ${authToken()}` }
|
|
5571
|
-
}));
|
|
5572
|
-
if (!res.ok) throw new Error(`get_thread failed: ${res.status} ${await res.text()}`);
|
|
5573
|
-
return await res.json();
|
|
5574
|
-
}
|
|
5575
|
-
async function searchThreads(q) {
|
|
5576
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/search?q=${encodeURIComponent(q)}`, {
|
|
5577
|
-
headers: { authorization: `Bearer ${authToken()}` }
|
|
5578
|
-
}));
|
|
5579
|
-
if (!res.ok) throw new Error(`search_threads failed: ${res.status} ${await res.text()}`);
|
|
5580
|
-
return await res.json();
|
|
5581
|
-
}
|
|
5582
|
-
async function sweepLeased(opts = {}) {
|
|
5583
|
-
const token = authToken(opts.token);
|
|
5584
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
|
|
5585
|
-
headers: {
|
|
5586
|
-
authorization: `Bearer ${token}`,
|
|
5587
|
-
"x-paigy-await-ack": "1"
|
|
5588
|
-
}
|
|
5423
|
+
async function getThread(parentId, opts = {}) {
|
|
5424
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/thread/${encodeURIComponent(parentId)}`, {
|
|
5425
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
5589
5426
|
}));
|
|
5590
|
-
if (!res.ok)
|
|
5427
|
+
if (!res.ok) await fail("get_thread", res);
|
|
5591
5428
|
return await res.json();
|
|
5592
5429
|
}
|
|
5593
|
-
async function
|
|
5594
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/
|
|
5595
|
-
|
|
5596
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
|
|
5597
|
-
body: JSON.stringify({ notificationId, answer })
|
|
5430
|
+
async function searchThreads(q, opts = {}) {
|
|
5431
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/search?q=${encodeURIComponent(q)}`, {
|
|
5432
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
5598
5433
|
}));
|
|
5599
|
-
if (!res.ok)
|
|
5434
|
+
if (!res.ok) await fail("search_threads", res);
|
|
5600
5435
|
return await res.json();
|
|
5601
5436
|
}
|
|
5602
5437
|
var tokenOverride = null;
|
|
@@ -5644,226 +5479,129 @@ async function heartbeat(runtime, opts = {}) {
|
|
|
5644
5479
|
}));
|
|
5645
5480
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
5646
5481
|
}
|
|
5647
|
-
async function registerDelivery(mode) {
|
|
5482
|
+
async function registerDelivery(mode, opts = {}) {
|
|
5648
5483
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
5649
5484
|
method: "POST",
|
|
5650
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
|
|
5485
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
5651
5486
|
body: JSON.stringify({ mode })
|
|
5652
5487
|
}));
|
|
5653
5488
|
if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
|
|
5654
5489
|
return await res.json();
|
|
5655
5490
|
}
|
|
5656
|
-
async function
|
|
5657
|
-
const
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
}
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5677
|
-
var ReplyLeaseExpiredError = class extends Error {
|
|
5678
|
-
constructor() {
|
|
5679
|
-
super(
|
|
5680
|
-
"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."
|
|
5681
|
-
);
|
|
5682
|
-
this.name = "ReplyLeaseExpiredError";
|
|
5683
|
-
}
|
|
5684
|
-
};
|
|
5685
|
-
var ReplyAckError = class extends Error {
|
|
5686
|
-
constructor(message, retryable) {
|
|
5687
|
-
super(message);
|
|
5688
|
-
this.retryable = retryable;
|
|
5689
|
-
this.name = "ReplyAckError";
|
|
5690
|
-
}
|
|
5691
|
-
retryable;
|
|
5692
|
-
};
|
|
5693
|
-
function decryptAnswer(reply, deps = {}) {
|
|
5694
|
-
if (!reply.sealed) return reply.answer;
|
|
5695
|
-
const keyFile = (deps.readKeyFile ?? readKeyFile)();
|
|
5696
|
-
if (!keyFile?.keypair) {
|
|
5697
|
-
throw new Error(
|
|
5698
|
-
"received a sealed answer but this agent has no device key to open it \u2014 re-pair."
|
|
5699
|
-
);
|
|
5491
|
+
async function contact(input, opts = {}) {
|
|
5492
|
+
const parsed = ContactSchema.parse(input);
|
|
5493
|
+
opts.signal?.throwIfAborted();
|
|
5494
|
+
const send = opts.reach ?? reach;
|
|
5495
|
+
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
|
|
5496
|
+
let deliveryId;
|
|
5497
|
+
if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
|
|
5498
|
+
else {
|
|
5499
|
+
if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
|
|
5500
|
+
const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
|
|
5501
|
+
const res = ensureAuthed(await send(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
|
|
5502
|
+
method: "POST",
|
|
5503
|
+
headers,
|
|
5504
|
+
signal: opts.signal,
|
|
5505
|
+
body: JSON.stringify({ operationId: opts.operationId ?? randomUUID3(), threadId: parsed.threadId ?? null, content: parsed.ask, select: shaped.select, options: shaped.options ?? [], blocking: parsed.waiting === "hard", channel: parsed.channel === "call" ? "call" : "message" })
|
|
5506
|
+
}));
|
|
5507
|
+
if (!res.ok) await fail("goal contact", res);
|
|
5508
|
+
const receipt = await res.json();
|
|
5509
|
+
deliveryId = receipt.deliveryId ?? receipt.callId ?? receipt.notificationId ?? "";
|
|
5510
|
+
if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
|
|
5700
5511
|
}
|
|
5701
|
-
|
|
5702
|
-
}
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
const
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
return null;
|
|
5714
|
-
}
|
|
5715
|
-
const delivery = item.delivery;
|
|
5716
|
-
if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
|
|
5717
|
-
throw new Error(
|
|
5718
|
-
"Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
|
|
5719
|
-
);
|
|
5720
|
-
}
|
|
5721
|
-
return delivery;
|
|
5722
|
-
}
|
|
5723
|
-
async function responseDetail(res) {
|
|
5512
|
+
const read = async (signal2) => {
|
|
5513
|
+
const res = ensureAuthed(await send(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
5514
|
+
if (!res.ok) await fail("read_delivery", res);
|
|
5515
|
+
const delivery = await res.json();
|
|
5516
|
+
return delivery.kind === "notification" ? { ...delivery, message: `${delivery.message}
|
|
5517
|
+
Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answers. Do not poll this Notification.` } : delivery;
|
|
5518
|
+
};
|
|
5519
|
+
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
5520
|
+
if (opts.waits === false) return read(opts.signal);
|
|
5521
|
+
const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
|
|
5522
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
|
|
5523
|
+
let latest;
|
|
5724
5524
|
try {
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
}
|
|
5730
|
-
async function acknowledge(token, leaseId, opts = {}) {
|
|
5731
|
-
const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
|
|
5732
|
-
const backoffMs = Math.max(0, opts.backoffMs ?? 100);
|
|
5733
|
-
const doSleep = opts.doSleep ?? sleep3;
|
|
5734
|
-
let lastError;
|
|
5735
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
5736
|
-
try {
|
|
5737
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await/ack`, {
|
|
5738
|
-
method: "POST",
|
|
5739
|
-
headers: {
|
|
5740
|
-
authorization: `Bearer ${token}`,
|
|
5741
|
-
"content-type": "application/json"
|
|
5742
|
-
},
|
|
5743
|
-
body: JSON.stringify({ leaseId })
|
|
5744
|
-
}));
|
|
5745
|
-
if (res.ok) return;
|
|
5746
|
-
const detail2 = await responseDetail(res);
|
|
5747
|
-
if (res.status === 409) throw new ReplyLeaseExpiredError();
|
|
5748
|
-
const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
|
|
5749
|
-
throw new ReplyAckError(
|
|
5750
|
-
`Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
|
|
5751
|
-
retryable
|
|
5752
|
-
);
|
|
5753
|
-
} catch (error) {
|
|
5754
|
-
if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
|
|
5755
|
-
throw error;
|
|
5756
|
-
}
|
|
5757
|
-
lastError = error;
|
|
5758
|
-
if (attempt + 1 < attempts) {
|
|
5759
|
-
await doSleep(backoffMs * 2 ** attempt);
|
|
5760
|
-
}
|
|
5525
|
+
while (true) {
|
|
5526
|
+
signal.throwIfAborted();
|
|
5527
|
+
latest = await read(signal);
|
|
5528
|
+
if (settled(latest)) return latest;
|
|
5529
|
+
await sleep2(5e3, void 0, { signal });
|
|
5761
5530
|
}
|
|
5531
|
+
} catch (error) {
|
|
5532
|
+
opts.signal?.throwIfAborted();
|
|
5533
|
+
if (window.aborted && latest) return latest;
|
|
5534
|
+
throw error;
|
|
5762
5535
|
}
|
|
5763
|
-
const detail = lastError instanceof Error ? lastError.message : String(lastError);
|
|
5764
|
-
throw new Error(
|
|
5765
|
-
`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.`
|
|
5766
|
-
);
|
|
5767
|
-
}
|
|
5768
|
-
async function ackReply(item, token, opts = {}) {
|
|
5769
|
-
const delivery = deliveryOf(item);
|
|
5770
|
-
if (!delivery) throw new Error("Paigy returned a reply without a delivery lease; nothing to acknowledge.");
|
|
5771
|
-
await acknowledge(token, delivery.leaseId, {
|
|
5772
|
-
attempts: opts.ackAttempts,
|
|
5773
|
-
backoffMs: opts.ackBackoffMs,
|
|
5774
|
-
doSleep: opts.ackSleep
|
|
5775
|
-
});
|
|
5776
|
-
}
|
|
5777
|
-
async function receiveReply(item, token, opts = {}) {
|
|
5778
|
-
const decoded = decodeReply(item, opts);
|
|
5779
|
-
const delivery = deliveryOf(item);
|
|
5780
|
-
if (!delivery) {
|
|
5781
|
-
if (opts.requireDelivery) {
|
|
5782
|
-
throw new Error("Paigy returned a catch-up reply without a delivery lease; the answer was not exposed.");
|
|
5783
|
-
}
|
|
5784
|
-
return decoded;
|
|
5785
|
-
}
|
|
5786
|
-
await acknowledge(token, delivery.leaseId, {
|
|
5787
|
-
attempts: opts.ackAttempts,
|
|
5788
|
-
backoffMs: opts.ackBackoffMs,
|
|
5789
|
-
doSleep: opts.ackSleep
|
|
5790
|
-
});
|
|
5791
|
-
return decoded;
|
|
5792
|
-
}
|
|
5793
|
-
async function pollAnswer(notificationId, opts = {}) {
|
|
5794
|
-
const token = authToken() ?? "";
|
|
5795
|
-
const res = ensureAuthed(await reach(
|
|
5796
|
-
`${BACKEND_URL}/api/await?notificationId=${encodeURIComponent(notificationId)}`,
|
|
5797
|
-
{
|
|
5798
|
-
headers: {
|
|
5799
|
-
authorization: `Bearer ${token}`,
|
|
5800
|
-
"x-paigy-await-ack": "1"
|
|
5801
|
-
}
|
|
5802
|
-
}
|
|
5803
|
-
));
|
|
5804
|
-
if (!res.ok) throw new Error(`await failed: ${res.status} ${await res.text()}`);
|
|
5805
|
-
const wire = await res.json();
|
|
5806
|
-
if (wire.type !== "reply") return wire;
|
|
5807
|
-
return receiveReply(wire, token, opts);
|
|
5808
|
-
}
|
|
5809
|
-
async function awaitReply2(notificationId, opts = {}) {
|
|
5810
|
-
return awaitReply(notificationId, { ...opts, read: pollAnswer });
|
|
5811
5536
|
}
|
|
5812
5537
|
async function checkReplies(opts = {}) {
|
|
5813
|
-
const
|
|
5814
|
-
|
|
5815
|
-
const replies = await Promise.all(pending.replies.map(async (reply) => {
|
|
5816
|
-
const received = await receiveReply(
|
|
5817
|
-
{ type: "reply", ...reply },
|
|
5818
|
-
token,
|
|
5819
|
-
{ requireDelivery: true }
|
|
5820
|
-
);
|
|
5821
|
-
const { type: _type, ...withoutType } = received;
|
|
5822
|
-
return withoutType;
|
|
5538
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
5539
|
+
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
5823
5540
|
}));
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
replies,
|
|
5827
|
-
work: pending.work ?? []
|
|
5828
|
-
};
|
|
5541
|
+
if (!res.ok) await fail("check_replies", res);
|
|
5542
|
+
return await res.json();
|
|
5829
5543
|
}
|
|
5830
|
-
async function
|
|
5831
|
-
const
|
|
5832
|
-
const
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
}
|
|
5841
|
-
|
|
5544
|
+
async function runTool(name, args, opts) {
|
|
5545
|
+
const { waits, signal, ...client } = opts;
|
|
5546
|
+
const input = args ?? {};
|
|
5547
|
+
switch (name) {
|
|
5548
|
+
case "contact":
|
|
5549
|
+
return contact(input, { ...client, signal, waits });
|
|
5550
|
+
case "check_replies":
|
|
5551
|
+
CheckRepliesSchema.parse(input);
|
|
5552
|
+
return checkReplies(client);
|
|
5553
|
+
case "get_thread": {
|
|
5554
|
+
const { parentId } = GetThreadSchema.parse(input);
|
|
5555
|
+
return getThread(parentId, client);
|
|
5556
|
+
}
|
|
5557
|
+
case "search_threads": {
|
|
5558
|
+
const { q } = SearchThreadsSchema.parse(input);
|
|
5559
|
+
return searchThreads(q, client);
|
|
5560
|
+
}
|
|
5561
|
+
case "create_goal": {
|
|
5562
|
+
const goal = CreateGoalToolSchema.parse(input);
|
|
5563
|
+
return createGoal({ ...goal, idempotencyKey: goal.idempotencyKey ?? randomUUID4() }, client);
|
|
5564
|
+
}
|
|
5565
|
+
case "claim_goal": {
|
|
5566
|
+
const { goalId } = ClaimGoalSchema.parse(input);
|
|
5567
|
+
return claimGoal(goalId, client);
|
|
5568
|
+
}
|
|
5569
|
+
case "get_goal": {
|
|
5570
|
+
const { goalId } = GetGoalSchema.parse(input);
|
|
5571
|
+
return getGoal(goalId, client);
|
|
5572
|
+
}
|
|
5573
|
+
case "update_goal": {
|
|
5574
|
+
const { goalId, ...body } = UpdateGoalToolSchema.parse(input);
|
|
5575
|
+
return updateGoal(goalId, body, client);
|
|
5842
5576
|
}
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
throw new Error(`${operation} failed: ${res.status} ${await res.text()}`);
|
|
5577
|
+
default:
|
|
5578
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
5846
5579
|
}
|
|
5847
|
-
return await res.json();
|
|
5848
5580
|
}
|
|
5849
|
-
function
|
|
5850
|
-
|
|
5851
|
-
}
|
|
5852
|
-
|
|
5853
|
-
const
|
|
5854
|
-
const
|
|
5855
|
-
|
|
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
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
};
|
|
5856
5598
|
}
|
|
5857
5599
|
|
|
5858
5600
|
export {
|
|
5859
5601
|
BACKEND_URL,
|
|
5860
5602
|
isNetworkError,
|
|
5861
5603
|
reach,
|
|
5862
|
-
NotifyRequestSchema,
|
|
5863
|
-
SetWorkStateSchema,
|
|
5864
|
-
ScheduleCallbackSchema,
|
|
5865
5604
|
sessionSlot,
|
|
5866
|
-
contactParentId,
|
|
5867
5605
|
AGENT_NAME,
|
|
5868
5606
|
TOKEN_PATH,
|
|
5869
5607
|
saveToken,
|
|
@@ -5882,27 +5620,13 @@ export {
|
|
|
5882
5620
|
fetchCredential,
|
|
5883
5621
|
pairStep,
|
|
5884
5622
|
UnpairedError,
|
|
5885
|
-
|
|
5886
|
-
claimGoal,
|
|
5887
|
-
updateGoal,
|
|
5888
|
-
submitNotification,
|
|
5889
|
-
getThread,
|
|
5890
|
-
searchThreads,
|
|
5891
|
-
sweepLeased,
|
|
5892
|
-
answerCallerQuestion,
|
|
5623
|
+
getGoal,
|
|
5893
5624
|
overrideToken,
|
|
5894
|
-
authToken,
|
|
5895
5625
|
hatch,
|
|
5896
5626
|
whoAmI,
|
|
5897
5627
|
setIdentity,
|
|
5898
5628
|
heartbeat,
|
|
5899
|
-
registerDelivery,
|
|
5900
|
-
scheduleCallback,
|
|
5901
|
-
handoff,
|
|
5902
|
-
decodeReply,
|
|
5903
|
-
ackReply,
|
|
5904
|
-
awaitReply2,
|
|
5905
5629
|
checkReplies,
|
|
5906
|
-
|
|
5907
|
-
|
|
5630
|
+
runTool,
|
|
5631
|
+
subscribeWake
|
|
5908
5632
|
};
|