@paigy/harness 0.3.29 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli.js +412 -68
- package/dist/main.js +415 -71
- package/package.json +1 -1
package/dist/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",
|
|
@@ -24186,7 +24204,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24186
24204
|
}
|
|
24187
24205
|
};
|
|
24188
24206
|
}
|
|
24189
|
-
var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24207
|
+
var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, WORKING_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24190
24208
|
var init_dist = __esm({
|
|
24191
24209
|
"../../packages/sdk/dist/index.js"() {
|
|
24192
24210
|
"use strict";
|
|
@@ -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
|
|
@@ -24666,11 +24697,11 @@ var init_dist = __esm({
|
|
|
24666
24697
|
history: external_exports.boolean().optional(),
|
|
24667
24698
|
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
24668
24699
|
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
24669
|
-
* between two of them named. Off by default; `
|
|
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();
|
|
@@ -24684,6 +24715,7 @@ var init_dist = __esm({
|
|
|
24684
24715
|
];
|
|
24685
24716
|
AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
24686
24717
|
LIVE_MS = 3 * 6e4;
|
|
24718
|
+
WORKING_MS = 30 * 6e4;
|
|
24687
24719
|
ContextSchema = external_exports.object({
|
|
24688
24720
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
24689
24721
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -24754,7 +24786,7 @@ var init_dist = __esm({
|
|
|
24754
24786
|
points: external_exports.array(external_exports.string()).nullable(),
|
|
24755
24787
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
24756
24788
|
blocking: external_exports.boolean(),
|
|
24757
|
-
/** Reserved (
|
|
24789
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
24758
24790
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
24759
24791
|
deadline: external_exports.string().datetime().nullable().optional()
|
|
24760
24792
|
});
|
|
@@ -24805,7 +24837,7 @@ var init_dist = __esm({
|
|
|
24805
24837
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
24806
24838
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
24807
24839
|
// the ask into topics and gives each one at most three sentences and one question
|
|
24808
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
24840
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
24809
24841
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
24810
24842
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
24811
24843
|
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
@@ -24882,7 +24914,7 @@ var init_dist = __esm({
|
|
|
24882
24914
|
external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
|
|
24883
24915
|
external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
|
|
24884
24916
|
external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
|
|
24885
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
24917
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
24886
24918
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
24887
24919
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
24888
24920
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -24931,7 +24963,7 @@ var init_dist = __esm({
|
|
|
24931
24963
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
24932
24964
|
* a sentence with a hole in it is worse than no sentence. */
|
|
24933
24965
|
note: external_exports.string().optional(),
|
|
24934
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
24966
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
24935
24967
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
24936
24968
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
24937
24969
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -25027,10 +25059,11 @@ var init_dist = __esm({
|
|
|
25027
25059
|
agentId: external_exports.string().optional(),
|
|
25028
25060
|
select: SelectShapeSchema.optional(),
|
|
25029
25061
|
options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
|
|
25033
|
-
|
|
25062
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
25063
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
25064
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
25065
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
25066
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
25034
25067
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
25035
25068
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
25036
25069
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -25065,7 +25098,7 @@ var init_dist = __esm({
|
|
|
25065
25098
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
25066
25099
|
* `createdAt`. */
|
|
25067
25100
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
25068
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
25101
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
25069
25102
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
25070
25103
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
25071
25104
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -25126,7 +25159,7 @@ var init_dist = __esm({
|
|
|
25126
25159
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
25127
25160
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
25128
25161
|
onCall: external_exports.literal(true).optional(),
|
|
25129
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
25162
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
25130
25163
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
25131
25164
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
25132
25165
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -25157,7 +25190,7 @@ var init_dist = __esm({
|
|
|
25157
25190
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
25158
25191
|
),
|
|
25159
25192
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
25160
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
25193
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
25161
25194
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
25162
25195
|
blocking: external_exports.boolean().default(false),
|
|
25163
25196
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -25231,9 +25264,17 @@ var init_dist = __esm({
|
|
|
25231
25264
|
banner: external_exports.boolean(),
|
|
25232
25265
|
push: external_exports.boolean()
|
|
25233
25266
|
}),
|
|
25267
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
25268
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
25269
|
+
* what people read (owner, 2026-09-30). */
|
|
25234
25270
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
25235
|
-
silentPush
|
|
25236
|
-
|
|
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. */
|
|
25237
25278
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
25238
25279
|
improveConsent: external_exports.boolean(),
|
|
25239
25280
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -25244,12 +25285,16 @@ var init_dist = __esm({
|
|
|
25244
25285
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
25245
25286
|
* write, 'hosted' on read (see store.ts). */
|
|
25246
25287
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
25247
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
25288
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
25248
25289
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
25249
25290
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
25250
25291
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
25251
25292
|
* read (see store.ts). */
|
|
25252
25293
|
talk: external_exports.boolean().optional(),
|
|
25294
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
25295
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
25296
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
25297
|
+
callDiagnostics: external_exports.boolean().optional(),
|
|
25253
25298
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
25254
25299
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
25255
25300
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -25258,9 +25303,6 @@ var init_dist = __esm({
|
|
|
25258
25303
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
25259
25304
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
25260
25305
|
voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
|
|
25261
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
25262
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
25263
|
-
pstnCalls: external_exports.boolean().optional(),
|
|
25264
25306
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
25265
25307
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
25266
25308
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -25304,7 +25346,7 @@ var init_dist = __esm({
|
|
|
25304
25346
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
25305
25347
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
25306
25348
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
25307
|
-
*
|
|
25349
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
25308
25350
|
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
25309
25351
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
25310
25352
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -25322,7 +25364,7 @@ var init_dist = __esm({
|
|
|
25322
25364
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
25323
25365
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
25324
25366
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
25325
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
25367
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
25326
25368
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
25327
25369
|
* set once must not overrule a rule they set about one agent. */
|
|
25328
25370
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
@@ -25333,7 +25375,14 @@ var init_dist = __esm({
|
|
|
25333
25375
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
25334
25376
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
25335
25377
|
lastSeenAt: external_exports.string().datetime().nullable().optional(),
|
|
25336
|
-
/**
|
|
25378
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
25379
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
25380
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
25381
|
+
lastWorkedAt: external_exports.string().datetime().nullable().optional(),
|
|
25382
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
25383
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
25384
|
+
oldestReadyAt: external_exports.string().datetime().nullable().optional(),
|
|
25385
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
25337
25386
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
25338
25387
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
25339
25388
|
runtime: external_exports.object({
|
|
@@ -25341,7 +25390,12 @@ var init_dist = __esm({
|
|
|
25341
25390
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
25342
25391
|
version: external_exports.string().optional(),
|
|
25343
25392
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
25344
|
-
workspaces: external_exports.array(external_exports.string()).optional()
|
|
25393
|
+
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
25394
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
25395
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
25396
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
25397
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
25398
|
+
repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
|
|
25345
25399
|
}).optional(),
|
|
25346
25400
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
25347
25401
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -25354,7 +25408,8 @@ var init_dist = __esm({
|
|
|
25354
25408
|
});
|
|
25355
25409
|
LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
25356
25410
|
AgentLedgerSchema = external_exports.object({
|
|
25357
|
-
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() }),
|
|
25358
25413
|
/** Its own questions you have not answered. */
|
|
25359
25414
|
asks: external_exports.array(LedgerItemSchema),
|
|
25360
25415
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -25424,7 +25479,7 @@ var init_dist = __esm({
|
|
|
25424
25479
|
* for a settled question whose reply carried nothing readable. */
|
|
25425
25480
|
answer: external_exports.string().nullable().default(null),
|
|
25426
25481
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
25427
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
25482
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
25428
25483
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
25429
25484
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
25430
25485
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -25516,7 +25571,19 @@ var init_dist = __esm({
|
|
|
25516
25571
|
said: external_exports.string(),
|
|
25517
25572
|
at: external_exports.string(),
|
|
25518
25573
|
entryId: external_exports.string()
|
|
25519
|
-
}).nullable().optional()
|
|
25574
|
+
}).nullable().optional(),
|
|
25575
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
25576
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
25577
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
25578
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
25579
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
25580
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
25581
|
+
*
|
|
25582
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
25583
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
25584
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
25585
|
+
* Entry with no `about_id` is in neither. */
|
|
25586
|
+
lastPersonAt: external_exports.string().nullable().optional()
|
|
25520
25587
|
});
|
|
25521
25588
|
COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
25522
25589
|
NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -25546,7 +25613,7 @@ var init_dist = __esm({
|
|
|
25546
25613
|
assignee: external_exports.string().nullable(),
|
|
25547
25614
|
/** The request thread minted at assignment; null until assigned. */
|
|
25548
25615
|
parentId: external_exports.string().nullable(),
|
|
25549
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
25616
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
25550
25617
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
25551
25618
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
25552
25619
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -25808,6 +25875,31 @@ var init_dist = __esm({
|
|
|
25808
25875
|
computers: external_exports.array(ComputerRowSchema).nullable()
|
|
25809
25876
|
}).nullable()
|
|
25810
25877
|
});
|
|
25878
|
+
CallRecapSchema = external_exports.object({
|
|
25879
|
+
call: external_exports.object({
|
|
25880
|
+
status: external_exports.string(),
|
|
25881
|
+
startedAt: external_exports.string(),
|
|
25882
|
+
durationMs: external_exports.number().nullable(),
|
|
25883
|
+
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
25884
|
+
}),
|
|
25885
|
+
topics: external_exports.array(external_exports.object({
|
|
25886
|
+
goalId: external_exports.string().uuid(),
|
|
25887
|
+
title: external_exports.string(),
|
|
25888
|
+
owner: external_exports.string(),
|
|
25889
|
+
state: external_exports.string(),
|
|
25890
|
+
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
25891
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
25892
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
25893
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
25894
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
25895
|
+
* anything that is not an answer. */
|
|
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() }))
|
|
25899
|
+
})),
|
|
25900
|
+
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
25901
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
25902
|
+
});
|
|
25811
25903
|
TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
|
|
25812
25904
|
KEY_PATH = join22(homedir22(), ".paigy", "key.json");
|
|
25813
25905
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -32924,7 +33016,20 @@ var AskInputSchema2 = external_exports.object({
|
|
|
32924
33016
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32925
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."
|
|
32926
33018
|
),
|
|
32927
|
-
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
|
+
)
|
|
32928
33033
|
}).strict();
|
|
32929
33034
|
var StartContactSchema2 = external_exports.object({
|
|
32930
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."),
|
|
@@ -32933,7 +33038,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
32933
33038
|
}).strict();
|
|
32934
33039
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32935
33040
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32936
|
-
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.";
|
|
32937
33042
|
var CreateGoalSchema2 = external_exports.object({
|
|
32938
33043
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32939
33044
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -32989,11 +33094,11 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
32989
33094
|
history: external_exports.boolean().optional(),
|
|
32990
33095
|
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
32991
33096
|
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
32992
|
-
* between two of them named. Off by default; `
|
|
33097
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
32993
33098
|
diagnose: external_exports.boolean().optional()
|
|
32994
33099
|
}).strict();
|
|
32995
|
-
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.";
|
|
32996
|
-
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.`;
|
|
32997
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.";
|
|
32998
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.";
|
|
32999
33104
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -33022,6 +33127,7 @@ function entryWords(entry) {
|
|
|
33022
33127
|
return entry.sources.map((source) => source.text).join("\n");
|
|
33023
33128
|
}
|
|
33024
33129
|
var LIVE_MS2 = 3 * 6e4;
|
|
33130
|
+
var WORKING_MS2 = 30 * 6e4;
|
|
33025
33131
|
var ContextSchema2 = external_exports.object({
|
|
33026
33132
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
33027
33133
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -33092,7 +33198,7 @@ var AttentionSchema2 = external_exports.object({
|
|
|
33092
33198
|
points: external_exports.array(external_exports.string()).nullable(),
|
|
33093
33199
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
33094
33200
|
blocking: external_exports.boolean(),
|
|
33095
|
-
/** Reserved (
|
|
33201
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
33096
33202
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
33097
33203
|
deadline: external_exports.string().datetime().nullable().optional()
|
|
33098
33204
|
});
|
|
@@ -33143,7 +33249,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
33143
33249
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
33144
33250
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
33145
33251
|
// the ask into topics and gives each one at most three sentences and one question
|
|
33146
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
33252
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
33147
33253
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
33148
33254
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
33149
33255
|
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
@@ -33220,7 +33326,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
|
|
|
33220
33326
|
external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
|
|
33221
33327
|
external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
|
|
33222
33328
|
external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
|
|
33223
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
33329
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
33224
33330
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
33225
33331
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
33226
33332
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -33269,7 +33375,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
33269
33375
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
33270
33376
|
* a sentence with a hole in it is worse than no sentence. */
|
|
33271
33377
|
note: external_exports.string().optional(),
|
|
33272
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
33378
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
33273
33379
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
33274
33380
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
33275
33381
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -33365,10 +33471,11 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
33365
33471
|
agentId: external_exports.string().optional(),
|
|
33366
33472
|
select: SelectShapeSchema2.optional(),
|
|
33367
33473
|
options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
|
|
33368
|
-
|
|
33369
|
-
|
|
33370
|
-
|
|
33371
|
-
|
|
33474
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
33475
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
33476
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
33477
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
33478
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
33372
33479
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
33373
33480
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
33374
33481
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -33403,7 +33510,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33403
33510
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
33404
33511
|
* `createdAt`. */
|
|
33405
33512
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
33406
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
33513
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
33407
33514
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
33408
33515
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
33409
33516
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -33464,7 +33571,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33464
33571
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
33465
33572
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
33466
33573
|
onCall: external_exports.literal(true).optional(),
|
|
33467
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
33574
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
33468
33575
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
33469
33576
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
33470
33577
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -33495,7 +33602,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33495
33602
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
33496
33603
|
),
|
|
33497
33604
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
33498
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
33605
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
33499
33606
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
33500
33607
|
blocking: external_exports.boolean().default(false),
|
|
33501
33608
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -33569,9 +33676,17 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33569
33676
|
banner: external_exports.boolean(),
|
|
33570
33677
|
push: external_exports.boolean()
|
|
33571
33678
|
}),
|
|
33679
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
33680
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
33681
|
+
* what people read (owner, 2026-09-30). */
|
|
33572
33682
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
33573
|
-
silentPush
|
|
33574
|
-
|
|
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. */
|
|
33575
33690
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
33576
33691
|
improveConsent: external_exports.boolean(),
|
|
33577
33692
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -33582,12 +33697,16 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33582
33697
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
33583
33698
|
* write, 'hosted' on read (see store.ts). */
|
|
33584
33699
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
33585
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
33700
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
33586
33701
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
33587
33702
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
33588
33703
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
33589
33704
|
* read (see store.ts). */
|
|
33590
33705
|
talk: external_exports.boolean().optional(),
|
|
33706
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
33707
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
33708
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
33709
|
+
callDiagnostics: external_exports.boolean().optional(),
|
|
33591
33710
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
33592
33711
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
33593
33712
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -33596,9 +33715,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33596
33715
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
33597
33716
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
33598
33717
|
voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
|
|
33599
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
33600
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
33601
|
-
pstnCalls: external_exports.boolean().optional(),
|
|
33602
33718
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
33603
33719
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
33604
33720
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -33642,7 +33758,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33642
33758
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
33643
33759
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
33644
33760
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
33645
|
-
*
|
|
33761
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
33646
33762
|
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
33647
33763
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
33648
33764
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -33660,7 +33776,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33660
33776
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
33661
33777
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
33662
33778
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
33663
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
33779
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
33664
33780
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
33665
33781
|
* set once must not overrule a rule they set about one agent. */
|
|
33666
33782
|
reach: NotifyLevelSchema2.nullable().optional(),
|
|
@@ -33671,7 +33787,14 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33671
33787
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
33672
33788
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
33673
33789
|
lastSeenAt: external_exports.string().datetime().nullable().optional(),
|
|
33674
|
-
/**
|
|
33790
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
33791
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
33792
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
33793
|
+
lastWorkedAt: external_exports.string().datetime().nullable().optional(),
|
|
33794
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
33795
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
33796
|
+
oldestReadyAt: external_exports.string().datetime().nullable().optional(),
|
|
33797
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
33675
33798
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
33676
33799
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
33677
33800
|
runtime: external_exports.object({
|
|
@@ -33679,7 +33802,12 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33679
33802
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
33680
33803
|
version: external_exports.string().optional(),
|
|
33681
33804
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
33682
|
-
workspaces: external_exports.array(external_exports.string()).optional()
|
|
33805
|
+
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
33806
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
33807
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
33808
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
33809
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
33810
|
+
repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
|
|
33683
33811
|
}).optional(),
|
|
33684
33812
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
33685
33813
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -33692,7 +33820,8 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33692
33820
|
});
|
|
33693
33821
|
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
33694
33822
|
var AgentLedgerSchema2 = external_exports.object({
|
|
33695
|
-
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() }),
|
|
33696
33825
|
/** Its own questions you have not answered. */
|
|
33697
33826
|
asks: external_exports.array(LedgerItemSchema2),
|
|
33698
33827
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -33762,7 +33891,7 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
33762
33891
|
* for a settled question whose reply carried nothing readable. */
|
|
33763
33892
|
answer: external_exports.string().nullable().default(null),
|
|
33764
33893
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
33765
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
33894
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
33766
33895
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
33767
33896
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
33768
33897
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -33854,7 +33983,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33854
33983
|
said: external_exports.string(),
|
|
33855
33984
|
at: external_exports.string(),
|
|
33856
33985
|
entryId: external_exports.string()
|
|
33857
|
-
}).nullable().optional()
|
|
33986
|
+
}).nullable().optional(),
|
|
33987
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
33988
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
33989
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
33990
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
33991
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
33992
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
33993
|
+
*
|
|
33994
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
33995
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
33996
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
33997
|
+
* Entry with no `about_id` is in neither. */
|
|
33998
|
+
lastPersonAt: external_exports.string().nullable().optional()
|
|
33858
33999
|
});
|
|
33859
34000
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
33860
34001
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -33884,7 +34025,7 @@ var NoteSchema2 = external_exports.object({
|
|
|
33884
34025
|
assignee: external_exports.string().nullable(),
|
|
33885
34026
|
/** The request thread minted at assignment; null until assigned. */
|
|
33886
34027
|
parentId: external_exports.string().nullable(),
|
|
33887
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
34028
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
33888
34029
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
33889
34030
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
33890
34031
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -34144,6 +34285,31 @@ var SnapshotSchema2 = external_exports.object({
|
|
|
34144
34285
|
computers: external_exports.array(ComputerRowSchema2).nullable()
|
|
34145
34286
|
}).nullable()
|
|
34146
34287
|
});
|
|
34288
|
+
var CallRecapSchema2 = external_exports.object({
|
|
34289
|
+
call: external_exports.object({
|
|
34290
|
+
status: external_exports.string(),
|
|
34291
|
+
startedAt: external_exports.string(),
|
|
34292
|
+
durationMs: external_exports.number().nullable(),
|
|
34293
|
+
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
34294
|
+
}),
|
|
34295
|
+
topics: external_exports.array(external_exports.object({
|
|
34296
|
+
goalId: external_exports.string().uuid(),
|
|
34297
|
+
title: external_exports.string(),
|
|
34298
|
+
owner: external_exports.string(),
|
|
34299
|
+
state: external_exports.string(),
|
|
34300
|
+
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
34301
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
34302
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
34303
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
34304
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
34305
|
+
* anything that is not an answer. */
|
|
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() }))
|
|
34309
|
+
})),
|
|
34310
|
+
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
34311
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
34312
|
+
});
|
|
34147
34313
|
|
|
34148
34314
|
// src/paigy/activity.ts
|
|
34149
34315
|
function shortenPaths(s) {
|
|
@@ -34878,7 +35044,7 @@ function ago(iso, now) {
|
|
|
34878
35044
|
init_catalog();
|
|
34879
35045
|
|
|
34880
35046
|
// src/workspaces.ts
|
|
34881
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
35047
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
34882
35048
|
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
34883
35049
|
import { homedir as homedir6 } from "os";
|
|
34884
35050
|
function workspacesFile() {
|
|
@@ -34913,11 +35079,42 @@ function resolveWakeDir(pinned, deps) {
|
|
|
34913
35079
|
}
|
|
34914
35080
|
return listWorkspaces(deps)[0];
|
|
34915
35081
|
}
|
|
35082
|
+
function originOf(dir) {
|
|
35083
|
+
try {
|
|
35084
|
+
if (!statSync3(join5(dir, ".git")).isDirectory()) return null;
|
|
35085
|
+
const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
|
|
35086
|
+
const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
|
|
35087
|
+
return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
35088
|
+
} catch {
|
|
35089
|
+
return null;
|
|
35090
|
+
}
|
|
35091
|
+
}
|
|
35092
|
+
var MAX_CHILDREN = 300;
|
|
35093
|
+
var MAX_REPOS = 100;
|
|
35094
|
+
function listRepos(deps) {
|
|
35095
|
+
const repos = [];
|
|
35096
|
+
for (const root of listWorkspaces(deps)) {
|
|
35097
|
+
const own = originOf(root);
|
|
35098
|
+
if (own) repos.push({ path: root, remote: own });
|
|
35099
|
+
let children = [];
|
|
35100
|
+
try {
|
|
35101
|
+
children = readdirSync2(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
|
|
35102
|
+
} catch {
|
|
35103
|
+
}
|
|
35104
|
+
for (const name of children) {
|
|
35105
|
+
if (repos.length >= MAX_REPOS) return repos;
|
|
35106
|
+
const path = join5(root, name);
|
|
35107
|
+
const remote = originOf(path);
|
|
35108
|
+
if (remote) repos.push({ path, remote });
|
|
35109
|
+
}
|
|
35110
|
+
}
|
|
35111
|
+
return repos.slice(0, MAX_REPOS);
|
|
35112
|
+
}
|
|
34916
35113
|
|
|
34917
35114
|
// src/holder.ts
|
|
34918
35115
|
init_dist();
|
|
34919
35116
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
34920
|
-
import { readFileSync as readFileSync4, readdirSync as
|
|
35117
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
34921
35118
|
import { homedir as homedir7 } from "os";
|
|
34922
35119
|
import { join as join6 } from "path";
|
|
34923
35120
|
function isAlive(pid) {
|
|
@@ -34948,9 +35145,62 @@ function holds(pid, procStart, deps) {
|
|
|
34948
35145
|
}
|
|
34949
35146
|
return false;
|
|
34950
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
|
+
}
|
|
34951
35201
|
function localHolders(deps = {}) {
|
|
34952
35202
|
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
|
|
34953
|
-
const list = deps.list ?? ((d) =>
|
|
35203
|
+
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
34954
35204
|
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
34955
35205
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
34956
35206
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -34977,8 +35227,67 @@ function localHolders(deps = {}) {
|
|
|
34977
35227
|
return held;
|
|
34978
35228
|
}
|
|
34979
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
|
+
|
|
34980
35289
|
// src/owner.ts
|
|
34981
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as
|
|
35290
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
|
|
34982
35291
|
import { homedir as homedir8 } from "os";
|
|
34983
35292
|
import { join as join7 } from "path";
|
|
34984
35293
|
var HEAD_BYTES = 64e3;
|
|
@@ -34997,7 +35306,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34997
35306
|
if (!m) return null;
|
|
34998
35307
|
const prefix = m[1];
|
|
34999
35308
|
const root = deps.projectsDir ?? join7(homedir8(), ".claude", "projects");
|
|
35000
|
-
const list = deps.list ?? ((dir) =>
|
|
35309
|
+
const list = deps.list ?? ((dir) => readdirSync4(dir));
|
|
35001
35310
|
const head = deps.head ?? readHead;
|
|
35002
35311
|
let folders;
|
|
35003
35312
|
try {
|
|
@@ -35232,7 +35541,9 @@ function startHost(opts) {
|
|
|
35232
35541
|
void heartbeat({
|
|
35233
35542
|
version: VERSION,
|
|
35234
35543
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
35235
|
-
workspaces: listWorkspaces(opts.wsDeps)
|
|
35544
|
+
workspaces: listWorkspaces(opts.wsDeps),
|
|
35545
|
+
// The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
|
|
35546
|
+
repos: listRepos(opts.wsDeps)
|
|
35236
35547
|
}, asHost).catch(() => {
|
|
35237
35548
|
});
|
|
35238
35549
|
void listenSlots().then(sweepSlots).catch(() => {
|
|
@@ -35278,6 +35589,31 @@ function startHost(opts) {
|
|
|
35278
35589
|
void refreshIdentities();
|
|
35279
35590
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
35280
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
|
+
}
|
|
35281
35617
|
}
|
|
35282
35618
|
async function presenceMap() {
|
|
35283
35619
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -35302,6 +35638,14 @@ function startHost(opts) {
|
|
|
35302
35638
|
const key = runKey(slot);
|
|
35303
35639
|
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
35304
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
|
+
}
|
|
35305
35649
|
const tokenId = slotIdentity(slot).tokenId;
|
|
35306
35650
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
35307
35651
|
const holder = holders.get(slot);
|