@paigy/harness 0.3.1 → 0.3.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/dist/cli.js +251 -18
- package/dist/main.js +251 -18
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -29626,6 +29626,12 @@ var ReceiptEventSchema = external_exports.enum([
|
|
|
29626
29626
|
// the recipient opened it
|
|
29627
29627
|
"answered",
|
|
29628
29628
|
// the recipient replied
|
|
29629
|
+
// The recipient TURNED THE RING DOWN — CallKit ended it and no answer was ever tapped.
|
|
29630
|
+
// Written by the phone, on the same door that reports the ring itself, so it exists only
|
|
29631
|
+
// when a ring reached a running app and a person did not take it. That is what separates
|
|
29632
|
+
// it from "a ring with no answer", which our own crashes wrote just as readily and which
|
|
29633
|
+
// is why the responsiveness back-off had to be removed (#1144).
|
|
29634
|
+
"declined",
|
|
29629
29635
|
"escalated",
|
|
29630
29636
|
// re-reached at a higher level (re-ring / promote)
|
|
29631
29637
|
"coalesced",
|
|
@@ -29634,8 +29640,14 @@ var ReceiptEventSchema = external_exports.enum([
|
|
|
29634
29640
|
// deadline passed unanswered
|
|
29635
29641
|
"woke",
|
|
29636
29642
|
// the agent was woken for an owed obligation (callback)
|
|
29637
|
-
"gave_up"
|
|
29643
|
+
"gave_up",
|
|
29638
29644
|
// the budget was spent — stopped re-engaging
|
|
29645
|
+
// The ladder starts over — a silent pickup (the owner's fresh-miss rule, 2026-07-28), a
|
|
29646
|
+
// promote to call, a re-delivery. APPENDED, never a rewind: `ring_step` was a cache of
|
|
29647
|
+
// the escalated-count and a writer rewound it to 0 on every unanswered call (2026-08-24,
|
|
29648
|
+
// nine rings in an hour, `gaveUp` unreachable). A count since the last `restarted` cannot
|
|
29649
|
+
// be rewound by a writer that forgot to advance it.
|
|
29650
|
+
"restarted"
|
|
29639
29651
|
]);
|
|
29640
29652
|
var AttentionSchema = external_exports.object({
|
|
29641
29653
|
urgency: NotifyLevelSchema,
|
|
@@ -29880,6 +29892,14 @@ var IntentSchema = external_exports.object({
|
|
|
29880
29892
|
* about what cannot answer "is the bot looping less this week?". */
|
|
29881
29893
|
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
29882
29894
|
});
|
|
29895
|
+
var RideAlongSchema = external_exports.object({
|
|
29896
|
+
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
29897
|
+
noteId: external_exports.string(),
|
|
29898
|
+
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
29899
|
+
text: external_exports.string(),
|
|
29900
|
+
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
29901
|
+
parentId: external_exports.string().nullable()
|
|
29902
|
+
});
|
|
29883
29903
|
var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
29884
29904
|
external_exports.object({
|
|
29885
29905
|
type: external_exports.literal("reply"),
|
|
@@ -29900,7 +29920,13 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
29900
29920
|
transcript: external_exports.string().optional(),
|
|
29901
29921
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
29902
29922
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
29903
|
-
covered: external_exports.array(external_exports.string()).optional()
|
|
29923
|
+
covered: external_exports.array(external_exports.string()).optional(),
|
|
29924
|
+
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
29925
|
+
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
29926
|
+
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
29927
|
+
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
29928
|
+
* dropped. Absent/empty = nothing owed. */
|
|
29929
|
+
also: external_exports.array(RideAlongSchema).optional()
|
|
29904
29930
|
}),
|
|
29905
29931
|
external_exports.object({
|
|
29906
29932
|
type: external_exports.literal("remind"),
|
|
@@ -29935,7 +29961,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
29935
29961
|
acts: external_exports.array(IntentSchema).nullable().optional()
|
|
29936
29962
|
})
|
|
29937
29963
|
}),
|
|
29938
|
-
external_exports.object({ type: external_exports.literal("idle") })
|
|
29964
|
+
external_exports.object({ type: external_exports.literal("idle"), also: external_exports.array(RideAlongSchema).optional() })
|
|
29939
29965
|
]);
|
|
29940
29966
|
var CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
29941
29967
|
var ScheduleCallbackSchema = external_exports.object({
|
|
@@ -29966,6 +29992,12 @@ var PendingRepliesSchema = external_exports.object({
|
|
|
29966
29992
|
pending: external_exports.array(
|
|
29967
29993
|
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
29968
29994
|
),
|
|
29995
|
+
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
29996
|
+
* sees for this session's identity. From inside a session there was no way to find out —
|
|
29997
|
+
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
29998
|
+
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
29999
|
+
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
30000
|
+
you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
|
|
29969
30001
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
29970
30002
|
* contact on the same parentId. Keeps reappearing until you call
|
|
29971
30003
|
* set_task_state on its notificationId. */
|
|
@@ -29977,12 +30009,21 @@ var PendingRepliesSchema = external_exports.object({
|
|
|
29977
30009
|
createdAt: external_exports.string(),
|
|
29978
30010
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
29979
30011
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
29980
|
-
contextParentId: external_exports.string().optional()
|
|
30012
|
+
contextParentId: external_exports.string().optional(),
|
|
30013
|
+
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
30014
|
+
* the account — the name here — which has not been seen since it landed, so nobody
|
|
30015
|
+
* came for it. Handed to you because you are the session that is here. Take it like
|
|
30016
|
+
* any request (set_task_state claims it, reply with contact on its parentId), and say
|
|
30017
|
+
* whose it was, because the user chose that agent on purpose. */
|
|
30018
|
+
stranded: external_exports.string().optional()
|
|
29981
30019
|
})
|
|
29982
30020
|
),
|
|
29983
30021
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
29984
30022
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
29985
30023
|
* fulfill one by calling contact on its parentId. */
|
|
30024
|
+
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
30025
|
+
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
30026
|
+
also: external_exports.array(RideAlongSchema).optional(),
|
|
29986
30027
|
owedCallbacks: external_exports.array(
|
|
29987
30028
|
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema, note: external_exports.string() })
|
|
29988
30029
|
),
|
|
@@ -30018,7 +30059,11 @@ var NotifyResponseSchema = external_exports.object({
|
|
|
30018
30059
|
status: NotifyStatusSchema,
|
|
30019
30060
|
createdAt: external_exports.string().datetime(),
|
|
30020
30061
|
answer: UserAnswerSchema.optional(),
|
|
30021
|
-
answeredAt: external_exports.string().datetime().optional()
|
|
30062
|
+
answeredAt: external_exports.string().datetime().optional(),
|
|
30063
|
+
/** Ride-alongs for THIS agent — pending work it should pick up when it's done with
|
|
30064
|
+
* what it came for. Present on any reply, because an unwakeable agent's only
|
|
30065
|
+
* reliable moment is one it initiated. Absent/empty = nothing owed. */
|
|
30066
|
+
also: external_exports.array(RideAlongSchema).optional()
|
|
30022
30067
|
});
|
|
30023
30068
|
var NotifyPlanUnitSchema = external_exports.object({
|
|
30024
30069
|
notificationId: external_exports.string(),
|
|
@@ -30070,6 +30115,9 @@ var UserResponseSchema = external_exports.object({
|
|
|
30070
30115
|
});
|
|
30071
30116
|
var VoiceKeySchema = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
30072
30117
|
var AgendaTurnSchema = external_exports.object({
|
|
30118
|
+
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
30119
|
+
* the planner declares duplicates instead of asking them twice. */
|
|
30120
|
+
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
30073
30121
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
30074
30122
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
30075
30123
|
info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
|
|
@@ -30103,6 +30151,7 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
30103
30151
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
30104
30152
|
blocking: external_exports.boolean().optional()
|
|
30105
30153
|
});
|
|
30154
|
+
var CLAIM_STALE_MS = 30 * 6e4;
|
|
30106
30155
|
var InboxItemSchema = external_exports.object({
|
|
30107
30156
|
id: external_exports.string(),
|
|
30108
30157
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -30122,6 +30171,15 @@ var InboxItemSchema = external_exports.object({
|
|
|
30122
30171
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
30123
30172
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
30124
30173
|
seq: external_exports.number().int().optional(),
|
|
30174
|
+
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
30175
|
+
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
30176
|
+
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
30177
|
+
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
30178
|
+
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
30179
|
+
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
30180
|
+
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
30181
|
+
* the one part of its own arrival a single row cannot answer. */
|
|
30182
|
+
units: external_exports.number().int().positive().optional(),
|
|
30125
30183
|
tokenId: external_exports.string().optional(),
|
|
30126
30184
|
status: NotifyStatusSchema,
|
|
30127
30185
|
context: ContextSchema,
|
|
@@ -30142,6 +30200,16 @@ var InboxItemSchema = external_exports.object({
|
|
|
30142
30200
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
30143
30201
|
* which must never CLAIM stalled. */
|
|
30144
30202
|
lastSeenAt: external_exports.string().optional(),
|
|
30203
|
+
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
30204
|
+
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
30205
|
+
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
30206
|
+
* than `createdAt`: a claim is very often picked up long after the row was born — the
|
|
30207
|
+
* inbox keeps an ANSWERED row visible while the agent works the follow-up, so a question
|
|
30208
|
+
* asked this morning and claimed a minute ago is eight hours old and one minute into its
|
|
30209
|
+
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
30210
|
+
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
30211
|
+
* `createdAt`. */
|
|
30212
|
+
agentStateAt: external_exports.string().datetime().optional(),
|
|
30145
30213
|
agenda: external_exports.array(AgendaTurnSchema).optional(),
|
|
30146
30214
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
30147
30215
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -30213,11 +30281,23 @@ var SnoozeRequestSchema = external_exports.object({
|
|
|
30213
30281
|
requestId: external_exports.string(),
|
|
30214
30282
|
until: external_exports.string().datetime()
|
|
30215
30283
|
});
|
|
30284
|
+
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
30216
30285
|
var PushTokenSchema = external_exports.object({
|
|
30217
30286
|
voipToken: external_exports.string().min(1).optional(),
|
|
30218
30287
|
alertToken: external_exports.string().min(1).optional(),
|
|
30219
30288
|
fcmToken: external_exports.string().min(1).optional(),
|
|
30220
30289
|
platform: external_exports.enum(["ios", "android"])
|
|
30290
|
+
}).superRefine((v, ctx) => {
|
|
30291
|
+
if (v.platform !== "ios") return;
|
|
30292
|
+
for (const field of ["voipToken", "alertToken"]) {
|
|
30293
|
+
const token = v[field];
|
|
30294
|
+
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
30295
|
+
ctx.addIssue({
|
|
30296
|
+
code: external_exports.ZodIssueCode.custom,
|
|
30297
|
+
path: [field],
|
|
30298
|
+
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
30299
|
+
});
|
|
30300
|
+
}
|
|
30221
30301
|
});
|
|
30222
30302
|
var MissedCallSchema = external_exports.enum([
|
|
30223
30303
|
"retry_10m",
|
|
@@ -30268,6 +30348,15 @@ var UserSettingsSchema = external_exports.object({
|
|
|
30268
30348
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
30269
30349
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
30270
30350
|
pstnCalls: external_exports.boolean().optional(),
|
|
30351
|
+
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
30352
|
+
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
30353
|
+
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
30354
|
+
* the same reason `voiceMode` is (a stale client PATCHing the whole object must not
|
|
30355
|
+
* clobber it) and one more: a GUESSED timezone schedules reminders hours off, and
|
|
30356
|
+
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
30357
|
+
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
30358
|
+
* call — honest about what we know. */
|
|
30359
|
+
timezone: external_exports.string().min(1).max(64).optional(),
|
|
30271
30360
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
30272
30361
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
30273
30362
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -30408,7 +30497,8 @@ var HandoffSchema = external_exports.object({
|
|
|
30408
30497
|
recap: external_exports.boolean().optional()
|
|
30409
30498
|
});
|
|
30410
30499
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
30411
|
-
var NoteStatusSchema = external_exports.enum(["open", "assigned", "done"]);
|
|
30500
|
+
var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
30501
|
+
var NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
|
|
30412
30502
|
var DecisionSchema = external_exports.object({
|
|
30413
30503
|
id: external_exports.string(),
|
|
30414
30504
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
@@ -30433,11 +30523,29 @@ var NoteSchema = external_exports.object({
|
|
|
30433
30523
|
assignee: external_exports.string().nullable(),
|
|
30434
30524
|
/** The request thread minted at assignment; null until assigned. */
|
|
30435
30525
|
parentId: external_exports.string().nullable(),
|
|
30526
|
+
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
30527
|
+
* call — never a deadline, and nothing rings when it passes. Null = "the very next
|
|
30528
|
+
* call", the right reading of "remind me to…" with no time attached. */
|
|
30529
|
+
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
30530
|
+
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
30531
|
+
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
30532
|
+
dueAt: external_exports.string().nullable().default(null),
|
|
30533
|
+
repeat: NoteRepeatSchema.default("once"),
|
|
30534
|
+
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
30535
|
+
rides: external_exports.number().int().default(0),
|
|
30536
|
+
lastRideAt: external_exports.string().nullable().default(null),
|
|
30436
30537
|
createdAt: external_exports.string()
|
|
30437
30538
|
});
|
|
30438
30539
|
var CreateNoteSchema = external_exports.object({
|
|
30439
30540
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
30440
|
-
text: external_exports.string().min(1).max(4e3)
|
|
30541
|
+
text: external_exports.string().min(1).max(4e3),
|
|
30542
|
+
/** Capture it as a REMINDER — a note assigned to the user themselves, which rides
|
|
30543
|
+
* their next call instead of being handed to an agent. Everything else about the
|
|
30544
|
+
* note is identical; this is the one parameter that separates the two. */
|
|
30545
|
+
forMe: external_exports.boolean().optional(),
|
|
30546
|
+
/** The not-before, when the user already said one. Absent = the very next call. */
|
|
30547
|
+
dueAt: external_exports.string().datetime().optional(),
|
|
30548
|
+
repeat: NoteRepeatSchema.optional()
|
|
30441
30549
|
});
|
|
30442
30550
|
var RecordDecisionSchema = external_exports.object({
|
|
30443
30551
|
/** An open decision (from /clarify) to answer. */
|
|
@@ -31255,6 +31363,12 @@ var ReceiptEventSchema2 = external_exports.enum([
|
|
|
31255
31363
|
// the recipient opened it
|
|
31256
31364
|
"answered",
|
|
31257
31365
|
// the recipient replied
|
|
31366
|
+
// The recipient TURNED THE RING DOWN — CallKit ended it and no answer was ever tapped.
|
|
31367
|
+
// Written by the phone, on the same door that reports the ring itself, so it exists only
|
|
31368
|
+
// when a ring reached a running app and a person did not take it. That is what separates
|
|
31369
|
+
// it from "a ring with no answer", which our own crashes wrote just as readily and which
|
|
31370
|
+
// is why the responsiveness back-off had to be removed (#1144).
|
|
31371
|
+
"declined",
|
|
31258
31372
|
"escalated",
|
|
31259
31373
|
// re-reached at a higher level (re-ring / promote)
|
|
31260
31374
|
"coalesced",
|
|
@@ -31263,8 +31377,14 @@ var ReceiptEventSchema2 = external_exports.enum([
|
|
|
31263
31377
|
// deadline passed unanswered
|
|
31264
31378
|
"woke",
|
|
31265
31379
|
// the agent was woken for an owed obligation (callback)
|
|
31266
|
-
"gave_up"
|
|
31380
|
+
"gave_up",
|
|
31267
31381
|
// the budget was spent — stopped re-engaging
|
|
31382
|
+
// The ladder starts over — a silent pickup (the owner's fresh-miss rule, 2026-07-28), a
|
|
31383
|
+
// promote to call, a re-delivery. APPENDED, never a rewind: `ring_step` was a cache of
|
|
31384
|
+
// the escalated-count and a writer rewound it to 0 on every unanswered call (2026-08-24,
|
|
31385
|
+
// nine rings in an hour, `gaveUp` unreachable). A count since the last `restarted` cannot
|
|
31386
|
+
// be rewound by a writer that forgot to advance it.
|
|
31387
|
+
"restarted"
|
|
31268
31388
|
]);
|
|
31269
31389
|
var AttentionSchema2 = external_exports.object({
|
|
31270
31390
|
urgency: NotifyLevelSchema2,
|
|
@@ -31471,6 +31591,14 @@ var IntentSchema2 = external_exports.object({
|
|
|
31471
31591
|
* about what cannot answer "is the bot looping less this week?". */
|
|
31472
31592
|
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
31473
31593
|
});
|
|
31594
|
+
var RideAlongSchema2 = external_exports.object({
|
|
31595
|
+
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
31596
|
+
noteId: external_exports.string(),
|
|
31597
|
+
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
31598
|
+
text: external_exports.string(),
|
|
31599
|
+
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
31600
|
+
parentId: external_exports.string().nullable()
|
|
31601
|
+
});
|
|
31474
31602
|
var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
31475
31603
|
external_exports.object({
|
|
31476
31604
|
type: external_exports.literal("reply"),
|
|
@@ -31491,7 +31619,13 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
31491
31619
|
transcript: external_exports.string().optional(),
|
|
31492
31620
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
31493
31621
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
31494
|
-
covered: external_exports.array(external_exports.string()).optional()
|
|
31622
|
+
covered: external_exports.array(external_exports.string()).optional(),
|
|
31623
|
+
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
31624
|
+
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
31625
|
+
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
31626
|
+
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
31627
|
+
* dropped. Absent/empty = nothing owed. */
|
|
31628
|
+
also: external_exports.array(RideAlongSchema2).optional()
|
|
31495
31629
|
}),
|
|
31496
31630
|
external_exports.object({
|
|
31497
31631
|
type: external_exports.literal("remind"),
|
|
@@ -31526,7 +31660,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
31526
31660
|
acts: external_exports.array(IntentSchema2).nullable().optional()
|
|
31527
31661
|
})
|
|
31528
31662
|
}),
|
|
31529
|
-
external_exports.object({ type: external_exports.literal("idle") })
|
|
31663
|
+
external_exports.object({ type: external_exports.literal("idle"), also: external_exports.array(RideAlongSchema2).optional() })
|
|
31530
31664
|
]);
|
|
31531
31665
|
var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
31532
31666
|
var ScheduleCallbackSchema2 = external_exports.object({
|
|
@@ -31557,6 +31691,12 @@ var PendingRepliesSchema2 = external_exports.object({
|
|
|
31557
31691
|
pending: external_exports.array(
|
|
31558
31692
|
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
31559
31693
|
),
|
|
31694
|
+
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
31695
|
+
* sees for this session's identity. From inside a session there was no way to find out —
|
|
31696
|
+
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
31697
|
+
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
31698
|
+
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
31699
|
+
you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
|
|
31560
31700
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
31561
31701
|
* contact on the same parentId. Keeps reappearing until you call
|
|
31562
31702
|
* set_task_state on its notificationId. */
|
|
@@ -31568,12 +31708,21 @@ var PendingRepliesSchema2 = external_exports.object({
|
|
|
31568
31708
|
createdAt: external_exports.string(),
|
|
31569
31709
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
31570
31710
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
31571
|
-
contextParentId: external_exports.string().optional()
|
|
31711
|
+
contextParentId: external_exports.string().optional(),
|
|
31712
|
+
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
31713
|
+
* the account — the name here — which has not been seen since it landed, so nobody
|
|
31714
|
+
* came for it. Handed to you because you are the session that is here. Take it like
|
|
31715
|
+
* any request (set_task_state claims it, reply with contact on its parentId), and say
|
|
31716
|
+
* whose it was, because the user chose that agent on purpose. */
|
|
31717
|
+
stranded: external_exports.string().optional()
|
|
31572
31718
|
})
|
|
31573
31719
|
),
|
|
31574
31720
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
31575
31721
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
31576
31722
|
* fulfill one by calling contact on its parentId. */
|
|
31723
|
+
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
31724
|
+
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
31725
|
+
also: external_exports.array(RideAlongSchema2).optional(),
|
|
31577
31726
|
owedCallbacks: external_exports.array(
|
|
31578
31727
|
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema2, note: external_exports.string() })
|
|
31579
31728
|
),
|
|
@@ -31609,7 +31758,11 @@ var NotifyResponseSchema2 = external_exports.object({
|
|
|
31609
31758
|
status: NotifyStatusSchema2,
|
|
31610
31759
|
createdAt: external_exports.string().datetime(),
|
|
31611
31760
|
answer: UserAnswerSchema2.optional(),
|
|
31612
|
-
answeredAt: external_exports.string().datetime().optional()
|
|
31761
|
+
answeredAt: external_exports.string().datetime().optional(),
|
|
31762
|
+
/** Ride-alongs for THIS agent — pending work it should pick up when it's done with
|
|
31763
|
+
* what it came for. Present on any reply, because an unwakeable agent's only
|
|
31764
|
+
* reliable moment is one it initiated. Absent/empty = nothing owed. */
|
|
31765
|
+
also: external_exports.array(RideAlongSchema2).optional()
|
|
31613
31766
|
});
|
|
31614
31767
|
var NotifyPlanUnitSchema2 = external_exports.object({
|
|
31615
31768
|
notificationId: external_exports.string(),
|
|
@@ -31661,6 +31814,9 @@ var UserResponseSchema2 = external_exports.object({
|
|
|
31661
31814
|
});
|
|
31662
31815
|
var VoiceKeySchema2 = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
31663
31816
|
var AgendaTurnSchema2 = external_exports.object({
|
|
31817
|
+
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
31818
|
+
* the planner declares duplicates instead of asking them twice. */
|
|
31819
|
+
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
31664
31820
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
31665
31821
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
31666
31822
|
info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
|
|
@@ -31694,6 +31850,7 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
31694
31850
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
31695
31851
|
blocking: external_exports.boolean().optional()
|
|
31696
31852
|
});
|
|
31853
|
+
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
31697
31854
|
var InboxItemSchema2 = external_exports.object({
|
|
31698
31855
|
id: external_exports.string(),
|
|
31699
31856
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -31713,6 +31870,15 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31713
31870
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
31714
31871
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
31715
31872
|
seq: external_exports.number().int().optional(),
|
|
31873
|
+
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
31874
|
+
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
31875
|
+
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
31876
|
+
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
31877
|
+
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
31878
|
+
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
31879
|
+
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
31880
|
+
* the one part of its own arrival a single row cannot answer. */
|
|
31881
|
+
units: external_exports.number().int().positive().optional(),
|
|
31716
31882
|
tokenId: external_exports.string().optional(),
|
|
31717
31883
|
status: NotifyStatusSchema2,
|
|
31718
31884
|
context: ContextSchema2,
|
|
@@ -31733,6 +31899,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
31733
31899
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
31734
31900
|
* which must never CLAIM stalled. */
|
|
31735
31901
|
lastSeenAt: external_exports.string().optional(),
|
|
31902
|
+
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
31903
|
+
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
31904
|
+
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
31905
|
+
* than `createdAt`: a claim is very often picked up long after the row was born — the
|
|
31906
|
+
* inbox keeps an ANSWERED row visible while the agent works the follow-up, so a question
|
|
31907
|
+
* asked this morning and claimed a minute ago is eight hours old and one minute into its
|
|
31908
|
+
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
31909
|
+
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
31910
|
+
* `createdAt`. */
|
|
31911
|
+
agentStateAt: external_exports.string().datetime().optional(),
|
|
31736
31912
|
agenda: external_exports.array(AgendaTurnSchema2).optional(),
|
|
31737
31913
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
31738
31914
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -31804,11 +31980,23 @@ var SnoozeRequestSchema2 = external_exports.object({
|
|
|
31804
31980
|
requestId: external_exports.string(),
|
|
31805
31981
|
until: external_exports.string().datetime()
|
|
31806
31982
|
});
|
|
31983
|
+
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
31807
31984
|
var PushTokenSchema2 = external_exports.object({
|
|
31808
31985
|
voipToken: external_exports.string().min(1).optional(),
|
|
31809
31986
|
alertToken: external_exports.string().min(1).optional(),
|
|
31810
31987
|
fcmToken: external_exports.string().min(1).optional(),
|
|
31811
31988
|
platform: external_exports.enum(["ios", "android"])
|
|
31989
|
+
}).superRefine((v, ctx) => {
|
|
31990
|
+
if (v.platform !== "ios") return;
|
|
31991
|
+
for (const field of ["voipToken", "alertToken"]) {
|
|
31992
|
+
const token = v[field];
|
|
31993
|
+
if (token === void 0 || APNS_TOKEN_RE2.test(token)) continue;
|
|
31994
|
+
ctx.addIssue({
|
|
31995
|
+
code: external_exports.ZodIssueCode.custom,
|
|
31996
|
+
path: [field],
|
|
31997
|
+
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
31998
|
+
});
|
|
31999
|
+
}
|
|
31812
32000
|
});
|
|
31813
32001
|
var MissedCallSchema2 = external_exports.enum([
|
|
31814
32002
|
"retry_10m",
|
|
@@ -31859,6 +32047,15 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
31859
32047
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
31860
32048
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
31861
32049
|
pstnCalls: external_exports.boolean().optional(),
|
|
32050
|
+
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
32051
|
+
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
32052
|
+
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
32053
|
+
* the same reason `voiceMode` is (a stale client PATCHing the whole object must not
|
|
32054
|
+
* clobber it) and one more: a GUESSED timezone schedules reminders hours off, and
|
|
32055
|
+
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
32056
|
+
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
32057
|
+
* call — honest about what we know. */
|
|
32058
|
+
timezone: external_exports.string().min(1).max(64).optional(),
|
|
31862
32059
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
31863
32060
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
31864
32061
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -31999,7 +32196,8 @@ var HandoffSchema2 = external_exports.object({
|
|
|
31999
32196
|
recap: external_exports.boolean().optional()
|
|
32000
32197
|
});
|
|
32001
32198
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
32002
|
-
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "done"]);
|
|
32199
|
+
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
32200
|
+
var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
|
|
32003
32201
|
var DecisionSchema2 = external_exports.object({
|
|
32004
32202
|
id: external_exports.string(),
|
|
32005
32203
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
@@ -32024,11 +32222,29 @@ var NoteSchema2 = external_exports.object({
|
|
|
32024
32222
|
assignee: external_exports.string().nullable(),
|
|
32025
32223
|
/** The request thread minted at assignment; null until assigned. */
|
|
32026
32224
|
parentId: external_exports.string().nullable(),
|
|
32225
|
+
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
32226
|
+
* call — never a deadline, and nothing rings when it passes. Null = "the very next
|
|
32227
|
+
* call", the right reading of "remind me to…" with no time attached. */
|
|
32228
|
+
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
32229
|
+
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
32230
|
+
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
32231
|
+
dueAt: external_exports.string().nullable().default(null),
|
|
32232
|
+
repeat: NoteRepeatSchema2.default("once"),
|
|
32233
|
+
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
32234
|
+
rides: external_exports.number().int().default(0),
|
|
32235
|
+
lastRideAt: external_exports.string().nullable().default(null),
|
|
32027
32236
|
createdAt: external_exports.string()
|
|
32028
32237
|
});
|
|
32029
32238
|
var CreateNoteSchema2 = external_exports.object({
|
|
32030
32239
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
32031
|
-
text: external_exports.string().min(1).max(4e3)
|
|
32240
|
+
text: external_exports.string().min(1).max(4e3),
|
|
32241
|
+
/** Capture it as a REMINDER — a note assigned to the user themselves, which rides
|
|
32242
|
+
* their next call instead of being handed to an agent. Everything else about the
|
|
32243
|
+
* note is identical; this is the one parameter that separates the two. */
|
|
32244
|
+
forMe: external_exports.boolean().optional(),
|
|
32245
|
+
/** The not-before, when the user already said one. Absent = the very next call. */
|
|
32246
|
+
dueAt: external_exports.string().datetime().optional(),
|
|
32247
|
+
repeat: NoteRepeatSchema2.optional()
|
|
32032
32248
|
});
|
|
32033
32249
|
var RecordDecisionSchema2 = external_exports.object({
|
|
32034
32250
|
/** An open decision (from /clarify) to answer. */
|
|
@@ -32962,6 +33178,12 @@ function resolveWakeDir(pinned, deps) {
|
|
|
32962
33178
|
|
|
32963
33179
|
// src/host.ts
|
|
32964
33180
|
var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
|
|
33181
|
+
var IDLE_WAKE_MS = 10 * 6e4;
|
|
33182
|
+
function wakeAction(live, seenAt, now) {
|
|
33183
|
+
if (!live) return "spawn";
|
|
33184
|
+
if (seenAt === void 0) return "skip";
|
|
33185
|
+
return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
|
|
33186
|
+
}
|
|
32965
33187
|
function startHost(opts) {
|
|
32966
33188
|
const runs = /* @__PURE__ */ new Map();
|
|
32967
33189
|
const asHost = { token: opts.token };
|
|
@@ -33017,12 +33239,15 @@ function startHost(opts) {
|
|
|
33017
33239
|
}
|
|
33018
33240
|
}
|
|
33019
33241
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
33020
|
-
const
|
|
33242
|
+
const wakePrompt = (fresh) => "You were woken because Paigy work is waiting for you. " + (fresh ? "This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. " : "You have been running, so some of this may already be yours \u2014 but not what arrived while you were idle. ") + "Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
|
|
33021
33243
|
async function sweepSlots() {
|
|
33022
33244
|
for (const slot of listSlots()) {
|
|
33023
33245
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
33024
33246
|
const key = `slot:${slot}`;
|
|
33025
|
-
if (!harness
|
|
33247
|
+
if (!harness) continue;
|
|
33248
|
+
const live = runs.get(key);
|
|
33249
|
+
const action = wakeAction(!!live, published.get(key)?.movedAt, Date.now());
|
|
33250
|
+
if (action === "skip") continue;
|
|
33026
33251
|
const token = readToken(slot);
|
|
33027
33252
|
if (!token) continue;
|
|
33028
33253
|
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
@@ -33030,12 +33255,20 @@ function startHost(opts) {
|
|
|
33030
33255
|
const work = await checkReplies({ token }).catch(() => null);
|
|
33031
33256
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
33032
33257
|
const label = slotName(slot) ?? slot;
|
|
33258
|
+
if (live) {
|
|
33259
|
+
const seen = published.get(key);
|
|
33260
|
+
const quiet = seen ? Math.round((Date.now() - seen.movedAt) / 6e4) : 0;
|
|
33261
|
+
live.run.send(wakePrompt(false));
|
|
33262
|
+
if (seen) seen.movedAt = Date.now();
|
|
33263
|
+
opts.log(`\u25B6 re-woke ${label} \u2014 quiet ${quiet}m with work waiting`);
|
|
33264
|
+
continue;
|
|
33265
|
+
}
|
|
33033
33266
|
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
33034
33267
|
const run = runHarness({
|
|
33035
33268
|
harness,
|
|
33036
33269
|
cwd: workspace,
|
|
33037
33270
|
mode: "bypass",
|
|
33038
|
-
prompt:
|
|
33271
|
+
prompt: wakePrompt(true),
|
|
33039
33272
|
exclusive: true,
|
|
33040
33273
|
token,
|
|
33041
33274
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
@@ -33063,7 +33296,7 @@ function startHost(opts) {
|
|
|
33063
33296
|
const lines = r.run.tail();
|
|
33064
33297
|
const was = published.get(key);
|
|
33065
33298
|
if (was && sameTail(was.lines, lines)) continue;
|
|
33066
|
-
published.set(key, { token: r.token, lines });
|
|
33299
|
+
published.set(key, { token: r.token, lines, movedAt: Date.now() });
|
|
33067
33300
|
publishTail(r.token, lines);
|
|
33068
33301
|
}
|
|
33069
33302
|
for (const [key, was] of published) {
|
package/dist/main.js
CHANGED
|
@@ -10959,6 +10959,12 @@ var ReceiptEventSchema = external_exports.enum([
|
|
|
10959
10959
|
// the recipient opened it
|
|
10960
10960
|
"answered",
|
|
10961
10961
|
// the recipient replied
|
|
10962
|
+
// The recipient TURNED THE RING DOWN — CallKit ended it and no answer was ever tapped.
|
|
10963
|
+
// Written by the phone, on the same door that reports the ring itself, so it exists only
|
|
10964
|
+
// when a ring reached a running app and a person did not take it. That is what separates
|
|
10965
|
+
// it from "a ring with no answer", which our own crashes wrote just as readily and which
|
|
10966
|
+
// is why the responsiveness back-off had to be removed (#1144).
|
|
10967
|
+
"declined",
|
|
10962
10968
|
"escalated",
|
|
10963
10969
|
// re-reached at a higher level (re-ring / promote)
|
|
10964
10970
|
"coalesced",
|
|
@@ -10967,8 +10973,14 @@ var ReceiptEventSchema = external_exports.enum([
|
|
|
10967
10973
|
// deadline passed unanswered
|
|
10968
10974
|
"woke",
|
|
10969
10975
|
// the agent was woken for an owed obligation (callback)
|
|
10970
|
-
"gave_up"
|
|
10976
|
+
"gave_up",
|
|
10971
10977
|
// the budget was spent — stopped re-engaging
|
|
10978
|
+
// The ladder starts over — a silent pickup (the owner's fresh-miss rule, 2026-07-28), a
|
|
10979
|
+
// promote to call, a re-delivery. APPENDED, never a rewind: `ring_step` was a cache of
|
|
10980
|
+
// the escalated-count and a writer rewound it to 0 on every unanswered call (2026-08-24,
|
|
10981
|
+
// nine rings in an hour, `gaveUp` unreachable). A count since the last `restarted` cannot
|
|
10982
|
+
// be rewound by a writer that forgot to advance it.
|
|
10983
|
+
"restarted"
|
|
10972
10984
|
]);
|
|
10973
10985
|
var AttentionSchema = external_exports.object({
|
|
10974
10986
|
urgency: NotifyLevelSchema,
|
|
@@ -11213,6 +11225,14 @@ var IntentSchema = external_exports.object({
|
|
|
11213
11225
|
* about what cannot answer "is the bot looping less this week?". */
|
|
11214
11226
|
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
11215
11227
|
});
|
|
11228
|
+
var RideAlongSchema = external_exports.object({
|
|
11229
|
+
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
11230
|
+
noteId: external_exports.string(),
|
|
11231
|
+
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
11232
|
+
text: external_exports.string(),
|
|
11233
|
+
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
11234
|
+
parentId: external_exports.string().nullable()
|
|
11235
|
+
});
|
|
11216
11236
|
var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
11217
11237
|
external_exports.object({
|
|
11218
11238
|
type: external_exports.literal("reply"),
|
|
@@ -11233,7 +11253,13 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
11233
11253
|
transcript: external_exports.string().optional(),
|
|
11234
11254
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
11235
11255
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
11236
|
-
covered: external_exports.array(external_exports.string()).optional()
|
|
11256
|
+
covered: external_exports.array(external_exports.string()).optional(),
|
|
11257
|
+
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
11258
|
+
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
11259
|
+
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
11260
|
+
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
11261
|
+
* dropped. Absent/empty = nothing owed. */
|
|
11262
|
+
also: external_exports.array(RideAlongSchema).optional()
|
|
11237
11263
|
}),
|
|
11238
11264
|
external_exports.object({
|
|
11239
11265
|
type: external_exports.literal("remind"),
|
|
@@ -11268,7 +11294,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
11268
11294
|
acts: external_exports.array(IntentSchema).nullable().optional()
|
|
11269
11295
|
})
|
|
11270
11296
|
}),
|
|
11271
|
-
external_exports.object({ type: external_exports.literal("idle") })
|
|
11297
|
+
external_exports.object({ type: external_exports.literal("idle"), also: external_exports.array(RideAlongSchema).optional() })
|
|
11272
11298
|
]);
|
|
11273
11299
|
var CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
11274
11300
|
var ScheduleCallbackSchema = external_exports.object({
|
|
@@ -11299,6 +11325,12 @@ var PendingRepliesSchema = external_exports.object({
|
|
|
11299
11325
|
pending: external_exports.array(
|
|
11300
11326
|
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
11301
11327
|
),
|
|
11328
|
+
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
11329
|
+
* sees for this session's identity. From inside a session there was no way to find out —
|
|
11330
|
+
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
11331
|
+
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
11332
|
+
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
11333
|
+
you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
|
|
11302
11334
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
11303
11335
|
* contact on the same parentId. Keeps reappearing until you call
|
|
11304
11336
|
* set_task_state on its notificationId. */
|
|
@@ -11310,12 +11342,21 @@ var PendingRepliesSchema = external_exports.object({
|
|
|
11310
11342
|
createdAt: external_exports.string(),
|
|
11311
11343
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
11312
11344
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
11313
|
-
contextParentId: external_exports.string().optional()
|
|
11345
|
+
contextParentId: external_exports.string().optional(),
|
|
11346
|
+
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
11347
|
+
* the account — the name here — which has not been seen since it landed, so nobody
|
|
11348
|
+
* came for it. Handed to you because you are the session that is here. Take it like
|
|
11349
|
+
* any request (set_task_state claims it, reply with contact on its parentId), and say
|
|
11350
|
+
* whose it was, because the user chose that agent on purpose. */
|
|
11351
|
+
stranded: external_exports.string().optional()
|
|
11314
11352
|
})
|
|
11315
11353
|
),
|
|
11316
11354
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
11317
11355
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
11318
11356
|
* fulfill one by calling contact on its parentId. */
|
|
11357
|
+
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
11358
|
+
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
11359
|
+
also: external_exports.array(RideAlongSchema).optional(),
|
|
11319
11360
|
owedCallbacks: external_exports.array(
|
|
11320
11361
|
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema, note: external_exports.string() })
|
|
11321
11362
|
),
|
|
@@ -11351,7 +11392,11 @@ var NotifyResponseSchema = external_exports.object({
|
|
|
11351
11392
|
status: NotifyStatusSchema,
|
|
11352
11393
|
createdAt: external_exports.string().datetime(),
|
|
11353
11394
|
answer: UserAnswerSchema.optional(),
|
|
11354
|
-
answeredAt: external_exports.string().datetime().optional()
|
|
11395
|
+
answeredAt: external_exports.string().datetime().optional(),
|
|
11396
|
+
/** Ride-alongs for THIS agent — pending work it should pick up when it's done with
|
|
11397
|
+
* what it came for. Present on any reply, because an unwakeable agent's only
|
|
11398
|
+
* reliable moment is one it initiated. Absent/empty = nothing owed. */
|
|
11399
|
+
also: external_exports.array(RideAlongSchema).optional()
|
|
11355
11400
|
});
|
|
11356
11401
|
var NotifyPlanUnitSchema = external_exports.object({
|
|
11357
11402
|
notificationId: external_exports.string(),
|
|
@@ -11403,6 +11448,9 @@ var UserResponseSchema = external_exports.object({
|
|
|
11403
11448
|
});
|
|
11404
11449
|
var VoiceKeySchema = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
11405
11450
|
var AgendaTurnSchema = external_exports.object({
|
|
11451
|
+
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
11452
|
+
* the planner declares duplicates instead of asking them twice. */
|
|
11453
|
+
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
11406
11454
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
11407
11455
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
11408
11456
|
info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
|
|
@@ -11436,6 +11484,7 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
11436
11484
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
11437
11485
|
blocking: external_exports.boolean().optional()
|
|
11438
11486
|
});
|
|
11487
|
+
var CLAIM_STALE_MS = 30 * 6e4;
|
|
11439
11488
|
var InboxItemSchema = external_exports.object({
|
|
11440
11489
|
id: external_exports.string(),
|
|
11441
11490
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -11455,6 +11504,15 @@ var InboxItemSchema = external_exports.object({
|
|
|
11455
11504
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
11456
11505
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
11457
11506
|
seq: external_exports.number().int().optional(),
|
|
11507
|
+
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
11508
|
+
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
11509
|
+
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
11510
|
+
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
11511
|
+
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
11512
|
+
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
11513
|
+
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
11514
|
+
* the one part of its own arrival a single row cannot answer. */
|
|
11515
|
+
units: external_exports.number().int().positive().optional(),
|
|
11458
11516
|
tokenId: external_exports.string().optional(),
|
|
11459
11517
|
status: NotifyStatusSchema,
|
|
11460
11518
|
context: ContextSchema,
|
|
@@ -11475,6 +11533,16 @@ var InboxItemSchema = external_exports.object({
|
|
|
11475
11533
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
11476
11534
|
* which must never CLAIM stalled. */
|
|
11477
11535
|
lastSeenAt: external_exports.string().optional(),
|
|
11536
|
+
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
11537
|
+
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
11538
|
+
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
11539
|
+
* than `createdAt`: a claim is very often picked up long after the row was born — the
|
|
11540
|
+
* inbox keeps an ANSWERED row visible while the agent works the follow-up, so a question
|
|
11541
|
+
* asked this morning and claimed a minute ago is eight hours old and one minute into its
|
|
11542
|
+
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
11543
|
+
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
11544
|
+
* `createdAt`. */
|
|
11545
|
+
agentStateAt: external_exports.string().datetime().optional(),
|
|
11478
11546
|
agenda: external_exports.array(AgendaTurnSchema).optional(),
|
|
11479
11547
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
11480
11548
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -11546,11 +11614,23 @@ var SnoozeRequestSchema = external_exports.object({
|
|
|
11546
11614
|
requestId: external_exports.string(),
|
|
11547
11615
|
until: external_exports.string().datetime()
|
|
11548
11616
|
});
|
|
11617
|
+
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
11549
11618
|
var PushTokenSchema = external_exports.object({
|
|
11550
11619
|
voipToken: external_exports.string().min(1).optional(),
|
|
11551
11620
|
alertToken: external_exports.string().min(1).optional(),
|
|
11552
11621
|
fcmToken: external_exports.string().min(1).optional(),
|
|
11553
11622
|
platform: external_exports.enum(["ios", "android"])
|
|
11623
|
+
}).superRefine((v, ctx) => {
|
|
11624
|
+
if (v.platform !== "ios") return;
|
|
11625
|
+
for (const field of ["voipToken", "alertToken"]) {
|
|
11626
|
+
const token = v[field];
|
|
11627
|
+
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
11628
|
+
ctx.addIssue({
|
|
11629
|
+
code: external_exports.ZodIssueCode.custom,
|
|
11630
|
+
path: [field],
|
|
11631
|
+
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
11632
|
+
});
|
|
11633
|
+
}
|
|
11554
11634
|
});
|
|
11555
11635
|
var MissedCallSchema = external_exports.enum([
|
|
11556
11636
|
"retry_10m",
|
|
@@ -11601,6 +11681,15 @@ var UserSettingsSchema = external_exports.object({
|
|
|
11601
11681
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
11602
11682
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
11603
11683
|
pstnCalls: external_exports.boolean().optional(),
|
|
11684
|
+
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
11685
|
+
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
11686
|
+
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
11687
|
+
* the same reason `voiceMode` is (a stale client PATCHing the whole object must not
|
|
11688
|
+
* clobber it) and one more: a GUESSED timezone schedules reminders hours off, and
|
|
11689
|
+
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
11690
|
+
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
11691
|
+
* call — honest about what we know. */
|
|
11692
|
+
timezone: external_exports.string().min(1).max(64).optional(),
|
|
11604
11693
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
11605
11694
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
11606
11695
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -11741,7 +11830,8 @@ var HandoffSchema = external_exports.object({
|
|
|
11741
11830
|
recap: external_exports.boolean().optional()
|
|
11742
11831
|
});
|
|
11743
11832
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
11744
|
-
var NoteStatusSchema = external_exports.enum(["open", "assigned", "done"]);
|
|
11833
|
+
var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
11834
|
+
var NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
|
|
11745
11835
|
var DecisionSchema = external_exports.object({
|
|
11746
11836
|
id: external_exports.string(),
|
|
11747
11837
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
@@ -11766,11 +11856,29 @@ var NoteSchema = external_exports.object({
|
|
|
11766
11856
|
assignee: external_exports.string().nullable(),
|
|
11767
11857
|
/** The request thread minted at assignment; null until assigned. */
|
|
11768
11858
|
parentId: external_exports.string().nullable(),
|
|
11859
|
+
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
11860
|
+
* call — never a deadline, and nothing rings when it passes. Null = "the very next
|
|
11861
|
+
* call", the right reading of "remind me to…" with no time attached. */
|
|
11862
|
+
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
11863
|
+
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
11864
|
+
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
11865
|
+
dueAt: external_exports.string().nullable().default(null),
|
|
11866
|
+
repeat: NoteRepeatSchema.default("once"),
|
|
11867
|
+
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
11868
|
+
rides: external_exports.number().int().default(0),
|
|
11869
|
+
lastRideAt: external_exports.string().nullable().default(null),
|
|
11769
11870
|
createdAt: external_exports.string()
|
|
11770
11871
|
});
|
|
11771
11872
|
var CreateNoteSchema = external_exports.object({
|
|
11772
11873
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
11773
|
-
text: external_exports.string().min(1).max(4e3)
|
|
11874
|
+
text: external_exports.string().min(1).max(4e3),
|
|
11875
|
+
/** Capture it as a REMINDER — a note assigned to the user themselves, which rides
|
|
11876
|
+
* their next call instead of being handed to an agent. Everything else about the
|
|
11877
|
+
* note is identical; this is the one parameter that separates the two. */
|
|
11878
|
+
forMe: external_exports.boolean().optional(),
|
|
11879
|
+
/** The not-before, when the user already said one. Absent = the very next call. */
|
|
11880
|
+
dueAt: external_exports.string().datetime().optional(),
|
|
11881
|
+
repeat: NoteRepeatSchema.optional()
|
|
11774
11882
|
});
|
|
11775
11883
|
var RecordDecisionSchema = external_exports.object({
|
|
11776
11884
|
/** An open decision (from /clarify) to answer. */
|
|
@@ -12708,6 +12816,12 @@ var ReceiptEventSchema2 = external_exports.enum([
|
|
|
12708
12816
|
// the recipient opened it
|
|
12709
12817
|
"answered",
|
|
12710
12818
|
// the recipient replied
|
|
12819
|
+
// The recipient TURNED THE RING DOWN — CallKit ended it and no answer was ever tapped.
|
|
12820
|
+
// Written by the phone, on the same door that reports the ring itself, so it exists only
|
|
12821
|
+
// when a ring reached a running app and a person did not take it. That is what separates
|
|
12822
|
+
// it from "a ring with no answer", which our own crashes wrote just as readily and which
|
|
12823
|
+
// is why the responsiveness back-off had to be removed (#1144).
|
|
12824
|
+
"declined",
|
|
12711
12825
|
"escalated",
|
|
12712
12826
|
// re-reached at a higher level (re-ring / promote)
|
|
12713
12827
|
"coalesced",
|
|
@@ -12716,8 +12830,14 @@ var ReceiptEventSchema2 = external_exports.enum([
|
|
|
12716
12830
|
// deadline passed unanswered
|
|
12717
12831
|
"woke",
|
|
12718
12832
|
// the agent was woken for an owed obligation (callback)
|
|
12719
|
-
"gave_up"
|
|
12833
|
+
"gave_up",
|
|
12720
12834
|
// the budget was spent — stopped re-engaging
|
|
12835
|
+
// The ladder starts over — a silent pickup (the owner's fresh-miss rule, 2026-07-28), a
|
|
12836
|
+
// promote to call, a re-delivery. APPENDED, never a rewind: `ring_step` was a cache of
|
|
12837
|
+
// the escalated-count and a writer rewound it to 0 on every unanswered call (2026-08-24,
|
|
12838
|
+
// nine rings in an hour, `gaveUp` unreachable). A count since the last `restarted` cannot
|
|
12839
|
+
// be rewound by a writer that forgot to advance it.
|
|
12840
|
+
"restarted"
|
|
12721
12841
|
]);
|
|
12722
12842
|
var AttentionSchema2 = external_exports.object({
|
|
12723
12843
|
urgency: NotifyLevelSchema2,
|
|
@@ -12924,6 +13044,14 @@ var IntentSchema2 = external_exports.object({
|
|
|
12924
13044
|
* about what cannot answer "is the bot looping less this week?". */
|
|
12925
13045
|
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
12926
13046
|
});
|
|
13047
|
+
var RideAlongSchema2 = external_exports.object({
|
|
13048
|
+
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
13049
|
+
noteId: external_exports.string(),
|
|
13050
|
+
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
13051
|
+
text: external_exports.string(),
|
|
13052
|
+
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
13053
|
+
parentId: external_exports.string().nullable()
|
|
13054
|
+
});
|
|
12927
13055
|
var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
12928
13056
|
external_exports.object({
|
|
12929
13057
|
type: external_exports.literal("reply"),
|
|
@@ -12944,7 +13072,13 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
12944
13072
|
transcript: external_exports.string().optional(),
|
|
12945
13073
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
12946
13074
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
12947
|
-
covered: external_exports.array(external_exports.string()).optional()
|
|
13075
|
+
covered: external_exports.array(external_exports.string()).optional(),
|
|
13076
|
+
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
13077
|
+
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
13078
|
+
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
13079
|
+
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
13080
|
+
* dropped. Absent/empty = nothing owed. */
|
|
13081
|
+
also: external_exports.array(RideAlongSchema2).optional()
|
|
12948
13082
|
}),
|
|
12949
13083
|
external_exports.object({
|
|
12950
13084
|
type: external_exports.literal("remind"),
|
|
@@ -12979,7 +13113,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
12979
13113
|
acts: external_exports.array(IntentSchema2).nullable().optional()
|
|
12980
13114
|
})
|
|
12981
13115
|
}),
|
|
12982
|
-
external_exports.object({ type: external_exports.literal("idle") })
|
|
13116
|
+
external_exports.object({ type: external_exports.literal("idle"), also: external_exports.array(RideAlongSchema2).optional() })
|
|
12983
13117
|
]);
|
|
12984
13118
|
var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
12985
13119
|
var ScheduleCallbackSchema2 = external_exports.object({
|
|
@@ -13010,6 +13144,12 @@ var PendingRepliesSchema2 = external_exports.object({
|
|
|
13010
13144
|
pending: external_exports.array(
|
|
13011
13145
|
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
13012
13146
|
),
|
|
13147
|
+
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
13148
|
+
* sees for this session's identity. From inside a session there was no way to find out —
|
|
13149
|
+
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
13150
|
+
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
13151
|
+
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
13152
|
+
you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
|
|
13013
13153
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
13014
13154
|
* contact on the same parentId. Keeps reappearing until you call
|
|
13015
13155
|
* set_task_state on its notificationId. */
|
|
@@ -13021,12 +13161,21 @@ var PendingRepliesSchema2 = external_exports.object({
|
|
|
13021
13161
|
createdAt: external_exports.string(),
|
|
13022
13162
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
13023
13163
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
13024
|
-
contextParentId: external_exports.string().optional()
|
|
13164
|
+
contextParentId: external_exports.string().optional(),
|
|
13165
|
+
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
13166
|
+
* the account — the name here — which has not been seen since it landed, so nobody
|
|
13167
|
+
* came for it. Handed to you because you are the session that is here. Take it like
|
|
13168
|
+
* any request (set_task_state claims it, reply with contact on its parentId), and say
|
|
13169
|
+
* whose it was, because the user chose that agent on purpose. */
|
|
13170
|
+
stranded: external_exports.string().optional()
|
|
13025
13171
|
})
|
|
13026
13172
|
),
|
|
13027
13173
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
13028
13174
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
13029
13175
|
* fulfill one by calling contact on its parentId. */
|
|
13176
|
+
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
13177
|
+
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
13178
|
+
also: external_exports.array(RideAlongSchema2).optional(),
|
|
13030
13179
|
owedCallbacks: external_exports.array(
|
|
13031
13180
|
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema2, note: external_exports.string() })
|
|
13032
13181
|
),
|
|
@@ -13062,7 +13211,11 @@ var NotifyResponseSchema2 = external_exports.object({
|
|
|
13062
13211
|
status: NotifyStatusSchema2,
|
|
13063
13212
|
createdAt: external_exports.string().datetime(),
|
|
13064
13213
|
answer: UserAnswerSchema2.optional(),
|
|
13065
|
-
answeredAt: external_exports.string().datetime().optional()
|
|
13214
|
+
answeredAt: external_exports.string().datetime().optional(),
|
|
13215
|
+
/** Ride-alongs for THIS agent — pending work it should pick up when it's done with
|
|
13216
|
+
* what it came for. Present on any reply, because an unwakeable agent's only
|
|
13217
|
+
* reliable moment is one it initiated. Absent/empty = nothing owed. */
|
|
13218
|
+
also: external_exports.array(RideAlongSchema2).optional()
|
|
13066
13219
|
});
|
|
13067
13220
|
var NotifyPlanUnitSchema2 = external_exports.object({
|
|
13068
13221
|
notificationId: external_exports.string(),
|
|
@@ -13114,6 +13267,9 @@ var UserResponseSchema2 = external_exports.object({
|
|
|
13114
13267
|
});
|
|
13115
13268
|
var VoiceKeySchema2 = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
13116
13269
|
var AgendaTurnSchema2 = external_exports.object({
|
|
13270
|
+
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
13271
|
+
* the planner declares duplicates instead of asking them twice. */
|
|
13272
|
+
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
13117
13273
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
13118
13274
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
13119
13275
|
info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
|
|
@@ -13147,6 +13303,7 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13147
13303
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
13148
13304
|
blocking: external_exports.boolean().optional()
|
|
13149
13305
|
});
|
|
13306
|
+
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
13150
13307
|
var InboxItemSchema2 = external_exports.object({
|
|
13151
13308
|
id: external_exports.string(),
|
|
13152
13309
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -13166,6 +13323,15 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13166
13323
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
13167
13324
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
13168
13325
|
seq: external_exports.number().int().optional(),
|
|
13326
|
+
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
13327
|
+
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
13328
|
+
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
13329
|
+
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
13330
|
+
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
13331
|
+
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
13332
|
+
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
13333
|
+
* the one part of its own arrival a single row cannot answer. */
|
|
13334
|
+
units: external_exports.number().int().positive().optional(),
|
|
13169
13335
|
tokenId: external_exports.string().optional(),
|
|
13170
13336
|
status: NotifyStatusSchema2,
|
|
13171
13337
|
context: ContextSchema2,
|
|
@@ -13186,6 +13352,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13186
13352
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
13187
13353
|
* which must never CLAIM stalled. */
|
|
13188
13354
|
lastSeenAt: external_exports.string().optional(),
|
|
13355
|
+
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
13356
|
+
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
13357
|
+
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
13358
|
+
* than `createdAt`: a claim is very often picked up long after the row was born — the
|
|
13359
|
+
* inbox keeps an ANSWERED row visible while the agent works the follow-up, so a question
|
|
13360
|
+
* asked this morning and claimed a minute ago is eight hours old and one minute into its
|
|
13361
|
+
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
13362
|
+
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
13363
|
+
* `createdAt`. */
|
|
13364
|
+
agentStateAt: external_exports.string().datetime().optional(),
|
|
13189
13365
|
agenda: external_exports.array(AgendaTurnSchema2).optional(),
|
|
13190
13366
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
13191
13367
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -13257,11 +13433,23 @@ var SnoozeRequestSchema2 = external_exports.object({
|
|
|
13257
13433
|
requestId: external_exports.string(),
|
|
13258
13434
|
until: external_exports.string().datetime()
|
|
13259
13435
|
});
|
|
13436
|
+
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
13260
13437
|
var PushTokenSchema2 = external_exports.object({
|
|
13261
13438
|
voipToken: external_exports.string().min(1).optional(),
|
|
13262
13439
|
alertToken: external_exports.string().min(1).optional(),
|
|
13263
13440
|
fcmToken: external_exports.string().min(1).optional(),
|
|
13264
13441
|
platform: external_exports.enum(["ios", "android"])
|
|
13442
|
+
}).superRefine((v, ctx) => {
|
|
13443
|
+
if (v.platform !== "ios") return;
|
|
13444
|
+
for (const field of ["voipToken", "alertToken"]) {
|
|
13445
|
+
const token = v[field];
|
|
13446
|
+
if (token === void 0 || APNS_TOKEN_RE2.test(token)) continue;
|
|
13447
|
+
ctx.addIssue({
|
|
13448
|
+
code: external_exports.ZodIssueCode.custom,
|
|
13449
|
+
path: [field],
|
|
13450
|
+
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
13451
|
+
});
|
|
13452
|
+
}
|
|
13265
13453
|
});
|
|
13266
13454
|
var MissedCallSchema2 = external_exports.enum([
|
|
13267
13455
|
"retry_10m",
|
|
@@ -13312,6 +13500,15 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13312
13500
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
13313
13501
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
13314
13502
|
pstnCalls: external_exports.boolean().optional(),
|
|
13503
|
+
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
13504
|
+
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
13505
|
+
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
13506
|
+
* the same reason `voiceMode` is (a stale client PATCHing the whole object must not
|
|
13507
|
+
* clobber it) and one more: a GUESSED timezone schedules reminders hours off, and
|
|
13508
|
+
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
13509
|
+
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
13510
|
+
* call — honest about what we know. */
|
|
13511
|
+
timezone: external_exports.string().min(1).max(64).optional(),
|
|
13315
13512
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
13316
13513
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
13317
13514
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -13452,7 +13649,8 @@ var HandoffSchema2 = external_exports.object({
|
|
|
13452
13649
|
recap: external_exports.boolean().optional()
|
|
13453
13650
|
});
|
|
13454
13651
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
13455
|
-
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "done"]);
|
|
13652
|
+
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
13653
|
+
var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
|
|
13456
13654
|
var DecisionSchema2 = external_exports.object({
|
|
13457
13655
|
id: external_exports.string(),
|
|
13458
13656
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
@@ -13477,11 +13675,29 @@ var NoteSchema2 = external_exports.object({
|
|
|
13477
13675
|
assignee: external_exports.string().nullable(),
|
|
13478
13676
|
/** The request thread minted at assignment; null until assigned. */
|
|
13479
13677
|
parentId: external_exports.string().nullable(),
|
|
13678
|
+
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
13679
|
+
* call — never a deadline, and nothing rings when it passes. Null = "the very next
|
|
13680
|
+
* call", the right reading of "remind me to…" with no time attached. */
|
|
13681
|
+
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
13682
|
+
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
13683
|
+
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
13684
|
+
dueAt: external_exports.string().nullable().default(null),
|
|
13685
|
+
repeat: NoteRepeatSchema2.default("once"),
|
|
13686
|
+
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
13687
|
+
rides: external_exports.number().int().default(0),
|
|
13688
|
+
lastRideAt: external_exports.string().nullable().default(null),
|
|
13480
13689
|
createdAt: external_exports.string()
|
|
13481
13690
|
});
|
|
13482
13691
|
var CreateNoteSchema2 = external_exports.object({
|
|
13483
13692
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
13484
|
-
text: external_exports.string().min(1).max(4e3)
|
|
13693
|
+
text: external_exports.string().min(1).max(4e3),
|
|
13694
|
+
/** Capture it as a REMINDER — a note assigned to the user themselves, which rides
|
|
13695
|
+
* their next call instead of being handed to an agent. Everything else about the
|
|
13696
|
+
* note is identical; this is the one parameter that separates the two. */
|
|
13697
|
+
forMe: external_exports.boolean().optional(),
|
|
13698
|
+
/** The not-before, when the user already said one. Absent = the very next call. */
|
|
13699
|
+
dueAt: external_exports.string().datetime().optional(),
|
|
13700
|
+
repeat: NoteRepeatSchema2.optional()
|
|
13485
13701
|
});
|
|
13486
13702
|
var RecordDecisionSchema2 = external_exports.object({
|
|
13487
13703
|
/** An open decision (from /clarify) to answer. */
|
|
@@ -14377,6 +14593,12 @@ function hostElsewhere() {
|
|
|
14377
14593
|
const s = readHostState();
|
|
14378
14594
|
return !!s && s.pid !== process.pid;
|
|
14379
14595
|
}
|
|
14596
|
+
var IDLE_WAKE_MS = 10 * 6e4;
|
|
14597
|
+
function wakeAction(live, seenAt, now) {
|
|
14598
|
+
if (!live) return "spawn";
|
|
14599
|
+
if (seenAt === void 0) return "skip";
|
|
14600
|
+
return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
|
|
14601
|
+
}
|
|
14380
14602
|
function startHost(opts) {
|
|
14381
14603
|
const runs = /* @__PURE__ */ new Map();
|
|
14382
14604
|
const asHost = { token: opts.token };
|
|
@@ -14432,12 +14654,15 @@ function startHost(opts) {
|
|
|
14432
14654
|
}
|
|
14433
14655
|
}
|
|
14434
14656
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
14435
|
-
const
|
|
14657
|
+
const wakePrompt = (fresh) => "You were woken because Paigy work is waiting for you. " + (fresh ? "This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. " : "You have been running, so some of this may already be yours \u2014 but not what arrived while you were idle. ") + "Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
|
|
14436
14658
|
async function sweepSlots() {
|
|
14437
14659
|
for (const slot of listSlots()) {
|
|
14438
14660
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
14439
14661
|
const key = `slot:${slot}`;
|
|
14440
|
-
if (!harness
|
|
14662
|
+
if (!harness) continue;
|
|
14663
|
+
const live = runs.get(key);
|
|
14664
|
+
const action = wakeAction(!!live, published.get(key)?.movedAt, Date.now());
|
|
14665
|
+
if (action === "skip") continue;
|
|
14441
14666
|
const token = readToken(slot);
|
|
14442
14667
|
if (!token) continue;
|
|
14443
14668
|
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
@@ -14445,12 +14670,20 @@ function startHost(opts) {
|
|
|
14445
14670
|
const work = await checkReplies({ token }).catch(() => null);
|
|
14446
14671
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
14447
14672
|
const label = slotName(slot) ?? slot;
|
|
14673
|
+
if (live) {
|
|
14674
|
+
const seen = published.get(key);
|
|
14675
|
+
const quiet = seen ? Math.round((Date.now() - seen.movedAt) / 6e4) : 0;
|
|
14676
|
+
live.run.send(wakePrompt(false));
|
|
14677
|
+
if (seen) seen.movedAt = Date.now();
|
|
14678
|
+
opts.log(`\u25B6 re-woke ${label} \u2014 quiet ${quiet}m with work waiting`);
|
|
14679
|
+
continue;
|
|
14680
|
+
}
|
|
14448
14681
|
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
14449
14682
|
const run = runHarness({
|
|
14450
14683
|
harness,
|
|
14451
14684
|
cwd: workspace,
|
|
14452
14685
|
mode: "bypass",
|
|
14453
|
-
prompt:
|
|
14686
|
+
prompt: wakePrompt(true),
|
|
14454
14687
|
exclusive: true,
|
|
14455
14688
|
token,
|
|
14456
14689
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
@@ -14478,7 +14711,7 @@ function startHost(opts) {
|
|
|
14478
14711
|
const lines = r.run.tail();
|
|
14479
14712
|
const was = published.get(key);
|
|
14480
14713
|
if (was && sameTail(was.lines, lines)) continue;
|
|
14481
|
-
published.set(key, { token: r.token, lines });
|
|
14714
|
+
published.set(key, { token: r.token, lines, movedAt: Date.now() });
|
|
14482
14715
|
publishTail(r.token, lines);
|
|
14483
14716
|
}
|
|
14484
14717
|
for (const [key, was] of published) {
|