@paigy/harness 0.3.15 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +302 -137
- package/dist/main.js +297 -136
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -4580,7 +4580,7 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
|
4580
4580
|
import { spawn as spawn2 } from "child_process";
|
|
4581
4581
|
import { readFileSync as readFileSync5 } from "fs";
|
|
4582
4582
|
import { fileURLToPath } from "url";
|
|
4583
|
-
import { dirname as dirname4, join as
|
|
4583
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
4584
4584
|
|
|
4585
4585
|
// ../../packages/sdk/dist/index.js
|
|
4586
4586
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
@@ -9973,7 +9973,7 @@ var AskInputSchema = external_exports.object({
|
|
|
9973
9973
|
parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
9974
9974
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
9975
9975
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
9976
|
-
"
|
|
9976
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
|
|
9977
9977
|
),
|
|
9978
9978
|
options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
|
|
9979
9979
|
}).strict();
|
|
@@ -9984,7 +9984,7 @@ var StartContactSchema = external_exports.object({
|
|
|
9984
9984
|
}).strict();
|
|
9985
9985
|
var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
9986
9986
|
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}.
|
|
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 \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: 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).";
|
|
9988
9988
|
var CreateGoalSchema = external_exports.object({
|
|
9989
9989
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
9990
9990
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -10031,9 +10031,9 @@ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({
|
|
|
10031
10031
|
var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
10032
10032
|
var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
10033
10033
|
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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
|
|
10034
|
-
var UPDATE_GOAL_DESCRIPTION =
|
|
10035
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
10036
|
-
var CHECK_REPLIES_DESCRIPTION = "
|
|
10034
|
+
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. 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. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
10035
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
10036
|
+
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.";
|
|
10037
10037
|
var CheckRepliesSchema = external_exports.object({}).strict();
|
|
10038
10038
|
var AGENT_TOOLS = [
|
|
10039
10039
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
@@ -10363,9 +10363,9 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10363
10363
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
10364
10364
|
* the planner declares duplicates instead of asking them twice. */
|
|
10365
10365
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
10366
|
-
/**
|
|
10367
|
-
*
|
|
10368
|
-
info: external_exports.array(external_exports.string().min(1)).
|
|
10366
|
+
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
10367
|
+
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
10368
|
+
info: external_exports.array(external_exports.string().min(1)).default([]),
|
|
10369
10369
|
question: external_exports.string().min(1).nullable(),
|
|
10370
10370
|
/** True on the one turn carrying the agent's own declared question. */
|
|
10371
10371
|
asks: external_exports.boolean().optional(),
|
|
@@ -10400,32 +10400,6 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10400
10400
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
10401
10401
|
var InboxItemSchema = external_exports.object({
|
|
10402
10402
|
id: external_exports.string(),
|
|
10403
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
10404
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
10405
|
-
parentId: external_exports.string().optional(),
|
|
10406
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
10407
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
10408
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
10409
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
10410
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
10411
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
10412
|
-
* thread, which is what the client did for all rows until now. */
|
|
10413
|
-
askId: external_exports.string().optional(),
|
|
10414
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
10415
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
10416
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
10417
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
10418
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
10419
|
-
seq: external_exports.number().int().optional(),
|
|
10420
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
10421
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
10422
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
10423
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
10424
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
10425
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
10426
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
10427
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
10428
|
-
units: external_exports.number().int().positive().optional(),
|
|
10429
10403
|
tokenId: external_exports.string().optional(),
|
|
10430
10404
|
status: NotifyStatusSchema,
|
|
10431
10405
|
context: ContextSchema,
|
|
@@ -10470,7 +10444,18 @@ var InboxItemSchema = external_exports.object({
|
|
|
10470
10444
|
entryId: external_exports.string(),
|
|
10471
10445
|
title: external_exports.string(),
|
|
10472
10446
|
state: external_exports.enum(["open", "answered"]),
|
|
10473
|
-
answer: external_exports.string().nullable()
|
|
10447
|
+
answer: external_exports.string().nullable(),
|
|
10448
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
10449
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
10450
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
10451
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
10452
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
10453
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
10454
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
10455
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
10456
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
10457
|
+
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
10458
|
+
options: external_exports.array(OptionSchema).optional()
|
|
10474
10459
|
})).optional(),
|
|
10475
10460
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
10476
10461
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -10488,10 +10473,20 @@ var InboxItemSchema = external_exports.object({
|
|
|
10488
10473
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
10489
10474
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
10490
10475
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
10491
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
10492
|
-
*
|
|
10476
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
10477
|
+
* error badge + Retry. */
|
|
10493
10478
|
error: external_exports.string().optional(),
|
|
10479
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
10480
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
10481
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
10482
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
10483
|
+
cold: external_exports.string().datetime().optional(),
|
|
10494
10484
|
clarifies: external_exports.string().optional(),
|
|
10485
|
+
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
10486
|
+
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
10487
|
+
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
10488
|
+
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
10489
|
+
onCall: external_exports.literal(true).optional(),
|
|
10495
10490
|
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
10496
10491
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
10497
10492
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -10540,7 +10535,10 @@ var InboxItemSchema = external_exports.object({
|
|
|
10540
10535
|
goalIds: external_exports.array(external_exports.string()),
|
|
10541
10536
|
decisionNeedId: external_exports.string().optional(),
|
|
10542
10537
|
sealed: external_exports.boolean(),
|
|
10543
|
-
goalState: external_exports.string().optional()
|
|
10538
|
+
goalState: external_exports.string().optional(),
|
|
10539
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
10540
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
10541
|
+
goalTitle: external_exports.string().optional()
|
|
10544
10542
|
}).optional()
|
|
10545
10543
|
});
|
|
10546
10544
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
@@ -10639,7 +10637,6 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10639
10637
|
});
|
|
10640
10638
|
var HistoryItemSchema = external_exports.object({
|
|
10641
10639
|
id: external_exports.string(),
|
|
10642
|
-
parentId: external_exports.string(),
|
|
10643
10640
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
10644
10641
|
initiator: external_exports.enum(["user", "agent"]),
|
|
10645
10642
|
title: external_exports.string(),
|
|
@@ -10798,6 +10795,24 @@ var QueueQuestionSchema = external_exports.object({
|
|
|
10798
10795
|
* gate at all. */
|
|
10799
10796
|
blocked: external_exports.boolean().default(false)
|
|
10800
10797
|
});
|
|
10798
|
+
var QueueReplySchema = external_exports.object({
|
|
10799
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
10800
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
10801
|
+
* walk can name it in its zoom. */
|
|
10802
|
+
id: external_exports.string(),
|
|
10803
|
+
/** The Goal the note is on. */
|
|
10804
|
+
goalId: external_exports.string(),
|
|
10805
|
+
/** What the note said. */
|
|
10806
|
+
note: external_exports.string(),
|
|
10807
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
10808
|
+
deliveryId: external_exports.string(),
|
|
10809
|
+
requestEntryId: external_exports.string(),
|
|
10810
|
+
askedAt: external_exports.string(),
|
|
10811
|
+
/** When the person last replied — the window's start. */
|
|
10812
|
+
repliedAt: external_exports.string(),
|
|
10813
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
10814
|
+
reply: external_exports.string().nullable()
|
|
10815
|
+
});
|
|
10801
10816
|
var QueueItemSchema = external_exports.object({
|
|
10802
10817
|
id: external_exports.string(),
|
|
10803
10818
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -10825,6 +10840,10 @@ var QueueItemSchema = external_exports.object({
|
|
|
10825
10840
|
blocked: external_exports.boolean().default(false),
|
|
10826
10841
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
10827
10842
|
questions: external_exports.array(QueueQuestionSchema).default([]),
|
|
10843
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
10844
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
10845
|
+
* not spell an empty list. */
|
|
10846
|
+
replies: external_exports.array(QueueReplySchema).optional(),
|
|
10828
10847
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
10829
10848
|
repo: external_exports.string().nullable().optional(),
|
|
10830
10849
|
createdAt: external_exports.string(),
|
|
@@ -11285,6 +11304,7 @@ function currentRepo(cwd = process.cwd()) {
|
|
|
11285
11304
|
}
|
|
11286
11305
|
return asked;
|
|
11287
11306
|
}
|
|
11307
|
+
var NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
|
|
11288
11308
|
async function contact(input, opts = {}) {
|
|
11289
11309
|
const parsed = ContactSchema.parse(input);
|
|
11290
11310
|
opts.signal?.throwIfAborted();
|
|
@@ -11292,6 +11312,7 @@ async function contact(input, opts = {}) {
|
|
|
11292
11312
|
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
|
|
11293
11313
|
let deliveryId;
|
|
11294
11314
|
let deliveries = [];
|
|
11315
|
+
let recorded = [];
|
|
11295
11316
|
if ("deliveryId" in parsed) {
|
|
11296
11317
|
deliveryId = parsed.deliveryId;
|
|
11297
11318
|
} else {
|
|
@@ -11320,16 +11341,24 @@ async function contact(input, opts = {}) {
|
|
|
11320
11341
|
if (!res.ok) await fail("batch contact", res);
|
|
11321
11342
|
const receipt = await res.json();
|
|
11322
11343
|
deliveries = receipt.deliveries ?? [];
|
|
11344
|
+
recorded = receipt.recorded ?? [];
|
|
11345
|
+
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
11323
11346
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
11324
11347
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
11325
11348
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
11326
11349
|
}
|
|
11327
|
-
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
|
|
11350
|
+
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
|
|
11351
|
+
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
11328
11352
|
const read = async (signal2) => {
|
|
11329
11353
|
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
11330
11354
|
if (!res.ok) await fail("read_delivery", res);
|
|
11331
11355
|
const one = await res.json();
|
|
11332
|
-
return
|
|
11356
|
+
return {
|
|
11357
|
+
...one,
|
|
11358
|
+
...all.length > 1 ? { deliveries: all } : {},
|
|
11359
|
+
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
11360
|
+
...joinedCard ? { joinedCard: true } : {}
|
|
11361
|
+
};
|
|
11333
11362
|
};
|
|
11334
11363
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
11335
11364
|
if (opts.waits === false) return read(opts.signal);
|
|
@@ -11359,47 +11388,108 @@ async function checkReplies(opts = {}) {
|
|
|
11359
11388
|
var HEARTBEAT_MS = 25e3;
|
|
11360
11389
|
var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
11361
11390
|
var JOIN = { config: { broadcast: { self: false } } };
|
|
11391
|
+
var JOIN_TIMEOUT_MS = 1e4;
|
|
11362
11392
|
var lines = /* @__PURE__ */ new Map();
|
|
11363
11393
|
function frame(line, topic, event, payload) {
|
|
11394
|
+
const ref = String(++line.ref);
|
|
11364
11395
|
try {
|
|
11365
|
-
line.ws?.send(JSON.stringify({ topic, event, payload, ref
|
|
11396
|
+
line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
|
|
11366
11397
|
} catch {
|
|
11367
11398
|
}
|
|
11399
|
+
return ref;
|
|
11368
11400
|
}
|
|
11369
11401
|
function stop(line) {
|
|
11370
11402
|
line.open = false;
|
|
11403
|
+
line.beat = void 0;
|
|
11371
11404
|
clearInterval(line.heart);
|
|
11372
11405
|
clearTimeout(line.retry);
|
|
11406
|
+
for (const t of line.topics.values()) {
|
|
11407
|
+
clearTimeout(t.timer);
|
|
11408
|
+
t.joined = false;
|
|
11409
|
+
t.ref = void 0;
|
|
11410
|
+
}
|
|
11411
|
+
}
|
|
11412
|
+
function join3(line, name, t) {
|
|
11413
|
+
clearTimeout(t.timer);
|
|
11414
|
+
t.joined = false;
|
|
11415
|
+
t.ref = frame(line, `realtime:${name}`, "phx_join", JOIN);
|
|
11416
|
+
t.timer = setTimeout(() => rejoin(line, name, t), JOIN_TIMEOUT_MS);
|
|
11417
|
+
}
|
|
11418
|
+
function rejoin(line, name, t) {
|
|
11419
|
+
clearTimeout(t.timer);
|
|
11420
|
+
t.joined = false;
|
|
11421
|
+
t.ref = void 0;
|
|
11422
|
+
if (!line.open || line.topics.get(name) !== t) return;
|
|
11423
|
+
t.timer = setTimeout(() => {
|
|
11424
|
+
if (line.open && line.topics.get(name) === t) join3(line, name, t);
|
|
11425
|
+
}, RETRY_MS[Math.min(t.attempt++, RETRY_MS.length - 1)]);
|
|
11426
|
+
}
|
|
11427
|
+
function joined(t) {
|
|
11428
|
+
clearTimeout(t.timer);
|
|
11429
|
+
t.joined = true;
|
|
11430
|
+
t.attempt = 0;
|
|
11431
|
+
for (const l of t.listeners) l.onJoined?.(++l.joins);
|
|
11373
11432
|
}
|
|
11374
11433
|
function connect(line) {
|
|
11375
11434
|
const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
11376
11435
|
const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
|
|
11436
|
+
let gone = false;
|
|
11437
|
+
const again = () => {
|
|
11438
|
+
if (gone) return;
|
|
11439
|
+
gone = true;
|
|
11440
|
+
stop(line);
|
|
11441
|
+
if (!line.topics.size) return;
|
|
11442
|
+
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
11443
|
+
};
|
|
11377
11444
|
ws.addEventListener("open", () => {
|
|
11445
|
+
if (gone) return;
|
|
11378
11446
|
line.open = true;
|
|
11379
11447
|
line.attempt = 0;
|
|
11380
|
-
for (const
|
|
11381
|
-
line.heart = setInterval(() =>
|
|
11448
|
+
for (const [name, t] of line.topics) join3(line, name, t);
|
|
11449
|
+
line.heart = setInterval(() => {
|
|
11450
|
+
if (line.beat) {
|
|
11451
|
+
try {
|
|
11452
|
+
ws.close();
|
|
11453
|
+
} catch {
|
|
11454
|
+
}
|
|
11455
|
+
again();
|
|
11456
|
+
return;
|
|
11457
|
+
}
|
|
11458
|
+
line.beat = frame(line, "phoenix", "heartbeat", {});
|
|
11459
|
+
}, HEARTBEAT_MS);
|
|
11382
11460
|
});
|
|
11383
11461
|
ws.addEventListener("message", (e) => {
|
|
11462
|
+
if (gone) return;
|
|
11384
11463
|
let m;
|
|
11385
11464
|
try {
|
|
11386
11465
|
m = JSON.parse(String(e.data));
|
|
11387
11466
|
} catch {
|
|
11388
11467
|
return;
|
|
11389
11468
|
}
|
|
11390
|
-
if (m.
|
|
11391
|
-
|
|
11469
|
+
if (m.topic === "phoenix") {
|
|
11470
|
+
if (m.event === "phx_reply" && m.ref === line.beat) line.beat = void 0;
|
|
11471
|
+
return;
|
|
11472
|
+
}
|
|
11473
|
+
if (!m.topic?.startsWith("realtime:")) return;
|
|
11474
|
+
const name = m.topic.slice("realtime:".length);
|
|
11475
|
+
const t = line.topics.get(name);
|
|
11476
|
+
if (!t) return;
|
|
11477
|
+
if (m.event === "phx_reply") {
|
|
11478
|
+
if (!t.ref || m.ref !== t.ref) return;
|
|
11479
|
+
if (m.payload?.status === "ok") joined(t);
|
|
11480
|
+
else rejoin(line, name, t);
|
|
11481
|
+
return;
|
|
11482
|
+
}
|
|
11483
|
+
if (m.event === "phx_error" || m.event === "phx_close") {
|
|
11484
|
+
if (m.ref && m.ref !== t.ref) return;
|
|
11485
|
+
rejoin(line, name, t);
|
|
11486
|
+
return;
|
|
11487
|
+
}
|
|
11488
|
+
if (m.event !== "broadcast") return;
|
|
11489
|
+
for (const l of t.listeners) {
|
|
11392
11490
|
if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
|
|
11393
11491
|
}
|
|
11394
11492
|
});
|
|
11395
|
-
let gone = false;
|
|
11396
|
-
const again = () => {
|
|
11397
|
-
if (gone) return;
|
|
11398
|
-
gone = true;
|
|
11399
|
-
stop(line);
|
|
11400
|
-
if (!line.topics.size) return;
|
|
11401
|
-
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
11402
|
-
};
|
|
11403
11493
|
ws.addEventListener("close", again);
|
|
11404
11494
|
ws.addEventListener("error", again);
|
|
11405
11495
|
}
|
|
@@ -11407,17 +11497,22 @@ function subscribeRealtime(args) {
|
|
|
11407
11497
|
const key = `${args.url} ${args.anonKey}`;
|
|
11408
11498
|
let line = lines.get(key);
|
|
11409
11499
|
if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
11410
|
-
let
|
|
11411
|
-
if (!
|
|
11412
|
-
line.topics.set(args.topic,
|
|
11413
|
-
if (line.open)
|
|
11414
|
-
}
|
|
11415
|
-
const
|
|
11416
|
-
|
|
11500
|
+
let t = line.topics.get(args.topic);
|
|
11501
|
+
if (!t) {
|
|
11502
|
+
line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
|
|
11503
|
+
if (line.open) join3(line, args.topic, t);
|
|
11504
|
+
}
|
|
11505
|
+
const topic = t;
|
|
11506
|
+
const me = { event: args.event, onMessage: args.onMessage, onJoined: args.onJoined, joins: 0 };
|
|
11507
|
+
topic.listeners.add(me);
|
|
11508
|
+
if (topic.joined && me.onJoined) queueMicrotask(() => {
|
|
11509
|
+
if (topic.joined && topic.listeners.has(me)) me.onJoined?.(++me.joins);
|
|
11510
|
+
});
|
|
11417
11511
|
if (!line.ws) connect(line);
|
|
11418
11512
|
return {
|
|
11419
11513
|
close: () => {
|
|
11420
|
-
if (!
|
|
11514
|
+
if (!topic.listeners.delete(me) || topic.listeners.size) return;
|
|
11515
|
+
clearTimeout(topic.timer);
|
|
11421
11516
|
line.topics.delete(args.topic);
|
|
11422
11517
|
if (line.topics.size) {
|
|
11423
11518
|
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
@@ -11433,7 +11528,7 @@ function subscribeRealtime(args) {
|
|
|
11433
11528
|
};
|
|
11434
11529
|
}
|
|
11435
11530
|
async function subscribeWake(onNudge, opts = {}) {
|
|
11436
|
-
const { release = true, ...client } = opts;
|
|
11531
|
+
const { release = true, onJoined, ...client } = opts;
|
|
11437
11532
|
const cfg = await registerDelivery("self_hosted", client);
|
|
11438
11533
|
const { url, anonKey } = cfg.realtime;
|
|
11439
11534
|
const channel = wakeChannel(cfg.tokenId);
|
|
@@ -11442,7 +11537,8 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
11442
11537
|
anonKey,
|
|
11443
11538
|
topic: channel,
|
|
11444
11539
|
event: WAKE_EVENT,
|
|
11445
|
-
onMessage: (payload) => onNudge(payload ?? {})
|
|
11540
|
+
onMessage: (payload) => onNudge(payload ?? {}),
|
|
11541
|
+
onJoined
|
|
11446
11542
|
});
|
|
11447
11543
|
return {
|
|
11448
11544
|
tokenId: cfg.tokenId,
|
|
@@ -11463,7 +11559,7 @@ import { homedir as homedir9, hostname } from "os";
|
|
|
11463
11559
|
import { spawnSync } from "child_process";
|
|
11464
11560
|
import { existsSync as existsSync2 } from "fs";
|
|
11465
11561
|
import { homedir as homedir3 } from "os";
|
|
11466
|
-
import { delimiter, dirname, join as
|
|
11562
|
+
import { delimiter, dirname, join as join4 } from "path";
|
|
11467
11563
|
var CATALOG = [
|
|
11468
11564
|
{
|
|
11469
11565
|
name: "claude",
|
|
@@ -11499,10 +11595,10 @@ var CATALOG = [
|
|
|
11499
11595
|
function probeDirs(home = homedir3()) {
|
|
11500
11596
|
return [
|
|
11501
11597
|
dirname(process.execPath),
|
|
11502
|
-
|
|
11598
|
+
join4(home, ".local", "bin"),
|
|
11503
11599
|
"/usr/local/bin",
|
|
11504
11600
|
"/opt/homebrew/bin",
|
|
11505
|
-
|
|
11601
|
+
join4(home, ".npm-global", "bin")
|
|
11506
11602
|
];
|
|
11507
11603
|
}
|
|
11508
11604
|
function which(bin) {
|
|
@@ -11510,7 +11606,7 @@ function which(bin) {
|
|
|
11510
11606
|
const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
|
|
11511
11607
|
if (fromPath) return fromPath;
|
|
11512
11608
|
for (const dir of probeDirs()) {
|
|
11513
|
-
const candidate =
|
|
11609
|
+
const candidate = join4(dir, bin);
|
|
11514
11610
|
if (existsSync2(candidate)) return candidate;
|
|
11515
11611
|
}
|
|
11516
11612
|
return null;
|
|
@@ -11552,14 +11648,14 @@ function detectAll(deps = {}) {
|
|
|
11552
11648
|
// src/host.ts
|
|
11553
11649
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
11554
11650
|
import { homedir as homedir7 } from "os";
|
|
11555
|
-
import { join as
|
|
11651
|
+
import { join as join8 } from "path";
|
|
11556
11652
|
|
|
11557
11653
|
// src/workspaces.ts
|
|
11558
11654
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
11559
|
-
import { dirname as dirname2, join as
|
|
11655
|
+
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
11560
11656
|
import { homedir as homedir4 } from "os";
|
|
11561
11657
|
function workspacesFile() {
|
|
11562
|
-
return
|
|
11658
|
+
return join5(homedir4(), ".paigy", "workspaces.json");
|
|
11563
11659
|
}
|
|
11564
11660
|
function listWorkspaces(deps) {
|
|
11565
11661
|
try {
|
|
@@ -11600,7 +11696,7 @@ function resolveWakeDir(pinned, deps) {
|
|
|
11600
11696
|
// src/owner.ts
|
|
11601
11697
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
11602
11698
|
import { homedir as homedir5 } from "os";
|
|
11603
|
-
import { join as
|
|
11699
|
+
import { join as join6 } from "path";
|
|
11604
11700
|
var HEAD_BYTES = 64e3;
|
|
11605
11701
|
function readHead(file) {
|
|
11606
11702
|
const fd = openSync2(file, "r");
|
|
@@ -11616,7 +11712,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11616
11712
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
11617
11713
|
if (!m) return null;
|
|
11618
11714
|
const prefix = m[1];
|
|
11619
|
-
const root = deps.projectsDir ??
|
|
11715
|
+
const root = deps.projectsDir ?? join6(homedir5(), ".claude", "projects");
|
|
11620
11716
|
const list = deps.list ?? ((dir) => readdirSync(dir));
|
|
11621
11717
|
const head = deps.head ?? readHead;
|
|
11622
11718
|
let folders;
|
|
@@ -11628,7 +11724,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11628
11724
|
for (const folder of folders) {
|
|
11629
11725
|
let files;
|
|
11630
11726
|
try {
|
|
11631
|
-
files = list(
|
|
11727
|
+
files = list(join6(root, folder));
|
|
11632
11728
|
} catch {
|
|
11633
11729
|
continue;
|
|
11634
11730
|
}
|
|
@@ -11636,7 +11732,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11636
11732
|
if (!file) continue;
|
|
11637
11733
|
let text;
|
|
11638
11734
|
try {
|
|
11639
|
-
text = head(
|
|
11735
|
+
text = head(join6(root, folder, file));
|
|
11640
11736
|
} catch {
|
|
11641
11737
|
continue;
|
|
11642
11738
|
}
|
|
@@ -12991,7 +13087,7 @@ var AskInputSchema2 = external_exports.object({
|
|
|
12991
13087
|
parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
12992
13088
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
12993
13089
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
12994
|
-
"
|
|
13090
|
+
"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 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
|
|
12995
13091
|
),
|
|
12996
13092
|
options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
|
|
12997
13093
|
}).strict();
|
|
@@ -13002,7 +13098,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
13002
13098
|
}).strict();
|
|
13003
13099
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
13004
13100
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
13005
|
-
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}.
|
|
13101
|
+
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 \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: 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).";
|
|
13006
13102
|
var CreateGoalSchema2 = external_exports.object({
|
|
13007
13103
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
13008
13104
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -13049,9 +13145,9 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
|
|
|
13049
13145
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
13050
13146
|
var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
13051
13147
|
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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
|
|
13052
|
-
var UPDATE_GOAL_DESCRIPTION2 =
|
|
13053
|
-
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
13054
|
-
var CHECK_REPLIES_DESCRIPTION2 = "
|
|
13148
|
+
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. 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. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
13149
|
+
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.";
|
|
13150
|
+
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.";
|
|
13055
13151
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
13056
13152
|
var AGENT_TOOLS2 = [
|
|
13057
13153
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
@@ -13396,9 +13492,9 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13396
13492
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
13397
13493
|
* the planner declares duplicates instead of asking them twice. */
|
|
13398
13494
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
13399
|
-
/**
|
|
13400
|
-
*
|
|
13401
|
-
info: external_exports.array(external_exports.string().min(1)).
|
|
13495
|
+
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
13496
|
+
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
13497
|
+
info: external_exports.array(external_exports.string().min(1)).default([]),
|
|
13402
13498
|
question: external_exports.string().min(1).nullable(),
|
|
13403
13499
|
/** True on the one turn carrying the agent's own declared question. */
|
|
13404
13500
|
asks: external_exports.boolean().optional(),
|
|
@@ -13433,32 +13529,6 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13433
13529
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
13434
13530
|
var InboxItemSchema2 = external_exports.object({
|
|
13435
13531
|
id: external_exports.string(),
|
|
13436
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
13437
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
13438
|
-
parentId: external_exports.string().optional(),
|
|
13439
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
13440
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
13441
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
13442
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
13443
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
13444
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
13445
|
-
* thread, which is what the client did for all rows until now. */
|
|
13446
|
-
askId: external_exports.string().optional(),
|
|
13447
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
13448
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
13449
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
13450
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
13451
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
13452
|
-
seq: external_exports.number().int().optional(),
|
|
13453
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
13454
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
13455
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
13456
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
13457
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
13458
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
13459
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
13460
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
13461
|
-
units: external_exports.number().int().positive().optional(),
|
|
13462
13532
|
tokenId: external_exports.string().optional(),
|
|
13463
13533
|
status: NotifyStatusSchema2,
|
|
13464
13534
|
context: ContextSchema2,
|
|
@@ -13503,7 +13573,18 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13503
13573
|
entryId: external_exports.string(),
|
|
13504
13574
|
title: external_exports.string(),
|
|
13505
13575
|
state: external_exports.enum(["open", "answered"]),
|
|
13506
|
-
answer: external_exports.string().nullable()
|
|
13576
|
+
answer: external_exports.string().nullable(),
|
|
13577
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
13578
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
13579
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
13580
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
13581
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
|
|
13582
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
13583
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
13584
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
13585
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
13586
|
+
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
13587
|
+
options: external_exports.array(OptionSchema2).optional()
|
|
13507
13588
|
})).optional(),
|
|
13508
13589
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
13509
13590
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -13521,10 +13602,20 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13521
13602
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
13522
13603
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
13523
13604
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
13524
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
13525
|
-
*
|
|
13605
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
13606
|
+
* error badge + Retry. */
|
|
13526
13607
|
error: external_exports.string().optional(),
|
|
13608
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
13609
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
13610
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
13611
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
13612
|
+
cold: external_exports.string().datetime().optional(),
|
|
13527
13613
|
clarifies: external_exports.string().optional(),
|
|
13614
|
+
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
13615
|
+
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
13616
|
+
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
13617
|
+
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
13618
|
+
onCall: external_exports.literal(true).optional(),
|
|
13528
13619
|
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
13529
13620
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
13530
13621
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -13573,7 +13664,10 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13573
13664
|
goalIds: external_exports.array(external_exports.string()),
|
|
13574
13665
|
decisionNeedId: external_exports.string().optional(),
|
|
13575
13666
|
sealed: external_exports.boolean(),
|
|
13576
|
-
goalState: external_exports.string().optional()
|
|
13667
|
+
goalState: external_exports.string().optional(),
|
|
13668
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
13669
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
13670
|
+
goalTitle: external_exports.string().optional()
|
|
13577
13671
|
}).optional()
|
|
13578
13672
|
});
|
|
13579
13673
|
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
@@ -13672,7 +13766,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13672
13766
|
});
|
|
13673
13767
|
var HistoryItemSchema2 = external_exports.object({
|
|
13674
13768
|
id: external_exports.string(),
|
|
13675
|
-
parentId: external_exports.string(),
|
|
13676
13769
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
13677
13770
|
initiator: external_exports.enum(["user", "agent"]),
|
|
13678
13771
|
title: external_exports.string(),
|
|
@@ -13831,6 +13924,24 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
13831
13924
|
* gate at all. */
|
|
13832
13925
|
blocked: external_exports.boolean().default(false)
|
|
13833
13926
|
});
|
|
13927
|
+
var QueueReplySchema2 = external_exports.object({
|
|
13928
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
13929
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
13930
|
+
* walk can name it in its zoom. */
|
|
13931
|
+
id: external_exports.string(),
|
|
13932
|
+
/** The Goal the note is on. */
|
|
13933
|
+
goalId: external_exports.string(),
|
|
13934
|
+
/** What the note said. */
|
|
13935
|
+
note: external_exports.string(),
|
|
13936
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
13937
|
+
deliveryId: external_exports.string(),
|
|
13938
|
+
requestEntryId: external_exports.string(),
|
|
13939
|
+
askedAt: external_exports.string(),
|
|
13940
|
+
/** When the person last replied — the window's start. */
|
|
13941
|
+
repliedAt: external_exports.string(),
|
|
13942
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
13943
|
+
reply: external_exports.string().nullable()
|
|
13944
|
+
});
|
|
13834
13945
|
var QueueItemSchema2 = external_exports.object({
|
|
13835
13946
|
id: external_exports.string(),
|
|
13836
13947
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -13858,6 +13969,10 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13858
13969
|
blocked: external_exports.boolean().default(false),
|
|
13859
13970
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
13860
13971
|
questions: external_exports.array(QueueQuestionSchema2).default([]),
|
|
13972
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
13973
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
13974
|
+
* not spell an empty list. */
|
|
13975
|
+
replies: external_exports.array(QueueReplySchema2).optional(),
|
|
13861
13976
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
13862
13977
|
repo: external_exports.string().nullable().optional(),
|
|
13863
13978
|
createdAt: external_exports.string(),
|
|
@@ -14070,6 +14185,7 @@ import { resolve as resolve2, delimiter as delimiter2 } from "path";
|
|
|
14070
14185
|
|
|
14071
14186
|
// src/harness/acp.ts
|
|
14072
14187
|
var none = { events: [], writes: [] };
|
|
14188
|
+
var NOTHING_TO_RESUME = /* @__PURE__ */ new Set([-32002, -32601]);
|
|
14073
14189
|
function optionFor(options, decision) {
|
|
14074
14190
|
const want = decision.allow ? "allow_once" : "reject_once";
|
|
14075
14191
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
@@ -14166,20 +14282,29 @@ var AcpDriver = class {
|
|
|
14166
14282
|
if (msg.id === this.initId) {
|
|
14167
14283
|
this.initId = void 0;
|
|
14168
14284
|
if (msg.error) {
|
|
14169
|
-
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}
|
|
14285
|
+
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
|
|
14170
14286
|
}
|
|
14171
14287
|
this.sessionNewId = this.nextId++;
|
|
14172
14288
|
return {
|
|
14173
14289
|
events: [],
|
|
14174
|
-
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } :
|
|
14290
|
+
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
|
|
14175
14291
|
};
|
|
14176
14292
|
}
|
|
14177
14293
|
if (msg.id === this.sessionNewId) {
|
|
14178
14294
|
this.sessionNewId = void 0;
|
|
14295
|
+
const resuming = this.resume !== void 0;
|
|
14296
|
+
if (resuming && msg.error?.code !== void 0 && NOTHING_TO_RESUME.has(msg.error.code)) {
|
|
14297
|
+
this.resume = void 0;
|
|
14298
|
+
this.sessionNewId = this.nextId++;
|
|
14299
|
+
return {
|
|
14300
|
+
events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
|
|
14301
|
+
writes: [frame2(this.sessionNew(this.sessionNewId))]
|
|
14302
|
+
};
|
|
14303
|
+
}
|
|
14179
14304
|
const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
|
|
14180
14305
|
if (!sessionId2) {
|
|
14181
14306
|
return {
|
|
14182
|
-
events: [{ kind: "error", message: `${
|
|
14307
|
+
events: [{ kind: "error", message: `${resuming ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}`, fatal: true }],
|
|
14183
14308
|
writes: []
|
|
14184
14309
|
};
|
|
14185
14310
|
}
|
|
@@ -14272,6 +14397,9 @@ var AcpDriver = class {
|
|
|
14272
14397
|
writes: []
|
|
14273
14398
|
};
|
|
14274
14399
|
}
|
|
14400
|
+
sessionNew(id) {
|
|
14401
|
+
return { id, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } };
|
|
14402
|
+
}
|
|
14275
14403
|
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
14276
14404
|
flush() {
|
|
14277
14405
|
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
@@ -14316,7 +14444,7 @@ function startSession(opts) {
|
|
|
14316
14444
|
...process.env,
|
|
14317
14445
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
14318
14446
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
14319
|
-
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
|
|
14447
|
+
...opts.resume ?? opts.session ? { PAIGY_SESSION_ID: opts.resume ?? opts.session } : {},
|
|
14320
14448
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
14321
14449
|
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
14322
14450
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
@@ -14326,7 +14454,11 @@ function startSession(opts) {
|
|
|
14326
14454
|
},
|
|
14327
14455
|
stdio: ["pipe", "pipe", "pipe"]
|
|
14328
14456
|
});
|
|
14457
|
+
let ended2 = false;
|
|
14458
|
+
child.stdin?.on("error", () => {
|
|
14459
|
+
});
|
|
14329
14460
|
const write = (frames) => {
|
|
14461
|
+
if (ended2) return;
|
|
14330
14462
|
for (const f of frames) child.stdin?.write(`${f}
|
|
14331
14463
|
`);
|
|
14332
14464
|
};
|
|
@@ -14335,6 +14467,7 @@ function startSession(opts) {
|
|
|
14335
14467
|
if (text) opts.onEvent({ kind: "error", message: text });
|
|
14336
14468
|
});
|
|
14337
14469
|
child.on("exit", (code) => {
|
|
14470
|
+
ended2 = true;
|
|
14338
14471
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
14339
14472
|
opts.onExit?.();
|
|
14340
14473
|
});
|
|
@@ -14363,6 +14496,7 @@ function startSession(opts) {
|
|
|
14363
14496
|
},
|
|
14364
14497
|
stop() {
|
|
14365
14498
|
write(driver.close());
|
|
14499
|
+
ended2 = true;
|
|
14366
14500
|
child.kill();
|
|
14367
14501
|
}
|
|
14368
14502
|
};
|
|
@@ -14529,6 +14663,7 @@ async function ask(state, rail, input) {
|
|
|
14529
14663
|
waiting: "hard",
|
|
14530
14664
|
channel: input.call ? "call" : "notification"
|
|
14531
14665
|
});
|
|
14666
|
+
if ("recorded" in sent) return null;
|
|
14532
14667
|
const entryId = sent.requestEntryIds[0];
|
|
14533
14668
|
if (!entryId) return null;
|
|
14534
14669
|
(state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
|
|
@@ -14604,6 +14739,23 @@ async function finish(result, state, rail, opts) {
|
|
|
14604
14739
|
opts?.log?.(`\u26A0 finish contact failed: ${error.message}`);
|
|
14605
14740
|
}
|
|
14606
14741
|
}
|
|
14742
|
+
async function failedToStart(reason, state, rail, opts) {
|
|
14743
|
+
try {
|
|
14744
|
+
const goalId = state.goal?.id ?? (await rail.claim()).goalId;
|
|
14745
|
+
state.goal = void 0;
|
|
14746
|
+
state.resting = true;
|
|
14747
|
+
if (!goalId) return;
|
|
14748
|
+
await rail.contact({
|
|
14749
|
+
asks: [{ ask: `The session could not start, so nothing is working on this yet.
|
|
14750
|
+
|
|
14751
|
+
${reason}`, parentId: goalId }],
|
|
14752
|
+
waiting: "none",
|
|
14753
|
+
channel: "notification"
|
|
14754
|
+
});
|
|
14755
|
+
} catch (error) {
|
|
14756
|
+
opts?.log?.(`\u26A0 could not report the failed start: ${error.message}`);
|
|
14757
|
+
}
|
|
14758
|
+
}
|
|
14607
14759
|
async function afterTurn(event, state, rail, opts) {
|
|
14608
14760
|
if (!event.failed && !endsWithQuestion(event.result)) {
|
|
14609
14761
|
await finish(event.result, state, rail, opts);
|
|
@@ -14628,10 +14780,16 @@ function runHarness(opts) {
|
|
|
14628
14780
|
let tail = [];
|
|
14629
14781
|
const state = { resting: true };
|
|
14630
14782
|
let session = null;
|
|
14631
|
-
const
|
|
14783
|
+
const speaksAs = opts.resume ?? opts.session;
|
|
14784
|
+
const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
|
|
14632
14785
|
async function handle(event) {
|
|
14633
14786
|
if (event.kind === "error") {
|
|
14634
14787
|
opts.log(`\u26A0 ${event.message}`);
|
|
14788
|
+
if (event.fatal && running) {
|
|
14789
|
+
opts.log(`\u2716 could not start: ${event.message}`);
|
|
14790
|
+
await failedToStart(event.message, state, rail, { log: opts.log });
|
|
14791
|
+
stop2();
|
|
14792
|
+
}
|
|
14635
14793
|
return;
|
|
14636
14794
|
}
|
|
14637
14795
|
if (event.kind === "permission") {
|
|
@@ -14677,6 +14835,7 @@ function runHarness(opts) {
|
|
|
14677
14835
|
mode: opts.mode,
|
|
14678
14836
|
...opts.token ? { token: opts.token } : {},
|
|
14679
14837
|
...opts.resume ? { resume: opts.resume } : {},
|
|
14838
|
+
...opts.session ? { session: opts.session } : {},
|
|
14680
14839
|
...opts.bin ? { bin: opts.bin } : {},
|
|
14681
14840
|
...opts.onExit ? { onExit: opts.onExit } : {},
|
|
14682
14841
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
@@ -14685,27 +14844,28 @@ function runHarness(opts) {
|
|
|
14685
14844
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
14686
14845
|
);
|
|
14687
14846
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
14847
|
+
function stop2() {
|
|
14848
|
+
running = false;
|
|
14849
|
+
tail = [];
|
|
14850
|
+
cancelAsks(state);
|
|
14851
|
+
session?.stop();
|
|
14852
|
+
session = null;
|
|
14853
|
+
opts.log("\u25A0 stopped");
|
|
14854
|
+
}
|
|
14688
14855
|
return {
|
|
14689
14856
|
send(text) {
|
|
14690
14857
|
session?.send(text);
|
|
14691
14858
|
},
|
|
14692
14859
|
working: () => running && state.resting !== true,
|
|
14693
14860
|
tail: () => [...tail],
|
|
14694
|
-
stop
|
|
14695
|
-
running = false;
|
|
14696
|
-
tail = [];
|
|
14697
|
-
cancelAsks(state);
|
|
14698
|
-
session?.stop();
|
|
14699
|
-
session = null;
|
|
14700
|
-
opts.log("\u25A0 stopped");
|
|
14701
|
-
}
|
|
14861
|
+
stop: stop2
|
|
14702
14862
|
};
|
|
14703
14863
|
}
|
|
14704
14864
|
|
|
14705
14865
|
// src/update.ts
|
|
14706
14866
|
import { execFile } from "child_process";
|
|
14707
14867
|
import { createRequire } from "module";
|
|
14708
|
-
import { dirname as dirname3, join as
|
|
14868
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
14709
14869
|
import { promisify } from "util";
|
|
14710
14870
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
14711
14871
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -14732,7 +14892,7 @@ async function latestVersion() {
|
|
|
14732
14892
|
}
|
|
14733
14893
|
async function install(version) {
|
|
14734
14894
|
const bin = dirname3(process.execPath);
|
|
14735
|
-
await promisify(execFile)(
|
|
14895
|
+
await promisify(execFile)(join7(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
14736
14896
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
14737
14897
|
timeout: 5 * 6e4
|
|
14738
14898
|
});
|
|
@@ -14783,7 +14943,7 @@ function selfUpdater(deps) {
|
|
|
14783
14943
|
}
|
|
14784
14944
|
|
|
14785
14945
|
// src/host.ts
|
|
14786
|
-
var HOST_FILE =
|
|
14946
|
+
var HOST_FILE = join8(homedir7(), ".paigy", "host.json");
|
|
14787
14947
|
function readHostState() {
|
|
14788
14948
|
try {
|
|
14789
14949
|
const s = JSON.parse(readFileSync4(HOST_FILE, "utf8"));
|
|
@@ -14807,6 +14967,7 @@ var claimedRun = (sessionId2) => {
|
|
|
14807
14967
|
const slot = sessionSlot(sessionId2);
|
|
14808
14968
|
return { slot, key: runKey(slot) };
|
|
14809
14969
|
};
|
|
14970
|
+
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
14810
14971
|
async function slotHasWaitingWork(token, session) {
|
|
14811
14972
|
return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
|
|
14812
14973
|
}
|
|
@@ -14865,7 +15026,7 @@ function startHost(opts) {
|
|
|
14865
15026
|
mode: "bypass",
|
|
14866
15027
|
prompt: "",
|
|
14867
15028
|
token: spec.token,
|
|
14868
|
-
|
|
15029
|
+
...launchRun(spec),
|
|
14869
15030
|
onExit: () => {
|
|
14870
15031
|
runs.delete(key);
|
|
14871
15032
|
opts.log(`\u2716 ${label} ended`);
|
|
@@ -15646,7 +15807,7 @@ var win = null;
|
|
|
15646
15807
|
function log(message) {
|
|
15647
15808
|
win?.webContents.send("paigy:log", message);
|
|
15648
15809
|
}
|
|
15649
|
-
var ICON =
|
|
15810
|
+
var ICON = join9(here, "..", "assets", "icon.png");
|
|
15650
15811
|
function createWindow() {
|
|
15651
15812
|
win = new BrowserWindow({
|
|
15652
15813
|
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
@@ -15658,9 +15819,9 @@ function createWindow() {
|
|
|
15658
15819
|
minHeight: 520,
|
|
15659
15820
|
icon: ICON,
|
|
15660
15821
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
15661
|
-
webPreferences: { preload:
|
|
15822
|
+
webPreferences: { preload: join9(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
15662
15823
|
});
|
|
15663
|
-
void win.loadFile(
|
|
15824
|
+
void win.loadFile(join9(here, "..", "renderer", "index.html"));
|
|
15664
15825
|
}
|
|
15665
15826
|
var host = null;
|
|
15666
15827
|
app.whenReady().then(() => {
|
|
@@ -15729,7 +15890,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
15729
15890
|
});
|
|
15730
15891
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
15731
15892
|
try {
|
|
15732
|
-
const all = readFileSync5(
|
|
15893
|
+
const all = readFileSync5(join9(homedir9(), ".paigy", "host.log"), "utf8").split("\n");
|
|
15733
15894
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
15734
15895
|
return mine.slice(-500).join("\n");
|
|
15735
15896
|
} catch {
|