@paigy/harness 0.3.0 → 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 +454 -86
- package/dist/main.js +833 -507
- package/package.json +13 -12
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,
|
|
@@ -11100,7 +11112,7 @@ var NotifyRequestSchema = external_exports.object({
|
|
|
11100
11112
|
if (r.ask !== void 0) {
|
|
11101
11113
|
for (const f of ["context", "select", "points"]) {
|
|
11102
11114
|
if (r[f] !== void 0)
|
|
11103
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it
|
|
11115
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
11104
11116
|
}
|
|
11105
11117
|
return;
|
|
11106
11118
|
}
|
|
@@ -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(),
|
|
@@ -11373,7 +11418,13 @@ var NotifyPlanUnitSchema = external_exports.object({
|
|
|
11373
11418
|
proposal: external_exports.object({
|
|
11374
11419
|
select: SelectShapeSchema,
|
|
11375
11420
|
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
11376
|
-
}).optional()
|
|
11421
|
+
}).optional(),
|
|
11422
|
+
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
11423
|
+
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
11424
|
+
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
11425
|
+
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
11426
|
+
* agent declared (#731, #923). */
|
|
11427
|
+
wants: external_exports.enum(["decision", "approval", "knowledge"]).optional()
|
|
11377
11428
|
});
|
|
11378
11429
|
var NotifyPlanSchema = external_exports.object({
|
|
11379
11430
|
units: external_exports.array(NotifyPlanUnitSchema),
|
|
@@ -11397,6 +11448,9 @@ var UserResponseSchema = external_exports.object({
|
|
|
11397
11448
|
});
|
|
11398
11449
|
var VoiceKeySchema = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
11399
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(),
|
|
11400
11454
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
11401
11455
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
11402
11456
|
info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
|
|
@@ -11430,6 +11484,7 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
11430
11484
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
11431
11485
|
blocking: external_exports.boolean().optional()
|
|
11432
11486
|
});
|
|
11487
|
+
var CLAIM_STALE_MS = 30 * 6e4;
|
|
11433
11488
|
var InboxItemSchema = external_exports.object({
|
|
11434
11489
|
id: external_exports.string(),
|
|
11435
11490
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -11449,6 +11504,15 @@ var InboxItemSchema = external_exports.object({
|
|
|
11449
11504
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
11450
11505
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
11451
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(),
|
|
11452
11516
|
tokenId: external_exports.string().optional(),
|
|
11453
11517
|
status: NotifyStatusSchema,
|
|
11454
11518
|
context: ContextSchema,
|
|
@@ -11469,6 +11533,16 @@ var InboxItemSchema = external_exports.object({
|
|
|
11469
11533
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
11470
11534
|
* which must never CLAIM stalled. */
|
|
11471
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(),
|
|
11472
11546
|
agenda: external_exports.array(AgendaTurnSchema).optional(),
|
|
11473
11547
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
11474
11548
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -11540,11 +11614,23 @@ var SnoozeRequestSchema = external_exports.object({
|
|
|
11540
11614
|
requestId: external_exports.string(),
|
|
11541
11615
|
until: external_exports.string().datetime()
|
|
11542
11616
|
});
|
|
11617
|
+
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
11543
11618
|
var PushTokenSchema = external_exports.object({
|
|
11544
11619
|
voipToken: external_exports.string().min(1).optional(),
|
|
11545
11620
|
alertToken: external_exports.string().min(1).optional(),
|
|
11546
11621
|
fcmToken: external_exports.string().min(1).optional(),
|
|
11547
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
|
+
}
|
|
11548
11634
|
});
|
|
11549
11635
|
var MissedCallSchema = external_exports.enum([
|
|
11550
11636
|
"retry_10m",
|
|
@@ -11595,6 +11681,15 @@ var UserSettingsSchema = external_exports.object({
|
|
|
11595
11681
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
11596
11682
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
11597
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(),
|
|
11598
11693
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
11599
11694
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
11600
11695
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -11620,6 +11715,15 @@ var HistoryItemSchema = external_exports.object({
|
|
|
11620
11715
|
/** When you answered the agent's notification (agent→user only). */
|
|
11621
11716
|
humanAckedAt: external_exports.string().nullable()
|
|
11622
11717
|
});
|
|
11718
|
+
var ACTIVITY_LINES = 2;
|
|
11719
|
+
var ACTIVITY_LINE_MAX = 80;
|
|
11720
|
+
var AgentActivitySchema = external_exports.object({
|
|
11721
|
+
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
11722
|
+
lines: external_exports.array(external_exports.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
11723
|
+
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
11724
|
+
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
11725
|
+
at: external_exports.string().datetime()
|
|
11726
|
+
});
|
|
11623
11727
|
var ConnectionSummarySchema = external_exports.object({
|
|
11624
11728
|
/** The connection = the agent's token id (used to address a request). */
|
|
11625
11729
|
id: external_exports.string(),
|
|
@@ -11653,6 +11757,11 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
11653
11757
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
11654
11758
|
workspaces: external_exports.array(external_exports.string()).optional()
|
|
11655
11759
|
}).optional(),
|
|
11760
|
+
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
11761
|
+
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
11762
|
+
* used straight from a terminal emits no work events; the page says so rather than
|
|
11763
|
+
* drawing an empty box). */
|
|
11764
|
+
activity: AgentActivitySchema.optional(),
|
|
11656
11765
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
11657
11766
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
11658
11767
|
managed: external_exports.boolean()
|
|
@@ -11721,7 +11830,8 @@ var HandoffSchema = external_exports.object({
|
|
|
11721
11830
|
recap: external_exports.boolean().optional()
|
|
11722
11831
|
});
|
|
11723
11832
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
11724
|
-
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"]);
|
|
11725
11835
|
var DecisionSchema = external_exports.object({
|
|
11726
11836
|
id: external_exports.string(),
|
|
11727
11837
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
@@ -11746,11 +11856,29 @@ var NoteSchema = external_exports.object({
|
|
|
11746
11856
|
assignee: external_exports.string().nullable(),
|
|
11747
11857
|
/** The request thread minted at assignment; null until assigned. */
|
|
11748
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),
|
|
11749
11870
|
createdAt: external_exports.string()
|
|
11750
11871
|
});
|
|
11751
11872
|
var CreateNoteSchema = external_exports.object({
|
|
11752
11873
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
11753
|
-
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()
|
|
11754
11882
|
});
|
|
11755
11883
|
var RecordDecisionSchema = external_exports.object({
|
|
11756
11884
|
/** An open decision (from /clarify) to answer. */
|
|
@@ -12475,10 +12603,14 @@ async function claimSessions(opts = {}) {
|
|
|
12475
12603
|
return (await res.json()).sessions;
|
|
12476
12604
|
}
|
|
12477
12605
|
async function heartbeat(runtime, opts = {}) {
|
|
12606
|
+
const body = {
|
|
12607
|
+
...runtime !== void 0 ? { runtime } : {},
|
|
12608
|
+
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
12609
|
+
};
|
|
12478
12610
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
12479
12611
|
method: "POST",
|
|
12480
12612
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
12481
|
-
...
|
|
12613
|
+
...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
|
|
12482
12614
|
}));
|
|
12483
12615
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
12484
12616
|
}
|
|
@@ -12632,497 +12764,212 @@ function resolveWakeDir(pinned, deps) {
|
|
|
12632
12764
|
return listWorkspaces(deps)[0];
|
|
12633
12765
|
}
|
|
12634
12766
|
|
|
12635
|
-
//
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
var
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
|
|
12808
|
-
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
const
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
|
|
12836
|
-
}
|
|
12837
|
-
this.pending.set(String(id), { id, options });
|
|
12838
|
-
return {
|
|
12839
|
-
events: [{ kind: "permission", id: String(id), tool, summary: title }],
|
|
12840
|
-
writes: []
|
|
12841
|
-
};
|
|
12842
|
-
}
|
|
12843
|
-
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
12844
|
-
flush() {
|
|
12845
|
-
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
12846
|
-
const blocks = this.queued.map((text) => ({ type: "text", text }));
|
|
12847
|
-
this.queued = [];
|
|
12848
|
-
this.promptId = this.nextId++;
|
|
12849
|
-
return [
|
|
12850
|
-
frame({
|
|
12851
|
-
id: this.promptId,
|
|
12852
|
-
method: "session/prompt",
|
|
12853
|
-
params: { sessionId: this.sessionId, prompt: blocks }
|
|
12854
|
-
})
|
|
12855
|
-
];
|
|
12856
|
-
}
|
|
12857
|
-
};
|
|
12858
|
-
var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
12859
|
-
|
|
12860
|
-
// src/harness/session.ts
|
|
12861
|
-
var ADAPTER_BIN = {
|
|
12862
|
-
claude: "claude-agent-acp",
|
|
12863
|
-
codex: "codex-acp",
|
|
12864
|
-
agy: "agy"
|
|
12865
|
-
};
|
|
12866
|
-
function splitLines(buffer, chunk) {
|
|
12867
|
-
const combined = buffer + chunk;
|
|
12868
|
-
const parts = combined.split("\n");
|
|
12869
|
-
const rest = parts.pop() ?? "";
|
|
12870
|
-
return { lines: parts.filter((l) => l.trim()), rest };
|
|
12871
|
-
}
|
|
12872
|
-
function startSession(opts) {
|
|
12873
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
12874
|
-
if (!existsSync4(cwd)) {
|
|
12875
|
-
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
12876
|
-
}
|
|
12877
|
-
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
12878
|
-
cwd,
|
|
12879
|
-
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
12880
|
-
// launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
|
|
12881
|
-
// node's own bin dir (nvm installs the adapters next to node; launchd's bare
|
|
12882
|
-
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
12883
|
-
env: {
|
|
12884
|
-
...process.env,
|
|
12885
|
-
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
12886
|
-
...opts.token ? { PAIGY_TOKEN: opts.token } : {}
|
|
12887
|
-
},
|
|
12888
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
12889
|
-
});
|
|
12890
|
-
const write = (frames) => {
|
|
12891
|
-
for (const f of frames) child.stdin?.write(`${f}
|
|
12892
|
-
`);
|
|
12893
|
-
};
|
|
12894
|
-
child.stderr?.on("data", (chunk) => {
|
|
12895
|
-
const text = String(chunk).trim();
|
|
12896
|
-
if (text) opts.onEvent({ kind: "error", message: text });
|
|
12897
|
-
});
|
|
12898
|
-
child.on("exit", (code) => {
|
|
12899
|
-
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
12900
|
-
opts.onExit?.();
|
|
12901
|
-
});
|
|
12902
|
-
child.on("error", (e) => {
|
|
12903
|
-
opts.onEvent({ kind: "error", message: e.message });
|
|
12904
|
-
opts.onExit?.();
|
|
12905
|
-
});
|
|
12906
|
-
const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
|
|
12907
|
-
let buffer = "";
|
|
12908
|
-
child.stdout?.on("data", (chunk) => {
|
|
12909
|
-
const { lines, rest } = splitLines(buffer, String(chunk));
|
|
12910
|
-
buffer = rest;
|
|
12911
|
-
for (const line of lines) {
|
|
12912
|
-
const { events, writes } = driver.handleLine(line);
|
|
12913
|
-
write(writes);
|
|
12914
|
-
for (const event of events) opts.onEvent(event);
|
|
12915
|
-
}
|
|
12916
|
-
});
|
|
12917
|
-
write(driver.open());
|
|
12918
|
-
return {
|
|
12919
|
-
send(text) {
|
|
12920
|
-
write(driver.send(text));
|
|
12921
|
-
},
|
|
12922
|
-
respond(id, decision) {
|
|
12923
|
-
write(driver.respond(id, decision));
|
|
12924
|
-
},
|
|
12925
|
-
stop() {
|
|
12926
|
-
write(driver.close());
|
|
12927
|
-
child.kill();
|
|
12928
|
-
}
|
|
12929
|
-
};
|
|
12930
|
-
}
|
|
12931
|
-
|
|
12932
|
-
// ../../packages/schema/dist/index.js
|
|
12933
|
-
var ContextSchema2 = external_exports.object({
|
|
12934
|
-
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
12935
|
-
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
12936
|
-
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
12937
|
-
)
|
|
12938
|
-
});
|
|
12939
|
-
var ParticipantSchema2 = external_exports.object({
|
|
12940
|
-
kind: external_exports.enum(["human", "agent"]),
|
|
12941
|
-
id: external_exports.string()
|
|
12942
|
-
});
|
|
12943
|
-
var TransformSchema2 = external_exports.enum([
|
|
12944
|
-
"structure",
|
|
12945
|
-
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
12946
|
-
"request_more",
|
|
12947
|
-
// clarify / follow-ups / uncovered points; escalate inbox→call — {kind:'clarify'}, escalate, blocking
|
|
12948
|
-
"redirect",
|
|
12949
|
-
// seed / hand off a thread to a new recipient — handoff, "new session from this"
|
|
12950
|
-
"break_down",
|
|
12951
|
-
// one bundle → many sub-asks — checklist fan-out, `points`
|
|
12952
|
-
"coalesce",
|
|
12953
|
-
// many bundles → one — morning triage (#347), threading-supersede, digest
|
|
12954
|
-
"organize",
|
|
12955
|
-
// group related bundles onto one thread — threading (`parentId`), parent/clarify links
|
|
12956
|
-
"summarize"
|
|
12957
|
-
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
12958
|
-
]);
|
|
12959
|
-
var OptionSchema2 = external_exports.object({
|
|
12960
|
-
id: external_exports.string(),
|
|
12961
|
-
label: external_exports.string(),
|
|
12962
|
-
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
12963
|
-
// the constraints below are what an agent reads when deciding to use these.
|
|
12964
|
-
html: external_exports.string().max(16384).describe(
|
|
12965
|
-
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
12966
|
-
).optional(),
|
|
12967
|
-
image: external_exports.string().url().describe(
|
|
12968
|
-
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
12969
|
-
).optional()
|
|
12970
|
-
});
|
|
12971
|
-
var VisualSchema2 = external_exports.object({
|
|
12972
|
-
url: external_exports.string().url(),
|
|
12973
|
-
label: external_exports.string().optional()
|
|
12974
|
-
});
|
|
12975
|
-
var NotifyLevelSchema2 = external_exports.enum(["inbox", "push", "banner", "call"]);
|
|
12976
|
-
var SelectShapeSchema2 = external_exports.enum(["one", "many", "rank", "confirm", "text"]);
|
|
12977
|
-
var ReceiptEventSchema2 = external_exports.enum([
|
|
12978
|
-
"delivered",
|
|
12979
|
-
// the bundle reached the recipient at some level
|
|
12980
|
-
"seen",
|
|
12981
|
-
// the recipient opened it
|
|
12982
|
-
"answered",
|
|
12983
|
-
// the recipient replied
|
|
12984
|
-
"escalated",
|
|
12985
|
-
// re-reached at a higher level (re-ring / promote)
|
|
12986
|
-
"coalesced",
|
|
12987
|
-
// merged into another live claim
|
|
12988
|
-
"expired",
|
|
12989
|
-
// deadline passed unanswered
|
|
12990
|
-
"woke",
|
|
12991
|
-
// the agent was woken for an owed obligation (callback)
|
|
12992
|
-
"gave_up"
|
|
12993
|
-
// the budget was spent — stopped re-engaging
|
|
12994
|
-
]);
|
|
12995
|
-
var AttentionSchema2 = external_exports.object({
|
|
12996
|
-
urgency: NotifyLevelSchema2,
|
|
12997
|
-
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
12998
|
-
select: SelectShapeSchema2.nullable(),
|
|
12999
|
-
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
13000
|
-
points: external_exports.array(external_exports.string()).nullable(),
|
|
13001
|
-
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
13002
|
-
blocking: external_exports.boolean(),
|
|
13003
|
-
/** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
13004
|
-
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
13005
|
-
deadline: external_exports.string().datetime().nullable().optional()
|
|
13006
|
-
});
|
|
13007
|
-
var NotifyRequestSchema2 = external_exports.object({
|
|
13008
|
-
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
13009
|
-
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
13010
|
-
* superRefine at the bottom enforces exactly one of the two. */
|
|
13011
|
-
context: ContextSchema2.optional(),
|
|
13012
|
-
options: external_exports.array(OptionSchema2.omit({ id: true })).min(1).optional().describe(
|
|
13013
|
-
"The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
|
|
13014
|
-
),
|
|
13015
|
-
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
13016
|
-
"The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
|
|
13017
|
-
),
|
|
13018
|
-
visuals: external_exports.array(VisualSchema2).optional().describe(
|
|
13019
|
-
"Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
|
|
13020
|
-
),
|
|
13021
|
-
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
13022
|
-
repo: external_exports.string().optional(),
|
|
13023
|
-
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
13024
|
-
branch: external_exports.string().optional(),
|
|
13025
|
-
/** Continue an existing conversation — the id of any notification in it (its root
|
|
13026
|
-
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
13027
|
-
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
13028
|
-
* still accepts the old name from older clients. */
|
|
13029
|
-
parentId: external_exports.string().uuid().optional(),
|
|
13030
|
-
urgency: NotifyLevelSchema2.default("inbox").describe(
|
|
13031
|
-
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
13032
|
-
),
|
|
13033
|
-
/** The request this one CLARIFIES — spawning a clarification keeps that parent
|
|
13034
|
-
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
13035
|
-
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
13036
|
-
* says HOW. */
|
|
13037
|
-
clarifies: external_exports.string().optional(),
|
|
13038
|
-
/** E2EE (text lane): when the pairing is E2EE, the sealed replacements for the
|
|
13039
|
-
* plaintext content fields, keyed by field name. FINALIZED wire shape (was
|
|
13040
|
-
* provisional in the storage PR): a per-field map `{ context?, options?,
|
|
13041
|
-
* visuals? }` of opaque Envelopes — one seal per present content field, so a
|
|
13042
|
-
* message with no options/visuals seals only `context`. It COEXISTS with the
|
|
13043
|
-
* plaintext fields by mutual exclusion: the superRefine below requires that
|
|
13044
|
-
* when `envelope` is present the plaintext `context`/`options`/`visuals` are
|
|
13045
|
-
* ABSENT (and vice-versa), so a row is either fully plaintext or fully sealed —
|
|
13046
|
-
* never a readable half. The server persists this OPAQUELY into
|
|
13047
|
-
* notifications.envelope and relays it blindly; it never decrypts. Absent =
|
|
13048
|
-
* today's plaintext path (context/options/visuals carry the cleartext).
|
|
13049
|
-
* z.lazy because EnvelopeSchema is declared further down (E2EE section). */
|
|
13050
|
-
envelope: external_exports.object({
|
|
13051
|
-
context: external_exports.lazy(() => EnvelopeSchema2).optional(),
|
|
13052
|
-
options: external_exports.lazy(() => EnvelopeSchema2).optional(),
|
|
13053
|
-
visuals: external_exports.lazy(() => EnvelopeSchema2).optional()
|
|
13054
|
-
}).optional(),
|
|
13055
|
-
select: SelectShapeSchema2.optional().describe(
|
|
13056
|
-
"How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
|
|
13057
|
-
),
|
|
13058
|
-
/** The simplified form (#395): instead of shaping the notification yourself
|
|
13059
|
-
* (context/select/options/urgency), state what you need to learn and why it
|
|
13060
|
-
* matters now — the broker derives the optimal shape and channel. Mutually
|
|
13061
|
-
* exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
|
|
13062
|
-
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
13063
|
-
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
13064
|
-
// the ask into topics and gives each one at most three sentences and one question
|
|
13065
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
13066
|
-
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
13067
|
-
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
13068
|
-
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
13069
|
-
'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
|
|
13070
|
-
),
|
|
13071
|
-
needs: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
13072
|
-
"With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
|
|
13073
|
-
),
|
|
13074
|
-
urgencyHint: external_exports.enum(["whenever", "soon", "now"]).optional().describe(
|
|
13075
|
-
"With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
|
|
13076
|
-
),
|
|
13077
|
-
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
13078
|
-
* to the agent's work while it waits. Normalized server-side into those two
|
|
13079
|
-
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
13080
|
-
* urgencyHint/blocking win when both are sent. */
|
|
13081
|
-
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
13082
|
-
"With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
|
|
13083
|
-
),
|
|
13084
|
-
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
13085
|
-
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
13086
|
-
* holding by default would charge every quiet claim that minute before any agent could
|
|
13087
|
-
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
13088
|
-
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
13089
|
-
confirm: external_exports.boolean().optional().describe(
|
|
13090
|
-
"Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
|
|
13091
|
-
),
|
|
13092
|
-
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
13093
|
-
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
13094
|
-
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
13095
|
-
* waiting:'hard'. */
|
|
13096
|
-
channel: external_exports.enum(["call", "message"]).optional().describe(
|
|
13097
|
-
"Only if the user explicitly said how to reach them \u2014 'call me' \u2192 'call', 'just message/text me' \u2192 'message'. Omit otherwise; Paigy picks."
|
|
13098
|
-
),
|
|
13099
|
-
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
13100
|
-
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
13101
|
-
),
|
|
13102
|
-
blocking: external_exports.boolean().default(false).describe(
|
|
13103
|
-
"Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
|
|
13104
|
-
)
|
|
13105
|
-
}).superRefine((r, ctx) => {
|
|
13106
|
-
const sealed = !!r.envelope;
|
|
13107
|
-
if (sealed) {
|
|
13108
|
-
if (!r.envelope?.context)
|
|
13109
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
|
|
13110
|
-
for (const f of ["context", "options", "visuals"]) {
|
|
13111
|
-
if (r[f] !== void 0)
|
|
13112
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
|
|
13113
|
-
}
|
|
13114
|
-
if (r.points !== void 0)
|
|
13115
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
|
|
13116
|
-
if (r.ask !== void 0)
|
|
13117
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
|
|
13118
|
-
if (r.needs !== void 0)
|
|
13119
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
|
|
13120
|
-
return;
|
|
12767
|
+
// ../../packages/schema/dist/index.js
|
|
12768
|
+
var ContextSchema2 = external_exports.object({
|
|
12769
|
+
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
12770
|
+
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
12771
|
+
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
12772
|
+
)
|
|
12773
|
+
});
|
|
12774
|
+
var ParticipantSchema2 = external_exports.object({
|
|
12775
|
+
kind: external_exports.enum(["human", "agent"]),
|
|
12776
|
+
id: external_exports.string()
|
|
12777
|
+
});
|
|
12778
|
+
var TransformSchema2 = external_exports.enum([
|
|
12779
|
+
"structure",
|
|
12780
|
+
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
12781
|
+
"request_more",
|
|
12782
|
+
// clarify / follow-ups / uncovered points; escalate inbox→call — {kind:'clarify'}, escalate, blocking
|
|
12783
|
+
"redirect",
|
|
12784
|
+
// seed / hand off a thread to a new recipient — handoff, "new session from this"
|
|
12785
|
+
"break_down",
|
|
12786
|
+
// one bundle → many sub-asks — checklist fan-out, `points`
|
|
12787
|
+
"coalesce",
|
|
12788
|
+
// many bundles → one — morning triage (#347), threading-supersede, digest
|
|
12789
|
+
"organize",
|
|
12790
|
+
// group related bundles onto one thread — threading (`parentId`), parent/clarify links
|
|
12791
|
+
"summarize"
|
|
12792
|
+
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
12793
|
+
]);
|
|
12794
|
+
var OptionSchema2 = external_exports.object({
|
|
12795
|
+
id: external_exports.string(),
|
|
12796
|
+
label: external_exports.string(),
|
|
12797
|
+
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
12798
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
12799
|
+
html: external_exports.string().max(16384).describe(
|
|
12800
|
+
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
12801
|
+
).optional(),
|
|
12802
|
+
image: external_exports.string().url().describe(
|
|
12803
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
12804
|
+
).optional()
|
|
12805
|
+
});
|
|
12806
|
+
var VisualSchema2 = external_exports.object({
|
|
12807
|
+
url: external_exports.string().url(),
|
|
12808
|
+
label: external_exports.string().optional()
|
|
12809
|
+
});
|
|
12810
|
+
var NotifyLevelSchema2 = external_exports.enum(["inbox", "push", "banner", "call"]);
|
|
12811
|
+
var SelectShapeSchema2 = external_exports.enum(["one", "many", "rank", "confirm", "text"]);
|
|
12812
|
+
var ReceiptEventSchema2 = external_exports.enum([
|
|
12813
|
+
"delivered",
|
|
12814
|
+
// the bundle reached the recipient at some level
|
|
12815
|
+
"seen",
|
|
12816
|
+
// the recipient opened it
|
|
12817
|
+
"answered",
|
|
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",
|
|
12825
|
+
"escalated",
|
|
12826
|
+
// re-reached at a higher level (re-ring / promote)
|
|
12827
|
+
"coalesced",
|
|
12828
|
+
// merged into another live claim
|
|
12829
|
+
"expired",
|
|
12830
|
+
// deadline passed unanswered
|
|
12831
|
+
"woke",
|
|
12832
|
+
// the agent was woken for an owed obligation (callback)
|
|
12833
|
+
"gave_up",
|
|
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"
|
|
12841
|
+
]);
|
|
12842
|
+
var AttentionSchema2 = external_exports.object({
|
|
12843
|
+
urgency: NotifyLevelSchema2,
|
|
12844
|
+
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
12845
|
+
select: SelectShapeSchema2.nullable(),
|
|
12846
|
+
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
12847
|
+
points: external_exports.array(external_exports.string()).nullable(),
|
|
12848
|
+
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
12849
|
+
blocking: external_exports.boolean(),
|
|
12850
|
+
/** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
12851
|
+
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
12852
|
+
deadline: external_exports.string().datetime().nullable().optional()
|
|
12853
|
+
});
|
|
12854
|
+
var NotifyRequestSchema2 = external_exports.object({
|
|
12855
|
+
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
12856
|
+
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
12857
|
+
* superRefine at the bottom enforces exactly one of the two. */
|
|
12858
|
+
context: ContextSchema2.optional(),
|
|
12859
|
+
options: external_exports.array(OptionSchema2.omit({ id: true })).min(1).optional().describe(
|
|
12860
|
+
"The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
|
|
12861
|
+
),
|
|
12862
|
+
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
12863
|
+
"The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
|
|
12864
|
+
),
|
|
12865
|
+
visuals: external_exports.array(VisualSchema2).optional().describe(
|
|
12866
|
+
"Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
|
|
12867
|
+
),
|
|
12868
|
+
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
12869
|
+
repo: external_exports.string().optional(),
|
|
12870
|
+
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
12871
|
+
branch: external_exports.string().optional(),
|
|
12872
|
+
/** Continue an existing conversation — the id of any notification in it (its root
|
|
12873
|
+
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
12874
|
+
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
12875
|
+
* still accepts the old name from older clients. */
|
|
12876
|
+
parentId: external_exports.string().uuid().optional(),
|
|
12877
|
+
urgency: NotifyLevelSchema2.default("inbox").describe(
|
|
12878
|
+
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
12879
|
+
),
|
|
12880
|
+
/** The request this one CLARIFIES — spawning a clarification keeps that parent
|
|
12881
|
+
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
12882
|
+
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
12883
|
+
* says HOW. */
|
|
12884
|
+
clarifies: external_exports.string().optional(),
|
|
12885
|
+
/** E2EE (text lane): when the pairing is E2EE, the sealed replacements for the
|
|
12886
|
+
* plaintext content fields, keyed by field name. FINALIZED wire shape (was
|
|
12887
|
+
* provisional in the storage PR): a per-field map `{ context?, options?,
|
|
12888
|
+
* visuals? }` of opaque Envelopes — one seal per present content field, so a
|
|
12889
|
+
* message with no options/visuals seals only `context`. It COEXISTS with the
|
|
12890
|
+
* plaintext fields by mutual exclusion: the superRefine below requires that
|
|
12891
|
+
* when `envelope` is present the plaintext `context`/`options`/`visuals` are
|
|
12892
|
+
* ABSENT (and vice-versa), so a row is either fully plaintext or fully sealed —
|
|
12893
|
+
* never a readable half. The server persists this OPAQUELY into
|
|
12894
|
+
* notifications.envelope and relays it blindly; it never decrypts. Absent =
|
|
12895
|
+
* today's plaintext path (context/options/visuals carry the cleartext).
|
|
12896
|
+
* z.lazy because EnvelopeSchema is declared further down (E2EE section). */
|
|
12897
|
+
envelope: external_exports.object({
|
|
12898
|
+
context: external_exports.lazy(() => EnvelopeSchema2).optional(),
|
|
12899
|
+
options: external_exports.lazy(() => EnvelopeSchema2).optional(),
|
|
12900
|
+
visuals: external_exports.lazy(() => EnvelopeSchema2).optional()
|
|
12901
|
+
}).optional(),
|
|
12902
|
+
select: SelectShapeSchema2.optional().describe(
|
|
12903
|
+
"How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
|
|
12904
|
+
),
|
|
12905
|
+
/** The simplified form (#395): instead of shaping the notification yourself
|
|
12906
|
+
* (context/select/options/urgency), state what you need to learn and why it
|
|
12907
|
+
* matters now — the broker derives the optimal shape and channel. Mutually
|
|
12908
|
+
* exclusive with `context` (and never sent alongside `envelope`: E2EE pairings
|
|
12909
|
+
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
12910
|
+
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
12911
|
+
// the ask into topics and gives each one at most three sentences and one question
|
|
12912
|
+
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
12913
|
+
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
12914
|
+
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
12915
|
+
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
12916
|
+
'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
|
|
12917
|
+
),
|
|
12918
|
+
needs: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
12919
|
+
"With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
|
|
12920
|
+
),
|
|
12921
|
+
urgencyHint: external_exports.enum(["whenever", "soon", "now"]).optional().describe(
|
|
12922
|
+
"With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
|
|
12923
|
+
),
|
|
12924
|
+
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
12925
|
+
* to the agent's work while it waits. Normalized server-side into those two
|
|
12926
|
+
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
12927
|
+
* urgencyHint/blocking win when both are sent. */
|
|
12928
|
+
waiting: external_exports.enum(["none", "soft", "hard"]).optional().describe(
|
|
12929
|
+
"With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
|
|
12930
|
+
),
|
|
12931
|
+
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
12932
|
+
* interrupted. Opt-in per claim, because the fail-open is a minute-granularity cron —
|
|
12933
|
+
* holding by default would charge every quiet claim that minute before any agent could
|
|
12934
|
+
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
12935
|
+
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
12936
|
+
confirm: external_exports.boolean().optional().describe(
|
|
12937
|
+
"Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
|
|
12938
|
+
),
|
|
12939
|
+
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
12940
|
+
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
12941
|
+
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
12942
|
+
* waiting:'hard'. */
|
|
12943
|
+
channel: external_exports.enum(["call", "message"]).optional().describe(
|
|
12944
|
+
"Only if the user explicitly said how to reach them \u2014 'call me' \u2192 'call', 'just message/text me' \u2192 'message'. Omit otherwise; Paigy picks."
|
|
12945
|
+
),
|
|
12946
|
+
confirmStyle: external_exports.enum(["yesno", "approve"]).default("yesno").describe(
|
|
12947
|
+
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
12948
|
+
),
|
|
12949
|
+
blocking: external_exports.boolean().default(false).describe(
|
|
12950
|
+
"Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
|
|
12951
|
+
)
|
|
12952
|
+
}).superRefine((r, ctx) => {
|
|
12953
|
+
const sealed = !!r.envelope;
|
|
12954
|
+
if (sealed) {
|
|
12955
|
+
if (!r.envelope?.context)
|
|
12956
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
|
|
12957
|
+
for (const f of ["context", "options", "visuals"]) {
|
|
12958
|
+
if (r[f] !== void 0)
|
|
12959
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
|
|
12960
|
+
}
|
|
12961
|
+
if (r.points !== void 0)
|
|
12962
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
|
|
12963
|
+
if (r.ask !== void 0)
|
|
12964
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
|
|
12965
|
+
if (r.needs !== void 0)
|
|
12966
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
|
|
12967
|
+
return;
|
|
13121
12968
|
}
|
|
13122
12969
|
if (r.ask !== void 0) {
|
|
13123
12970
|
for (const f of ["context", "select", "points"]) {
|
|
13124
12971
|
if (r[f] !== void 0)
|
|
13125
|
-
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives it
|
|
12972
|
+
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
13126
12973
|
}
|
|
13127
12974
|
return;
|
|
13128
12975
|
}
|
|
@@ -13197,6 +13044,14 @@ var IntentSchema2 = external_exports.object({
|
|
|
13197
13044
|
* about what cannot answer "is the bot looping less this week?". */
|
|
13198
13045
|
fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
13199
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
|
+
});
|
|
13200
13055
|
var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
13201
13056
|
external_exports.object({
|
|
13202
13057
|
type: external_exports.literal("reply"),
|
|
@@ -13217,7 +13072,13 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
13217
13072
|
transcript: external_exports.string().optional(),
|
|
13218
13073
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
13219
13074
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
13220
|
-
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()
|
|
13221
13082
|
}),
|
|
13222
13083
|
external_exports.object({
|
|
13223
13084
|
type: external_exports.literal("remind"),
|
|
@@ -13252,7 +13113,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
13252
13113
|
acts: external_exports.array(IntentSchema2).nullable().optional()
|
|
13253
13114
|
})
|
|
13254
13115
|
}),
|
|
13255
|
-
external_exports.object({ type: external_exports.literal("idle") })
|
|
13116
|
+
external_exports.object({ type: external_exports.literal("idle"), also: external_exports.array(RideAlongSchema2).optional() })
|
|
13256
13117
|
]);
|
|
13257
13118
|
var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
13258
13119
|
var ScheduleCallbackSchema2 = external_exports.object({
|
|
@@ -13283,6 +13144,12 @@ var PendingRepliesSchema2 = external_exports.object({
|
|
|
13283
13144
|
pending: external_exports.array(
|
|
13284
13145
|
external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
|
|
13285
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(),
|
|
13286
13153
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
13287
13154
|
* contact on the same parentId. Keeps reappearing until you call
|
|
13288
13155
|
* set_task_state on its notificationId. */
|
|
@@ -13294,12 +13161,21 @@ var PendingRepliesSchema2 = external_exports.object({
|
|
|
13294
13161
|
createdAt: external_exports.string(),
|
|
13295
13162
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
13296
13163
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
13297
|
-
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()
|
|
13298
13171
|
})
|
|
13299
13172
|
),
|
|
13300
13173
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
13301
13174
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
13302
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(),
|
|
13303
13179
|
owedCallbacks: external_exports.array(
|
|
13304
13180
|
external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema2, note: external_exports.string() })
|
|
13305
13181
|
),
|
|
@@ -13335,7 +13211,11 @@ var NotifyResponseSchema2 = external_exports.object({
|
|
|
13335
13211
|
status: NotifyStatusSchema2,
|
|
13336
13212
|
createdAt: external_exports.string().datetime(),
|
|
13337
13213
|
answer: UserAnswerSchema2.optional(),
|
|
13338
|
-
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()
|
|
13339
13219
|
});
|
|
13340
13220
|
var NotifyPlanUnitSchema2 = external_exports.object({
|
|
13341
13221
|
notificationId: external_exports.string(),
|
|
@@ -13357,7 +13237,13 @@ var NotifyPlanUnitSchema2 = external_exports.object({
|
|
|
13357
13237
|
proposal: external_exports.object({
|
|
13358
13238
|
select: SelectShapeSchema2,
|
|
13359
13239
|
options: external_exports.array(external_exports.object({ label: external_exports.string().min(1) })).optional()
|
|
13360
|
-
}).optional()
|
|
13240
|
+
}).optional(),
|
|
13241
|
+
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
13242
|
+
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
13243
|
+
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
13244
|
+
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
13245
|
+
* agent declared (#731, #923). */
|
|
13246
|
+
wants: external_exports.enum(["decision", "approval", "knowledge"]).optional()
|
|
13361
13247
|
});
|
|
13362
13248
|
var NotifyPlanSchema2 = external_exports.object({
|
|
13363
13249
|
units: external_exports.array(NotifyPlanUnitSchema2),
|
|
@@ -13381,6 +13267,9 @@ var UserResponseSchema2 = external_exports.object({
|
|
|
13381
13267
|
});
|
|
13382
13268
|
var VoiceKeySchema2 = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
13383
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(),
|
|
13384
13273
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
13385
13274
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
13386
13275
|
info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
|
|
@@ -13414,6 +13303,7 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13414
13303
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
13415
13304
|
blocking: external_exports.boolean().optional()
|
|
13416
13305
|
});
|
|
13306
|
+
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
13417
13307
|
var InboxItemSchema2 = external_exports.object({
|
|
13418
13308
|
id: external_exports.string(),
|
|
13419
13309
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
@@ -13433,6 +13323,15 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13433
13323
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
13434
13324
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
13435
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(),
|
|
13436
13335
|
tokenId: external_exports.string().optional(),
|
|
13437
13336
|
status: NotifyStatusSchema2,
|
|
13438
13337
|
context: ContextSchema2,
|
|
@@ -13453,6 +13352,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13453
13352
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
13454
13353
|
* which must never CLAIM stalled. */
|
|
13455
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(),
|
|
13456
13365
|
agenda: external_exports.array(AgendaTurnSchema2).optional(),
|
|
13457
13366
|
/** On a replied detail (#397): the next steps the user attached to the answer
|
|
13458
13367
|
* ("call back after lunch") — shown so they can see the commitment was captured. */
|
|
@@ -13524,11 +13433,23 @@ var SnoozeRequestSchema2 = external_exports.object({
|
|
|
13524
13433
|
requestId: external_exports.string(),
|
|
13525
13434
|
until: external_exports.string().datetime()
|
|
13526
13435
|
});
|
|
13436
|
+
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
13527
13437
|
var PushTokenSchema2 = external_exports.object({
|
|
13528
13438
|
voipToken: external_exports.string().min(1).optional(),
|
|
13529
13439
|
alertToken: external_exports.string().min(1).optional(),
|
|
13530
13440
|
fcmToken: external_exports.string().min(1).optional(),
|
|
13531
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
|
+
}
|
|
13532
13453
|
});
|
|
13533
13454
|
var MissedCallSchema2 = external_exports.enum([
|
|
13534
13455
|
"retry_10m",
|
|
@@ -13579,6 +13500,15 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13579
13500
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
13580
13501
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
13581
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(),
|
|
13582
13512
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
13583
13513
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
13584
13514
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
@@ -13604,6 +13534,15 @@ var HistoryItemSchema2 = external_exports.object({
|
|
|
13604
13534
|
/** When you answered the agent's notification (agent→user only). */
|
|
13605
13535
|
humanAckedAt: external_exports.string().nullable()
|
|
13606
13536
|
});
|
|
13537
|
+
var ACTIVITY_LINES2 = 2;
|
|
13538
|
+
var ACTIVITY_LINE_MAX2 = 80;
|
|
13539
|
+
var AgentActivitySchema2 = external_exports.object({
|
|
13540
|
+
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
13541
|
+
lines: external_exports.array(external_exports.string().max(ACTIVITY_LINE_MAX2)).max(ACTIVITY_LINES2),
|
|
13542
|
+
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
13543
|
+
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
13544
|
+
at: external_exports.string().datetime()
|
|
13545
|
+
});
|
|
13607
13546
|
var ConnectionSummarySchema2 = external_exports.object({
|
|
13608
13547
|
/** The connection = the agent's token id (used to address a request). */
|
|
13609
13548
|
id: external_exports.string(),
|
|
@@ -13637,6 +13576,11 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
13637
13576
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
13638
13577
|
workspaces: external_exports.array(external_exports.string()).optional()
|
|
13639
13578
|
}).optional(),
|
|
13579
|
+
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
13580
|
+
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
13581
|
+
* used straight from a terminal emits no work events; the page says so rather than
|
|
13582
|
+
* drawing an empty box). */
|
|
13583
|
+
activity: AgentActivitySchema2.optional(),
|
|
13640
13584
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
13641
13585
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
13642
13586
|
managed: external_exports.boolean()
|
|
@@ -13705,7 +13649,8 @@ var HandoffSchema2 = external_exports.object({
|
|
|
13705
13649
|
recap: external_exports.boolean().optional()
|
|
13706
13650
|
});
|
|
13707
13651
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
13708
|
-
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"]);
|
|
13709
13654
|
var DecisionSchema2 = external_exports.object({
|
|
13710
13655
|
id: external_exports.string(),
|
|
13711
13656
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
@@ -13730,11 +13675,29 @@ var NoteSchema2 = external_exports.object({
|
|
|
13730
13675
|
assignee: external_exports.string().nullable(),
|
|
13731
13676
|
/** The request thread minted at assignment; null until assigned. */
|
|
13732
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),
|
|
13733
13689
|
createdAt: external_exports.string()
|
|
13734
13690
|
});
|
|
13735
13691
|
var CreateNoteSchema2 = external_exports.object({
|
|
13736
13692
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
13737
|
-
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()
|
|
13738
13701
|
});
|
|
13739
13702
|
var RecordDecisionSchema2 = external_exports.object({
|
|
13740
13703
|
/** An open decision (from /clarify) to answer. */
|
|
@@ -13940,6 +13903,323 @@ var FeedbackOutcomeSchema2 = external_exports.object({
|
|
|
13940
13903
|
childIds: external_exports.array(external_exports.string()).optional()
|
|
13941
13904
|
});
|
|
13942
13905
|
|
|
13906
|
+
// src/paigy/activity.ts
|
|
13907
|
+
function shortenPaths(s) {
|
|
13908
|
+
return s.replace(/(?<![\w.@+-])(?:\/[\w.@+-]+){3,}/g, (p) => {
|
|
13909
|
+
const parts = p.split("/").filter(Boolean);
|
|
13910
|
+
return `\u2026/${parts.slice(-2).join("/")}`;
|
|
13911
|
+
});
|
|
13912
|
+
}
|
|
13913
|
+
function workLine(event) {
|
|
13914
|
+
const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
13915
|
+
const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
13916
|
+
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
13917
|
+
}
|
|
13918
|
+
function pushWork(lines, line) {
|
|
13919
|
+
if (!line || lines[lines.length - 1] === line) return [...lines];
|
|
13920
|
+
return [...lines, line].slice(-ACTIVITY_LINES2);
|
|
13921
|
+
}
|
|
13922
|
+
function sameTail(a, b) {
|
|
13923
|
+
return a.length === b.length && a.every((l, i) => l === b[i]);
|
|
13924
|
+
}
|
|
13925
|
+
|
|
13926
|
+
// src/harness/session.ts
|
|
13927
|
+
import { spawn } from "child_process";
|
|
13928
|
+
import { existsSync as existsSync4 } from "fs";
|
|
13929
|
+
import { homedir as homedir4 } from "os";
|
|
13930
|
+
import { resolve as resolve2, delimiter as delimiter2 } from "path";
|
|
13931
|
+
|
|
13932
|
+
// src/harness/acp.ts
|
|
13933
|
+
var none = { events: [], writes: [] };
|
|
13934
|
+
function optionFor(options, decision) {
|
|
13935
|
+
const want = decision.allow ? "allow_once" : "reject_once";
|
|
13936
|
+
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
13937
|
+
}
|
|
13938
|
+
function createAcpDriver(opts) {
|
|
13939
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
|
|
13940
|
+
}
|
|
13941
|
+
var AcpDriver = class {
|
|
13942
|
+
constructor(cwd, mode, mcp = []) {
|
|
13943
|
+
this.cwd = cwd;
|
|
13944
|
+
this.mode = mode;
|
|
13945
|
+
this.mcp = mcp;
|
|
13946
|
+
}
|
|
13947
|
+
cwd;
|
|
13948
|
+
mode;
|
|
13949
|
+
mcp;
|
|
13950
|
+
nextId = 1;
|
|
13951
|
+
initId;
|
|
13952
|
+
sessionNewId;
|
|
13953
|
+
promptId;
|
|
13954
|
+
sessionId;
|
|
13955
|
+
queued = [];
|
|
13956
|
+
/** Options of each unanswered permission request, keyed by its JSON-RPC id. */
|
|
13957
|
+
pending = /* @__PURE__ */ new Map();
|
|
13958
|
+
/** Where the REPORT starts in `text` — everything before the LAST tool call is working
|
|
13959
|
+
* narration ("Now the API endpoints." → runs a tool), and it used to ship: the chunks
|
|
13960
|
+
* concatenate with no separator, so the owner's phone got "…find the repo.Now I have
|
|
13961
|
+
* the full picture. Writing the migration.Now…" as the opening paragraph of a finished
|
|
13962
|
+
* task (live, 2026-08-11 — "looks like a working log"). The narration's audience is the
|
|
13963
|
+
* terminal and host.log; what the agent composed AFTER its last tool call is the part
|
|
13964
|
+
* addressed to a human, and that is what leaves the machine. */
|
|
13965
|
+
reportFrom = 0;
|
|
13966
|
+
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
13967
|
+
text = "";
|
|
13968
|
+
tools = [];
|
|
13969
|
+
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
13970
|
+
open() {
|
|
13971
|
+
this.initId = this.nextId++;
|
|
13972
|
+
return [
|
|
13973
|
+
frame({
|
|
13974
|
+
id: this.initId,
|
|
13975
|
+
method: "initialize",
|
|
13976
|
+
params: {
|
|
13977
|
+
protocolVersion: 2,
|
|
13978
|
+
// We are not an editor: no file services offered, the agent uses its own.
|
|
13979
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
13980
|
+
clientInfo: { name: "paigy-desktop", version: "0.0.0" }
|
|
13981
|
+
}
|
|
13982
|
+
})
|
|
13983
|
+
];
|
|
13984
|
+
}
|
|
13985
|
+
/** Queue a prompt; it goes out when the session exists and no turn is running. */
|
|
13986
|
+
send(text) {
|
|
13987
|
+
this.queued.push(text);
|
|
13988
|
+
return this.flush();
|
|
13989
|
+
}
|
|
13990
|
+
/** Answer a PermissionEvent (ask mode). The id is the request's JSON-RPC id as a string. */
|
|
13991
|
+
respond(id, decision) {
|
|
13992
|
+
const request = this.pending.get(id);
|
|
13993
|
+
if (!request) return [];
|
|
13994
|
+
this.pending.delete(id);
|
|
13995
|
+
const optionId = optionFor(request.options, decision);
|
|
13996
|
+
return [
|
|
13997
|
+
optionId ? frame({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
13998
|
+
];
|
|
13999
|
+
}
|
|
14000
|
+
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
14001
|
+
close() {
|
|
14002
|
+
const writes = [...this.pending.values()].map(
|
|
14003
|
+
(r) => frame({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
|
|
14004
|
+
);
|
|
14005
|
+
this.pending.clear();
|
|
14006
|
+
return writes;
|
|
14007
|
+
}
|
|
14008
|
+
handleLine(line) {
|
|
14009
|
+
const trimmed = line.trim();
|
|
14010
|
+
if (!trimmed) return none;
|
|
14011
|
+
let msg;
|
|
14012
|
+
try {
|
|
14013
|
+
msg = JSON.parse(trimmed);
|
|
14014
|
+
} catch {
|
|
14015
|
+
return none;
|
|
14016
|
+
}
|
|
14017
|
+
if (msg.method !== void 0) {
|
|
14018
|
+
return msg.id !== void 0 ? this.handleRequest(msg) : this.handleNotification(msg);
|
|
14019
|
+
}
|
|
14020
|
+
if (msg.id !== void 0) return this.handleResponse(msg);
|
|
14021
|
+
return none;
|
|
14022
|
+
}
|
|
14023
|
+
// ── responses to our requests ──
|
|
14024
|
+
handleResponse(msg) {
|
|
14025
|
+
if (msg.id === this.initId) {
|
|
14026
|
+
this.initId = void 0;
|
|
14027
|
+
if (msg.error) {
|
|
14028
|
+
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
|
|
14029
|
+
}
|
|
14030
|
+
this.sessionNewId = this.nextId++;
|
|
14031
|
+
return {
|
|
14032
|
+
events: [],
|
|
14033
|
+
writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
|
|
14034
|
+
};
|
|
14035
|
+
}
|
|
14036
|
+
if (msg.id === this.sessionNewId) {
|
|
14037
|
+
this.sessionNewId = void 0;
|
|
14038
|
+
const sessionId = msg.result?.sessionId;
|
|
14039
|
+
if (!sessionId) {
|
|
14040
|
+
return {
|
|
14041
|
+
events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
|
|
14042
|
+
writes: []
|
|
14043
|
+
};
|
|
14044
|
+
}
|
|
14045
|
+
this.sessionId = sessionId;
|
|
14046
|
+
const value = this.mode === "bypass" ? "bypassPermissions" : "default";
|
|
14047
|
+
const writes = [
|
|
14048
|
+
frame({
|
|
14049
|
+
id: this.nextId++,
|
|
14050
|
+
method: "session/set_config_option",
|
|
14051
|
+
params: { sessionId, configId: "mode", value }
|
|
14052
|
+
}),
|
|
14053
|
+
frame({ id: this.nextId++, method: "session/set_mode", params: { sessionId, modeId: value } })
|
|
14054
|
+
];
|
|
14055
|
+
writes.push(...this.flush());
|
|
14056
|
+
return { events: [], writes };
|
|
14057
|
+
}
|
|
14058
|
+
if (msg.id === this.promptId) {
|
|
14059
|
+
this.promptId = void 0;
|
|
14060
|
+
const events = [];
|
|
14061
|
+
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
14062
|
+
if (report || this.tools.length) {
|
|
14063
|
+
events.push({
|
|
14064
|
+
kind: "turn",
|
|
14065
|
+
role: "agent",
|
|
14066
|
+
text: report,
|
|
14067
|
+
...this.tools.length ? { tools: [...this.tools] } : {}
|
|
14068
|
+
});
|
|
14069
|
+
}
|
|
14070
|
+
const result = report;
|
|
14071
|
+
const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
|
|
14072
|
+
this.text = "";
|
|
14073
|
+
this.tools = [];
|
|
14074
|
+
this.reportFrom = 0;
|
|
14075
|
+
const writes = this.flush();
|
|
14076
|
+
if (!writes.length) {
|
|
14077
|
+
events.push({
|
|
14078
|
+
kind: "idle",
|
|
14079
|
+
...result ? { result } : {},
|
|
14080
|
+
...failed ? { failed: true } : {}
|
|
14081
|
+
});
|
|
14082
|
+
}
|
|
14083
|
+
return { events, writes };
|
|
14084
|
+
}
|
|
14085
|
+
return none;
|
|
14086
|
+
}
|
|
14087
|
+
// ── the agent talking to us ──
|
|
14088
|
+
handleNotification(msg) {
|
|
14089
|
+
if (msg.method !== "session/update") return none;
|
|
14090
|
+
const update = msg.params?.update;
|
|
14091
|
+
switch (update?.sessionUpdate) {
|
|
14092
|
+
case "agent_message_chunk":
|
|
14093
|
+
this.text += update.content?.text ?? "";
|
|
14094
|
+
return none;
|
|
14095
|
+
case "tool_call": {
|
|
14096
|
+
const title = update.title?.trim() || update.kind || "tool";
|
|
14097
|
+
this.tools.push(title);
|
|
14098
|
+
const note = this.text.slice(this.reportFrom).trim();
|
|
14099
|
+
this.reportFrom = this.text.length;
|
|
14100
|
+
return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
|
|
14101
|
+
}
|
|
14102
|
+
default:
|
|
14103
|
+
return none;
|
|
14104
|
+
}
|
|
14105
|
+
}
|
|
14106
|
+
handleRequest(msg) {
|
|
14107
|
+
if (msg.method !== "session/request_permission") {
|
|
14108
|
+
return {
|
|
14109
|
+
events: [],
|
|
14110
|
+
writes: [frame({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
|
|
14111
|
+
};
|
|
14112
|
+
}
|
|
14113
|
+
const id = msg.id;
|
|
14114
|
+
const options = msg.params?.options ?? [];
|
|
14115
|
+
const title = msg.params?.toolCall?.title?.trim() || "a tool call";
|
|
14116
|
+
const tool = msg.params?.toolCall?.kind ?? "tool";
|
|
14117
|
+
if (this.mode === "bypass") {
|
|
14118
|
+
const optionId = optionFor(options, { allow: true }) ?? optionFor(options, { allow: false });
|
|
14119
|
+
return {
|
|
14120
|
+
// The decision still goes through Paigy — as history, not a question. Bypass
|
|
14121
|
+
// means "don't stall the agent", never "don't tell the user".
|
|
14122
|
+
events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
|
|
14123
|
+
writes: [
|
|
14124
|
+
optionId ? frame({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id, result: { outcome: { outcome: "cancelled" } } })
|
|
14125
|
+
]
|
|
14126
|
+
};
|
|
14127
|
+
}
|
|
14128
|
+
this.pending.set(String(id), { id, options });
|
|
14129
|
+
return {
|
|
14130
|
+
events: [{ kind: "permission", id: String(id), tool, summary: title }],
|
|
14131
|
+
writes: []
|
|
14132
|
+
};
|
|
14133
|
+
}
|
|
14134
|
+
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
14135
|
+
flush() {
|
|
14136
|
+
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
14137
|
+
const blocks = this.queued.map((text) => ({ type: "text", text }));
|
|
14138
|
+
this.queued = [];
|
|
14139
|
+
this.promptId = this.nextId++;
|
|
14140
|
+
return [
|
|
14141
|
+
frame({
|
|
14142
|
+
id: this.promptId,
|
|
14143
|
+
method: "session/prompt",
|
|
14144
|
+
params: { sessionId: this.sessionId, prompt: blocks }
|
|
14145
|
+
})
|
|
14146
|
+
];
|
|
14147
|
+
}
|
|
14148
|
+
};
|
|
14149
|
+
var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
14150
|
+
|
|
14151
|
+
// src/harness/session.ts
|
|
14152
|
+
var ADAPTER_BIN = {
|
|
14153
|
+
claude: "claude-agent-acp",
|
|
14154
|
+
codex: "codex-acp",
|
|
14155
|
+
agy: "agy"
|
|
14156
|
+
};
|
|
14157
|
+
function splitLines(buffer, chunk) {
|
|
14158
|
+
const combined = buffer + chunk;
|
|
14159
|
+
const parts = combined.split("\n");
|
|
14160
|
+
const rest = parts.pop() ?? "";
|
|
14161
|
+
return { lines: parts.filter((l) => l.trim()), rest };
|
|
14162
|
+
}
|
|
14163
|
+
function startSession(opts) {
|
|
14164
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
14165
|
+
if (!existsSync4(cwd)) {
|
|
14166
|
+
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
14167
|
+
}
|
|
14168
|
+
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
14169
|
+
cwd,
|
|
14170
|
+
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
14171
|
+
// launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
|
|
14172
|
+
// node's own bin dir (nvm installs the adapters next to node; launchd's bare
|
|
14173
|
+
// PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
|
|
14174
|
+
env: {
|
|
14175
|
+
...process.env,
|
|
14176
|
+
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
14177
|
+
...opts.token ? { PAIGY_TOKEN: opts.token } : {}
|
|
14178
|
+
},
|
|
14179
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
14180
|
+
});
|
|
14181
|
+
const write = (frames) => {
|
|
14182
|
+
for (const f of frames) child.stdin?.write(`${f}
|
|
14183
|
+
`);
|
|
14184
|
+
};
|
|
14185
|
+
child.stderr?.on("data", (chunk) => {
|
|
14186
|
+
const text = String(chunk).trim();
|
|
14187
|
+
if (text) opts.onEvent({ kind: "error", message: text });
|
|
14188
|
+
});
|
|
14189
|
+
child.on("exit", (code) => {
|
|
14190
|
+
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
14191
|
+
opts.onExit?.();
|
|
14192
|
+
});
|
|
14193
|
+
child.on("error", (e) => {
|
|
14194
|
+
opts.onEvent({ kind: "error", message: e.message });
|
|
14195
|
+
opts.onExit?.();
|
|
14196
|
+
});
|
|
14197
|
+
const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
|
|
14198
|
+
let buffer = "";
|
|
14199
|
+
child.stdout?.on("data", (chunk) => {
|
|
14200
|
+
const { lines, rest } = splitLines(buffer, String(chunk));
|
|
14201
|
+
buffer = rest;
|
|
14202
|
+
for (const line of lines) {
|
|
14203
|
+
const { events, writes } = driver.handleLine(line);
|
|
14204
|
+
write(writes);
|
|
14205
|
+
for (const event of events) opts.onEvent(event);
|
|
14206
|
+
}
|
|
14207
|
+
});
|
|
14208
|
+
write(driver.open());
|
|
14209
|
+
return {
|
|
14210
|
+
send(text) {
|
|
14211
|
+
write(driver.send(text));
|
|
14212
|
+
},
|
|
14213
|
+
respond(id, decision) {
|
|
14214
|
+
write(driver.respond(id, decision));
|
|
14215
|
+
},
|
|
14216
|
+
stop() {
|
|
14217
|
+
write(driver.close());
|
|
14218
|
+
child.kill();
|
|
14219
|
+
}
|
|
14220
|
+
};
|
|
14221
|
+
}
|
|
14222
|
+
|
|
13943
14223
|
// src/paigy/conversation.ts
|
|
13944
14224
|
function endsWithQuestion(text) {
|
|
13945
14225
|
if (!text) return false;
|
|
@@ -14205,6 +14485,7 @@ var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text
|
|
|
14205
14485
|
// src/run.ts
|
|
14206
14486
|
function runHarness(opts) {
|
|
14207
14487
|
let running = true;
|
|
14488
|
+
let tail = [];
|
|
14208
14489
|
const state = { ...opts.exclusive ? { exclusive: true } : {} };
|
|
14209
14490
|
let session = null;
|
|
14210
14491
|
const asMe = { token: opts.token };
|
|
@@ -14234,6 +14515,7 @@ function runHarness(opts) {
|
|
|
14234
14515
|
}
|
|
14235
14516
|
if (event.kind === "work") {
|
|
14236
14517
|
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
14518
|
+
tail = pushWork(tail, workLine(event));
|
|
14237
14519
|
return;
|
|
14238
14520
|
}
|
|
14239
14521
|
const { parentId } = await mirror(event, state, deps);
|
|
@@ -14241,6 +14523,7 @@ function runHarness(opts) {
|
|
|
14241
14523
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
14242
14524
|
if (event.kind === "idle") {
|
|
14243
14525
|
state.resting = true;
|
|
14526
|
+
tail = [];
|
|
14244
14527
|
if (endsWithQuestion(event.result)) {
|
|
14245
14528
|
const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
|
|
14246
14529
|
if (local?.trim() && session && running) {
|
|
@@ -14278,8 +14561,10 @@ function runHarness(opts) {
|
|
|
14278
14561
|
session?.send(text);
|
|
14279
14562
|
},
|
|
14280
14563
|
working: () => running && state.resting !== true,
|
|
14564
|
+
tail: () => [...tail],
|
|
14281
14565
|
stop() {
|
|
14282
14566
|
running = false;
|
|
14567
|
+
tail = [];
|
|
14283
14568
|
cancelAsks(state);
|
|
14284
14569
|
session?.stop();
|
|
14285
14570
|
session = null;
|
|
@@ -14308,6 +14593,12 @@ function hostElsewhere() {
|
|
|
14308
14593
|
const s = readHostState();
|
|
14309
14594
|
return !!s && s.pid !== process.pid;
|
|
14310
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
|
+
}
|
|
14311
14602
|
function startHost(opts) {
|
|
14312
14603
|
const runs = /* @__PURE__ */ new Map();
|
|
14313
14604
|
const asHost = { token: opts.token };
|
|
@@ -14363,12 +14654,15 @@ function startHost(opts) {
|
|
|
14363
14654
|
}
|
|
14364
14655
|
}
|
|
14365
14656
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
14366
|
-
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.";
|
|
14367
14658
|
async function sweepSlots() {
|
|
14368
14659
|
for (const slot of listSlots()) {
|
|
14369
14660
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
14370
14661
|
const key = `slot:${slot}`;
|
|
14371
|
-
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;
|
|
14372
14666
|
const token = readToken(slot);
|
|
14373
14667
|
if (!token) continue;
|
|
14374
14668
|
const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
@@ -14376,12 +14670,20 @@ function startHost(opts) {
|
|
|
14376
14670
|
const work = await checkReplies({ token }).catch(() => null);
|
|
14377
14671
|
if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
|
|
14378
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
|
+
}
|
|
14379
14681
|
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
14380
14682
|
const run = runHarness({
|
|
14381
14683
|
harness,
|
|
14382
14684
|
cwd: workspace,
|
|
14383
14685
|
mode: "bypass",
|
|
14384
|
-
prompt:
|
|
14686
|
+
prompt: wakePrompt(true),
|
|
14385
14687
|
exclusive: true,
|
|
14386
14688
|
token,
|
|
14387
14689
|
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
@@ -14397,6 +14699,27 @@ function startHost(opts) {
|
|
|
14397
14699
|
opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
|
|
14398
14700
|
}
|
|
14399
14701
|
}
|
|
14702
|
+
const ACTIVITY_MS = 2e3;
|
|
14703
|
+
const published = /* @__PURE__ */ new Map();
|
|
14704
|
+
const streamActivity = () => {
|
|
14705
|
+
const publishTail = (token, lines) => {
|
|
14706
|
+
void heartbeat(void 0, { token, activity: { lines, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
|
|
14707
|
+
});
|
|
14708
|
+
};
|
|
14709
|
+
for (const [key, r] of runs) {
|
|
14710
|
+
if (!r.token) continue;
|
|
14711
|
+
const lines = r.run.tail();
|
|
14712
|
+
const was = published.get(key);
|
|
14713
|
+
if (was && sameTail(was.lines, lines)) continue;
|
|
14714
|
+
published.set(key, { token: r.token, lines, movedAt: Date.now() });
|
|
14715
|
+
publishTail(r.token, lines);
|
|
14716
|
+
}
|
|
14717
|
+
for (const [key, was] of published) {
|
|
14718
|
+
if (runs.has(key)) continue;
|
|
14719
|
+
published.delete(key);
|
|
14720
|
+
if (was.lines.length > 0) publishTail(was.token, []);
|
|
14721
|
+
}
|
|
14722
|
+
};
|
|
14400
14723
|
const publish = () => {
|
|
14401
14724
|
try {
|
|
14402
14725
|
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
@@ -14410,6 +14733,7 @@ function startHost(opts) {
|
|
|
14410
14733
|
void sweepSlots();
|
|
14411
14734
|
publish();
|
|
14412
14735
|
}, 5e3);
|
|
14736
|
+
const activityTick = setInterval(streamActivity, ACTIVITY_MS);
|
|
14413
14737
|
const stopSessions = () => {
|
|
14414
14738
|
for (const { run, label } of runs.values()) {
|
|
14415
14739
|
run.stop();
|
|
@@ -14447,6 +14771,8 @@ function startHost(opts) {
|
|
|
14447
14771
|
clearInterval(pulse);
|
|
14448
14772
|
clearInterval(spawnPoll);
|
|
14449
14773
|
stopSessions();
|
|
14774
|
+
streamActivity();
|
|
14775
|
+
clearInterval(activityTick);
|
|
14450
14776
|
try {
|
|
14451
14777
|
writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
14452
14778
|
} catch {
|