@paigy/harness 0.3.30 → 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/dist/cli.js +224 -27
- package/dist/main.js +224 -27
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -23858,6 +23858,22 @@ async function claimSessions(opts = {}) {
|
|
|
23858
23858
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
23859
23859
|
return (await res.json()).sessions;
|
|
23860
23860
|
}
|
|
23861
|
+
async function claimSessionEnds(opts = {}) {
|
|
23862
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
|
|
23863
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23864
|
+
// the HOST's identity
|
|
23865
|
+
}));
|
|
23866
|
+
if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
|
|
23867
|
+
return (await res.json()).ends ?? [];
|
|
23868
|
+
}
|
|
23869
|
+
async function finishSessionEnd(endId, result, opts = {}) {
|
|
23870
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
|
|
23871
|
+
method: "POST",
|
|
23872
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23873
|
+
body: JSON.stringify({ result })
|
|
23874
|
+
}));
|
|
23875
|
+
if (!res.ok) await fail("finish_session_end", res);
|
|
23876
|
+
}
|
|
23861
23877
|
async function recordDecision(decision, opts = {}) {
|
|
23862
23878
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
23863
23879
|
method: "POST",
|
|
@@ -23964,7 +23980,9 @@ async function contact(input, opts = {}) {
|
|
|
23964
23980
|
// this cannot see.
|
|
23965
23981
|
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
23966
23982
|
ask: a.ask,
|
|
23967
|
-
options: a.options ?? []
|
|
23983
|
+
options: a.options ?? [],
|
|
23984
|
+
...a.select ? { select: a.select } : {},
|
|
23985
|
+
...a.answers ? { answers: a.answers } : {}
|
|
23968
23986
|
}));
|
|
23969
23987
|
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
23970
23988
|
method: "POST",
|
|
@@ -24601,7 +24619,20 @@ var init_dist = __esm({
|
|
|
24601
24619
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
24602
24620
|
"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."
|
|
24603
24621
|
),
|
|
24604
|
-
options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
|
|
24622
|
+
options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
|
|
24623
|
+
// A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
|
|
24624
|
+
// checklist mode of answering"). The answer shape was taken off this surface with the wire form
|
|
24625
|
+
// (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
|
|
24626
|
+
// only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
|
|
24627
|
+
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
24628
|
+
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
24629
|
+
// not: whether its own options exclude each other.
|
|
24630
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
24631
|
+
'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.'
|
|
24632
|
+
),
|
|
24633
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
24634
|
+
"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."
|
|
24635
|
+
)
|
|
24605
24636
|
}).strict();
|
|
24606
24637
|
StartContactSchema = external_exports.object({
|
|
24607
24638
|
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."),
|
|
@@ -24610,7 +24641,7 @@ var init_dist = __esm({
|
|
|
24610
24641
|
}).strict();
|
|
24611
24642
|
ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
24612
24643
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
24613
|
-
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.";
|
|
24644
|
+
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.";
|
|
24614
24645
|
CreateGoalSchema = external_exports.object({
|
|
24615
24646
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
24616
24647
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -24669,8 +24700,8 @@ var init_dist = __esm({
|
|
|
24669
24700
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
24670
24701
|
diagnose: external_exports.boolean().optional()
|
|
24671
24702
|
}).strict();
|
|
24672
|
-
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.";
|
|
24673
|
-
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.`;
|
|
24703
|
+
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.";
|
|
24704
|
+
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.`;
|
|
24674
24705
|
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.";
|
|
24675
24706
|
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.";
|
|
24676
24707
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
@@ -25237,12 +25268,13 @@ var init_dist = __esm({
|
|
|
25237
25268
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
25238
25269
|
* what people read (owner, 2026-09-30). */
|
|
25239
25270
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
25240
|
-
/**
|
|
25241
|
-
*
|
|
25242
|
-
*
|
|
25243
|
-
* wanting
|
|
25244
|
-
*
|
|
25245
|
-
|
|
25271
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
25272
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
25273
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
25274
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
25275
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
25276
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
25277
|
+
* next dead field for the same reason. */
|
|
25246
25278
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
25247
25279
|
improveConsent: external_exports.boolean(),
|
|
25248
25280
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -25376,7 +25408,8 @@ var init_dist = __esm({
|
|
|
25376
25408
|
});
|
|
25377
25409
|
LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
25378
25410
|
AgentLedgerSchema = external_exports.object({
|
|
25379
|
-
agent
|
|
25411
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
25412
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
25380
25413
|
/** Its own questions you have not answered. */
|
|
25381
25414
|
asks: external_exports.array(LedgerItemSchema),
|
|
25382
25415
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -25543,7 +25576,7 @@ var init_dist = __esm({
|
|
|
25543
25576
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
25544
25577
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
25545
25578
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
25546
|
-
*
|
|
25579
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
25547
25580
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
25548
25581
|
*
|
|
25549
25582
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -25860,7 +25893,9 @@ var init_dist = __esm({
|
|
|
25860
25893
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
25861
25894
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
25862
25895
|
* anything that is not an answer. */
|
|
25863
|
-
|
|
25896
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
25897
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
25898
|
+
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() }))
|
|
25864
25899
|
})),
|
|
25865
25900
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
25866
25901
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -32981,7 +33016,20 @@ var AskInputSchema2 = external_exports.object({
|
|
|
32981
33016
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32982
33017
|
"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."
|
|
32983
33018
|
),
|
|
32984
|
-
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
33019
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
|
|
33020
|
+
// A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
|
|
33021
|
+
// checklist mode of answering"). The answer shape was taken off this surface with the wire form
|
|
33022
|
+
// (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
|
|
33023
|
+
// only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
|
|
33024
|
+
// as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
|
|
33025
|
+
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
33026
|
+
// not: whether its own options exclude each other.
|
|
33027
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
33028
|
+
'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.'
|
|
33029
|
+
),
|
|
33030
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
33031
|
+
"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."
|
|
33032
|
+
)
|
|
32985
33033
|
}).strict();
|
|
32986
33034
|
var StartContactSchema2 = external_exports.object({
|
|
32987
33035
|
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."),
|
|
@@ -32990,7 +33038,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
32990
33038
|
}).strict();
|
|
32991
33039
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32992
33040
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32993
|
-
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.";
|
|
33041
|
+
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.";
|
|
32994
33042
|
var CreateGoalSchema2 = external_exports.object({
|
|
32995
33043
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32996
33044
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -33049,8 +33097,8 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
33049
33097
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
33050
33098
|
diagnose: external_exports.boolean().optional()
|
|
33051
33099
|
}).strict();
|
|
33052
|
-
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.";
|
|
33053
|
-
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.`;
|
|
33100
|
+
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.";
|
|
33101
|
+
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.`;
|
|
33054
33102
|
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.";
|
|
33055
33103
|
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.";
|
|
33056
33104
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -33632,12 +33680,13 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33632
33680
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
33633
33681
|
* what people read (owner, 2026-09-30). */
|
|
33634
33682
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
33635
|
-
/**
|
|
33636
|
-
*
|
|
33637
|
-
*
|
|
33638
|
-
* wanting
|
|
33639
|
-
*
|
|
33640
|
-
silentPush
|
|
33683
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
33684
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
33685
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
33686
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
33687
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
33688
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
33689
|
+
* next dead field for the same reason. */
|
|
33641
33690
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
33642
33691
|
improveConsent: external_exports.boolean(),
|
|
33643
33692
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -33771,7 +33820,8 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33771
33820
|
});
|
|
33772
33821
|
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
33773
33822
|
var AgentLedgerSchema2 = external_exports.object({
|
|
33774
|
-
agent
|
|
33823
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
33824
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
33775
33825
|
/** Its own questions you have not answered. */
|
|
33776
33826
|
asks: external_exports.array(LedgerItemSchema2),
|
|
33777
33827
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -33938,7 +33988,7 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33938
33988
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
33939
33989
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
33940
33990
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
33941
|
-
*
|
|
33991
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
33942
33992
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
33943
33993
|
*
|
|
33944
33994
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -34253,7 +34303,9 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
34253
34303
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
34254
34304
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
34255
34305
|
* anything that is not an answer. */
|
|
34256
|
-
|
|
34306
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
34307
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
34308
|
+
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() }))
|
|
34257
34309
|
})),
|
|
34258
34310
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
34259
34311
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -35093,6 +35145,59 @@ function holds(pid, procStart, deps) {
|
|
|
35093
35145
|
}
|
|
35094
35146
|
return false;
|
|
35095
35147
|
}
|
|
35148
|
+
function proven(pid, procStart, deps) {
|
|
35149
|
+
if (!deps.alive(pid) || typeof procStart !== "string" || !procStart.trim()) return false;
|
|
35150
|
+
const want = squash(procStart);
|
|
35151
|
+
return ["UTC", "local"].some((zone) => {
|
|
35152
|
+
const got = deps.started(pid, zone);
|
|
35153
|
+
return got !== null && squash(got) === want;
|
|
35154
|
+
});
|
|
35155
|
+
}
|
|
35156
|
+
function localHolderPids(deps = {}) {
|
|
35157
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
|
|
35158
|
+
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
35159
|
+
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
35160
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
35161
|
+
const held = /* @__PURE__ */ new Map();
|
|
35162
|
+
let files;
|
|
35163
|
+
try {
|
|
35164
|
+
files = list(dir);
|
|
35165
|
+
} catch {
|
|
35166
|
+
return held;
|
|
35167
|
+
}
|
|
35168
|
+
for (const file of files) {
|
|
35169
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
35170
|
+
let row;
|
|
35171
|
+
try {
|
|
35172
|
+
row = JSON.parse(read(join6(dir, file)));
|
|
35173
|
+
} catch {
|
|
35174
|
+
continue;
|
|
35175
|
+
}
|
|
35176
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
35177
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
35178
|
+
if (!holds(pid, row.procStart, check)) continue;
|
|
35179
|
+
const slot = sessionSlot(row.sessionId);
|
|
35180
|
+
held.set(slot, [...held.get(slot) ?? [], { pid, proven: proven(pid, row.procStart, check) }]);
|
|
35181
|
+
}
|
|
35182
|
+
return held;
|
|
35183
|
+
}
|
|
35184
|
+
function psParent(pid) {
|
|
35185
|
+
try {
|
|
35186
|
+
const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
|
|
35187
|
+
const n = Number(out);
|
|
35188
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
35189
|
+
} catch {
|
|
35190
|
+
return null;
|
|
35191
|
+
}
|
|
35192
|
+
}
|
|
35193
|
+
function descendsFrom(pid, ancestor, parent = psParent) {
|
|
35194
|
+
let at = pid;
|
|
35195
|
+
for (let i = 0; i < 32 && at !== null && at > 1; i += 1) {
|
|
35196
|
+
if (at === ancestor) return true;
|
|
35197
|
+
at = parent(at);
|
|
35198
|
+
}
|
|
35199
|
+
return false;
|
|
35200
|
+
}
|
|
35096
35201
|
function localHolders(deps = {}) {
|
|
35097
35202
|
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
|
|
35098
35203
|
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
@@ -35122,6 +35227,65 @@ function localHolders(deps = {}) {
|
|
|
35122
35227
|
return held;
|
|
35123
35228
|
}
|
|
35124
35229
|
|
|
35230
|
+
// src/end.ts
|
|
35231
|
+
var GRACE_MS = 5e3;
|
|
35232
|
+
async function endTerminal(pid, slot, deps) {
|
|
35233
|
+
try {
|
|
35234
|
+
deps.kill(pid, "SIGTERM");
|
|
35235
|
+
} catch (e) {
|
|
35236
|
+
return { ok: false, kind: "terminal", pid, why: `SIGTERM refused: ${e.message}`.slice(0, 200) };
|
|
35237
|
+
}
|
|
35238
|
+
const grace = deps.graceMs ?? GRACE_MS;
|
|
35239
|
+
for (let waited = 0; waited < grace; waited += 250) {
|
|
35240
|
+
if (!deps.alive(pid)) {
|
|
35241
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGTERM" })}`);
|
|
35242
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGTERM" };
|
|
35243
|
+
}
|
|
35244
|
+
await deps.sleep(250);
|
|
35245
|
+
}
|
|
35246
|
+
try {
|
|
35247
|
+
deps.kill(pid, "SIGKILL");
|
|
35248
|
+
} catch (e) {
|
|
35249
|
+
return { ok: false, kind: "terminal", pid, why: `SIGKILL refused: ${e.message}`.slice(0, 200) };
|
|
35250
|
+
}
|
|
35251
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGKILL" })}`);
|
|
35252
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGKILL" };
|
|
35253
|
+
}
|
|
35254
|
+
async function endSession(end, deps) {
|
|
35255
|
+
const slots = deps.slotsFor(end.agent);
|
|
35256
|
+
if (!slots.length) return { ok: false, why: "not_on_this_host" };
|
|
35257
|
+
let ended2 = null;
|
|
35258
|
+
if (end.which === "harness" || end.which === "any") {
|
|
35259
|
+
for (const slot of slots) {
|
|
35260
|
+
const run = deps.ownRun(slot);
|
|
35261
|
+
if (!run) continue;
|
|
35262
|
+
run.stop();
|
|
35263
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, kind: "harness", signal: "stop" })}`);
|
|
35264
|
+
ended2 = { ok: true, kind: "harness", signal: "stop" };
|
|
35265
|
+
}
|
|
35266
|
+
if (end.which === "harness" && !ended2) return { ok: false, kind: "harness", why: "no_harness_copy_here" };
|
|
35267
|
+
}
|
|
35268
|
+
if (end.which === "terminal" || end.which === "any") {
|
|
35269
|
+
let terminal = null;
|
|
35270
|
+
let unproven = null;
|
|
35271
|
+
for (const slot of slots) {
|
|
35272
|
+
for (const { pid, proven: proven2 } of deps.pidsFor(slot).filter((p) => !deps.isOwn(p.pid))) {
|
|
35273
|
+
if (!proven2) {
|
|
35274
|
+
deps.log(`session_end_refused ${JSON.stringify({ sessionId: slot, pid, why: "no_proc_start" })}`);
|
|
35275
|
+
unproven = { ok: false, kind: "terminal", pid, why: "no_proc_start" };
|
|
35276
|
+
continue;
|
|
35277
|
+
}
|
|
35278
|
+
terminal = await endTerminal(pid, slot, deps);
|
|
35279
|
+
if (!terminal.ok) return terminal;
|
|
35280
|
+
}
|
|
35281
|
+
}
|
|
35282
|
+
if (terminal) return terminal;
|
|
35283
|
+
if (unproven) return unproven;
|
|
35284
|
+
if (end.which === "terminal") return { ok: false, kind: "terminal", why: "no_terminal_copy_here" };
|
|
35285
|
+
}
|
|
35286
|
+
return ended2 ?? { ok: false, why: "nothing_running_here" };
|
|
35287
|
+
}
|
|
35288
|
+
|
|
35125
35289
|
// src/owner.ts
|
|
35126
35290
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
|
|
35127
35291
|
import { homedir as homedir8 } from "os";
|
|
@@ -35425,6 +35589,31 @@ function startHost(opts) {
|
|
|
35425
35589
|
void refreshIdentities();
|
|
35426
35590
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
35427
35591
|
}
|
|
35592
|
+
const ends = await claimSessionEnds(asHost).catch(() => []);
|
|
35593
|
+
for (const end of ends) {
|
|
35594
|
+
const result = await endSession(end, {
|
|
35595
|
+
slotsFor: (agent2) => listSlots().filter((slot) => slot !== "Desktop" && slotIdentity(slot).tokenId === agent2),
|
|
35596
|
+
pidsFor: (slot) => localHolderPids().get(slot) ?? [],
|
|
35597
|
+
ownRun: (slot) => runs.get(runKey(slot))?.run ?? null,
|
|
35598
|
+
isOwn: (pid) => descendsFrom(pid, process.pid),
|
|
35599
|
+
kill: (pid, signal) => {
|
|
35600
|
+
process.kill(pid, signal);
|
|
35601
|
+
},
|
|
35602
|
+
alive: (pid) => {
|
|
35603
|
+
try {
|
|
35604
|
+
process.kill(pid, 0);
|
|
35605
|
+
return true;
|
|
35606
|
+
} catch (e) {
|
|
35607
|
+
return e.code === "EPERM";
|
|
35608
|
+
}
|
|
35609
|
+
},
|
|
35610
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
35611
|
+
log: opts.log
|
|
35612
|
+
}).catch((e) => ({ ok: false, why: `end_threw: ${e.message}`.slice(0, 200) }));
|
|
35613
|
+
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}`);
|
|
35614
|
+
await finishSessionEnd(end.endId, result, asHost).catch(() => {
|
|
35615
|
+
});
|
|
35616
|
+
}
|
|
35428
35617
|
}
|
|
35429
35618
|
async function presenceMap() {
|
|
35430
35619
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -35449,6 +35638,14 @@ function startHost(opts) {
|
|
|
35449
35638
|
const key = runKey(slot);
|
|
35450
35639
|
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
35451
35640
|
const live = runs.get(key);
|
|
35641
|
+
if (live) {
|
|
35642
|
+
const theirs = (localHolderPids().get(slot) ?? []).map((p) => p.pid).filter((pid) => !descendsFrom(pid, process.pid));
|
|
35643
|
+
if (theirs.length) {
|
|
35644
|
+
opts.log(`session_stood_back ${JSON.stringify({ sessionId: slot, livePid: theirs[0] })} \u2014 a terminal runs this conversation; stopping this host's copy`);
|
|
35645
|
+
live.run.stop();
|
|
35646
|
+
return;
|
|
35647
|
+
}
|
|
35648
|
+
}
|
|
35452
35649
|
const tokenId = slotIdentity(slot).tokenId;
|
|
35453
35650
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
35454
35651
|
const holder = holders.get(slot);
|
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
|
|
@@ -10043,8 +10056,8 @@ var GetGoalSchema = external_exports.object({
|
|
|
10043
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();
|
|
@@ -10611,12 +10624,13 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10611
10624
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
10612
10625
|
* what people read (owner, 2026-09-30). */
|
|
10613
10626
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
10614
|
-
/**
|
|
10615
|
-
*
|
|
10616
|
-
*
|
|
10617
|
-
* wanting
|
|
10618
|
-
*
|
|
10619
|
-
silentPush
|
|
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. */
|
|
10620
10634
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
10621
10635
|
improveConsent: external_exports.boolean(),
|
|
10622
10636
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -10750,7 +10764,8 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10750
10764
|
});
|
|
10751
10765
|
var LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
10752
10766
|
var AgentLedgerSchema = external_exports.object({
|
|
10753
|
-
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() }),
|
|
10754
10769
|
/** Its own questions you have not answered. */
|
|
10755
10770
|
asks: external_exports.array(LedgerItemSchema),
|
|
10756
10771
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -10917,7 +10932,7 @@ var QueueItemSchema = external_exports.object({
|
|
|
10917
10932
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
10918
10933
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
10919
10934
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
10920
|
-
*
|
|
10935
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
10921
10936
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
10922
10937
|
*
|
|
10923
10938
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -11234,7 +11249,9 @@ var CallRecapSchema = external_exports.object({
|
|
|
11234
11249
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
11235
11250
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
11236
11251
|
* anything that is not an answer. */
|
|
11237
|
-
|
|
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() }))
|
|
11238
11255
|
})),
|
|
11239
11256
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
11240
11257
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -11433,6 +11450,22 @@ async function claimSessions(opts = {}) {
|
|
|
11433
11450
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
11434
11451
|
return (await res.json()).sessions;
|
|
11435
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
|
+
}
|
|
11436
11469
|
async function recordDecision(decision, opts = {}) {
|
|
11437
11470
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
11438
11471
|
method: "POST",
|
|
@@ -11556,7 +11589,9 @@ async function contact(input, opts = {}) {
|
|
|
11556
11589
|
// this cannot see.
|
|
11557
11590
|
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
11558
11591
|
ask: a.ask,
|
|
11559
|
-
options: a.options ?? []
|
|
11592
|
+
options: a.options ?? [],
|
|
11593
|
+
...a.select ? { select: a.select } : {},
|
|
11594
|
+
...a.answers ? { answers: a.answers } : {}
|
|
11560
11595
|
}));
|
|
11561
11596
|
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
11562
11597
|
method: "POST",
|
|
@@ -12554,6 +12589,59 @@ function holds(pid, procStart, deps) {
|
|
|
12554
12589
|
}
|
|
12555
12590
|
return false;
|
|
12556
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
|
+
}
|
|
12557
12645
|
function localHolders(deps = {}) {
|
|
12558
12646
|
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
|
|
12559
12647
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
@@ -12583,6 +12671,65 @@ function localHolders(deps = {}) {
|
|
|
12583
12671
|
return held;
|
|
12584
12672
|
}
|
|
12585
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
|
+
|
|
12586
12733
|
// src/owner.ts
|
|
12587
12734
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
12588
12735
|
import { homedir as homedir7 } from "os";
|
|
@@ -13979,7 +14126,20 @@ var AskInputSchema2 = external_exports.object({
|
|
|
13979
14126
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
13980
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."
|
|
13981
14128
|
),
|
|
13982
|
-
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
|
+
)
|
|
13983
14143
|
}).strict();
|
|
13984
14144
|
var StartContactSchema2 = external_exports.object({
|
|
13985
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."),
|
|
@@ -13988,7 +14148,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
13988
14148
|
}).strict();
|
|
13989
14149
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
13990
14150
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
13991
|
-
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.";
|
|
13992
14152
|
var CreateGoalSchema2 = external_exports.object({
|
|
13993
14153
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
13994
14154
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -14047,8 +14207,8 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
14047
14207
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
14048
14208
|
diagnose: external_exports.boolean().optional()
|
|
14049
14209
|
}).strict();
|
|
14050
|
-
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.";
|
|
14051
|
-
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.`;
|
|
14052
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.";
|
|
14053
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.";
|
|
14054
14214
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -14630,12 +14790,13 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
14630
14790
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
14631
14791
|
* what people read (owner, 2026-09-30). */
|
|
14632
14792
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
14633
|
-
/**
|
|
14634
|
-
*
|
|
14635
|
-
*
|
|
14636
|
-
* wanting
|
|
14637
|
-
*
|
|
14638
|
-
silentPush
|
|
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. */
|
|
14639
14800
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
14640
14801
|
improveConsent: external_exports.boolean(),
|
|
14641
14802
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -14769,7 +14930,8 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14769
14930
|
});
|
|
14770
14931
|
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
14771
14932
|
var AgentLedgerSchema2 = external_exports.object({
|
|
14772
|
-
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() }),
|
|
14773
14935
|
/** Its own questions you have not answered. */
|
|
14774
14936
|
asks: external_exports.array(LedgerItemSchema2),
|
|
14775
14937
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -14936,7 +15098,7 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
14936
15098
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
14937
15099
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
14938
15100
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
14939
|
-
*
|
|
15101
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
14940
15102
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
14941
15103
|
*
|
|
14942
15104
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -15251,7 +15413,9 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
15251
15413
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
15252
15414
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
15253
15415
|
* anything that is not an answer. */
|
|
15254
|
-
|
|
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() }))
|
|
15255
15419
|
})),
|
|
15256
15420
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
15257
15421
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -16687,6 +16851,31 @@ function startHost(opts) {
|
|
|
16687
16851
|
void refreshIdentities();
|
|
16688
16852
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
16689
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
|
+
}
|
|
16690
16879
|
}
|
|
16691
16880
|
async function presenceMap() {
|
|
16692
16881
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -16711,6 +16900,14 @@ function startHost(opts) {
|
|
|
16711
16900
|
const key = runKey(slot);
|
|
16712
16901
|
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
16713
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
|
+
}
|
|
16714
16911
|
const tokenId = slotIdentity(slot).tokenId;
|
|
16715
16912
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
16716
16913
|
const holder = holders.get(slot);
|