@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.
Files changed (3) hide show
  1. package/dist/cli.js +251 -18
  2. package/dist/main.js +251 -18
  3. 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 WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. 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.";
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 || runs.has(key)) continue;
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: WAKE_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 WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. 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.";
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 || runs.has(key)) continue;
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: WAKE_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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",