@paigy/harness 0.3.29 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli.js +412 -68
- package/dist/main.js +415 -71
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -9975,7 +9975,20 @@ var AskInputSchema = external_exports.object({
|
|
|
9975
9975
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
9976
9976
|
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
9977
9977
|
),
|
|
9978
|
-
options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
|
|
9978
|
+
options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
|
|
9979
|
+
// A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
|
|
9980
|
+
// checklist mode of answering"). The answer shape was taken off this surface with the wire form
|
|
9981
|
+
// (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
|
|
9982
|
+
// only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
|
|
9983
|
+
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
9984
|
+
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
9985
|
+
// not: whether its own options exclude each other.
|
|
9986
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
9987
|
+
'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
|
|
9988
|
+
),
|
|
9989
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
9990
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
9991
|
+
)
|
|
9979
9992
|
}).strict();
|
|
9980
9993
|
var StartContactSchema = external_exports.object({
|
|
9981
9994
|
asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
@@ -9984,7 +9997,7 @@ var StartContactSchema = external_exports.object({
|
|
|
9984
9997
|
}).strict();
|
|
9985
9998
|
var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
9986
9999
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
9987
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
10000
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'); when the options are independent and they may want several, pass select:'many' and they get a checklist. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
9988
10001
|
var CreateGoalSchema = external_exports.object({
|
|
9989
10002
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
9990
10003
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -10040,11 +10053,11 @@ var GetGoalSchema = external_exports.object({
|
|
|
10040
10053
|
history: external_exports.boolean().optional(),
|
|
10041
10054
|
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
10042
10055
|
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
10043
|
-
* between two of them named. Off by default; `
|
|
10056
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
10044
10057
|
diagnose: external_exports.boolean().optional()
|
|
10045
10058
|
}).strict();
|
|
10046
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
10047
|
-
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
10059
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
10060
|
+
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
10048
10061
|
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
10049
10062
|
var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
10050
10063
|
var CheckRepliesSchema = external_exports.object({}).strict();
|
|
@@ -10058,6 +10071,7 @@ var AGENT_TOOLS = [
|
|
|
10058
10071
|
];
|
|
10059
10072
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
10060
10073
|
var LIVE_MS = 3 * 6e4;
|
|
10074
|
+
var WORKING_MS = 30 * 6e4;
|
|
10061
10075
|
var ContextSchema = external_exports.object({
|
|
10062
10076
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
10063
10077
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -10128,7 +10142,7 @@ var AttentionSchema = external_exports.object({
|
|
|
10128
10142
|
points: external_exports.array(external_exports.string()).nullable(),
|
|
10129
10143
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
10130
10144
|
blocking: external_exports.boolean(),
|
|
10131
|
-
/** Reserved (
|
|
10145
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
10132
10146
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
10133
10147
|
deadline: external_exports.string().datetime().nullable().optional()
|
|
10134
10148
|
});
|
|
@@ -10179,7 +10193,7 @@ var NotifyRequestFields = external_exports.object({
|
|
|
10179
10193
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
10180
10194
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
10181
10195
|
// the ask into topics and gives each one at most three sentences and one question
|
|
10182
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
10196
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
10183
10197
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
10184
10198
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
10185
10199
|
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
@@ -10256,7 +10270,7 @@ var UserAnswerSchema = external_exports.discriminatedUnion("kind", [
|
|
|
10256
10270
|
external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
|
|
10257
10271
|
external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
|
|
10258
10272
|
external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
|
|
10259
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
10273
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
10260
10274
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
10261
10275
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
10262
10276
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -10305,7 +10319,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
10305
10319
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
10306
10320
|
* a sentence with a hole in it is worse than no sentence. */
|
|
10307
10321
|
note: external_exports.string().optional(),
|
|
10308
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
10322
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
10309
10323
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
10310
10324
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
10311
10325
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -10401,10 +10415,11 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10401
10415
|
agentId: external_exports.string().optional(),
|
|
10402
10416
|
select: SelectShapeSchema.optional(),
|
|
10403
10417
|
options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10418
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
10419
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
10420
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
10421
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
10422
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
10408
10423
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
10409
10424
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
10410
10425
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -10439,7 +10454,7 @@ var InboxItemSchema = external_exports.object({
|
|
|
10439
10454
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
10440
10455
|
* `createdAt`. */
|
|
10441
10456
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
10442
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
10457
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
10443
10458
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
10444
10459
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
10445
10460
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -10500,7 +10515,7 @@ var InboxItemSchema = external_exports.object({
|
|
|
10500
10515
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
10501
10516
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
10502
10517
|
onCall: external_exports.literal(true).optional(),
|
|
10503
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
10518
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
10504
10519
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
10505
10520
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
10506
10521
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -10531,7 +10546,7 @@ var InboxItemSchema = external_exports.object({
|
|
|
10531
10546
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
10532
10547
|
),
|
|
10533
10548
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
10534
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
10549
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
10535
10550
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
10536
10551
|
blocking: external_exports.boolean().default(false),
|
|
10537
10552
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -10605,9 +10620,17 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10605
10620
|
banner: external_exports.boolean(),
|
|
10606
10621
|
push: external_exports.boolean()
|
|
10607
10622
|
}),
|
|
10623
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
10624
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
10625
|
+
* what people read (owner, 2026-09-30). */
|
|
10608
10626
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
10609
|
-
silentPush
|
|
10610
|
-
|
|
10627
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
10628
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
10629
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
10630
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
10631
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
10632
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
10633
|
+
* next dead field for the same reason. */
|
|
10611
10634
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
10612
10635
|
improveConsent: external_exports.boolean(),
|
|
10613
10636
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -10618,12 +10641,16 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10618
10641
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
10619
10642
|
* write, 'hosted' on read (see store.ts). */
|
|
10620
10643
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
10621
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
10644
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
10622
10645
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
10623
10646
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
10624
10647
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
10625
10648
|
* read (see store.ts). */
|
|
10626
10649
|
talk: external_exports.boolean().optional(),
|
|
10650
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
10651
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
10652
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
10653
|
+
callDiagnostics: external_exports.boolean().optional(),
|
|
10627
10654
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
10628
10655
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
10629
10656
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -10632,9 +10659,6 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10632
10659
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
10633
10660
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
10634
10661
|
voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
|
|
10635
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
10636
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
10637
|
-
pstnCalls: external_exports.boolean().optional(),
|
|
10638
10662
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
10639
10663
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
10640
10664
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -10678,7 +10702,7 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10678
10702
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
10679
10703
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
10680
10704
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
10681
|
-
*
|
|
10705
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
10682
10706
|
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
10683
10707
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
10684
10708
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -10696,7 +10720,7 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10696
10720
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
10697
10721
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
10698
10722
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
10699
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
10723
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
10700
10724
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
10701
10725
|
* set once must not overrule a rule they set about one agent. */
|
|
10702
10726
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
@@ -10707,7 +10731,14 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10707
10731
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
10708
10732
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
10709
10733
|
lastSeenAt: external_exports.string().datetime().nullable().optional(),
|
|
10710
|
-
/**
|
|
10734
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
10735
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
10736
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
10737
|
+
lastWorkedAt: external_exports.string().datetime().nullable().optional(),
|
|
10738
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
10739
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
10740
|
+
oldestReadyAt: external_exports.string().datetime().nullable().optional(),
|
|
10741
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
10711
10742
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
10712
10743
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
10713
10744
|
runtime: external_exports.object({
|
|
@@ -10715,7 +10746,12 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10715
10746
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
10716
10747
|
version: external_exports.string().optional(),
|
|
10717
10748
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
10718
|
-
workspaces: external_exports.array(external_exports.string()).optional()
|
|
10749
|
+
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
10750
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
10751
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
10752
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
10753
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
10754
|
+
repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
|
|
10719
10755
|
}).optional(),
|
|
10720
10756
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
10721
10757
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -10728,7 +10764,8 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10728
10764
|
});
|
|
10729
10765
|
var LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
10730
10766
|
var AgentLedgerSchema = external_exports.object({
|
|
10731
|
-
agent
|
|
10767
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
10768
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
10732
10769
|
/** Its own questions you have not answered. */
|
|
10733
10770
|
asks: external_exports.array(LedgerItemSchema),
|
|
10734
10771
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -10798,7 +10835,7 @@ var QueueQuestionSchema = external_exports.object({
|
|
|
10798
10835
|
* for a settled question whose reply carried nothing readable. */
|
|
10799
10836
|
answer: external_exports.string().nullable().default(null),
|
|
10800
10837
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
10801
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
10838
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
10802
10839
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
10803
10840
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
10804
10841
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -10890,7 +10927,19 @@ var QueueItemSchema = external_exports.object({
|
|
|
10890
10927
|
said: external_exports.string(),
|
|
10891
10928
|
at: external_exports.string(),
|
|
10892
10929
|
entryId: external_exports.string()
|
|
10893
|
-
}).nullable().optional()
|
|
10930
|
+
}).nullable().optional(),
|
|
10931
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
10932
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
10933
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
10934
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
10935
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
10936
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
10937
|
+
*
|
|
10938
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
10939
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
10940
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
10941
|
+
* Entry with no `about_id` is in neither. */
|
|
10942
|
+
lastPersonAt: external_exports.string().nullable().optional()
|
|
10894
10943
|
});
|
|
10895
10944
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
10896
10945
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -10920,7 +10969,7 @@ var NoteSchema = external_exports.object({
|
|
|
10920
10969
|
assignee: external_exports.string().nullable(),
|
|
10921
10970
|
/** The request thread minted at assignment; null until assigned. */
|
|
10922
10971
|
parentId: external_exports.string().nullable(),
|
|
10923
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
10972
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
10924
10973
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
10925
10974
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
10926
10975
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -11182,6 +11231,31 @@ var SnapshotSchema = external_exports.object({
|
|
|
11182
11231
|
computers: external_exports.array(ComputerRowSchema).nullable()
|
|
11183
11232
|
}).nullable()
|
|
11184
11233
|
});
|
|
11234
|
+
var CallRecapSchema = external_exports.object({
|
|
11235
|
+
call: external_exports.object({
|
|
11236
|
+
status: external_exports.string(),
|
|
11237
|
+
startedAt: external_exports.string(),
|
|
11238
|
+
durationMs: external_exports.number().nullable(),
|
|
11239
|
+
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
11240
|
+
}),
|
|
11241
|
+
topics: external_exports.array(external_exports.object({
|
|
11242
|
+
goalId: external_exports.string().uuid(),
|
|
11243
|
+
title: external_exports.string(),
|
|
11244
|
+
owner: external_exports.string(),
|
|
11245
|
+
state: external_exports.string(),
|
|
11246
|
+
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
11247
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
11248
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
11249
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
11250
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
11251
|
+
* anything that is not an answer. */
|
|
11252
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
11253
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
11254
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
|
|
11255
|
+
})),
|
|
11256
|
+
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
11257
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
11258
|
+
});
|
|
11185
11259
|
function sessionSlot(sessionId2) {
|
|
11186
11260
|
const id = sessionId2 ?? sessionId();
|
|
11187
11261
|
return `session:${id.slice(0, 8)}`;
|
|
@@ -11376,6 +11450,22 @@ async function claimSessions(opts = {}) {
|
|
|
11376
11450
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
11377
11451
|
return (await res.json()).sessions;
|
|
11378
11452
|
}
|
|
11453
|
+
async function claimSessionEnds(opts = {}) {
|
|
11454
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
|
|
11455
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
11456
|
+
// the HOST's identity
|
|
11457
|
+
}));
|
|
11458
|
+
if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
|
|
11459
|
+
return (await res.json()).ends ?? [];
|
|
11460
|
+
}
|
|
11461
|
+
async function finishSessionEnd(endId, result, opts = {}) {
|
|
11462
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
|
|
11463
|
+
method: "POST",
|
|
11464
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
11465
|
+
body: JSON.stringify({ result })
|
|
11466
|
+
}));
|
|
11467
|
+
if (!res.ok) await fail("finish_session_end", res);
|
|
11468
|
+
}
|
|
11379
11469
|
async function recordDecision(decision, opts = {}) {
|
|
11380
11470
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
11381
11471
|
method: "POST",
|
|
@@ -11499,7 +11589,9 @@ async function contact(input, opts = {}) {
|
|
|
11499
11589
|
// this cannot see.
|
|
11500
11590
|
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
11501
11591
|
ask: a.ask,
|
|
11502
|
-
options: a.options ?? []
|
|
11592
|
+
options: a.options ?? [],
|
|
11593
|
+
...a.select ? { select: a.select } : {},
|
|
11594
|
+
...a.answers ? { answers: a.answers } : {}
|
|
11503
11595
|
}));
|
|
11504
11596
|
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
11505
11597
|
method: "POST",
|
|
@@ -12391,7 +12483,7 @@ async function brokerView(deps) {
|
|
|
12391
12483
|
}
|
|
12392
12484
|
|
|
12393
12485
|
// src/workspaces.ts
|
|
12394
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
12486
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
12395
12487
|
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
12396
12488
|
import { homedir as homedir5 } from "os";
|
|
12397
12489
|
function workspacesFile() {
|
|
@@ -12432,10 +12524,41 @@ function resolveWakeDir(pinned, deps) {
|
|
|
12432
12524
|
}
|
|
12433
12525
|
return listWorkspaces(deps)[0];
|
|
12434
12526
|
}
|
|
12527
|
+
function originOf(dir) {
|
|
12528
|
+
try {
|
|
12529
|
+
if (!statSync2(join5(dir, ".git")).isDirectory()) return null;
|
|
12530
|
+
const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
|
|
12531
|
+
const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
|
|
12532
|
+
return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
12533
|
+
} catch {
|
|
12534
|
+
return null;
|
|
12535
|
+
}
|
|
12536
|
+
}
|
|
12537
|
+
var MAX_CHILDREN = 300;
|
|
12538
|
+
var MAX_REPOS = 100;
|
|
12539
|
+
function listRepos(deps) {
|
|
12540
|
+
const repos = [];
|
|
12541
|
+
for (const root of listWorkspaces(deps)) {
|
|
12542
|
+
const own = originOf(root);
|
|
12543
|
+
if (own) repos.push({ path: root, remote: own });
|
|
12544
|
+
let children = [];
|
|
12545
|
+
try {
|
|
12546
|
+
children = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
|
|
12547
|
+
} catch {
|
|
12548
|
+
}
|
|
12549
|
+
for (const name of children) {
|
|
12550
|
+
if (repos.length >= MAX_REPOS) return repos;
|
|
12551
|
+
const path = join5(root, name);
|
|
12552
|
+
const remote = originOf(path);
|
|
12553
|
+
if (remote) repos.push({ path, remote });
|
|
12554
|
+
}
|
|
12555
|
+
}
|
|
12556
|
+
return repos.slice(0, MAX_REPOS);
|
|
12557
|
+
}
|
|
12435
12558
|
|
|
12436
12559
|
// src/holder.ts
|
|
12437
12560
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12438
|
-
import { readFileSync as readFileSync4, readdirSync } from "fs";
|
|
12561
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
12439
12562
|
import { homedir as homedir6 } from "os";
|
|
12440
12563
|
import { join as join6 } from "path";
|
|
12441
12564
|
function isAlive(pid) {
|
|
@@ -12466,9 +12589,62 @@ function holds(pid, procStart, deps) {
|
|
|
12466
12589
|
}
|
|
12467
12590
|
return false;
|
|
12468
12591
|
}
|
|
12592
|
+
function proven(pid, procStart, deps) {
|
|
12593
|
+
if (!deps.alive(pid) || typeof procStart !== "string" || !procStart.trim()) return false;
|
|
12594
|
+
const want = squash(procStart);
|
|
12595
|
+
return ["UTC", "local"].some((zone) => {
|
|
12596
|
+
const got = deps.started(pid, zone);
|
|
12597
|
+
return got !== null && squash(got) === want;
|
|
12598
|
+
});
|
|
12599
|
+
}
|
|
12600
|
+
function localHolderPids(deps = {}) {
|
|
12601
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
|
|
12602
|
+
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
12603
|
+
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
12604
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
12605
|
+
const held = /* @__PURE__ */ new Map();
|
|
12606
|
+
let files;
|
|
12607
|
+
try {
|
|
12608
|
+
files = list(dir);
|
|
12609
|
+
} catch {
|
|
12610
|
+
return held;
|
|
12611
|
+
}
|
|
12612
|
+
for (const file of files) {
|
|
12613
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
12614
|
+
let row;
|
|
12615
|
+
try {
|
|
12616
|
+
row = JSON.parse(read(join6(dir, file)));
|
|
12617
|
+
} catch {
|
|
12618
|
+
continue;
|
|
12619
|
+
}
|
|
12620
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
12621
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
12622
|
+
if (!holds(pid, row.procStart, check)) continue;
|
|
12623
|
+
const slot = sessionSlot(row.sessionId);
|
|
12624
|
+
held.set(slot, [...held.get(slot) ?? [], { pid, proven: proven(pid, row.procStart, check) }]);
|
|
12625
|
+
}
|
|
12626
|
+
return held;
|
|
12627
|
+
}
|
|
12628
|
+
function psParent(pid) {
|
|
12629
|
+
try {
|
|
12630
|
+
const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
|
|
12631
|
+
const n = Number(out);
|
|
12632
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
12633
|
+
} catch {
|
|
12634
|
+
return null;
|
|
12635
|
+
}
|
|
12636
|
+
}
|
|
12637
|
+
function descendsFrom(pid, ancestor, parent = psParent) {
|
|
12638
|
+
let at = pid;
|
|
12639
|
+
for (let i = 0; i < 32 && at !== null && at > 1; i += 1) {
|
|
12640
|
+
if (at === ancestor) return true;
|
|
12641
|
+
at = parent(at);
|
|
12642
|
+
}
|
|
12643
|
+
return false;
|
|
12644
|
+
}
|
|
12469
12645
|
function localHolders(deps = {}) {
|
|
12470
12646
|
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
|
|
12471
|
-
const list = deps.list ?? ((d) =>
|
|
12647
|
+
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
12472
12648
|
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
12473
12649
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
12474
12650
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -12495,8 +12671,67 @@ function localHolders(deps = {}) {
|
|
|
12495
12671
|
return held;
|
|
12496
12672
|
}
|
|
12497
12673
|
|
|
12674
|
+
// src/end.ts
|
|
12675
|
+
var GRACE_MS = 5e3;
|
|
12676
|
+
async function endTerminal(pid, slot, deps) {
|
|
12677
|
+
try {
|
|
12678
|
+
deps.kill(pid, "SIGTERM");
|
|
12679
|
+
} catch (e) {
|
|
12680
|
+
return { ok: false, kind: "terminal", pid, why: `SIGTERM refused: ${e.message}`.slice(0, 200) };
|
|
12681
|
+
}
|
|
12682
|
+
const grace = deps.graceMs ?? GRACE_MS;
|
|
12683
|
+
for (let waited = 0; waited < grace; waited += 250) {
|
|
12684
|
+
if (!deps.alive(pid)) {
|
|
12685
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGTERM" })}`);
|
|
12686
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGTERM" };
|
|
12687
|
+
}
|
|
12688
|
+
await deps.sleep(250);
|
|
12689
|
+
}
|
|
12690
|
+
try {
|
|
12691
|
+
deps.kill(pid, "SIGKILL");
|
|
12692
|
+
} catch (e) {
|
|
12693
|
+
return { ok: false, kind: "terminal", pid, why: `SIGKILL refused: ${e.message}`.slice(0, 200) };
|
|
12694
|
+
}
|
|
12695
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGKILL" })}`);
|
|
12696
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGKILL" };
|
|
12697
|
+
}
|
|
12698
|
+
async function endSession(end, deps) {
|
|
12699
|
+
const slots = deps.slotsFor(end.agent);
|
|
12700
|
+
if (!slots.length) return { ok: false, why: "not_on_this_host" };
|
|
12701
|
+
let ended2 = null;
|
|
12702
|
+
if (end.which === "harness" || end.which === "any") {
|
|
12703
|
+
for (const slot of slots) {
|
|
12704
|
+
const run = deps.ownRun(slot);
|
|
12705
|
+
if (!run) continue;
|
|
12706
|
+
run.stop();
|
|
12707
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, kind: "harness", signal: "stop" })}`);
|
|
12708
|
+
ended2 = { ok: true, kind: "harness", signal: "stop" };
|
|
12709
|
+
}
|
|
12710
|
+
if (end.which === "harness" && !ended2) return { ok: false, kind: "harness", why: "no_harness_copy_here" };
|
|
12711
|
+
}
|
|
12712
|
+
if (end.which === "terminal" || end.which === "any") {
|
|
12713
|
+
let terminal = null;
|
|
12714
|
+
let unproven = null;
|
|
12715
|
+
for (const slot of slots) {
|
|
12716
|
+
for (const { pid, proven: proven2 } of deps.pidsFor(slot).filter((p) => !deps.isOwn(p.pid))) {
|
|
12717
|
+
if (!proven2) {
|
|
12718
|
+
deps.log(`session_end_refused ${JSON.stringify({ sessionId: slot, pid, why: "no_proc_start" })}`);
|
|
12719
|
+
unproven = { ok: false, kind: "terminal", pid, why: "no_proc_start" };
|
|
12720
|
+
continue;
|
|
12721
|
+
}
|
|
12722
|
+
terminal = await endTerminal(pid, slot, deps);
|
|
12723
|
+
if (!terminal.ok) return terminal;
|
|
12724
|
+
}
|
|
12725
|
+
}
|
|
12726
|
+
if (terminal) return terminal;
|
|
12727
|
+
if (unproven) return unproven;
|
|
12728
|
+
if (end.which === "terminal") return { ok: false, kind: "terminal", why: "no_terminal_copy_here" };
|
|
12729
|
+
}
|
|
12730
|
+
return ended2 ?? { ok: false, why: "nothing_running_here" };
|
|
12731
|
+
}
|
|
12732
|
+
|
|
12498
12733
|
// src/owner.ts
|
|
12499
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as
|
|
12734
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
12500
12735
|
import { homedir as homedir7 } from "os";
|
|
12501
12736
|
import { join as join7 } from "path";
|
|
12502
12737
|
var HEAD_BYTES = 64e3;
|
|
@@ -12515,7 +12750,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
12515
12750
|
if (!m) return null;
|
|
12516
12751
|
const prefix = m[1];
|
|
12517
12752
|
const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
|
|
12518
|
-
const list = deps.list ?? ((dir) =>
|
|
12753
|
+
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
12519
12754
|
const head = deps.head ?? readHead;
|
|
12520
12755
|
let folders;
|
|
12521
12756
|
try {
|
|
@@ -13891,7 +14126,20 @@ var AskInputSchema2 = external_exports.object({
|
|
|
13891
14126
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
13892
14127
|
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
13893
14128
|
),
|
|
13894
|
-
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
14129
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
|
|
14130
|
+
// A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
|
|
14131
|
+
// checklist mode of answering"). The answer shape was taken off this surface with the wire form
|
|
14132
|
+
// (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
|
|
14133
|
+
// only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
|
|
14134
|
+
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
14135
|
+
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
14136
|
+
// not: whether its own options exclude each other.
|
|
14137
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
14138
|
+
'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
|
|
14139
|
+
),
|
|
14140
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
14141
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
14142
|
+
)
|
|
13895
14143
|
}).strict();
|
|
13896
14144
|
var StartContactSchema2 = external_exports.object({
|
|
13897
14145
|
asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
@@ -13900,7 +14148,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
13900
14148
|
}).strict();
|
|
13901
14149
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
13902
14150
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
13903
|
-
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
14151
|
+
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'); when the options are independent and they may want several, pass select:'many' and they get a checklist. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
13904
14152
|
var CreateGoalSchema2 = external_exports.object({
|
|
13905
14153
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
13906
14154
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -13956,11 +14204,11 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
13956
14204
|
history: external_exports.boolean().optional(),
|
|
13957
14205
|
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
13958
14206
|
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
13959
|
-
* between two of them named. Off by default; `
|
|
14207
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
13960
14208
|
diagnose: external_exports.boolean().optional()
|
|
13961
14209
|
}).strict();
|
|
13962
|
-
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
13963
|
-
var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
14210
|
+
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
14211
|
+
var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
13964
14212
|
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
13965
14213
|
var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
13966
14214
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -13989,6 +14237,7 @@ function entryWords(entry) {
|
|
|
13989
14237
|
return entry.sources.map((source) => source.text).join("\n");
|
|
13990
14238
|
}
|
|
13991
14239
|
var LIVE_MS2 = 3 * 6e4;
|
|
14240
|
+
var WORKING_MS2 = 30 * 6e4;
|
|
13992
14241
|
var ContextSchema2 = external_exports.object({
|
|
13993
14242
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
13994
14243
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -14059,7 +14308,7 @@ var AttentionSchema2 = external_exports.object({
|
|
|
14059
14308
|
points: external_exports.array(external_exports.string()).nullable(),
|
|
14060
14309
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
14061
14310
|
blocking: external_exports.boolean(),
|
|
14062
|
-
/** Reserved (
|
|
14311
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
14063
14312
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
14064
14313
|
deadline: external_exports.string().datetime().nullable().optional()
|
|
14065
14314
|
});
|
|
@@ -14110,7 +14359,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
14110
14359
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
14111
14360
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
14112
14361
|
// the ask into topics and gives each one at most three sentences and one question
|
|
14113
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
14362
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
14114
14363
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
14115
14364
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
14116
14365
|
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
@@ -14187,7 +14436,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
|
|
|
14187
14436
|
external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
|
|
14188
14437
|
external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
|
|
14189
14438
|
external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
|
|
14190
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
14439
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
14191
14440
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
14192
14441
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
14193
14442
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -14236,7 +14485,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
14236
14485
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
14237
14486
|
* a sentence with a hole in it is worse than no sentence. */
|
|
14238
14487
|
note: external_exports.string().optional(),
|
|
14239
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
14488
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
14240
14489
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
14241
14490
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
14242
14491
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -14332,10 +14581,11 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
14332
14581
|
agentId: external_exports.string().optional(),
|
|
14333
14582
|
select: SelectShapeSchema2.optional(),
|
|
14334
14583
|
options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14584
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
14585
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
14586
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
14587
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
14588
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
14339
14589
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
14340
14590
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
14341
14591
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -14370,7 +14620,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
14370
14620
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
14371
14621
|
* `createdAt`. */
|
|
14372
14622
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
14373
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
14623
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
14374
14624
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
14375
14625
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
14376
14626
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -14431,7 +14681,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
14431
14681
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
14432
14682
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
14433
14683
|
onCall: external_exports.literal(true).optional(),
|
|
14434
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
14684
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
14435
14685
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
14436
14686
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
14437
14687
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -14462,7 +14712,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
14462
14712
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
14463
14713
|
),
|
|
14464
14714
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
14465
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
14715
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
14466
14716
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
14467
14717
|
blocking: external_exports.boolean().default(false),
|
|
14468
14718
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -14536,9 +14786,17 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
14536
14786
|
banner: external_exports.boolean(),
|
|
14537
14787
|
push: external_exports.boolean()
|
|
14538
14788
|
}),
|
|
14789
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
14790
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
14791
|
+
* what people read (owner, 2026-09-30). */
|
|
14539
14792
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
14540
|
-
silentPush
|
|
14541
|
-
|
|
14793
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
14794
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
14795
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
14796
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
14797
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
14798
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
14799
|
+
* next dead field for the same reason. */
|
|
14542
14800
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
14543
14801
|
improveConsent: external_exports.boolean(),
|
|
14544
14802
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -14549,12 +14807,16 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
14549
14807
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
14550
14808
|
* write, 'hosted' on read (see store.ts). */
|
|
14551
14809
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
14552
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
14810
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
14553
14811
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
14554
14812
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
14555
14813
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
14556
14814
|
* read (see store.ts). */
|
|
14557
14815
|
talk: external_exports.boolean().optional(),
|
|
14816
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
14817
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
14818
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
14819
|
+
callDiagnostics: external_exports.boolean().optional(),
|
|
14558
14820
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
14559
14821
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
14560
14822
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -14563,9 +14825,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
14563
14825
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
14564
14826
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
14565
14827
|
voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
|
|
14566
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
14567
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
14568
|
-
pstnCalls: external_exports.boolean().optional(),
|
|
14569
14828
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
14570
14829
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
14571
14830
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -14609,7 +14868,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14609
14868
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
14610
14869
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
14611
14870
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
14612
|
-
*
|
|
14871
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
14613
14872
|
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
14614
14873
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
14615
14874
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -14627,7 +14886,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14627
14886
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
14628
14887
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
14629
14888
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
14630
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
14889
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
14631
14890
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
14632
14891
|
* set once must not overrule a rule they set about one agent. */
|
|
14633
14892
|
reach: NotifyLevelSchema2.nullable().optional(),
|
|
@@ -14638,7 +14897,14 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14638
14897
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
14639
14898
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
14640
14899
|
lastSeenAt: external_exports.string().datetime().nullable().optional(),
|
|
14641
|
-
/**
|
|
14900
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
14901
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
14902
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
14903
|
+
lastWorkedAt: external_exports.string().datetime().nullable().optional(),
|
|
14904
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
14905
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
14906
|
+
oldestReadyAt: external_exports.string().datetime().nullable().optional(),
|
|
14907
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
14642
14908
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
14643
14909
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
14644
14910
|
runtime: external_exports.object({
|
|
@@ -14646,7 +14912,12 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14646
14912
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
14647
14913
|
version: external_exports.string().optional(),
|
|
14648
14914
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
14649
|
-
workspaces: external_exports.array(external_exports.string()).optional()
|
|
14915
|
+
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
14916
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
14917
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
14918
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
14919
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
14920
|
+
repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
|
|
14650
14921
|
}).optional(),
|
|
14651
14922
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
14652
14923
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -14659,7 +14930,8 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14659
14930
|
});
|
|
14660
14931
|
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
14661
14932
|
var AgentLedgerSchema2 = external_exports.object({
|
|
14662
|
-
agent
|
|
14933
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
14934
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
14663
14935
|
/** Its own questions you have not answered. */
|
|
14664
14936
|
asks: external_exports.array(LedgerItemSchema2),
|
|
14665
14937
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -14729,7 +15001,7 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
14729
15001
|
* for a settled question whose reply carried nothing readable. */
|
|
14730
15002
|
answer: external_exports.string().nullable().default(null),
|
|
14731
15003
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
14732
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
15004
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
14733
15005
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
14734
15006
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
14735
15007
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -14821,7 +15093,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
14821
15093
|
said: external_exports.string(),
|
|
14822
15094
|
at: external_exports.string(),
|
|
14823
15095
|
entryId: external_exports.string()
|
|
14824
|
-
}).nullable().optional()
|
|
15096
|
+
}).nullable().optional(),
|
|
15097
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
15098
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
15099
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
15100
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
15101
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
15102
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
15103
|
+
*
|
|
15104
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
15105
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
15106
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
15107
|
+
* Entry with no `about_id` is in neither. */
|
|
15108
|
+
lastPersonAt: external_exports.string().nullable().optional()
|
|
14825
15109
|
});
|
|
14826
15110
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
14827
15111
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -14851,7 +15135,7 @@ var NoteSchema2 = external_exports.object({
|
|
|
14851
15135
|
assignee: external_exports.string().nullable(),
|
|
14852
15136
|
/** The request thread minted at assignment; null until assigned. */
|
|
14853
15137
|
parentId: external_exports.string().nullable(),
|
|
14854
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
15138
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
14855
15139
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
14856
15140
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
14857
15141
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -15111,6 +15395,31 @@ var SnapshotSchema2 = external_exports.object({
|
|
|
15111
15395
|
computers: external_exports.array(ComputerRowSchema2).nullable()
|
|
15112
15396
|
}).nullable()
|
|
15113
15397
|
});
|
|
15398
|
+
var CallRecapSchema2 = external_exports.object({
|
|
15399
|
+
call: external_exports.object({
|
|
15400
|
+
status: external_exports.string(),
|
|
15401
|
+
startedAt: external_exports.string(),
|
|
15402
|
+
durationMs: external_exports.number().nullable(),
|
|
15403
|
+
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
15404
|
+
}),
|
|
15405
|
+
topics: external_exports.array(external_exports.object({
|
|
15406
|
+
goalId: external_exports.string().uuid(),
|
|
15407
|
+
title: external_exports.string(),
|
|
15408
|
+
owner: external_exports.string(),
|
|
15409
|
+
state: external_exports.string(),
|
|
15410
|
+
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
15411
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
15412
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
15413
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
15414
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
15415
|
+
* anything that is not an answer. */
|
|
15416
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
15417
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
15418
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
|
|
15419
|
+
})),
|
|
15420
|
+
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
15421
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
15422
|
+
});
|
|
15114
15423
|
|
|
15115
15424
|
// src/paigy/activity.ts
|
|
15116
15425
|
function shortenPaths(s) {
|
|
@@ -15411,7 +15720,7 @@ var AcpDriver = class {
|
|
|
15411
15720
|
var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
15412
15721
|
|
|
15413
15722
|
// src/paigy/inbox.ts
|
|
15414
|
-
import { mkdirSync as mkdirSync3, readdirSync as
|
|
15723
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
15415
15724
|
import { join as join8 } from "path";
|
|
15416
15725
|
import { homedir as homedir8 } from "os";
|
|
15417
15726
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
@@ -15423,7 +15732,7 @@ function openInbox(dir) {
|
|
|
15423
15732
|
const receipt = (name) => join8(dir, "taken", `${name}.note`);
|
|
15424
15733
|
const taken = (name) => {
|
|
15425
15734
|
try {
|
|
15426
|
-
return
|
|
15735
|
+
return statSync3(receipt(name)).mtimeMs;
|
|
15427
15736
|
} catch (error) {
|
|
15428
15737
|
if (missing(error)) return null;
|
|
15429
15738
|
throw error;
|
|
@@ -15467,7 +15776,7 @@ var alive = (pid) => {
|
|
|
15467
15776
|
};
|
|
15468
15777
|
function inboxFor(root = INBOX_ROOT) {
|
|
15469
15778
|
try {
|
|
15470
|
-
for (const name of
|
|
15779
|
+
for (const name of readdirSync4(root)) {
|
|
15471
15780
|
const pid = Number(name.split("-")[0]);
|
|
15472
15781
|
if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join8(root, name), { recursive: true, force: true });
|
|
15473
15782
|
}
|
|
@@ -16494,7 +16803,9 @@ function startHost(opts) {
|
|
|
16494
16803
|
void heartbeat({
|
|
16495
16804
|
version: VERSION,
|
|
16496
16805
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
16497
|
-
workspaces: listWorkspaces(opts.wsDeps)
|
|
16806
|
+
workspaces: listWorkspaces(opts.wsDeps),
|
|
16807
|
+
// The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
|
|
16808
|
+
repos: listRepos(opts.wsDeps)
|
|
16498
16809
|
}, asHost).catch(() => {
|
|
16499
16810
|
});
|
|
16500
16811
|
void listenSlots().then(sweepSlots).catch(() => {
|
|
@@ -16540,6 +16851,31 @@ function startHost(opts) {
|
|
|
16540
16851
|
void refreshIdentities();
|
|
16541
16852
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
16542
16853
|
}
|
|
16854
|
+
const ends = await claimSessionEnds(asHost).catch(() => []);
|
|
16855
|
+
for (const end of ends) {
|
|
16856
|
+
const result = await endSession(end, {
|
|
16857
|
+
slotsFor: (agent2) => listSlots().filter((slot) => slot !== "Desktop" && slotIdentity(slot).tokenId === agent2),
|
|
16858
|
+
pidsFor: (slot) => localHolderPids().get(slot) ?? [],
|
|
16859
|
+
ownRun: (slot) => runs.get(runKey(slot))?.run ?? null,
|
|
16860
|
+
isOwn: (pid) => descendsFrom(pid, process.pid),
|
|
16861
|
+
kill: (pid, signal) => {
|
|
16862
|
+
process.kill(pid, signal);
|
|
16863
|
+
},
|
|
16864
|
+
alive: (pid) => {
|
|
16865
|
+
try {
|
|
16866
|
+
process.kill(pid, 0);
|
|
16867
|
+
return true;
|
|
16868
|
+
} catch (e) {
|
|
16869
|
+
return e.code === "EPERM";
|
|
16870
|
+
}
|
|
16871
|
+
},
|
|
16872
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
16873
|
+
log: opts.log
|
|
16874
|
+
}).catch((e) => ({ ok: false, why: `end_threw: ${e.message}`.slice(0, 200) }));
|
|
16875
|
+
opts.log(result.ok ? `\u25A0 ended ${end.which} copy of ${end.agent.slice(0, 8)}` : `\u25CC end of ${end.agent.slice(0, 8)} not done here: ${result.why}`);
|
|
16876
|
+
await finishSessionEnd(end.endId, result, asHost).catch(() => {
|
|
16877
|
+
});
|
|
16878
|
+
}
|
|
16543
16879
|
}
|
|
16544
16880
|
async function presenceMap() {
|
|
16545
16881
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -16564,6 +16900,14 @@ function startHost(opts) {
|
|
|
16564
16900
|
const key = runKey(slot);
|
|
16565
16901
|
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
16566
16902
|
const live = runs.get(key);
|
|
16903
|
+
if (live) {
|
|
16904
|
+
const theirs = (localHolderPids().get(slot) ?? []).map((p) => p.pid).filter((pid) => !descendsFrom(pid, process.pid));
|
|
16905
|
+
if (theirs.length) {
|
|
16906
|
+
opts.log(`session_stood_back ${JSON.stringify({ sessionId: slot, livePid: theirs[0] })} \u2014 a terminal runs this conversation; stopping this host's copy`);
|
|
16907
|
+
live.run.stop();
|
|
16908
|
+
return;
|
|
16909
|
+
}
|
|
16910
|
+
}
|
|
16567
16911
|
const tokenId = slotIdentity(slot).tokenId;
|
|
16568
16912
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
16569
16913
|
const holder = holders.get(slot);
|
|
@@ -16849,7 +17193,7 @@ ipcMain.handle("paigy:status", () => ({
|
|
|
16849
17193
|
paired: Boolean(deviceToken()),
|
|
16850
17194
|
device: Boolean(readToken(DEVICE_SLOT)),
|
|
16851
17195
|
sessions: host?.sessions().map(([, label]) => label) ?? [],
|
|
16852
|
-
// The party (companion.md / party design.md): who lives on this machine and what
|
|
17196
|
+
// The party (docs/clients/desktop/companion.md / party design.md): who lives on this machine and what
|
|
16853
17197
|
// each one is doing THIS SECOND — the one thing the laptop knows better than the phone.
|
|
16854
17198
|
roster: host?.roster() ?? readHostState()?.roster ?? []
|
|
16855
17199
|
}));
|