@paigy/harness 0.3.30 → 0.3.32
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 +10 -4
- package/dist/cli.js +380 -112
- package/dist/main.js +315 -68
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -4578,9 +4578,9 @@ var require_lib = __commonJS({
|
|
|
4578
4578
|
// src/main.ts
|
|
4579
4579
|
import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
4580
4580
|
import { spawn as spawn2 } from "child_process";
|
|
4581
|
-
import { readFileSync as
|
|
4581
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
4582
4582
|
import { fileURLToPath } from "url";
|
|
4583
|
-
import { dirname as dirname4, join as
|
|
4583
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
4584
4584
|
|
|
4585
4585
|
// ../../packages/sdk/dist/index.js
|
|
4586
4586
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
@@ -8641,6 +8641,7 @@ import { join as join2 } from "path";
|
|
|
8641
8641
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
8642
8642
|
var import_undici = __toESM(require_undici(), 1);
|
|
8643
8643
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
8644
|
+
var CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID"];
|
|
8644
8645
|
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
8645
8646
|
var NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
|
|
8646
8647
|
var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
@@ -8695,14 +8696,14 @@ function sessionId() {
|
|
|
8695
8696
|
async function reach(url, init) {
|
|
8696
8697
|
return send(url, init, sessionId());
|
|
8697
8698
|
}
|
|
8698
|
-
function reachAs(session) {
|
|
8699
|
-
return (url, init) => send(url, init, session);
|
|
8699
|
+
function reachAs(session, instance) {
|
|
8700
|
+
return (url, init) => send(url, init, session, instance);
|
|
8700
8701
|
}
|
|
8701
|
-
async function send(url, init, session) {
|
|
8702
|
+
async function send(url, init, session, instance = envSession("PAIGY_INSTANCE_ID") ?? INSTANCE_ID) {
|
|
8702
8703
|
try {
|
|
8703
8704
|
const headers = {
|
|
8704
8705
|
...init?.headers,
|
|
8705
|
-
"x-paigy-instance":
|
|
8706
|
+
"x-paigy-instance": instance,
|
|
8706
8707
|
"x-paigy-session": session
|
|
8707
8708
|
};
|
|
8708
8709
|
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
@@ -9975,7 +9976,18 @@ var AskInputSchema = external_exports.object({
|
|
|
9975
9976
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
9976
9977
|
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
9977
9978
|
),
|
|
9978
|
-
options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
|
|
9979
|
+
options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
|
|
9980
|
+
// YOUR VIEW OF ONE OR MANY, A HINT TO PAIGY'S READ (owner, 2026-10-02: "one and many makes sense...
|
|
9981
|
+
// make sure the brain that formats is aware of how it should work"). Three independent fixes went
|
|
9982
|
+
// out as "All three / 1 and 3 only / Just log it" because every card was pick-one. #2943 made this
|
|
9983
|
+
// field override the read; no agent sent it, so nothing changed. The read now decides each card's
|
|
9984
|
+
// shape (`goal/intake.ts` `answerOf`), and this reaches it as the agent's view, weighed, not obeyed.
|
|
9985
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
9986
|
+
`Your view of how the options are answered: "one" to pick one, "many" to tick any number of them. A hint: Paigy's read decides each card's shape, a checklist when the options can all be wanted at once, a pick when choosing one rules out the others. The person can always answer in their own words as well. Ignored without options.`
|
|
9987
|
+
),
|
|
9988
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
9989
|
+
"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."
|
|
9990
|
+
)
|
|
9979
9991
|
}).strict();
|
|
9980
9992
|
var StartContactSchema = external_exports.object({
|
|
9981
9993
|
asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
@@ -9984,7 +9996,7 @@ var StartContactSchema = external_exports.object({
|
|
|
9984
9996
|
}).strict();
|
|
9985
9997
|
var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
9986
9998
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
9987
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
9999
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). Whether a card is a checklist or a pick is decided by Paigy's read: independent options they may want several of become a checklist, alternatives a pick. select:'many' or select:'one' tells it your view, a hint it weighs. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
9988
10000
|
var CreateGoalSchema = external_exports.object({
|
|
9989
10001
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
9990
10002
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -10007,7 +10019,7 @@ var CreateGoalSchema = external_exports.object({
|
|
|
10007
10019
|
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
10008
10020
|
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
10009
10021
|
}).strict();
|
|
10010
|
-
var CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only:
|
|
10022
|
+
var CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
|
|
10011
10023
|
var UpdateGoalSchema = external_exports.object({
|
|
10012
10024
|
revision: external_exports.number().int().positive(),
|
|
10013
10025
|
changes: external_exports.object({
|
|
@@ -10043,12 +10055,13 @@ var GetGoalSchema = external_exports.object({
|
|
|
10043
10055
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
10044
10056
|
diagnose: external_exports.boolean().optional()
|
|
10045
10057
|
}).strict();
|
|
10046
|
-
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full.
|
|
10047
|
-
var UPDATE_GOAL_DESCRIPTION = `Update
|
|
10048
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own
|
|
10058
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): 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. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals 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.";
|
|
10059
|
+
var UPDATE_GOAL_DESCRIPTION = `Update a Goal you are on at an exact revision. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). 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.`;
|
|
10060
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change its state; use update_goal with ownerParticipant when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
10049
10061
|
var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
10050
10062
|
var CheckRepliesSchema = external_exports.object({}).strict();
|
|
10051
10063
|
var AGENT_TOOLS = [
|
|
10064
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
10052
10065
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
10053
10066
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
10054
10067
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
@@ -10058,7 +10071,7 @@ var AGENT_TOOLS = [
|
|
|
10058
10071
|
];
|
|
10059
10072
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
10060
10073
|
var LIVE_MS = 3 * 6e4;
|
|
10061
|
-
var WORKING_MS =
|
|
10074
|
+
var WORKING_MS = 60 * 6e4;
|
|
10062
10075
|
var ContextSchema = external_exports.object({
|
|
10063
10076
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
10064
10077
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -10410,7 +10423,13 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10410
10423
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
10411
10424
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
10412
10425
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
10413
|
-
blocking: external_exports.boolean().optional()
|
|
10426
|
+
blocking: external_exports.boolean().optional(),
|
|
10427
|
+
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
10428
|
+
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
10429
|
+
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
10430
|
+
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
10431
|
+
* speak, it is dropped and their words are taken like any reply. */
|
|
10432
|
+
ifSilent: external_exports.boolean().optional()
|
|
10414
10433
|
});
|
|
10415
10434
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
10416
10435
|
var InboxItemSchema = external_exports.object({
|
|
@@ -10611,12 +10630,13 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10611
10630
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
10612
10631
|
* what people read (owner, 2026-09-30). */
|
|
10613
10632
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
10614
|
-
/**
|
|
10615
|
-
*
|
|
10616
|
-
*
|
|
10617
|
-
* wanting
|
|
10618
|
-
*
|
|
10619
|
-
silentPush
|
|
10633
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
10634
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
10635
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
10636
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
10637
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
10638
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
10639
|
+
* next dead field for the same reason. */
|
|
10620
10640
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
10621
10641
|
improveConsent: external_exports.boolean(),
|
|
10622
10642
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -10750,7 +10770,8 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
10750
10770
|
});
|
|
10751
10771
|
var LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
10752
10772
|
var AgentLedgerSchema = external_exports.object({
|
|
10753
|
-
agent
|
|
10773
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
10774
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
10754
10775
|
/** Its own questions you have not answered. */
|
|
10755
10776
|
asks: external_exports.array(LedgerItemSchema),
|
|
10756
10777
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -10917,7 +10938,7 @@ var QueueItemSchema = external_exports.object({
|
|
|
10917
10938
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
10918
10939
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
10919
10940
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
10920
|
-
*
|
|
10941
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
10921
10942
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
10922
10943
|
*
|
|
10923
10944
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -11234,7 +11255,9 @@ var CallRecapSchema = external_exports.object({
|
|
|
11234
11255
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
11235
11256
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
11236
11257
|
* anything that is not an answer. */
|
|
11237
|
-
|
|
11258
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
11259
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
11260
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
|
|
11238
11261
|
})),
|
|
11239
11262
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
11240
11263
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -11433,6 +11456,22 @@ async function claimSessions(opts = {}) {
|
|
|
11433
11456
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
11434
11457
|
return (await res.json()).sessions;
|
|
11435
11458
|
}
|
|
11459
|
+
async function claimSessionEnds(opts = {}) {
|
|
11460
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
|
|
11461
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
11462
|
+
// the HOST's identity
|
|
11463
|
+
}));
|
|
11464
|
+
if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
|
|
11465
|
+
return (await res.json()).ends ?? [];
|
|
11466
|
+
}
|
|
11467
|
+
async function finishSessionEnd(endId, result, opts = {}) {
|
|
11468
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
|
|
11469
|
+
method: "POST",
|
|
11470
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
11471
|
+
body: JSON.stringify({ result })
|
|
11472
|
+
}));
|
|
11473
|
+
if (!res.ok) await fail("finish_session_end", res);
|
|
11474
|
+
}
|
|
11436
11475
|
async function recordDecision(decision, opts = {}) {
|
|
11437
11476
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
11438
11477
|
method: "POST",
|
|
@@ -11556,7 +11595,9 @@ async function contact(input, opts = {}) {
|
|
|
11556
11595
|
// this cannot see.
|
|
11557
11596
|
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
11558
11597
|
ask: a.ask,
|
|
11559
|
-
options: a.options ?? []
|
|
11598
|
+
options: a.options ?? [],
|
|
11599
|
+
...a.select ? { select: a.select } : {},
|
|
11600
|
+
...a.answers ? { answers: a.answers } : {}
|
|
11560
11601
|
}));
|
|
11561
11602
|
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
11562
11603
|
method: "POST",
|
|
@@ -11879,9 +11920,9 @@ function detectAll(deps = {}) {
|
|
|
11879
11920
|
}
|
|
11880
11921
|
|
|
11881
11922
|
// src/host.ts
|
|
11882
|
-
import { readFileSync as
|
|
11923
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
11883
11924
|
import { homedir as homedir10 } from "os";
|
|
11884
|
-
import { join as
|
|
11925
|
+
import { join as join11 } from "path";
|
|
11885
11926
|
|
|
11886
11927
|
// src/triage/evidence.ts
|
|
11887
11928
|
var OPEN_STATUSES = ["open", "assigned", "in_progress"];
|
|
@@ -12554,6 +12595,59 @@ function holds(pid, procStart, deps) {
|
|
|
12554
12595
|
}
|
|
12555
12596
|
return false;
|
|
12556
12597
|
}
|
|
12598
|
+
function proven(pid, procStart, deps) {
|
|
12599
|
+
if (!deps.alive(pid) || typeof procStart !== "string" || !procStart.trim()) return false;
|
|
12600
|
+
const want = squash(procStart);
|
|
12601
|
+
return ["UTC", "local"].some((zone) => {
|
|
12602
|
+
const got = deps.started(pid, zone);
|
|
12603
|
+
return got !== null && squash(got) === want;
|
|
12604
|
+
});
|
|
12605
|
+
}
|
|
12606
|
+
function localHolderPids(deps = {}) {
|
|
12607
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
|
|
12608
|
+
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
12609
|
+
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
12610
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
12611
|
+
const held = /* @__PURE__ */ new Map();
|
|
12612
|
+
let files;
|
|
12613
|
+
try {
|
|
12614
|
+
files = list(dir);
|
|
12615
|
+
} catch {
|
|
12616
|
+
return held;
|
|
12617
|
+
}
|
|
12618
|
+
for (const file of files) {
|
|
12619
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
12620
|
+
let row;
|
|
12621
|
+
try {
|
|
12622
|
+
row = JSON.parse(read(join6(dir, file)));
|
|
12623
|
+
} catch {
|
|
12624
|
+
continue;
|
|
12625
|
+
}
|
|
12626
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
12627
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
12628
|
+
if (!holds(pid, row.procStart, check)) continue;
|
|
12629
|
+
const slot = sessionSlot(row.sessionId);
|
|
12630
|
+
held.set(slot, [...held.get(slot) ?? [], { pid, proven: proven(pid, row.procStart, check) }]);
|
|
12631
|
+
}
|
|
12632
|
+
return held;
|
|
12633
|
+
}
|
|
12634
|
+
function psParent(pid) {
|
|
12635
|
+
try {
|
|
12636
|
+
const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
|
|
12637
|
+
const n = Number(out);
|
|
12638
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
12639
|
+
} catch {
|
|
12640
|
+
return null;
|
|
12641
|
+
}
|
|
12642
|
+
}
|
|
12643
|
+
function descendsFrom(pid, ancestor, parent = psParent) {
|
|
12644
|
+
let at = pid;
|
|
12645
|
+
for (let i = 0; i < 32 && at !== null && at > 1; i += 1) {
|
|
12646
|
+
if (at === ancestor) return true;
|
|
12647
|
+
at = parent(at);
|
|
12648
|
+
}
|
|
12649
|
+
return false;
|
|
12650
|
+
}
|
|
12557
12651
|
function localHolders(deps = {}) {
|
|
12558
12652
|
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
|
|
12559
12653
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
@@ -12583,6 +12677,65 @@ function localHolders(deps = {}) {
|
|
|
12583
12677
|
return held;
|
|
12584
12678
|
}
|
|
12585
12679
|
|
|
12680
|
+
// src/end.ts
|
|
12681
|
+
var GRACE_MS = 5e3;
|
|
12682
|
+
async function endTerminal(pid, slot, deps) {
|
|
12683
|
+
try {
|
|
12684
|
+
deps.kill(pid, "SIGTERM");
|
|
12685
|
+
} catch (e) {
|
|
12686
|
+
return { ok: false, kind: "terminal", pid, why: `SIGTERM refused: ${e.message}`.slice(0, 200) };
|
|
12687
|
+
}
|
|
12688
|
+
const grace = deps.graceMs ?? GRACE_MS;
|
|
12689
|
+
for (let waited = 0; waited < grace; waited += 250) {
|
|
12690
|
+
if (!deps.alive(pid)) {
|
|
12691
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGTERM" })}`);
|
|
12692
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGTERM" };
|
|
12693
|
+
}
|
|
12694
|
+
await deps.sleep(250);
|
|
12695
|
+
}
|
|
12696
|
+
try {
|
|
12697
|
+
deps.kill(pid, "SIGKILL");
|
|
12698
|
+
} catch (e) {
|
|
12699
|
+
return { ok: false, kind: "terminal", pid, why: `SIGKILL refused: ${e.message}`.slice(0, 200) };
|
|
12700
|
+
}
|
|
12701
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGKILL" })}`);
|
|
12702
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGKILL" };
|
|
12703
|
+
}
|
|
12704
|
+
async function endSession(end, deps) {
|
|
12705
|
+
const slots = deps.slotsFor(end.agent);
|
|
12706
|
+
if (!slots.length) return { ok: false, why: "not_on_this_host" };
|
|
12707
|
+
let ended2 = null;
|
|
12708
|
+
if (end.which === "harness" || end.which === "any") {
|
|
12709
|
+
for (const slot of slots) {
|
|
12710
|
+
const run = deps.ownRun(slot);
|
|
12711
|
+
if (!run) continue;
|
|
12712
|
+
run.stop();
|
|
12713
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, kind: "harness", signal: "stop" })}`);
|
|
12714
|
+
ended2 = { ok: true, kind: "harness", signal: "stop" };
|
|
12715
|
+
}
|
|
12716
|
+
if (end.which === "harness" && !ended2) return { ok: false, kind: "harness", why: "no_harness_copy_here" };
|
|
12717
|
+
}
|
|
12718
|
+
if (end.which === "terminal" || end.which === "any") {
|
|
12719
|
+
let terminal = null;
|
|
12720
|
+
let unproven = null;
|
|
12721
|
+
for (const slot of slots) {
|
|
12722
|
+
for (const { pid, proven: proven2 } of deps.pidsFor(slot).filter((p) => !deps.isOwn(p.pid))) {
|
|
12723
|
+
if (!proven2) {
|
|
12724
|
+
deps.log(`session_end_refused ${JSON.stringify({ sessionId: slot, pid, why: "no_proc_start" })}`);
|
|
12725
|
+
unproven = { ok: false, kind: "terminal", pid, why: "no_proc_start" };
|
|
12726
|
+
continue;
|
|
12727
|
+
}
|
|
12728
|
+
terminal = await endTerminal(pid, slot, deps);
|
|
12729
|
+
if (!terminal.ok) return terminal;
|
|
12730
|
+
}
|
|
12731
|
+
}
|
|
12732
|
+
if (terminal) return terminal;
|
|
12733
|
+
if (unproven) return unproven;
|
|
12734
|
+
if (end.which === "terminal") return { ok: false, kind: "terminal", why: "no_terminal_copy_here" };
|
|
12735
|
+
}
|
|
12736
|
+
return ended2 ?? { ok: false, why: "nothing_running_here" };
|
|
12737
|
+
}
|
|
12738
|
+
|
|
12586
12739
|
// src/owner.ts
|
|
12587
12740
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
12588
12741
|
import { homedir as homedir7 } from "os";
|
|
@@ -13979,7 +14132,18 @@ var AskInputSchema2 = external_exports.object({
|
|
|
13979
14132
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
13980
14133
|
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
13981
14134
|
),
|
|
13982
|
-
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
14135
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
|
|
14136
|
+
// YOUR VIEW OF ONE OR MANY, A HINT TO PAIGY'S READ (owner, 2026-10-02: "one and many makes sense...
|
|
14137
|
+
// make sure the brain that formats is aware of how it should work"). Three independent fixes went
|
|
14138
|
+
// out as "All three / 1 and 3 only / Just log it" because every card was pick-one. #2943 made this
|
|
14139
|
+
// field override the read; no agent sent it, so nothing changed. The read now decides each card's
|
|
14140
|
+
// shape (`goal/intake.ts` `answerOf`), and this reaches it as the agent's view, weighed, not obeyed.
|
|
14141
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
14142
|
+
`Your view of how the options are answered: "one" to pick one, "many" to tick any number of them. A hint: Paigy's read decides each card's shape, a checklist when the options can all be wanted at once, a pick when choosing one rules out the others. The person can always answer in their own words as well. Ignored without options.`
|
|
14143
|
+
),
|
|
14144
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
14145
|
+
"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."
|
|
14146
|
+
)
|
|
13983
14147
|
}).strict();
|
|
13984
14148
|
var StartContactSchema2 = external_exports.object({
|
|
13985
14149
|
asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
@@ -13988,7 +14152,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
13988
14152
|
}).strict();
|
|
13989
14153
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
13990
14154
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
13991
|
-
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
14155
|
+
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'). Whether a card is a checklist or a pick is decided by Paigy's read: independent options they may want several of become a checklist, alternatives a pick. select:'many' or select:'one' tells it your view, a hint it weighs. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
13992
14156
|
var CreateGoalSchema2 = external_exports.object({
|
|
13993
14157
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
13994
14158
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -14011,7 +14175,7 @@ var CreateGoalSchema2 = external_exports.object({
|
|
|
14011
14175
|
var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
|
|
14012
14176
|
idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
14013
14177
|
}).strict();
|
|
14014
|
-
var CREATE_GOAL_DESCRIPTION2 = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only:
|
|
14178
|
+
var CREATE_GOAL_DESCRIPTION2 = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
|
|
14015
14179
|
var UpdateGoalSchema2 = external_exports.object({
|
|
14016
14180
|
revision: external_exports.number().int().positive(),
|
|
14017
14181
|
changes: external_exports.object({
|
|
@@ -14047,12 +14211,13 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
14047
14211
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
14048
14212
|
diagnose: external_exports.boolean().optional()
|
|
14049
14213
|
}).strict();
|
|
14050
|
-
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full.
|
|
14051
|
-
var UPDATE_GOAL_DESCRIPTION2 = `Update
|
|
14052
|
-
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own
|
|
14214
|
+
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): 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. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals 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.";
|
|
14215
|
+
var UPDATE_GOAL_DESCRIPTION2 = `Update a Goal you are on at an exact revision. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). 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.`;
|
|
14216
|
+
var CLAIM_GOAL_DESCRIPTION2 = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change its state; use update_goal with ownerParticipant when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
14053
14217
|
var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
14054
14218
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
14055
14219
|
var AGENT_TOOLS2 = [
|
|
14220
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
14056
14221
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
14057
14222
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
14058
14223
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
@@ -14077,7 +14242,7 @@ function entryWords(entry) {
|
|
|
14077
14242
|
return entry.sources.map((source) => source.text).join("\n");
|
|
14078
14243
|
}
|
|
14079
14244
|
var LIVE_MS2 = 3 * 6e4;
|
|
14080
|
-
var WORKING_MS2 =
|
|
14245
|
+
var WORKING_MS2 = 60 * 6e4;
|
|
14081
14246
|
var ContextSchema2 = external_exports.object({
|
|
14082
14247
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
14083
14248
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -14429,7 +14594,13 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
14429
14594
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
14430
14595
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
14431
14596
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
14432
|
-
blocking: external_exports.boolean().optional()
|
|
14597
|
+
blocking: external_exports.boolean().optional(),
|
|
14598
|
+
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
14599
|
+
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
14600
|
+
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
14601
|
+
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
14602
|
+
* speak, it is dropped and their words are taken like any reply. */
|
|
14603
|
+
ifSilent: external_exports.boolean().optional()
|
|
14433
14604
|
});
|
|
14434
14605
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
14435
14606
|
var InboxItemSchema2 = external_exports.object({
|
|
@@ -14630,12 +14801,13 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
14630
14801
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
14631
14802
|
* what people read (owner, 2026-09-30). */
|
|
14632
14803
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
14633
|
-
/**
|
|
14634
|
-
*
|
|
14635
|
-
*
|
|
14636
|
-
* wanting
|
|
14637
|
-
*
|
|
14638
|
-
silentPush
|
|
14804
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
14805
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
14806
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
14807
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
14808
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
14809
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
14810
|
+
* next dead field for the same reason. */
|
|
14639
14811
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
14640
14812
|
improveConsent: external_exports.boolean(),
|
|
14641
14813
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -14769,7 +14941,8 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
14769
14941
|
});
|
|
14770
14942
|
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
14771
14943
|
var AgentLedgerSchema2 = external_exports.object({
|
|
14772
|
-
agent
|
|
14944
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
14945
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
14773
14946
|
/** Its own questions you have not answered. */
|
|
14774
14947
|
asks: external_exports.array(LedgerItemSchema2),
|
|
14775
14948
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -14936,7 +15109,7 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
14936
15109
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
14937
15110
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
14938
15111
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
14939
|
-
*
|
|
15112
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
14940
15113
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
14941
15114
|
*
|
|
14942
15115
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -15251,7 +15424,9 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
15251
15424
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
15252
15425
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
15253
15426
|
* anything that is not an answer. */
|
|
15254
|
-
|
|
15427
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
15428
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
15429
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
|
|
15255
15430
|
})),
|
|
15256
15431
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
15257
15432
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -15277,11 +15452,14 @@ function sameTail(a, b) {
|
|
|
15277
15452
|
return a.length === b.length && a.every((l, i) => l === b[i]);
|
|
15278
15453
|
}
|
|
15279
15454
|
|
|
15455
|
+
// src/run.ts
|
|
15456
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
15457
|
+
|
|
15280
15458
|
// src/harness/session.ts
|
|
15281
15459
|
import { spawn } from "child_process";
|
|
15282
|
-
import { existsSync as existsSync4 } from "fs";
|
|
15460
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
15283
15461
|
import { homedir as homedir9 } from "os";
|
|
15284
|
-
import { resolve as resolve2, delimiter as delimiter3 } from "path";
|
|
15462
|
+
import { resolve as resolve2, delimiter as delimiter3, join as join9 } from "path";
|
|
15285
15463
|
|
|
15286
15464
|
// src/harness/acp.ts
|
|
15287
15465
|
var none = { events: [], writes: [] };
|
|
@@ -15652,6 +15830,21 @@ function startSession(opts) {
|
|
|
15652
15830
|
if (!existsSync4(cwd)) {
|
|
15653
15831
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
15654
15832
|
}
|
|
15833
|
+
const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
|
|
15834
|
+
if (codexConfig) {
|
|
15835
|
+
const key = "mcp_servers.paigy.env_vars";
|
|
15836
|
+
const configPath = join9(process.env.CODEX_HOME || join9(homedir9(), ".codex"), "config.toml");
|
|
15837
|
+
const saved = existsSync4(configPath) ? readFileSync5(configPath, "utf8") : "";
|
|
15838
|
+
const registered = /\[mcp_servers\.paigy\][\s\S]*?(?=\n\[|$)/.exec(saved)?.[0];
|
|
15839
|
+
const configured = codexConfig["mcp_servers.paigy.command"] || codexConfig["mcp_servers.paigy.url"] || codexConfig.mcp_servers?.paigy?.command || codexConfig.mcp_servers?.paigy?.url;
|
|
15840
|
+
if (!configured && !registered) {
|
|
15841
|
+
codexConfig["mcp_servers.paigy.command"] = "npx";
|
|
15842
|
+
codexConfig["mcp_servers.paigy.args"] = ["-y", "@paigy/mcp@latest"];
|
|
15843
|
+
}
|
|
15844
|
+
const savedVars = registered?.match(/env_vars\s*=\s*\[([\s\S]*?)\]/)?.[1] ?? "";
|
|
15845
|
+
const inherited = [...savedVars.matchAll(/["']([A-Za-z_][A-Za-z0-9_]*)["']/g)].map((match) => match[1]);
|
|
15846
|
+
codexConfig[key] = [.../* @__PURE__ */ new Set([...inherited, ...codexConfig[key] ?? codexConfig.mcp_servers?.paigy?.env_vars ?? [], ...CODEX_ENV])];
|
|
15847
|
+
}
|
|
15655
15848
|
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
15656
15849
|
cwd,
|
|
15657
15850
|
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
@@ -15662,7 +15855,9 @@ function startSession(opts) {
|
|
|
15662
15855
|
...process.env,
|
|
15663
15856
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter3),
|
|
15664
15857
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
15665
|
-
...opts.
|
|
15858
|
+
...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
|
|
15859
|
+
...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
|
|
15860
|
+
...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
|
|
15666
15861
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
15667
15862
|
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
15668
15863
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
@@ -15921,9 +16116,13 @@ async function finish(result, state, rail, opts) {
|
|
|
15921
16116
|
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
15922
16117
|
let revision = goal.revision;
|
|
15923
16118
|
try {
|
|
15924
|
-
const held = await rail.
|
|
16119
|
+
const held = await rail.read(goal.id);
|
|
15925
16120
|
revision = held.revision ?? revision;
|
|
15926
16121
|
const owed = state.noted?.some((b) => b.goals.includes(goal.id));
|
|
16122
|
+
if (held.state === "done" || held.state === "cancelled") {
|
|
16123
|
+
if (!owed) state.goal = void 0;
|
|
16124
|
+
return;
|
|
16125
|
+
}
|
|
15927
16126
|
if (!held.openDecisionNeedIds?.length && !owed) {
|
|
15928
16127
|
await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
|
|
15929
16128
|
state.goal = void 0;
|
|
@@ -16012,11 +16211,16 @@ function beat() {
|
|
|
16012
16211
|
}
|
|
16013
16212
|
var BEAT_REPORT_MS = 10 * 6e4;
|
|
16014
16213
|
var ended = (state) => state === "done" || state === "cancelled";
|
|
16015
|
-
function unheard(brief, state, since = null, after = -1) {
|
|
16214
|
+
function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
16016
16215
|
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
16017
16216
|
const words2 = [];
|
|
16217
|
+
const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
|
|
16018
16218
|
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
16019
16219
|
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
16220
|
+
if (pendingAt !== null && Date.parse(e.createdAt) < pendingAt) {
|
|
16221
|
+
seen.add(e.entryId);
|
|
16222
|
+
continue;
|
|
16223
|
+
}
|
|
16020
16224
|
if (i <= after) {
|
|
16021
16225
|
seen.add(e.entryId);
|
|
16022
16226
|
continue;
|
|
@@ -16027,6 +16231,12 @@ function unheard(brief, state, since = null, after = -1) {
|
|
|
16027
16231
|
const text = entryWords(e).trim();
|
|
16028
16232
|
if (text) words2.push(text);
|
|
16029
16233
|
}
|
|
16234
|
+
if (pendingAt !== null) for (const answer of brief.answers ?? []) {
|
|
16235
|
+
const key = `answer:${answer.decisionNeedId}`;
|
|
16236
|
+
if (seen.has(key) || Date.parse(answer.acceptedAt) < pendingAt) continue;
|
|
16237
|
+
seen.add(key);
|
|
16238
|
+
words2.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
|
|
16239
|
+
}
|
|
16030
16240
|
return words2;
|
|
16031
16241
|
}
|
|
16032
16242
|
function sinceTheWork(brief) {
|
|
@@ -16095,9 +16305,9 @@ async function gather(state, rail, held) {
|
|
|
16095
16305
|
}
|
|
16096
16306
|
}
|
|
16097
16307
|
read.add(goalId);
|
|
16098
|
-
const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
|
|
16308
|
+
const fresh = !brief.reviewSince && (found.via === "waiting work" || found.via === "assigned") ? sinceTheWork(brief) : { after: -1 };
|
|
16099
16309
|
if (found.via === "assigned") announced.add(goalId);
|
|
16100
|
-
heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
|
|
16310
|
+
heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince), ...fresh.note ? { note: fresh.note } : {} });
|
|
16101
16311
|
}
|
|
16102
16312
|
for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
16103
16313
|
return { heard, listed };
|
|
@@ -16183,8 +16393,8 @@ async function acknowledge(goalId, brief, state, rail, log2) {
|
|
|
16183
16393
|
if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
|
|
16184
16394
|
acked.set(goalId, brief.revision);
|
|
16185
16395
|
try {
|
|
16186
|
-
|
|
16187
|
-
const done = await rail.update(goalId, { revision:
|
|
16396
|
+
if (brief.revision === void 0) throw new Error("review_revision_missing");
|
|
16397
|
+
const done = await rail.update(goalId, { revision: brief.revision, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
|
|
16188
16398
|
acked.set(goalId, done.revision);
|
|
16189
16399
|
if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
|
|
16190
16400
|
} catch (error) {
|
|
@@ -16221,25 +16431,26 @@ async function look(session, state, rail, opts) {
|
|
|
16221
16431
|
brief2 = await rail.claim();
|
|
16222
16432
|
if (!brief2.goalId || brief2.revision === void 0) {
|
|
16223
16433
|
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
16224
|
-
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
16434
|
+
send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
|
|
16225
16435
|
return;
|
|
16226
16436
|
}
|
|
16227
16437
|
} else {
|
|
16228
16438
|
return;
|
|
16229
16439
|
}
|
|
16230
16440
|
}
|
|
16231
|
-
unheard(brief2, state);
|
|
16232
|
-
if (
|
|
16233
|
-
|
|
16441
|
+
const pendingWords = brief2.reviewSince ? unheard(brief2, state, null, -1, brief2.reviewSince) : null;
|
|
16442
|
+
if (!pendingWords) unheard(brief2, state);
|
|
16443
|
+
if (!ended(brief2.state)) {
|
|
16234
16444
|
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
16235
16445
|
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
16236
16446
|
}
|
|
16237
16447
|
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
16238
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
16448
|
+
const said = (pendingWords ?? (brief2.entries ?? []).map((e) => entryWords(e).trim())).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
16239
16449
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
16240
16450
|
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
16241
16451
|
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
16242
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
16452
|
+
send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
|
|
16453
|
+
if (ended(brief2.state)) await acknowledge(brief2.goalId, brief2, state, rail, opts?.log);
|
|
16243
16454
|
return;
|
|
16244
16455
|
}
|
|
16245
16456
|
const brief = await rail.read(state.goal.id);
|
|
@@ -16248,6 +16459,7 @@ async function look(session, state, rail, opts) {
|
|
|
16248
16459
|
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
16249
16460
|
if (!resolve3) continue;
|
|
16250
16461
|
state.asks.delete(answer.requestEntryId);
|
|
16462
|
+
(state.seen ??= /* @__PURE__ */ new Set()).add(`answer:${answer.decisionNeedId}`);
|
|
16251
16463
|
resolve3(answer.result);
|
|
16252
16464
|
}
|
|
16253
16465
|
if (ended(brief.state)) {
|
|
@@ -16325,8 +16537,9 @@ function runHarness(opts) {
|
|
|
16325
16537
|
const state = { resting: true };
|
|
16326
16538
|
const clock4 = beat();
|
|
16327
16539
|
let session = null;
|
|
16328
|
-
const speaksAs = opts.
|
|
16329
|
-
const
|
|
16540
|
+
const speaksAs = opts.session ?? opts.resume ?? sessionId();
|
|
16541
|
+
const instance = randomUUID6();
|
|
16542
|
+
const rail = railFor({ token: opts.token, reach: reachAs(speaksAs, instance) });
|
|
16330
16543
|
let inbox;
|
|
16331
16544
|
if (opts.harness === "claude") {
|
|
16332
16545
|
try {
|
|
@@ -16382,6 +16595,7 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
16382
16595
|
}
|
|
16383
16596
|
}
|
|
16384
16597
|
session = startSession({
|
|
16598
|
+
instance,
|
|
16385
16599
|
harness: opts.harness,
|
|
16386
16600
|
cwd: opts.cwd,
|
|
16387
16601
|
mode: opts.mode,
|
|
@@ -16425,7 +16639,7 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
16425
16639
|
// src/update.ts
|
|
16426
16640
|
import { execFile } from "child_process";
|
|
16427
16641
|
import { createRequire } from "module";
|
|
16428
|
-
import { dirname as dirname3, join as
|
|
16642
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
16429
16643
|
import { promisify } from "util";
|
|
16430
16644
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
16431
16645
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -16452,7 +16666,7 @@ async function latestVersion() {
|
|
|
16452
16666
|
}
|
|
16453
16667
|
async function install(version) {
|
|
16454
16668
|
const bin = dirname3(process.execPath);
|
|
16455
|
-
await promisify(execFile)(
|
|
16669
|
+
await promisify(execFile)(join10(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
16456
16670
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
16457
16671
|
timeout: 5 * 6e4
|
|
16458
16672
|
});
|
|
@@ -16503,7 +16717,7 @@ function selfUpdater(deps) {
|
|
|
16503
16717
|
}
|
|
16504
16718
|
|
|
16505
16719
|
// src/host.ts
|
|
16506
|
-
var HOST_FILE =
|
|
16720
|
+
var HOST_FILE = join11(homedir10(), ".paigy", "host.json");
|
|
16507
16721
|
function wakeHarness(slot, saved, claudeTranscript) {
|
|
16508
16722
|
if (slot === "Desktop") return void 0;
|
|
16509
16723
|
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
@@ -16515,7 +16729,7 @@ function wakeHarness(slot, saved, claudeTranscript) {
|
|
|
16515
16729
|
}
|
|
16516
16730
|
function readHostState() {
|
|
16517
16731
|
try {
|
|
16518
|
-
const s = JSON.parse(
|
|
16732
|
+
const s = JSON.parse(readFileSync6(HOST_FILE, "utf8"));
|
|
16519
16733
|
if (Date.now() - s.at > 2e4) return null;
|
|
16520
16734
|
try {
|
|
16521
16735
|
process.kill(s.pid, 0);
|
|
@@ -16687,6 +16901,31 @@ function startHost(opts) {
|
|
|
16687
16901
|
void refreshIdentities();
|
|
16688
16902
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
16689
16903
|
}
|
|
16904
|
+
const ends = await claimSessionEnds(asHost).catch(() => []);
|
|
16905
|
+
for (const end of ends) {
|
|
16906
|
+
const result = await endSession(end, {
|
|
16907
|
+
slotsFor: (agent2) => listSlots().filter((slot) => slot !== "Desktop" && slotIdentity(slot).tokenId === agent2),
|
|
16908
|
+
pidsFor: (slot) => localHolderPids().get(slot) ?? [],
|
|
16909
|
+
ownRun: (slot) => runs.get(runKey(slot))?.run ?? null,
|
|
16910
|
+
isOwn: (pid) => descendsFrom(pid, process.pid),
|
|
16911
|
+
kill: (pid, signal) => {
|
|
16912
|
+
process.kill(pid, signal);
|
|
16913
|
+
},
|
|
16914
|
+
alive: (pid) => {
|
|
16915
|
+
try {
|
|
16916
|
+
process.kill(pid, 0);
|
|
16917
|
+
return true;
|
|
16918
|
+
} catch (e) {
|
|
16919
|
+
return e.code === "EPERM";
|
|
16920
|
+
}
|
|
16921
|
+
},
|
|
16922
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
16923
|
+
log: opts.log
|
|
16924
|
+
}).catch((e) => ({ ok: false, why: `end_threw: ${e.message}`.slice(0, 200) }));
|
|
16925
|
+
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}`);
|
|
16926
|
+
await finishSessionEnd(end.endId, result, asHost).catch(() => {
|
|
16927
|
+
});
|
|
16928
|
+
}
|
|
16690
16929
|
}
|
|
16691
16930
|
async function presenceMap() {
|
|
16692
16931
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -16711,6 +16950,14 @@ function startHost(opts) {
|
|
|
16711
16950
|
const key = runKey(slot);
|
|
16712
16951
|
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
16713
16952
|
const live = runs.get(key);
|
|
16953
|
+
if (live) {
|
|
16954
|
+
const theirs = (localHolderPids().get(slot) ?? []).map((p) => p.pid).filter((pid) => !descendsFrom(pid, process.pid));
|
|
16955
|
+
if (theirs.length) {
|
|
16956
|
+
opts.log(`session_stood_back ${JSON.stringify({ sessionId: slot, livePid: theirs[0] })} \u2014 a terminal runs this conversation; stopping this host's copy`);
|
|
16957
|
+
live.run.stop();
|
|
16958
|
+
return;
|
|
16959
|
+
}
|
|
16960
|
+
}
|
|
16714
16961
|
const tokenId = slotIdentity(slot).tokenId;
|
|
16715
16962
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
16716
16963
|
const holder = holders.get(slot);
|
|
@@ -16956,7 +17203,7 @@ var win = null;
|
|
|
16956
17203
|
function log(message) {
|
|
16957
17204
|
win?.webContents.send("paigy:log", message);
|
|
16958
17205
|
}
|
|
16959
|
-
var ICON =
|
|
17206
|
+
var ICON = join12(here, "..", "assets", "icon.png");
|
|
16960
17207
|
function createWindow() {
|
|
16961
17208
|
win = new BrowserWindow({
|
|
16962
17209
|
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
@@ -16968,9 +17215,9 @@ function createWindow() {
|
|
|
16968
17215
|
minHeight: 520,
|
|
16969
17216
|
icon: ICON,
|
|
16970
17217
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
16971
|
-
webPreferences: { preload:
|
|
17218
|
+
webPreferences: { preload: join12(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
16972
17219
|
});
|
|
16973
|
-
void win.loadFile(
|
|
17220
|
+
void win.loadFile(join12(here, "..", "renderer", "index.html"));
|
|
16974
17221
|
}
|
|
16975
17222
|
var host = null;
|
|
16976
17223
|
app.whenReady().then(() => {
|
|
@@ -17039,7 +17286,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
17039
17286
|
});
|
|
17040
17287
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
17041
17288
|
try {
|
|
17042
|
-
const all =
|
|
17289
|
+
const all = readFileSync7(join12(homedir11(), ".paigy", "host.log"), "utf8").split("\n");
|
|
17043
17290
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
17044
17291
|
return mine.slice(-500).join("\n");
|
|
17045
17292
|
} catch {
|