@paigy/harness 0.3.16 → 0.3.28
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 +15 -14
- package/dist/cli.js +1222 -364
- package/dist/main.js +1173 -321
- 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 readFileSync6 } from "fs";
|
|
4582
4582
|
import { fileURLToPath } from "url";
|
|
4583
|
-
import { dirname as dirname4, join as
|
|
4583
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
4584
4584
|
|
|
4585
4585
|
// ../../packages/sdk/dist/index.js
|
|
4586
4586
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
@@ -10029,8 +10029,13 @@ var UpdateGoalSchema = external_exports.object({
|
|
|
10029
10029
|
}).strict();
|
|
10030
10030
|
var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
10031
10031
|
var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
10032
|
-
var GetGoalSchema = external_exports.object({
|
|
10033
|
-
|
|
10032
|
+
var GetGoalSchema = external_exports.object({
|
|
10033
|
+
goalId: external_exports.string().uuid(),
|
|
10034
|
+
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
10035
|
+
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
10036
|
+
history: external_exports.boolean().optional()
|
|
10037
|
+
}).strict();
|
|
10038
|
+
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
|
|
10034
10039
|
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
10040
|
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
10041
|
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.";
|
|
@@ -10449,7 +10454,13 @@ var InboxItemSchema = external_exports.object({
|
|
|
10449
10454
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
10450
10455
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
10451
10456
|
* 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()
|
|
10457
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
10458
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
10459
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
10460
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
10461
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
10462
|
+
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
10463
|
+
options: external_exports.array(OptionSchema).optional()
|
|
10453
10464
|
})).optional(),
|
|
10454
10465
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
10455
10466
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -10845,7 +10856,19 @@ var QueueItemSchema = external_exports.object({
|
|
|
10845
10856
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
10846
10857
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
10847
10858
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
10848
|
-
lastProgressAt: external_exports.string().nullable().optional()
|
|
10859
|
+
lastProgressAt: external_exports.string().nullable().optional(),
|
|
10860
|
+
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
10861
|
+
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
10862
|
+
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
10863
|
+
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
10864
|
+
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
10865
|
+
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
10866
|
+
latest: external_exports.object({
|
|
10867
|
+
from: external_exports.enum(["person", "agent"]),
|
|
10868
|
+
said: external_exports.string(),
|
|
10869
|
+
at: external_exports.string(),
|
|
10870
|
+
entryId: external_exports.string()
|
|
10871
|
+
}).nullable().optional()
|
|
10849
10872
|
});
|
|
10850
10873
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
10851
10874
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -11023,6 +11046,104 @@ var NotificationFeedbackKindSchema = external_exports.enum([
|
|
|
11023
11046
|
"other"
|
|
11024
11047
|
// anything else — the note carries it.
|
|
11025
11048
|
]);
|
|
11049
|
+
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
11050
|
+
var QuestionRowSchema = external_exports.object({
|
|
11051
|
+
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
11052
|
+
id: external_exports.string(),
|
|
11053
|
+
deliveryId: external_exports.string(),
|
|
11054
|
+
entryId: external_exports.string(),
|
|
11055
|
+
/** The decision it waits on; null for an update, which asks nothing. */
|
|
11056
|
+
needId: external_exports.string().nullable(),
|
|
11057
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
11058
|
+
/** The name of the work it is about, when the read could word it. */
|
|
11059
|
+
goalTitle: external_exports.string().optional(),
|
|
11060
|
+
tokenId: external_exports.string().optional(),
|
|
11061
|
+
name: external_exports.string(),
|
|
11062
|
+
title: external_exports.string(),
|
|
11063
|
+
body: external_exports.string(),
|
|
11064
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
|
|
11065
|
+
options: external_exports.array(SlimOptionSchema),
|
|
11066
|
+
hasPreview: external_exports.boolean(),
|
|
11067
|
+
blocking: external_exports.boolean(),
|
|
11068
|
+
askedAt: external_exports.string().datetime(),
|
|
11069
|
+
ring: InboxItemSchema.shape.ring,
|
|
11070
|
+
onCall: external_exports.literal(true).optional(),
|
|
11071
|
+
sealed: external_exports.boolean()
|
|
11072
|
+
});
|
|
11073
|
+
var WorkStateSchema = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
11074
|
+
var WorkRowSchema = external_exports.object({
|
|
11075
|
+
id: external_exports.string(),
|
|
11076
|
+
parentId: external_exports.string().nullable(),
|
|
11077
|
+
title: external_exports.string(),
|
|
11078
|
+
/** Straight off the Goal. */
|
|
11079
|
+
state: WorkStateSchema,
|
|
11080
|
+
owner: external_exports.string().nullable(),
|
|
11081
|
+
revision: external_exports.number().int(),
|
|
11082
|
+
/** Open questions on it, counted to 100. */
|
|
11083
|
+
waiting: external_exports.number().int(),
|
|
11084
|
+
/** Held by a gate on work that is not done. */
|
|
11085
|
+
blocked: external_exports.boolean(),
|
|
11086
|
+
lastProgressAt: external_exports.string().datetime().nullable(),
|
|
11087
|
+
/** The line written for its newest progress note, else that note's first words. */
|
|
11088
|
+
line: external_exports.string().nullable(),
|
|
11089
|
+
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
11090
|
+
children: external_exports.number().int(),
|
|
11091
|
+
createdAt: external_exports.string().datetime(),
|
|
11092
|
+
updatedAt: external_exports.string().datetime(),
|
|
11093
|
+
/** When anything at or under it last moved — the order the list is in. */
|
|
11094
|
+
activeAt: external_exports.string().datetime(),
|
|
11095
|
+
/** A sealed outcome has no title here; the work's page opens it. */
|
|
11096
|
+
sealed: external_exports.boolean()
|
|
11097
|
+
});
|
|
11098
|
+
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
11099
|
+
var AgentRowSchema = ComputerRowSchema.extend({
|
|
11100
|
+
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
11101
|
+
asking: external_exports.number().int().nullable(),
|
|
11102
|
+
oldestAskAt: external_exports.string().datetime().nullable(),
|
|
11103
|
+
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
11104
|
+
* many in all among the account's 200 most recently active agent-held live Goals
|
|
11105
|
+
* (`agent_holds`); null when that read failed. */
|
|
11106
|
+
holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
|
|
11107
|
+
held: external_exports.number().int().nullable(),
|
|
11108
|
+
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
11109
|
+
* while none has, or when that read failed. */
|
|
11110
|
+
cold: external_exports.string().datetime().nullable(),
|
|
11111
|
+
/** The newest line of its working log, and when the harness saw it. */
|
|
11112
|
+
line: external_exports.string().nullable(),
|
|
11113
|
+
lineAt: external_exports.string().datetime().nullable()
|
|
11114
|
+
});
|
|
11115
|
+
var SnapshotSchema = external_exports.object({
|
|
11116
|
+
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
11117
|
+
at: external_exports.string().datetime(),
|
|
11118
|
+
questions: external_exports.object({
|
|
11119
|
+
/** The newest 30 open cards, questions before updates. */
|
|
11120
|
+
items: external_exports.array(QuestionRowSchema),
|
|
11121
|
+
/** Every open question, and apart from them every update, and what was put off. */
|
|
11122
|
+
total: external_exports.number().int(),
|
|
11123
|
+
updates: external_exports.number().int(),
|
|
11124
|
+
putOff: external_exports.number().int()
|
|
11125
|
+
}).nullable(),
|
|
11126
|
+
agents: external_exports.object({
|
|
11127
|
+
/** Up to 60, most recently seen first. */
|
|
11128
|
+
items: external_exports.array(AgentRowSchema),
|
|
11129
|
+
more: external_exports.boolean()
|
|
11130
|
+
}).nullable(),
|
|
11131
|
+
work: external_exports.object({
|
|
11132
|
+
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
11133
|
+
items: external_exports.array(WorkRowSchema),
|
|
11134
|
+
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
11135
|
+
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
11136
|
+
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
11137
|
+
* is `ready`; Done is `done` or `cancelled`. */
|
|
11138
|
+
counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
|
|
11139
|
+
}).nullable(),
|
|
11140
|
+
you: external_exports.object({
|
|
11141
|
+
settings: UserSettingsSchema,
|
|
11142
|
+
callable: external_exports.boolean(),
|
|
11143
|
+
/** Up to 20 paired computers; null when the roster read failed. */
|
|
11144
|
+
computers: external_exports.array(ComputerRowSchema).nullable()
|
|
11145
|
+
}).nullable()
|
|
11146
|
+
});
|
|
11026
11147
|
function sessionSlot(sessionId2) {
|
|
11027
11148
|
const id = sessionId2 ?? sessionId();
|
|
11028
11149
|
return `session:${id.slice(0, 8)}`;
|
|
@@ -11169,9 +11290,9 @@ async function claimGoal(goalId, opts = {}) {
|
|
|
11169
11290
|
if (!res.ok) await fail("claim_goal", res);
|
|
11170
11291
|
return await res.json();
|
|
11171
11292
|
}
|
|
11172
|
-
async function getGoal(goalId, opts = {}) {
|
|
11293
|
+
async function getGoal(goalId, opts = {}, read = {}) {
|
|
11173
11294
|
const token = authToken(opts.token) ?? "";
|
|
11174
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
|
|
11295
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
|
|
11175
11296
|
method: "GET",
|
|
11176
11297
|
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
11177
11298
|
}));
|
|
@@ -11547,7 +11668,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
11547
11668
|
|
|
11548
11669
|
// src/main.ts
|
|
11549
11670
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
11550
|
-
import { homedir as
|
|
11671
|
+
import { homedir as homedir11, hostname } from "os";
|
|
11551
11672
|
|
|
11552
11673
|
// src/harness/catalog.ts
|
|
11553
11674
|
import { spawnSync } from "child_process";
|
|
@@ -11640,9 +11761,9 @@ function detectAll(deps = {}) {
|
|
|
11640
11761
|
}
|
|
11641
11762
|
|
|
11642
11763
|
// src/host.ts
|
|
11643
|
-
import { readFileSync as
|
|
11644
|
-
import { homedir as
|
|
11645
|
-
import { join as
|
|
11764
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
11765
|
+
import { homedir as homedir9 } from "os";
|
|
11766
|
+
import { join as join10 } from "path";
|
|
11646
11767
|
|
|
11647
11768
|
// src/workspaces.ts
|
|
11648
11769
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -11687,10 +11808,72 @@ function resolveWakeDir(pinned, deps) {
|
|
|
11687
11808
|
return listWorkspaces(deps)[0];
|
|
11688
11809
|
}
|
|
11689
11810
|
|
|
11690
|
-
// src/
|
|
11691
|
-
import {
|
|
11811
|
+
// src/holder.ts
|
|
11812
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
11813
|
+
import { readFileSync as readFileSync4, readdirSync } from "fs";
|
|
11692
11814
|
import { homedir as homedir5 } from "os";
|
|
11693
11815
|
import { join as join6 } from "path";
|
|
11816
|
+
function isAlive(pid) {
|
|
11817
|
+
try {
|
|
11818
|
+
process.kill(pid, 0);
|
|
11819
|
+
return true;
|
|
11820
|
+
} catch (e) {
|
|
11821
|
+
return e.code === "EPERM";
|
|
11822
|
+
}
|
|
11823
|
+
}
|
|
11824
|
+
function psStart(pid, zone) {
|
|
11825
|
+
try {
|
|
11826
|
+
const env = zone === "UTC" ? { ...process.env, TZ: "UTC", LC_ALL: "C" } : { ...process.env, LC_ALL: "C" };
|
|
11827
|
+
return execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
|
|
11828
|
+
} catch {
|
|
11829
|
+
return null;
|
|
11830
|
+
}
|
|
11831
|
+
}
|
|
11832
|
+
var squash = (s) => s.trim().replace(/\s+/g, " ");
|
|
11833
|
+
function holds(pid, procStart, deps) {
|
|
11834
|
+
if (!deps.alive(pid)) return false;
|
|
11835
|
+
if (typeof procStart !== "string" || !procStart.trim()) return true;
|
|
11836
|
+
const want = squash(procStart);
|
|
11837
|
+
for (const zone of ["UTC", "local"]) {
|
|
11838
|
+
const got = deps.started(pid, zone);
|
|
11839
|
+
if (got === null) return true;
|
|
11840
|
+
if (squash(got) === want) return true;
|
|
11841
|
+
}
|
|
11842
|
+
return false;
|
|
11843
|
+
}
|
|
11844
|
+
function localHolders(deps = {}) {
|
|
11845
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
|
|
11846
|
+
const list = deps.list ?? ((d) => readdirSync(d));
|
|
11847
|
+
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
11848
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
11849
|
+
const held = /* @__PURE__ */ new Map();
|
|
11850
|
+
let files;
|
|
11851
|
+
try {
|
|
11852
|
+
files = list(dir);
|
|
11853
|
+
} catch {
|
|
11854
|
+
return held;
|
|
11855
|
+
}
|
|
11856
|
+
for (const file of files) {
|
|
11857
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
11858
|
+
let row;
|
|
11859
|
+
try {
|
|
11860
|
+
row = JSON.parse(read(join6(dir, file)));
|
|
11861
|
+
} catch {
|
|
11862
|
+
continue;
|
|
11863
|
+
}
|
|
11864
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
11865
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
11866
|
+
const slot = sessionSlot(row.sessionId);
|
|
11867
|
+
if (held.has(slot)) continue;
|
|
11868
|
+
if (holds(pid, row.procStart, check)) held.set(slot, { pid });
|
|
11869
|
+
}
|
|
11870
|
+
return held;
|
|
11871
|
+
}
|
|
11872
|
+
|
|
11873
|
+
// src/owner.ts
|
|
11874
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync2 } from "fs";
|
|
11875
|
+
import { homedir as homedir6 } from "os";
|
|
11876
|
+
import { join as join7 } from "path";
|
|
11694
11877
|
var HEAD_BYTES = 64e3;
|
|
11695
11878
|
function readHead(file) {
|
|
11696
11879
|
const fd = openSync2(file, "r");
|
|
@@ -11706,8 +11889,8 @@ function ownerSession(slot, deps = {}) {
|
|
|
11706
11889
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
11707
11890
|
if (!m) return null;
|
|
11708
11891
|
const prefix = m[1];
|
|
11709
|
-
const root = deps.projectsDir ??
|
|
11710
|
-
const list = deps.list ?? ((dir) =>
|
|
11892
|
+
const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
|
|
11893
|
+
const list = deps.list ?? ((dir) => readdirSync2(dir));
|
|
11711
11894
|
const head = deps.head ?? readHead;
|
|
11712
11895
|
let folders;
|
|
11713
11896
|
try {
|
|
@@ -11718,7 +11901,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11718
11901
|
for (const folder of folders) {
|
|
11719
11902
|
let files;
|
|
11720
11903
|
try {
|
|
11721
|
-
files = list(
|
|
11904
|
+
files = list(join7(root, folder));
|
|
11722
11905
|
} catch {
|
|
11723
11906
|
continue;
|
|
11724
11907
|
}
|
|
@@ -11726,7 +11909,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11726
11909
|
if (!file) continue;
|
|
11727
11910
|
let text;
|
|
11728
11911
|
try {
|
|
11729
|
-
text = head(
|
|
11912
|
+
text = head(join7(root, folder, file));
|
|
11730
11913
|
} catch {
|
|
11731
11914
|
continue;
|
|
11732
11915
|
}
|
|
@@ -13137,8 +13320,13 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
13137
13320
|
}).strict();
|
|
13138
13321
|
var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
13139
13322
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
13140
|
-
var GetGoalSchema2 = external_exports.object({
|
|
13141
|
-
|
|
13323
|
+
var GetGoalSchema2 = external_exports.object({
|
|
13324
|
+
goalId: external_exports.string().uuid(),
|
|
13325
|
+
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
13326
|
+
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
13327
|
+
history: external_exports.boolean().optional()
|
|
13328
|
+
}).strict();
|
|
13329
|
+
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
|
|
13142
13330
|
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.`;
|
|
13143
13331
|
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.";
|
|
13144
13332
|
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.";
|
|
@@ -13152,6 +13340,7 @@ var AGENT_TOOLS2 = [
|
|
|
13152
13340
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
13153
13341
|
];
|
|
13154
13342
|
var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
|
|
13343
|
+
var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
|
|
13155
13344
|
function entryWords(entry) {
|
|
13156
13345
|
const content = entry.content;
|
|
13157
13346
|
if (content && "sealed" in content) return "";
|
|
@@ -13572,7 +13761,13 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13572
13761
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
13573
13762
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
13574
13763
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
13575
|
-
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
|
|
13764
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
|
|
13765
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
13766
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
13767
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
13768
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
13769
|
+
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
13770
|
+
options: external_exports.array(OptionSchema2).optional()
|
|
13576
13771
|
})).optional(),
|
|
13577
13772
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
13578
13773
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -13968,7 +14163,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13968
14163
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
13969
14164
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
13970
14165
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
13971
|
-
lastProgressAt: external_exports.string().nullable().optional()
|
|
14166
|
+
lastProgressAt: external_exports.string().nullable().optional(),
|
|
14167
|
+
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
14168
|
+
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
14169
|
+
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
14170
|
+
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
14171
|
+
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
14172
|
+
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
14173
|
+
latest: external_exports.object({
|
|
14174
|
+
from: external_exports.enum(["person", "agent"]),
|
|
14175
|
+
said: external_exports.string(),
|
|
14176
|
+
at: external_exports.string(),
|
|
14177
|
+
entryId: external_exports.string()
|
|
14178
|
+
}).nullable().optional()
|
|
13972
14179
|
});
|
|
13973
14180
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
13974
14181
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -14144,6 +14351,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
14144
14351
|
"other"
|
|
14145
14352
|
// anything else — the note carries it.
|
|
14146
14353
|
]);
|
|
14354
|
+
var SlimOptionSchema2 = OptionSchema2.omit({ html: true });
|
|
14355
|
+
var QuestionRowSchema2 = external_exports.object({
|
|
14356
|
+
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
14357
|
+
id: external_exports.string(),
|
|
14358
|
+
deliveryId: external_exports.string(),
|
|
14359
|
+
entryId: external_exports.string(),
|
|
14360
|
+
/** The decision it waits on; null for an update, which asks nothing. */
|
|
14361
|
+
needId: external_exports.string().nullable(),
|
|
14362
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
14363
|
+
/** The name of the work it is about, when the read could word it. */
|
|
14364
|
+
goalTitle: external_exports.string().optional(),
|
|
14365
|
+
tokenId: external_exports.string().optional(),
|
|
14366
|
+
name: external_exports.string(),
|
|
14367
|
+
title: external_exports.string(),
|
|
14368
|
+
body: external_exports.string(),
|
|
14369
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
|
|
14370
|
+
options: external_exports.array(SlimOptionSchema2),
|
|
14371
|
+
hasPreview: external_exports.boolean(),
|
|
14372
|
+
blocking: external_exports.boolean(),
|
|
14373
|
+
askedAt: external_exports.string().datetime(),
|
|
14374
|
+
ring: InboxItemSchema2.shape.ring,
|
|
14375
|
+
onCall: external_exports.literal(true).optional(),
|
|
14376
|
+
sealed: external_exports.boolean()
|
|
14377
|
+
});
|
|
14378
|
+
var WorkStateSchema2 = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
14379
|
+
var WorkRowSchema2 = external_exports.object({
|
|
14380
|
+
id: external_exports.string(),
|
|
14381
|
+
parentId: external_exports.string().nullable(),
|
|
14382
|
+
title: external_exports.string(),
|
|
14383
|
+
/** Straight off the Goal. */
|
|
14384
|
+
state: WorkStateSchema2,
|
|
14385
|
+
owner: external_exports.string().nullable(),
|
|
14386
|
+
revision: external_exports.number().int(),
|
|
14387
|
+
/** Open questions on it, counted to 100. */
|
|
14388
|
+
waiting: external_exports.number().int(),
|
|
14389
|
+
/** Held by a gate on work that is not done. */
|
|
14390
|
+
blocked: external_exports.boolean(),
|
|
14391
|
+
lastProgressAt: external_exports.string().datetime().nullable(),
|
|
14392
|
+
/** The line written for its newest progress note, else that note's first words. */
|
|
14393
|
+
line: external_exports.string().nullable(),
|
|
14394
|
+
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
14395
|
+
children: external_exports.number().int(),
|
|
14396
|
+
createdAt: external_exports.string().datetime(),
|
|
14397
|
+
updatedAt: external_exports.string().datetime(),
|
|
14398
|
+
/** When anything at or under it last moved — the order the list is in. */
|
|
14399
|
+
activeAt: external_exports.string().datetime(),
|
|
14400
|
+
/** A sealed outcome has no title here; the work's page opens it. */
|
|
14401
|
+
sealed: external_exports.boolean()
|
|
14402
|
+
});
|
|
14403
|
+
var ComputerRowSchema2 = ConnectionSummarySchema2.omit({ activity: true });
|
|
14404
|
+
var AgentRowSchema2 = ComputerRowSchema2.extend({
|
|
14405
|
+
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
14406
|
+
asking: external_exports.number().int().nullable(),
|
|
14407
|
+
oldestAskAt: external_exports.string().datetime().nullable(),
|
|
14408
|
+
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
14409
|
+
* many in all among the account's 200 most recently active agent-held live Goals
|
|
14410
|
+
* (`agent_holds`); null when that read failed. */
|
|
14411
|
+
holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
|
|
14412
|
+
held: external_exports.number().int().nullable(),
|
|
14413
|
+
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
14414
|
+
* while none has, or when that read failed. */
|
|
14415
|
+
cold: external_exports.string().datetime().nullable(),
|
|
14416
|
+
/** The newest line of its working log, and when the harness saw it. */
|
|
14417
|
+
line: external_exports.string().nullable(),
|
|
14418
|
+
lineAt: external_exports.string().datetime().nullable()
|
|
14419
|
+
});
|
|
14420
|
+
var SnapshotSchema2 = external_exports.object({
|
|
14421
|
+
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
14422
|
+
at: external_exports.string().datetime(),
|
|
14423
|
+
questions: external_exports.object({
|
|
14424
|
+
/** The newest 30 open cards, questions before updates. */
|
|
14425
|
+
items: external_exports.array(QuestionRowSchema2),
|
|
14426
|
+
/** Every open question, and apart from them every update, and what was put off. */
|
|
14427
|
+
total: external_exports.number().int(),
|
|
14428
|
+
updates: external_exports.number().int(),
|
|
14429
|
+
putOff: external_exports.number().int()
|
|
14430
|
+
}).nullable(),
|
|
14431
|
+
agents: external_exports.object({
|
|
14432
|
+
/** Up to 60, most recently seen first. */
|
|
14433
|
+
items: external_exports.array(AgentRowSchema2),
|
|
14434
|
+
more: external_exports.boolean()
|
|
14435
|
+
}).nullable(),
|
|
14436
|
+
work: external_exports.object({
|
|
14437
|
+
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
14438
|
+
items: external_exports.array(WorkRowSchema2),
|
|
14439
|
+
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
14440
|
+
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
14441
|
+
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
14442
|
+
* is `ready`; Done is `done` or `cancelled`. */
|
|
14443
|
+
counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
|
|
14444
|
+
}).nullable(),
|
|
14445
|
+
you: external_exports.object({
|
|
14446
|
+
settings: UserSettingsSchema2,
|
|
14447
|
+
callable: external_exports.boolean(),
|
|
14448
|
+
/** Up to 20 paired computers; null when the roster read failed. */
|
|
14449
|
+
computers: external_exports.array(ComputerRowSchema2).nullable()
|
|
14450
|
+
}).nullable()
|
|
14451
|
+
});
|
|
14147
14452
|
|
|
14148
14453
|
// src/paigy/activity.ts
|
|
14149
14454
|
function shortenPaths(s) {
|
|
@@ -14153,8 +14458,8 @@ function shortenPaths(s) {
|
|
|
14153
14458
|
});
|
|
14154
14459
|
}
|
|
14155
14460
|
function workLine(event) {
|
|
14156
|
-
const
|
|
14157
|
-
const line = shortenPaths([event.tool.trim(),
|
|
14461
|
+
const note2 = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
14462
|
+
const line = shortenPaths([event.tool.trim(), note2.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
14158
14463
|
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
14159
14464
|
}
|
|
14160
14465
|
function pushWork(lines2, line) {
|
|
@@ -14168,29 +14473,34 @@ function sameTail(a, b) {
|
|
|
14168
14473
|
// src/harness/session.ts
|
|
14169
14474
|
import { spawn } from "child_process";
|
|
14170
14475
|
import { existsSync as existsSync4 } from "fs";
|
|
14171
|
-
import { homedir as
|
|
14476
|
+
import { homedir as homedir8 } from "os";
|
|
14172
14477
|
import { resolve as resolve2, delimiter as delimiter2 } from "path";
|
|
14173
14478
|
|
|
14174
14479
|
// src/harness/acp.ts
|
|
14175
14480
|
var none = { events: [], writes: [] };
|
|
14481
|
+
var NOTHING_TO_RESUME = /* @__PURE__ */ new Set([-32002, -32601]);
|
|
14176
14482
|
function optionFor(options, decision) {
|
|
14177
14483
|
const want = decision.allow ? "allow_once" : "reject_once";
|
|
14178
14484
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
14179
14485
|
}
|
|
14180
14486
|
function createAcpDriver(opts) {
|
|
14181
|
-
return new AcpDriver(opts.cwd, opts.mode, opts.resume);
|
|
14487
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
|
|
14182
14488
|
}
|
|
14183
14489
|
var AcpDriver = class {
|
|
14184
14490
|
/** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
|
|
14185
|
-
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
14186
|
-
|
|
14491
|
+
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
14492
|
+
* `meta`: the adapter's own `_meta` on the frame that opens the session — where a switch only
|
|
14493
|
+
* one adapter understands rides (`session.ts`, a Claude session's tools). */
|
|
14494
|
+
constructor(cwd, mode, resume, meta) {
|
|
14187
14495
|
this.cwd = cwd;
|
|
14188
14496
|
this.mode = mode;
|
|
14189
14497
|
this.resume = resume;
|
|
14498
|
+
this.meta = meta;
|
|
14190
14499
|
}
|
|
14191
14500
|
cwd;
|
|
14192
14501
|
mode;
|
|
14193
14502
|
resume;
|
|
14503
|
+
meta;
|
|
14194
14504
|
nextId = 1;
|
|
14195
14505
|
initId;
|
|
14196
14506
|
sessionNewId;
|
|
@@ -14210,6 +14520,11 @@ var AcpDriver = class {
|
|
|
14210
14520
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
14211
14521
|
text = "";
|
|
14212
14522
|
tools = [];
|
|
14523
|
+
/** Tool calls opened and not yet reported `completed` or `failed`, by `toolCallId` — whoever
|
|
14524
|
+
* started them. A running tool is the one long silence that is still a working turn (`quiet`). */
|
|
14525
|
+
running = /* @__PURE__ */ new Set();
|
|
14526
|
+
/** The prompt in flight was already cancelled for silence: once is the whole remedy. */
|
|
14527
|
+
cancelled = false;
|
|
14213
14528
|
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
14214
14529
|
open() {
|
|
14215
14530
|
this.initId = this.nextId++;
|
|
@@ -14241,6 +14556,29 @@ var AcpDriver = class {
|
|
|
14241
14556
|
optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
14242
14557
|
];
|
|
14243
14558
|
}
|
|
14559
|
+
/**
|
|
14560
|
+
* THE ANSWER THAT NEVER COMES (2026-09-26, session "mute"). A turn ends when the adapter answers
|
|
14561
|
+
* its prompt, and nothing else ends one. claude-agent-acp can fold a prompt into a turn the agent
|
|
14562
|
+
* started on its own and never answer it (agentclientprotocol/claude-agent-acp#1145): the prompt
|
|
14563
|
+
* stayed in flight, no `idle` came, the pump never saw the agent rest, and the session was deaf
|
|
14564
|
+
* for twelve hours with its heartbeat alive.
|
|
14565
|
+
*
|
|
14566
|
+
* The caller's clock says nothing has crossed the pipe for `ms`; this says whether that silence
|
|
14567
|
+
* is still a turn. A running tool is (a ten-minute build streams nothing), and so is a permission
|
|
14568
|
+
* waiting on the person. Anything else, with a prompt in flight, is a turn that is over without
|
|
14569
|
+
* its answer, and the remedy is the protocol's own settle: `session/cancel`, which the adapter
|
|
14570
|
+
* answers `stopReason: "cancelled"` through the ordinary response path above — a turn that
|
|
14571
|
+
* ended, not one that failed. Once per prompt, and said as a crumb: a silent recovery is
|
|
14572
|
+
* indistinguishable from a working turn.
|
|
14573
|
+
*/
|
|
14574
|
+
quiet(ms) {
|
|
14575
|
+
if (this.promptId === void 0 || this.cancelled || this.running.size || this.pending.size) return none;
|
|
14576
|
+
this.cancelled = true;
|
|
14577
|
+
return {
|
|
14578
|
+
events: [{ kind: "error", message: `turn settled: no reply after ${Math.round(ms / 1e3)}s quiet with no tool running \u2014 sent session/cancel` }],
|
|
14579
|
+
writes: [frame2({ method: "session/cancel", params: { sessionId: this.sessionId } })]
|
|
14580
|
+
};
|
|
14581
|
+
}
|
|
14244
14582
|
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
14245
14583
|
close() {
|
|
14246
14584
|
const writes = [...this.pending.values()].map(
|
|
@@ -14269,20 +14607,26 @@ var AcpDriver = class {
|
|
|
14269
14607
|
if (msg.id === this.initId) {
|
|
14270
14608
|
this.initId = void 0;
|
|
14271
14609
|
if (msg.error) {
|
|
14272
|
-
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}
|
|
14610
|
+
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
|
|
14273
14611
|
}
|
|
14274
14612
|
this.sessionNewId = this.nextId++;
|
|
14275
|
-
return {
|
|
14276
|
-
events: [],
|
|
14277
|
-
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : { id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
|
|
14278
|
-
};
|
|
14613
|
+
return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
|
|
14279
14614
|
}
|
|
14280
14615
|
if (msg.id === this.sessionNewId) {
|
|
14281
14616
|
this.sessionNewId = void 0;
|
|
14617
|
+
const resuming = this.resume !== void 0;
|
|
14618
|
+
if (resuming && msg.error?.code !== void 0 && NOTHING_TO_RESUME.has(msg.error.code)) {
|
|
14619
|
+
this.resume = void 0;
|
|
14620
|
+
this.sessionNewId = this.nextId++;
|
|
14621
|
+
return {
|
|
14622
|
+
events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
|
|
14623
|
+
writes: [frame2(this.opening(this.sessionNewId))]
|
|
14624
|
+
};
|
|
14625
|
+
}
|
|
14282
14626
|
const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
|
|
14283
14627
|
if (!sessionId2) {
|
|
14284
14628
|
return {
|
|
14285
|
-
events: [{ kind: "error", message: `${
|
|
14629
|
+
events: [{ kind: "error", message: `${resuming ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}`, fatal: true }],
|
|
14286
14630
|
writes: []
|
|
14287
14631
|
};
|
|
14288
14632
|
}
|
|
@@ -14301,6 +14645,8 @@ var AcpDriver = class {
|
|
|
14301
14645
|
}
|
|
14302
14646
|
if (msg.id === this.promptId) {
|
|
14303
14647
|
this.promptId = void 0;
|
|
14648
|
+
this.running.clear();
|
|
14649
|
+
this.cancelled = false;
|
|
14304
14650
|
const events = [];
|
|
14305
14651
|
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
14306
14652
|
if (report || this.tools.length) {
|
|
@@ -14332,6 +14678,10 @@ var AcpDriver = class {
|
|
|
14332
14678
|
handleNotification(msg) {
|
|
14333
14679
|
if (msg.method !== "session/update") return none;
|
|
14334
14680
|
const update = msg.params?.update;
|
|
14681
|
+
if (update?.toolCallId) {
|
|
14682
|
+
if (update.status === "completed" || update.status === "failed") this.running.delete(update.toolCallId);
|
|
14683
|
+
else if (update.sessionUpdate === "tool_call") this.running.add(update.toolCallId);
|
|
14684
|
+
}
|
|
14335
14685
|
switch (update?.sessionUpdate) {
|
|
14336
14686
|
case "agent_message_chunk":
|
|
14337
14687
|
this.text += update.content?.text ?? "";
|
|
@@ -14339,9 +14689,9 @@ var AcpDriver = class {
|
|
|
14339
14689
|
case "tool_call": {
|
|
14340
14690
|
const title = update.title?.trim() || update.kind || "tool";
|
|
14341
14691
|
this.tools.push(title);
|
|
14342
|
-
const
|
|
14692
|
+
const note2 = this.text.slice(this.reportFrom).trim();
|
|
14343
14693
|
this.reportFrom = this.text.length;
|
|
14344
|
-
return { events: [{ kind: "work", tool: title, ...
|
|
14694
|
+
return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
|
|
14345
14695
|
}
|
|
14346
14696
|
default:
|
|
14347
14697
|
return none;
|
|
@@ -14375,6 +14725,12 @@ var AcpDriver = class {
|
|
|
14375
14725
|
writes: []
|
|
14376
14726
|
};
|
|
14377
14727
|
}
|
|
14728
|
+
/** The frame that opens the conversation: a resume of `resume` while there is one to try,
|
|
14729
|
+
* else a new session. */
|
|
14730
|
+
opening(id) {
|
|
14731
|
+
const params = { cwd: this.cwd, mcpServers: [], ...this.meta ? { _meta: this.meta } : {} };
|
|
14732
|
+
return this.resume ? { id, method: "session/resume", params: { sessionId: this.resume, ...params } } : { id, method: "session/new", params };
|
|
14733
|
+
}
|
|
14378
14734
|
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
14379
14735
|
flush() {
|
|
14380
14736
|
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
@@ -14392,12 +14748,92 @@ var AcpDriver = class {
|
|
|
14392
14748
|
};
|
|
14393
14749
|
var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
14394
14750
|
|
|
14751
|
+
// src/paigy/inbox.ts
|
|
14752
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
14753
|
+
import { join as join8 } from "path";
|
|
14754
|
+
import { homedir as homedir7 } from "os";
|
|
14755
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
14756
|
+
var MIDTURN_HOOK = `d="$PAIGY_INBOX"; [ -n "$d" ] || { cat >/dev/null; exit 0; }; set -- "$d"/*.note; [ -e "$1" ] || { cat >/dev/null; exit 0; }; grep '"agent_id"' >/dev/null && exit 0; mkdir -p "$d/taken"; o=""; for f in "$@"; do n=\${f##*/}; mv "$f" "$d/taken/$n" 2>/dev/null || continue; touch "$d/taken/$n"; o="$o\${o:+\\n\\n}$(cat "$d/taken/$n")"; done; [ -n "$o" ] || exit 0; printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$o"`;
|
|
14757
|
+
var missing = (error) => error.code === "ENOENT";
|
|
14758
|
+
function openInbox(dir) {
|
|
14759
|
+
mkdirSync3(dir, { recursive: true });
|
|
14760
|
+
let written = 0;
|
|
14761
|
+
const receipt = (name) => join8(dir, "taken", `${name}.note`);
|
|
14762
|
+
const taken = (name) => {
|
|
14763
|
+
try {
|
|
14764
|
+
return statSync2(receipt(name)).mtimeMs;
|
|
14765
|
+
} catch (error) {
|
|
14766
|
+
if (missing(error)) return null;
|
|
14767
|
+
throw error;
|
|
14768
|
+
}
|
|
14769
|
+
};
|
|
14770
|
+
return {
|
|
14771
|
+
dir,
|
|
14772
|
+
put(text) {
|
|
14773
|
+
const name = `${Date.now()}-${String(++written).padStart(6, "0")}`;
|
|
14774
|
+
const tmp = join8(dir, `${name}.tmp`);
|
|
14775
|
+
writeFileSync3(tmp, JSON.stringify(text).slice(1, -1));
|
|
14776
|
+
renameSync(tmp, join8(dir, `${name}.note`));
|
|
14777
|
+
return name;
|
|
14778
|
+
},
|
|
14779
|
+
taken,
|
|
14780
|
+
settle(name) {
|
|
14781
|
+
try {
|
|
14782
|
+
renameSync(join8(dir, `${name}.note`), join8(dir, `${name}.back`));
|
|
14783
|
+
rmSync2(join8(dir, `${name}.back`), { force: true });
|
|
14784
|
+
return "retracted";
|
|
14785
|
+
} catch (error) {
|
|
14786
|
+
if (!missing(error)) throw error;
|
|
14787
|
+
}
|
|
14788
|
+
const seen = taken(name) !== null;
|
|
14789
|
+
rmSync2(receipt(name), { force: true });
|
|
14790
|
+
return seen ? "taken" : "retracted";
|
|
14791
|
+
},
|
|
14792
|
+
close() {
|
|
14793
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
14794
|
+
}
|
|
14795
|
+
};
|
|
14796
|
+
}
|
|
14797
|
+
var INBOX_ROOT = join8(homedir7(), ".paigy", "inbox");
|
|
14798
|
+
var alive = (pid) => {
|
|
14799
|
+
try {
|
|
14800
|
+
process.kill(pid, 0);
|
|
14801
|
+
return true;
|
|
14802
|
+
} catch (error) {
|
|
14803
|
+
return error.code === "EPERM";
|
|
14804
|
+
}
|
|
14805
|
+
};
|
|
14806
|
+
function inboxFor(root = INBOX_ROOT) {
|
|
14807
|
+
try {
|
|
14808
|
+
for (const name of readdirSync3(root)) {
|
|
14809
|
+
const pid = Number(name.split("-")[0]);
|
|
14810
|
+
if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join8(root, name), { recursive: true, force: true });
|
|
14811
|
+
}
|
|
14812
|
+
} catch (error) {
|
|
14813
|
+
if (!missing(error)) throw error;
|
|
14814
|
+
}
|
|
14815
|
+
return openInbox(join8(root, `${process.pid}-${randomUUID4().slice(0, 8)}`));
|
|
14816
|
+
}
|
|
14817
|
+
|
|
14395
14818
|
// src/harness/session.ts
|
|
14396
14819
|
var ADAPTER_BIN = {
|
|
14397
14820
|
claude: "claude-agent-acp",
|
|
14398
14821
|
codex: "codex-acp",
|
|
14399
14822
|
agy: "paigy-agy-acp"
|
|
14400
14823
|
};
|
|
14824
|
+
var CLAUDE_ONE_WAY_IN = {
|
|
14825
|
+
env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
|
|
14826
|
+
disallowedTools: ["Monitor", "ScheduleWakeup", "CronCreate", "Workflow"]
|
|
14827
|
+
};
|
|
14828
|
+
var CLAUDE_META = {
|
|
14829
|
+
claudeCode: {
|
|
14830
|
+
options: {
|
|
14831
|
+
disallowedTools: CLAUDE_ONE_WAY_IN.disallowedTools,
|
|
14832
|
+
settings: { hooks: { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: MIDTURN_HOOK }] }] } }
|
|
14833
|
+
}
|
|
14834
|
+
}
|
|
14835
|
+
};
|
|
14836
|
+
var QUIET_MS = 15 * 6e4;
|
|
14401
14837
|
function splitLines(buffer, chunk) {
|
|
14402
14838
|
const combined = buffer + chunk;
|
|
14403
14839
|
const parts = combined.split("\n");
|
|
@@ -14405,7 +14841,7 @@ function splitLines(buffer, chunk) {
|
|
|
14405
14841
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
14406
14842
|
}
|
|
14407
14843
|
function startSession(opts) {
|
|
14408
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/,
|
|
14844
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
|
|
14409
14845
|
if (!existsSync4(cwd)) {
|
|
14410
14846
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
14411
14847
|
}
|
|
@@ -14419,25 +14855,43 @@ function startSession(opts) {
|
|
|
14419
14855
|
...process.env,
|
|
14420
14856
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
14421
14857
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
14422
|
-
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
|
|
14858
|
+
...opts.resume ?? opts.session ? { PAIGY_SESSION_ID: opts.resume ?? opts.session } : {},
|
|
14423
14859
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
14424
14860
|
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
14425
14861
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
14426
14862
|
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
14427
14863
|
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
14428
|
-
PAIGY_HARNESS: "1"
|
|
14864
|
+
PAIGY_HARNESS: "1",
|
|
14865
|
+
...opts.harness === "claude" ? CLAUDE_ONE_WAY_IN.env : {},
|
|
14866
|
+
...opts.harness === "claude" && opts.inbox ? { PAIGY_INBOX: opts.inbox } : {}
|
|
14429
14867
|
},
|
|
14430
14868
|
stdio: ["pipe", "pipe", "pipe"]
|
|
14431
14869
|
});
|
|
14870
|
+
let ended2 = false;
|
|
14871
|
+
child.stdin?.on("error", () => {
|
|
14872
|
+
});
|
|
14432
14873
|
const write = (frames) => {
|
|
14874
|
+
if (ended2) return;
|
|
14433
14875
|
for (const f of frames) child.stdin?.write(`${f}
|
|
14434
14876
|
`);
|
|
14877
|
+
if (frames.length) listen();
|
|
14878
|
+
};
|
|
14879
|
+
const apply = ({ events, writes }) => {
|
|
14880
|
+
write(writes);
|
|
14881
|
+
for (const event of events) opts.onEvent(event);
|
|
14882
|
+
};
|
|
14883
|
+
let hush;
|
|
14884
|
+
const listen = () => {
|
|
14885
|
+
clearTimeout(hush);
|
|
14886
|
+
if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
|
|
14435
14887
|
};
|
|
14436
14888
|
child.stderr?.on("data", (chunk) => {
|
|
14437
14889
|
const text = String(chunk).trim();
|
|
14438
14890
|
if (text) opts.onEvent({ kind: "error", message: text });
|
|
14439
14891
|
});
|
|
14440
14892
|
child.on("exit", (code) => {
|
|
14893
|
+
ended2 = true;
|
|
14894
|
+
clearTimeout(hush);
|
|
14441
14895
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
14442
14896
|
opts.onExit?.();
|
|
14443
14897
|
});
|
|
@@ -14445,16 +14899,18 @@ function startSession(opts) {
|
|
|
14445
14899
|
opts.onEvent({ kind: "error", message: e.message });
|
|
14446
14900
|
opts.onExit?.();
|
|
14447
14901
|
});
|
|
14448
|
-
const driver = createAcpDriver({
|
|
14902
|
+
const driver = createAcpDriver({
|
|
14903
|
+
cwd,
|
|
14904
|
+
mode: opts.mode,
|
|
14905
|
+
...opts.resume ? { resume: opts.resume } : {},
|
|
14906
|
+
...opts.harness === "claude" ? { meta: CLAUDE_META } : {}
|
|
14907
|
+
});
|
|
14449
14908
|
let buffer = "";
|
|
14450
14909
|
child.stdout?.on("data", (chunk) => {
|
|
14910
|
+
listen();
|
|
14451
14911
|
const { lines: lines2, rest } = splitLines(buffer, String(chunk));
|
|
14452
14912
|
buffer = rest;
|
|
14453
|
-
for (const line of lines2)
|
|
14454
|
-
const { events, writes } = driver.handleLine(line);
|
|
14455
|
-
write(writes);
|
|
14456
|
-
for (const event of events) opts.onEvent(event);
|
|
14457
|
-
}
|
|
14913
|
+
for (const line of lines2) apply(driver.handleLine(line));
|
|
14458
14914
|
});
|
|
14459
14915
|
write(driver.open());
|
|
14460
14916
|
return {
|
|
@@ -14466,132 +14922,138 @@ function startSession(opts) {
|
|
|
14466
14922
|
},
|
|
14467
14923
|
stop() {
|
|
14468
14924
|
write(driver.close());
|
|
14925
|
+
ended2 = true;
|
|
14926
|
+
clearTimeout(hush);
|
|
14469
14927
|
child.kill();
|
|
14470
14928
|
}
|
|
14471
14929
|
};
|
|
14472
14930
|
}
|
|
14473
14931
|
|
|
14474
|
-
// src/paigy/
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
/\b(how would you like|how should we|what would you like|what should we)\b/,
|
|
14500
|
-
/\b(please choose|choose one|select one|how do you want)\b/
|
|
14501
|
-
].some((re) => re.test(tail));
|
|
14502
|
-
}
|
|
14503
|
-
var POLL_MS = 5e3;
|
|
14504
|
-
var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
|
|
14505
|
-
var ended = (state) => state === "done" || state === "cancelled";
|
|
14506
|
-
function unheard(brief, state) {
|
|
14507
|
-
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
14508
|
-
const words2 = [];
|
|
14509
|
-
for (const e of brief.entries ?? []) {
|
|
14510
|
-
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
14511
|
-
seen.add(e.entryId);
|
|
14512
|
-
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
14513
|
-
const text = entryWords(e).trim();
|
|
14514
|
-
if (text) words2.push(text);
|
|
14515
|
-
}
|
|
14516
|
-
return words2;
|
|
14932
|
+
// src/paigy/limit.ts
|
|
14933
|
+
var LIMIT_PREFIXES = [
|
|
14934
|
+
"You've hit your",
|
|
14935
|
+
"You've reached your",
|
|
14936
|
+
"You're out of usage credits",
|
|
14937
|
+
"Your org is out of usage",
|
|
14938
|
+
"Your seat type doesn't include usage",
|
|
14939
|
+
"Your usage allocation has been disabled by your admin",
|
|
14940
|
+
"Your group's usage limit is set to $0",
|
|
14941
|
+
"requires usage credits",
|
|
14942
|
+
"You're out of extra usage",
|
|
14943
|
+
"Your seat type doesn't include extra usage"
|
|
14944
|
+
];
|
|
14945
|
+
var RETRY_MS2 = 15 * 6e4;
|
|
14946
|
+
var MARGIN_MS = 6e4;
|
|
14947
|
+
var FARTHEST_MS = 8 * 24 * 60 * 6e4;
|
|
14948
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
14949
|
+
function limitIn(text, now) {
|
|
14950
|
+
const line = (text ?? "").split("\n").map((l) => l.trim()).reverse().find((l) => LIMIT_PREFIXES.some((p) => l.startsWith(p) || p === "requires usage credits" && l.includes(p)));
|
|
14951
|
+
if (!line) return null;
|
|
14952
|
+
const reset = resetOf(line, now);
|
|
14953
|
+
if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
|
|
14954
|
+
return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
|
|
14955
|
+
}
|
|
14956
|
+
return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
|
|
14517
14957
|
}
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
|
|
14544
|
-
if (checkCallOnDone(fullText)) state.callOnDone = true;
|
|
14545
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
14546
|
-
return;
|
|
14547
|
-
}
|
|
14548
|
-
const brief = await rail.read(state.goal.id);
|
|
14549
|
-
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
14550
|
-
for (const answer of brief.answers ?? []) {
|
|
14551
|
-
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
14552
|
-
if (!resolve3) continue;
|
|
14553
|
-
state.asks.delete(answer.requestEntryId);
|
|
14554
|
-
resolve3(answer.result);
|
|
14555
|
-
}
|
|
14556
|
-
if (ended(brief.state)) {
|
|
14557
|
-
state.goal = void 0;
|
|
14558
|
-
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
14559
|
-
state.asks?.clear();
|
|
14560
|
-
return;
|
|
14561
|
-
}
|
|
14562
|
-
if (state.asks?.size || !state.resting) return;
|
|
14563
|
-
const words2 = unheard(brief, state);
|
|
14564
|
-
if (words2.length) {
|
|
14565
|
-
const text = words2.join("\n\n");
|
|
14566
|
-
if (checkCallOnDone(text)) state.callOnDone = true;
|
|
14567
|
-
send2(session, state, text);
|
|
14958
|
+
function resetOf(line, now) {
|
|
14959
|
+
const m = /resets\s+(?:at\s+)?(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b(?:\s*\(([^)]+)\))?/i.exec(line);
|
|
14960
|
+
if (!m) return null;
|
|
14961
|
+
const zone = zoneOr(m[6]);
|
|
14962
|
+
let hour = Number(m[3]) % 12;
|
|
14963
|
+
if (m[5].toLowerCase() === "pm") hour += 12;
|
|
14964
|
+
const minute = Number(m[4] ?? 0);
|
|
14965
|
+
const today = partsAt(now, zone);
|
|
14966
|
+
if (m[1]) {
|
|
14967
|
+
const month = MONTHS.indexOf(m[1].toLowerCase());
|
|
14968
|
+
if (month < 0) return null;
|
|
14969
|
+
const at2 = wallToEpoch(today.year, month, Number(m[2]), hour, minute, zone);
|
|
14970
|
+
return at2 > now ? at2 : wallToEpoch(today.year + 1, month, Number(m[2]), hour, minute, zone);
|
|
14971
|
+
}
|
|
14972
|
+
const at = wallToEpoch(today.year, today.month, today.day, hour, minute, zone);
|
|
14973
|
+
return at > now ? at : wallToEpoch(today.year, today.month, today.day + 1, hour, minute, zone);
|
|
14974
|
+
}
|
|
14975
|
+
function zoneOr(zone) {
|
|
14976
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
14977
|
+
if (!zone) return local;
|
|
14978
|
+
try {
|
|
14979
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone.trim() });
|
|
14980
|
+
return zone.trim();
|
|
14981
|
+
} catch {
|
|
14982
|
+
return local;
|
|
14568
14983
|
}
|
|
14569
14984
|
}
|
|
14570
|
-
function
|
|
14571
|
-
|
|
14572
|
-
|
|
14985
|
+
function partsAt(t, zone) {
|
|
14986
|
+
const p = Object.fromEntries(
|
|
14987
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(new Date(t)).map((x) => [x.type, x.value])
|
|
14988
|
+
);
|
|
14989
|
+
return { year: Number(p.year), month: Number(p.month) - 1, day: Number(p.day), hour: Number(p.hour), minute: Number(p.minute), second: Number(p.second) };
|
|
14573
14990
|
}
|
|
14574
|
-
|
|
14575
|
-
const
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14991
|
+
function wallToEpoch(year, month, day, hour, minute, zone) {
|
|
14992
|
+
const wall = Date.UTC(year, month, day, hour, minute);
|
|
14993
|
+
const offset = (t) => {
|
|
14994
|
+
const p = partsAt(t, zone);
|
|
14995
|
+
return Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(t / 1e3) * 1e3;
|
|
14996
|
+
};
|
|
14997
|
+
const first = wall - offset(wall);
|
|
14998
|
+
return wall - offset(first);
|
|
14999
|
+
}
|
|
15000
|
+
function clock3(t) {
|
|
15001
|
+
return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
|
|
15002
|
+
}
|
|
15003
|
+
var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
|
|
15004
|
+
var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on where you stopped.";
|
|
15005
|
+
var OWED = "That turn was given this when the limit stopped it, and may not have got to it. Act on it now, unless you already have:";
|
|
15006
|
+
var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
15007
|
+
var GATHER_MS = 9e4;
|
|
15008
|
+
function episodes(opts = {}) {
|
|
15009
|
+
const gather2 = opts.gatherMs ?? GATHER_MS;
|
|
15010
|
+
const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
|
|
15011
|
+
let open = null;
|
|
15012
|
+
return {
|
|
15013
|
+
paused(name, now) {
|
|
15014
|
+
if (!open || open.called) open = { since: now, paused: [], running: [], called: false, scheduled: false };
|
|
15015
|
+
if (!open.paused.includes(name)) open.paused.push(name);
|
|
15016
|
+
open.running = open.running.filter((n) => n !== name);
|
|
15017
|
+
},
|
|
15018
|
+
running(name, call, log2) {
|
|
15019
|
+
const episode = open;
|
|
15020
|
+
if (!episode || !episode.paused.includes(name)) return;
|
|
15021
|
+
if (episode.called) {
|
|
15022
|
+
log2?.(`\u25B6 ${name} is running again after the limit (the call already went out)`);
|
|
15023
|
+
return;
|
|
15024
|
+
}
|
|
15025
|
+
if (!episode.running.includes(name)) episode.running.push(name);
|
|
15026
|
+
if (episode.scheduled) return;
|
|
15027
|
+
episode.scheduled = true;
|
|
15028
|
+
later(() => {
|
|
15029
|
+
if (!episode.running.length) {
|
|
15030
|
+
episode.scheduled = false;
|
|
15031
|
+
return;
|
|
15032
|
+
}
|
|
15033
|
+
episode.called = true;
|
|
15034
|
+
const rest = episode.paused.filter((n) => !episode.running.includes(n));
|
|
15035
|
+
const text = [
|
|
15036
|
+
`Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
|
|
15037
|
+
rest.length ? `It has reset: ${names(episode.running)} ${episode.running.length > 1 ? "are" : "is"} running again, and ${names(rest)} ${rest.length > 1 ? "haven't" : "hasn't"} started again yet.` : `It has reset, and ${episode.paused.length > 1 ? "they have all" : "it has"} carried on.`
|
|
15038
|
+
].join(" ");
|
|
15039
|
+
log2?.(`\u260E one call for ${episode.paused.length} paused agent${episode.paused.length === 1 ? "" : "s"}: ${names(episode.paused)}`);
|
|
15040
|
+
void call(text).catch((error) => log2?.(`\u26A0 could not place the limit call: ${error.message}`));
|
|
15041
|
+
}, gather2);
|
|
14580
15042
|
}
|
|
14581
|
-
|
|
14582
|
-
await (opts.pause ?? wait)();
|
|
14583
|
-
}
|
|
15043
|
+
};
|
|
14584
15044
|
}
|
|
15045
|
+
var LIMITS = episodes();
|
|
14585
15046
|
|
|
14586
15047
|
// src/paigy/bridge.ts
|
|
14587
15048
|
function railFor(opts) {
|
|
14588
15049
|
return {
|
|
14589
|
-
claim: () => claimGoal(
|
|
15050
|
+
claim: (goalId) => claimGoal(goalId, opts),
|
|
14590
15051
|
read: (goalId) => getGoal(goalId, opts),
|
|
14591
15052
|
update: (goalId, input) => updateGoal(goalId, input, opts),
|
|
14592
15053
|
// One read and back: the pump owns the waiting, so a Call must not hold a window here.
|
|
14593
15054
|
contact: (input) => contact(input, { ...opts, waits: false }),
|
|
14594
|
-
create: (input) => createGoal(input, opts)
|
|
15055
|
+
create: (input) => createGoal(input, opts),
|
|
15056
|
+
waiting: () => checkReplies(opts)
|
|
14595
15057
|
};
|
|
14596
15058
|
}
|
|
14597
15059
|
function cancelAsks(state) {
|
|
@@ -14610,27 +15072,13 @@ function decisionFrom(result) {
|
|
|
14610
15072
|
}
|
|
14611
15073
|
return { allow: false, reason: "No approval given" };
|
|
14612
15074
|
}
|
|
14613
|
-
function wordsOf(result) {
|
|
14614
|
-
if (!result || !("kind" in result)) return null;
|
|
14615
|
-
switch (result.kind) {
|
|
14616
|
-
case "text":
|
|
14617
|
-
return result.text.trim() || null;
|
|
14618
|
-
case "confirm":
|
|
14619
|
-
return result.approved ? "yes" : "no";
|
|
14620
|
-
case "one":
|
|
14621
|
-
return result.option.label;
|
|
14622
|
-
case "many":
|
|
14623
|
-
case "rank":
|
|
14624
|
-
return result.options.map((o) => o.label).join(", ");
|
|
14625
|
-
}
|
|
14626
|
-
}
|
|
14627
15075
|
async function ask(state, rail, input) {
|
|
14628
15076
|
if (!state.goal) return null;
|
|
14629
15077
|
try {
|
|
14630
15078
|
const sent = await rail.contact({
|
|
14631
|
-
asks: [{ ask: input.ask, parentId: state.goal.id,
|
|
15079
|
+
asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
|
|
14632
15080
|
waiting: "hard",
|
|
14633
|
-
channel:
|
|
15081
|
+
channel: "call"
|
|
14634
15082
|
});
|
|
14635
15083
|
if ("recorded" in sent) return null;
|
|
14636
15084
|
const entryId = sent.requestEntryIds[0];
|
|
@@ -14641,89 +15089,416 @@ async function ask(state, rail, input) {
|
|
|
14641
15089
|
return null;
|
|
14642
15090
|
}
|
|
14643
15091
|
}
|
|
14644
|
-
function checkCallOnDone(text) {
|
|
14645
|
-
if (!text) return false;
|
|
14646
|
-
return /\b(call|ring|phone)\b.*\b(done|finish(ed)?|complete(d)?)\b/i.test(text) || /\b(when|if|once)\b.*\b(done|finish(ed)?|complete(d)?)\b.*\b(call|ring|phone)\b/i.test(text) || /\bcall\s+(me|us)\b/i.test(text);
|
|
14647
|
-
}
|
|
14648
|
-
function parseQuestionOptions(text) {
|
|
14649
|
-
if (!text) return { question: "" };
|
|
14650
|
-
const trimmed = text.trim();
|
|
14651
|
-
const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
14652
|
-
const optionLines = [];
|
|
14653
|
-
const questionLines = [];
|
|
14654
|
-
for (const line of lines2) {
|
|
14655
|
-
const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
|
|
14656
|
-
if (bulletMatch) {
|
|
14657
|
-
optionLines.push(bulletMatch[1].trim());
|
|
14658
|
-
} else {
|
|
14659
|
-
questionLines.push(line);
|
|
14660
|
-
}
|
|
14661
|
-
}
|
|
14662
|
-
if (optionLines.length >= 2 && optionLines.length <= 6) {
|
|
14663
|
-
return {
|
|
14664
|
-
question: questionLines.join("\n\n").trim() || trimmed,
|
|
14665
|
-
options: optionLines.map((raw) => {
|
|
14666
|
-
const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
|
|
14667
|
-
const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
|
|
14668
|
-
if (colonSplit && colonSplit[1] && colonSplit[2]) {
|
|
14669
|
-
return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
|
|
14670
|
-
}
|
|
14671
|
-
if (parenSplit && parenSplit[1] && parenSplit[2]) {
|
|
14672
|
-
return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
|
|
14673
|
-
}
|
|
14674
|
-
return { label: raw.slice(0, 1e3) };
|
|
14675
|
-
})
|
|
14676
|
-
};
|
|
14677
|
-
}
|
|
14678
|
-
return { question: trimmed };
|
|
14679
|
-
}
|
|
14680
15092
|
async function askPermission(event, state, rail) {
|
|
14681
15093
|
return decisionFrom(await ask(state, rail, {
|
|
14682
15094
|
ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
|
|
14683
15095
|
options: [
|
|
14684
15096
|
{ label: ALLOW, hint: "Permits the agent to run this command" },
|
|
14685
15097
|
{ label: "Deny", hint: "Blocks execution of this command" }
|
|
14686
|
-
]
|
|
14687
|
-
call: true
|
|
14688
|
-
}));
|
|
14689
|
-
}
|
|
14690
|
-
async function askQuestion(question, state, rail, opts) {
|
|
14691
|
-
return wordsOf(await ask(state, rail, {
|
|
14692
|
-
ask: question,
|
|
14693
|
-
call: opts?.call ?? true,
|
|
14694
|
-
options: opts?.options
|
|
15098
|
+
]
|
|
14695
15099
|
}));
|
|
14696
15100
|
}
|
|
14697
15101
|
async function finish(result, state, rail, opts) {
|
|
14698
15102
|
const goal = state.goal;
|
|
14699
15103
|
if (!goal) return;
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
15104
|
+
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
15105
|
+
let revision = goal.revision;
|
|
15106
|
+
try {
|
|
15107
|
+
const held = await rail.claim(goal.id);
|
|
15108
|
+
revision = held.revision ?? revision;
|
|
15109
|
+
const owed = state.noted?.some((b) => b.goals.includes(goal.id));
|
|
15110
|
+
if (!held.openDecisionNeedIds?.length && !owed) {
|
|
15111
|
+
await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
|
|
15112
|
+
state.goal = void 0;
|
|
15113
|
+
return;
|
|
15114
|
+
}
|
|
15115
|
+
} catch (error) {
|
|
15116
|
+
opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
|
|
15117
|
+
}
|
|
15118
|
+
if (!progress) return;
|
|
14703
15119
|
try {
|
|
14704
|
-
await rail.
|
|
14705
|
-
const { revision } = await rail.read(goal.id);
|
|
14706
|
-
await rail.update(goal.id, { revision: revision ?? goal.revision, changes: { state: "done", progress: report.slice(0, 1e4) }, reason: "The agent finished its turn without asking anything." });
|
|
15120
|
+
await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
|
|
14707
15121
|
} catch (error) {
|
|
14708
|
-
opts?.log?.(`\u26A0
|
|
15122
|
+
opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
|
|
15123
|
+
}
|
|
15124
|
+
}
|
|
15125
|
+
async function cannotWork(report, state, rail, opts) {
|
|
15126
|
+
state.resting = true;
|
|
15127
|
+
try {
|
|
15128
|
+
if (!state.goal) {
|
|
15129
|
+
const claimed = await rail.claim();
|
|
15130
|
+
if (!claimed.goalId || claimed.revision === void 0) return;
|
|
15131
|
+
state.goal = { id: claimed.goalId, revision: claimed.revision };
|
|
15132
|
+
}
|
|
15133
|
+
await rail.contact({ asks: [{ ask: report, parentId: state.goal.id }], waiting: "none", channel: "notification" });
|
|
15134
|
+
} catch (error) {
|
|
15135
|
+
opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
|
|
14709
15136
|
}
|
|
14710
15137
|
}
|
|
14711
15138
|
async function afterTurn(event, state, rail, opts) {
|
|
14712
|
-
if (
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
15139
|
+
if (event.failed) {
|
|
15140
|
+
const limit = limitIn(event.result, (opts?.now ?? Date.now)());
|
|
15141
|
+
if (limit) {
|
|
15142
|
+
state.paused = limit;
|
|
15143
|
+
if (state.typed) state.owed = state.typed;
|
|
15144
|
+
state.typed = void 0;
|
|
15145
|
+
state.carriedOn = false;
|
|
15146
|
+
state.resting = true;
|
|
15147
|
+
opts?.log?.(`\u23F8 paused by the usage limit until ${limit.resets}: nothing is typed in until then, and it carries on by itself`);
|
|
15148
|
+
return limit;
|
|
15149
|
+
}
|
|
15150
|
+
await cannotWork(`The agent stopped without finishing.
|
|
15151
|
+
|
|
15152
|
+
${event.result?.trim() || "No result reported."}`, state, rail, opts);
|
|
15153
|
+
return void 0;
|
|
14716
15154
|
}
|
|
14717
|
-
|
|
15155
|
+
await finish(event.result, state, rail, opts);
|
|
15156
|
+
state.typed = void 0;
|
|
15157
|
+
state.resting = true;
|
|
15158
|
+
return void 0;
|
|
15159
|
+
}
|
|
14718
15160
|
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
15161
|
+
// src/paigy/conversation.ts
|
|
15162
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
15163
|
+
var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId). " + DONE_MESSAGE;
|
|
15164
|
+
var SAFETY_MS = 6e4;
|
|
15165
|
+
var ASK_BEAT_MS = 5e3;
|
|
15166
|
+
function beat() {
|
|
15167
|
+
let pending = null;
|
|
15168
|
+
let wakeUp = null;
|
|
15169
|
+
return {
|
|
15170
|
+
nudge(cause) {
|
|
15171
|
+
if (wakeUp) {
|
|
15172
|
+
const w = wakeUp;
|
|
15173
|
+
wakeUp = null;
|
|
15174
|
+
w(cause);
|
|
15175
|
+
} else pending ??= cause;
|
|
15176
|
+
},
|
|
15177
|
+
next(ms) {
|
|
15178
|
+
if (pending) {
|
|
15179
|
+
const cause = pending;
|
|
15180
|
+
pending = null;
|
|
15181
|
+
return Promise.resolve(cause);
|
|
15182
|
+
}
|
|
15183
|
+
return new Promise((resolve3) => {
|
|
15184
|
+
const timer = setTimeout(() => {
|
|
15185
|
+
wakeUp = null;
|
|
15186
|
+
resolve3("safety");
|
|
15187
|
+
}, ms);
|
|
15188
|
+
wakeUp = (cause) => {
|
|
15189
|
+
clearTimeout(timer);
|
|
15190
|
+
resolve3(cause);
|
|
15191
|
+
};
|
|
15192
|
+
});
|
|
15193
|
+
}
|
|
15194
|
+
};
|
|
15195
|
+
}
|
|
15196
|
+
var BEAT_REPORT_MS = 10 * 6e4;
|
|
15197
|
+
var ended = (state) => state === "done" || state === "cancelled";
|
|
15198
|
+
function unheard(brief, state, since = null, after = -1) {
|
|
15199
|
+
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
15200
|
+
const words2 = [];
|
|
15201
|
+
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
15202
|
+
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
15203
|
+
if (i <= after) {
|
|
15204
|
+
seen.add(e.entryId);
|
|
15205
|
+
continue;
|
|
15206
|
+
}
|
|
15207
|
+
if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
|
|
15208
|
+
seen.add(e.entryId);
|
|
15209
|
+
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
15210
|
+
const text = entryWords(e).trim();
|
|
15211
|
+
if (text) words2.push(text);
|
|
15212
|
+
}
|
|
15213
|
+
return words2;
|
|
15214
|
+
}
|
|
15215
|
+
function sinceTheWork(brief) {
|
|
15216
|
+
const entries = brief.entries ?? [];
|
|
15217
|
+
let after = -1;
|
|
15218
|
+
for (const [i, e] of entries.entries()) if (!e.authorParticipant.startsWith("human:")) after = i;
|
|
15219
|
+
const last = entries[after];
|
|
15220
|
+
const handed = last && brief.ownerParticipant && last.authorParticipant !== brief.ownerParticipant;
|
|
15221
|
+
const note2 = handed ? entryWords(last).trim() : "";
|
|
15222
|
+
return { after, ...note2 ? { note: note2 } : {} };
|
|
15223
|
+
}
|
|
15224
|
+
var baseline = (goal, subtree) => ({
|
|
15225
|
+
goal,
|
|
15226
|
+
goals: new Map((subtree ?? []).map((g) => [g.goalId, { since: g.updatedAt ?? null, at: g.updatedAt ?? null }]))
|
|
15227
|
+
});
|
|
15228
|
+
function movedBelow(held, state) {
|
|
15229
|
+
const goals = state.below.goals;
|
|
15230
|
+
return (held.subtree ?? []).flatMap((g) => {
|
|
15231
|
+
const known = goals.get(g.goalId);
|
|
15232
|
+
const stamp = g.updatedAt ?? null;
|
|
15233
|
+
if (!known) return [{ goal: g, since: null }];
|
|
15234
|
+
if (known.at === null) {
|
|
15235
|
+
known.since = known.at = stamp;
|
|
15236
|
+
return [];
|
|
15237
|
+
}
|
|
15238
|
+
return stamp !== null && stamp !== known.at ? [{ goal: g, since: known.since }] : [];
|
|
14724
15239
|
});
|
|
14725
|
-
|
|
14726
|
-
|
|
15240
|
+
}
|
|
15241
|
+
function named(goalId, title, brief) {
|
|
15242
|
+
const line = (title ?? brief.outcome ?? "").trim().split("\n")[0].trim();
|
|
15243
|
+
const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
15244
|
+
return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
|
|
15245
|
+
}
|
|
15246
|
+
async function gather(state, rail, held) {
|
|
15247
|
+
const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
|
|
15248
|
+
const look2 = (goalId, via, title, since) => {
|
|
15249
|
+
if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
|
|
15250
|
+
};
|
|
15251
|
+
const waiting = await rail.waiting().catch(() => null);
|
|
15252
|
+
const listed = /* @__PURE__ */ new Set();
|
|
15253
|
+
const announced = state.announced ??= /* @__PURE__ */ new Set();
|
|
15254
|
+
if (waiting) {
|
|
15255
|
+
for (const id of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id)) announced.delete(id);
|
|
15256
|
+
}
|
|
15257
|
+
for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
|
|
15258
|
+
for (const g of waiting?.review ?? []) {
|
|
15259
|
+
listed.add(g.goalId);
|
|
15260
|
+
look2(g.goalId, "waiting work", g.title);
|
|
15261
|
+
}
|
|
15262
|
+
for (const d of waiting?.deliveries ?? []) for (const id of d.goalIds) {
|
|
15263
|
+
listed.add(id);
|
|
15264
|
+
look2(id, "waiting work");
|
|
15265
|
+
}
|
|
15266
|
+
const moved = movedBelow(held, state);
|
|
15267
|
+
for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
|
|
15268
|
+
const heard = [];
|
|
15269
|
+
const read = /* @__PURE__ */ new Set();
|
|
15270
|
+
for (const [goalId, { since, ...found }] of looks) {
|
|
15271
|
+
let brief = found.brief;
|
|
15272
|
+
if (!brief) {
|
|
15273
|
+
try {
|
|
15274
|
+
brief = await rail.read(goalId);
|
|
15275
|
+
} catch (error) {
|
|
15276
|
+
if (error.status === 404) read.add(goalId);
|
|
15277
|
+
continue;
|
|
15278
|
+
}
|
|
15279
|
+
}
|
|
15280
|
+
read.add(goalId);
|
|
15281
|
+
const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
|
|
15282
|
+
if (found.via === "assigned") announced.add(goalId);
|
|
15283
|
+
heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
|
|
15284
|
+
}
|
|
15285
|
+
for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
15286
|
+
return { heard, listed };
|
|
15287
|
+
}
|
|
15288
|
+
var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; claim_goal with its id when you take it up.";
|
|
15289
|
+
var says = (h) => h.words.length > 0 || h.via === "assigned";
|
|
15290
|
+
function whatItIs(h) {
|
|
15291
|
+
const outcome = h.brief.outcome?.trim() ?? "";
|
|
15292
|
+
const name = (h.title ?? outcome).trim().split("\n")[0].trim();
|
|
15293
|
+
return outcome && outcome !== name ? [outcome] : [];
|
|
15294
|
+
}
|
|
15295
|
+
var block = (h) => [
|
|
15296
|
+
...h.via === "assigned" ? [GIVEN] : [],
|
|
15297
|
+
named(h.goalId, h.title, h.brief),
|
|
15298
|
+
...h.via === "assigned" ? whatItIs(h) : [],
|
|
15299
|
+
...h.note ? [`The last note on it, from the agent before you: ${h.note}`] : [],
|
|
15300
|
+
...h.words
|
|
15301
|
+
].join("\n\n");
|
|
15302
|
+
var short = (ids) => ids.map((id) => id.slice(0, 8)).join(", ");
|
|
15303
|
+
var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
|
|
15304
|
+
async function hear(session, state, rail, held, opts) {
|
|
15305
|
+
const log2 = opts?.log;
|
|
15306
|
+
const { heard, listed } = await gather(state, rail, held);
|
|
15307
|
+
const said = heard.filter(says);
|
|
15308
|
+
if (said.length) {
|
|
15309
|
+
send2(session, state, said.map(block).join("\n\n"));
|
|
15310
|
+
const worded = said.filter((h) => h.words.length);
|
|
15311
|
+
if (worded.length) log2?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
|
|
15312
|
+
const goals = given(said);
|
|
15313
|
+
if (goals.length) {
|
|
15314
|
+
log2?.(`\u25B8 announced assigned ${short(goals)} (via rest)`);
|
|
15315
|
+
if (opts?.tally) opts.tally.announced += goals.length;
|
|
15316
|
+
}
|
|
15317
|
+
}
|
|
15318
|
+
for (const h of heard) if (listed.has(h.goalId)) await acknowledge(h.goalId, h.brief, state, rail, log2);
|
|
15319
|
+
}
|
|
15320
|
+
var NOTED = "New from the person while you work. Carry on unless it changes what you're doing: it comes back to you as your next turn when this one ends.";
|
|
15321
|
+
var SEEN_MID_TURN = "While you worked, the person said this, and you saw it mid-task. Act on it now, unless you already have.";
|
|
15322
|
+
async function note(state, rail, held, inbox, how) {
|
|
15323
|
+
const { heard } = await gather(state, rail, held);
|
|
15324
|
+
const said = heard.filter(says);
|
|
15325
|
+
if (!said.length) return 0;
|
|
15326
|
+
const words2 = said.map(block).join("\n\n");
|
|
15327
|
+
const goals = said.map((h) => h.goalId);
|
|
15328
|
+
const assigned = given(said);
|
|
15329
|
+
if (how.tally) how.tally.announced += assigned.length;
|
|
15330
|
+
let name = "";
|
|
15331
|
+
try {
|
|
15332
|
+
name = inbox.put(`${NOTED}
|
|
15333
|
+
|
|
15334
|
+
${words2}`);
|
|
15335
|
+
how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
|
|
15336
|
+
if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
|
|
15337
|
+
} catch (error) {
|
|
15338
|
+
how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
|
|
15339
|
+
}
|
|
15340
|
+
(state.noted ??= []).push({ name, goals, words: words2, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
|
|
15341
|
+
return 1;
|
|
15342
|
+
}
|
|
15343
|
+
function receipts(state, inbox, log2) {
|
|
15344
|
+
let seen = 0;
|
|
15345
|
+
for (const b of state.noted ?? []) {
|
|
15346
|
+
if (b.seen !== void 0 || !b.name) continue;
|
|
15347
|
+
const at = inbox.taken(b.name);
|
|
15348
|
+
if (at === null) continue;
|
|
15349
|
+
b.seen = at;
|
|
15350
|
+
seen++;
|
|
15351
|
+
log2?.(`\u2713 seen mid-turn: ${short(b.goals)} ${Math.max(0, Math.round((at - b.at) / 1e3))}s after the ${b.cause}`);
|
|
15352
|
+
}
|
|
15353
|
+
return seen;
|
|
15354
|
+
}
|
|
15355
|
+
function settle(session, state, inbox, log2) {
|
|
15356
|
+
const batches = state.noted ?? [];
|
|
15357
|
+
state.noted = [];
|
|
15358
|
+
const seen = [];
|
|
15359
|
+
const unseen = [];
|
|
15360
|
+
for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
|
|
15361
|
+
send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
|
|
15362
|
+
log2?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
|
|
15363
|
+
}
|
|
15364
|
+
async function acknowledge(goalId, brief, state, rail, log2) {
|
|
15365
|
+
const acked = state.acked ??= /* @__PURE__ */ new Map();
|
|
15366
|
+
if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
|
|
15367
|
+
acked.set(goalId, brief.revision);
|
|
15368
|
+
try {
|
|
15369
|
+
const claimed = await rail.claim(goalId);
|
|
15370
|
+
const done = await rail.update(goalId, { revision: claimed.revision ?? brief.revision ?? 1, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
|
|
15371
|
+
acked.set(goalId, done.revision);
|
|
15372
|
+
if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
|
|
15373
|
+
} catch (error) {
|
|
15374
|
+
log2?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${error.message} \u2014 its words are typed in; it stays listed until acknowledged`);
|
|
15375
|
+
}
|
|
15376
|
+
}
|
|
15377
|
+
async function step(session, state, rail, opts) {
|
|
15378
|
+
if (state.paused) {
|
|
15379
|
+
if ((opts?.now ?? Date.now)() < state.paused.until) {
|
|
15380
|
+
if (opts?.tally) opts.tally.paused = (opts.tally.paused ?? 0) + 1;
|
|
15381
|
+
return;
|
|
15382
|
+
}
|
|
15383
|
+
opts?.log?.(`\u25B6 resumed after the limit (it reset at ${state.paused.resets}): told to carry on, with what arrived meanwhile`);
|
|
15384
|
+
state.paused = void 0;
|
|
15385
|
+
state.carryOn = true;
|
|
15386
|
+
}
|
|
15387
|
+
await look(session, state, rail, opts);
|
|
15388
|
+
if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
|
|
15389
|
+
}
|
|
15390
|
+
async function look(session, state, rail, opts) {
|
|
15391
|
+
if (opts?.inbox) {
|
|
15392
|
+
const seen = receipts(state, opts.inbox, opts.log);
|
|
15393
|
+
if (opts.tally) opts.tally.seen += seen;
|
|
15394
|
+
}
|
|
15395
|
+
if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
|
|
15396
|
+
if (!state.goal) {
|
|
15397
|
+
if (!state.resting) return;
|
|
15398
|
+
let brief2 = await rail.claim();
|
|
15399
|
+
if (!brief2.goalId || brief2.revision === void 0) {
|
|
15400
|
+
if (opts?.initialPrompt?.trim()) {
|
|
15401
|
+
const prompt = opts.initialPrompt.trim();
|
|
15402
|
+
opts.initialPrompt = void 0;
|
|
15403
|
+
const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID5() });
|
|
15404
|
+
brief2 = await rail.claim();
|
|
15405
|
+
if (!brief2.goalId || brief2.revision === void 0) {
|
|
15406
|
+
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
15407
|
+
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
15408
|
+
return;
|
|
15409
|
+
}
|
|
15410
|
+
} else {
|
|
15411
|
+
return;
|
|
15412
|
+
}
|
|
15413
|
+
}
|
|
15414
|
+
unheard(brief2, state);
|
|
15415
|
+
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
15416
|
+
else {
|
|
15417
|
+
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
15418
|
+
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
15419
|
+
}
|
|
15420
|
+
const outcome = brief2.outcome?.trim() ?? "";
|
|
15421
|
+
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
|
|
15422
|
+
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
15423
|
+
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
15424
|
+
const fullText = [outcome, earlier, ...said].filter(Boolean).join("\n\n");
|
|
15425
|
+
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
15426
|
+
return;
|
|
15427
|
+
}
|
|
15428
|
+
const brief = await rail.read(state.goal.id);
|
|
15429
|
+
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
15430
|
+
for (const answer of brief.answers ?? []) {
|
|
15431
|
+
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
15432
|
+
if (!resolve3) continue;
|
|
15433
|
+
state.asks.delete(answer.requestEntryId);
|
|
15434
|
+
resolve3(answer.result);
|
|
15435
|
+
}
|
|
15436
|
+
if (ended(brief.state)) {
|
|
15437
|
+
state.goal = void 0;
|
|
15438
|
+
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
15439
|
+
state.asks?.clear();
|
|
15440
|
+
return;
|
|
15441
|
+
}
|
|
15442
|
+
if (state.below?.goal !== state.goal.id) state.below = baseline(state.goal.id, brief.subtree);
|
|
15443
|
+
if (state.asks?.size) return;
|
|
15444
|
+
if (!state.resting) {
|
|
15445
|
+
if (!opts?.inbox) return;
|
|
15446
|
+
const noted = await note(state, rail, brief, opts.inbox, { cause: opts.cause ?? "safety", now: opts.now ?? Date.now, ...opts.log ? { log: opts.log } : {}, ...opts.tally ? { tally: opts.tally } : {} });
|
|
15447
|
+
if (opts.tally) opts.tally.noted += noted;
|
|
15448
|
+
return;
|
|
15449
|
+
}
|
|
15450
|
+
await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
|
|
15451
|
+
}
|
|
15452
|
+
function send2(session, state, text) {
|
|
15453
|
+
state.resting = false;
|
|
15454
|
+
let words2 = text;
|
|
15455
|
+
if (state.carryOn) {
|
|
15456
|
+
state.carryOn = false;
|
|
15457
|
+
state.carriedOn = true;
|
|
15458
|
+
const owed = state.owed;
|
|
15459
|
+
state.owed = void 0;
|
|
15460
|
+
words2 = [owed, text].filter(Boolean).join("\n\n");
|
|
15461
|
+
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
15462
|
+
}
|
|
15463
|
+
state.typed = words2 || void 0;
|
|
15464
|
+
session.send(text);
|
|
15465
|
+
}
|
|
15466
|
+
async function pump(session, state, rail, opts) {
|
|
15467
|
+
const clock4 = opts.beat ?? beat();
|
|
15468
|
+
const now = opts.now ?? Date.now;
|
|
15469
|
+
const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
|
|
15470
|
+
const tally = { noted: 0, seen: 0, announced: 0 };
|
|
15471
|
+
const stepOpts = {
|
|
15472
|
+
...opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : {},
|
|
15473
|
+
...opts.log ? { log: opts.log } : {},
|
|
15474
|
+
...opts.inbox ? { inbox: opts.inbox } : {},
|
|
15475
|
+
now,
|
|
15476
|
+
tally
|
|
15477
|
+
};
|
|
15478
|
+
let since = now();
|
|
15479
|
+
let cause = "start";
|
|
15480
|
+
for (; ; ) {
|
|
15481
|
+
if (cause === "wake" && !state.resting && !state.asks?.size && !opts.inbox) counts.skipped++;
|
|
15482
|
+
else {
|
|
15483
|
+
if (cause !== "start") counts[cause]++;
|
|
15484
|
+
try {
|
|
15485
|
+
stepOpts.cause = cause;
|
|
15486
|
+
await step(session, state, rail, stepOpts);
|
|
15487
|
+
} catch {
|
|
15488
|
+
}
|
|
15489
|
+
}
|
|
15490
|
+
if (!await opts.onIdle()) return;
|
|
15491
|
+
if (now() - since >= BEAT_REPORT_MS) {
|
|
15492
|
+
const read = counts.wake + counts.rest + counts.safety;
|
|
15493
|
+
if (read || counts.skipped || tally.noted || tally.seen || tally.announced) {
|
|
15494
|
+
opts.log?.(`\u25B9 beats: ${read} read (${counts.wake} wake, ${counts.rest} rest, ${counts.safety} safety), ${counts.skipped} wake mid-turn skipped, ${tally.noted} noted mid-turn, ${tally.seen} seen by the hook, ${tally.announced} assigned announced${tally.paused ? `, ${tally.paused} held by the usage limit` : ""}`);
|
|
15495
|
+
}
|
|
15496
|
+
counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
|
|
15497
|
+
since = now();
|
|
15498
|
+
}
|
|
15499
|
+
cause = await clock4.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
|
|
15500
|
+
if (!await opts.onIdle()) return;
|
|
15501
|
+
}
|
|
14727
15502
|
}
|
|
14728
15503
|
|
|
14729
15504
|
// src/run.ts
|
|
@@ -14731,11 +15506,34 @@ function runHarness(opts) {
|
|
|
14731
15506
|
let running = true;
|
|
14732
15507
|
let tail = [];
|
|
14733
15508
|
const state = { resting: true };
|
|
15509
|
+
const clock4 = beat();
|
|
14734
15510
|
let session = null;
|
|
14735
|
-
const
|
|
15511
|
+
const speaksAs = opts.resume ?? opts.session;
|
|
15512
|
+
const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
|
|
15513
|
+
let inbox;
|
|
15514
|
+
if (opts.harness === "claude") {
|
|
15515
|
+
try {
|
|
15516
|
+
inbox = inboxFor();
|
|
15517
|
+
} catch (error) {
|
|
15518
|
+
opts.log(`\u26A0 no inbox (${error.message}): what the person says mid-turn waits for the turn to end`);
|
|
15519
|
+
}
|
|
15520
|
+
}
|
|
15521
|
+
const name = opts.name ?? "The agent";
|
|
15522
|
+
function runningAgain() {
|
|
15523
|
+
state.carriedOn = false;
|
|
15524
|
+
opts.log("\u25B6 running again after the limit");
|
|
15525
|
+
LIMITS.running(name, (text) => rail.contact({ asks: [{ ask: text, ...state.goal ? { parentId: state.goal.id } : {} }], waiting: "none", channel: "call" }), opts.log);
|
|
15526
|
+
}
|
|
14736
15527
|
async function handle(event) {
|
|
14737
15528
|
if (event.kind === "error") {
|
|
14738
15529
|
opts.log(`\u26A0 ${event.message}`);
|
|
15530
|
+
if (event.fatal && running) {
|
|
15531
|
+
opts.log(`\u2716 could not start: ${event.message}`);
|
|
15532
|
+
await cannotWork(`The session could not start, so nothing is working on this yet.
|
|
15533
|
+
|
|
15534
|
+
${event.message}`, state, rail, { log: opts.log });
|
|
15535
|
+
stop2();
|
|
15536
|
+
}
|
|
14739
15537
|
return;
|
|
14740
15538
|
}
|
|
14741
15539
|
if (event.kind === "permission") {
|
|
@@ -14752,6 +15550,7 @@ function runHarness(opts) {
|
|
|
14752
15550
|
return;
|
|
14753
15551
|
}
|
|
14754
15552
|
if (event.kind === "work") {
|
|
15553
|
+
if (state.carriedOn) runningAgain();
|
|
14755
15554
|
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
14756
15555
|
tail = pushWork(tail, workLine(event));
|
|
14757
15556
|
return;
|
|
@@ -14759,20 +15558,10 @@ function runHarness(opts) {
|
|
|
14759
15558
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
14760
15559
|
if (event.kind === "idle") {
|
|
14761
15560
|
tail = [];
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
state.resting = false;
|
|
14767
|
-
session.send(local);
|
|
14768
|
-
return;
|
|
14769
|
-
}
|
|
14770
|
-
}
|
|
14771
|
-
const reply = await afterTurn(event, state, rail, { log: opts.log });
|
|
14772
|
-
if (reply && session && running) {
|
|
14773
|
-
opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
|
|
14774
|
-
session.send(reply);
|
|
14775
|
-
}
|
|
15561
|
+
const carried = state.carriedOn && !event.failed;
|
|
15562
|
+
if (await afterTurn(event, state, rail, { log: opts.log })) LIMITS.paused(name, Date.now());
|
|
15563
|
+
else if (carried) runningAgain();
|
|
15564
|
+
clock4.nudge("rest");
|
|
14776
15565
|
}
|
|
14777
15566
|
}
|
|
14778
15567
|
session = startSession({
|
|
@@ -14781,35 +15570,45 @@ function runHarness(opts) {
|
|
|
14781
15570
|
mode: opts.mode,
|
|
14782
15571
|
...opts.token ? { token: opts.token } : {},
|
|
14783
15572
|
...opts.resume ? { resume: opts.resume } : {},
|
|
15573
|
+
...opts.session ? { session: opts.session } : {},
|
|
14784
15574
|
...opts.bin ? { bin: opts.bin } : {},
|
|
14785
|
-
...
|
|
15575
|
+
...inbox ? { inbox: inbox.dir } : {},
|
|
15576
|
+
onExit: () => {
|
|
15577
|
+
inbox?.close();
|
|
15578
|
+
opts.onExit?.();
|
|
15579
|
+
},
|
|
14786
15580
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
14787
15581
|
});
|
|
14788
|
-
void pump(session, state, rail, { onIdle: () => running, initialPrompt: opts.prompt }).catch(
|
|
15582
|
+
void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
|
|
14789
15583
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
14790
15584
|
);
|
|
14791
15585
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
15586
|
+
function stop2() {
|
|
15587
|
+
running = false;
|
|
15588
|
+
clock4.nudge("wake");
|
|
15589
|
+
tail = [];
|
|
15590
|
+
cancelAsks(state);
|
|
15591
|
+
session?.stop();
|
|
15592
|
+
session = null;
|
|
15593
|
+
inbox?.close();
|
|
15594
|
+
opts.log("\u25A0 stopped");
|
|
15595
|
+
}
|
|
14792
15596
|
return {
|
|
14793
15597
|
send(text) {
|
|
14794
15598
|
session?.send(text);
|
|
14795
15599
|
},
|
|
14796
15600
|
working: () => running && state.resting !== true,
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
cancelAsks(state);
|
|
14802
|
-
session?.stop();
|
|
14803
|
-
session = null;
|
|
14804
|
-
opts.log("\u25A0 stopped");
|
|
14805
|
-
}
|
|
15601
|
+
// Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
|
|
15602
|
+
tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
|
|
15603
|
+
nudge: () => clock4.nudge("wake"),
|
|
15604
|
+
stop: stop2
|
|
14806
15605
|
};
|
|
14807
15606
|
}
|
|
14808
15607
|
|
|
14809
15608
|
// src/update.ts
|
|
14810
15609
|
import { execFile } from "child_process";
|
|
14811
15610
|
import { createRequire } from "module";
|
|
14812
|
-
import { dirname as dirname3, join as
|
|
15611
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
14813
15612
|
import { promisify } from "util";
|
|
14814
15613
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
14815
15614
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -14836,7 +15635,7 @@ async function latestVersion() {
|
|
|
14836
15635
|
}
|
|
14837
15636
|
async function install(version) {
|
|
14838
15637
|
const bin = dirname3(process.execPath);
|
|
14839
|
-
await promisify(execFile)(
|
|
15638
|
+
await promisify(execFile)(join9(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
14840
15639
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
14841
15640
|
timeout: 5 * 6e4
|
|
14842
15641
|
});
|
|
@@ -14887,10 +15686,10 @@ function selfUpdater(deps) {
|
|
|
14887
15686
|
}
|
|
14888
15687
|
|
|
14889
15688
|
// src/host.ts
|
|
14890
|
-
var HOST_FILE =
|
|
15689
|
+
var HOST_FILE = join10(homedir9(), ".paigy", "host.json");
|
|
14891
15690
|
function readHostState() {
|
|
14892
15691
|
try {
|
|
14893
|
-
const s = JSON.parse(
|
|
15692
|
+
const s = JSON.parse(readFileSync5(HOST_FILE, "utf8"));
|
|
14894
15693
|
if (Date.now() - s.at > 2e4) return null;
|
|
14895
15694
|
try {
|
|
14896
15695
|
process.kill(s.pid, 0);
|
|
@@ -14911,18 +15710,37 @@ var claimedRun = (sessionId2) => {
|
|
|
14911
15710
|
const slot = sessionSlot(sessionId2);
|
|
14912
15711
|
return { slot, key: runKey(slot) };
|
|
14913
15712
|
};
|
|
14914
|
-
|
|
14915
|
-
|
|
15713
|
+
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
15714
|
+
async function waitingWork(token, session) {
|
|
15715
|
+
let found;
|
|
15716
|
+
try {
|
|
15717
|
+
found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
|
|
15718
|
+
} catch (error) {
|
|
15719
|
+
return { skip: `could not read what is waiting (${error.message})` };
|
|
15720
|
+
}
|
|
15721
|
+
const n = found.deliveries.length;
|
|
15722
|
+
if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
|
|
15723
|
+
const next = found.claimable;
|
|
15724
|
+
return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}` } : { skip: "has nothing waiting" };
|
|
15725
|
+
}
|
|
15726
|
+
function evict(runs, key, run) {
|
|
15727
|
+
if (runs.get(key)?.run === run) runs.delete(key);
|
|
14916
15728
|
}
|
|
14917
15729
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
14918
|
-
function wakeAction(live, now, serverSeenAt) {
|
|
15730
|
+
function wakeAction(live, now, serverSeenAt, here2 = {}) {
|
|
14919
15731
|
if (live) return { action: "skip", why: "run" };
|
|
14920
|
-
if (
|
|
15732
|
+
if (here2.holder) return { action: "skip", why: "holder" };
|
|
15733
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
|
|
15734
|
+
if (here2.goneAt !== void 0 && serverSeenAt <= here2.goneAt) return { action: "spawn", why: "gone" };
|
|
15735
|
+
return { action: "skip", why: "presence" };
|
|
15736
|
+
}
|
|
14921
15737
|
return { action: "spawn", why: "nobody" };
|
|
14922
15738
|
}
|
|
14923
15739
|
function startHost(opts) {
|
|
14924
15740
|
const runs = /* @__PURE__ */ new Map();
|
|
14925
15741
|
const asHost = { token: opts.token };
|
|
15742
|
+
const goneAt = /* @__PURE__ */ new Map();
|
|
15743
|
+
const heldHere = /* @__PURE__ */ new Set();
|
|
14926
15744
|
const refreshIdentities = async () => {
|
|
14927
15745
|
for (const slot of listSlots()) {
|
|
14928
15746
|
if (slot === "Desktop") continue;
|
|
@@ -14942,7 +15760,7 @@ function startHost(opts) {
|
|
|
14942
15760
|
working: () => [...runs.values()].filter((r) => r.run.working()).length,
|
|
14943
15761
|
log: opts.log
|
|
14944
15762
|
});
|
|
14945
|
-
const
|
|
15763
|
+
const beat2 = () => {
|
|
14946
15764
|
void refreshIdentities();
|
|
14947
15765
|
void update().catch(() => {
|
|
14948
15766
|
});
|
|
@@ -14969,11 +15787,13 @@ function startHost(opts) {
|
|
|
14969
15787
|
mode: "bypass",
|
|
14970
15788
|
prompt: "",
|
|
14971
15789
|
token: spec.token,
|
|
14972
|
-
|
|
15790
|
+
...launchRun(spec),
|
|
14973
15791
|
onExit: () => {
|
|
14974
|
-
runs
|
|
15792
|
+
evict(runs, key, run);
|
|
15793
|
+
goneAt.set(slot, Date.now());
|
|
14975
15794
|
opts.log(`\u2716 ${label} ended`);
|
|
14976
15795
|
},
|
|
15796
|
+
name: label,
|
|
14977
15797
|
log: log2
|
|
14978
15798
|
});
|
|
14979
15799
|
runs.set(key, { run, label, token: spec.token });
|
|
@@ -15000,7 +15820,13 @@ function startHost(opts) {
|
|
|
15000
15820
|
return presence;
|
|
15001
15821
|
}
|
|
15002
15822
|
let lastPresence = /* @__PURE__ */ new Map();
|
|
15003
|
-
async function
|
|
15823
|
+
async function evidence() {
|
|
15824
|
+
const presence = await presenceMap();
|
|
15825
|
+
lastHolders = localHolders();
|
|
15826
|
+
return { presence, holders: lastHolders };
|
|
15827
|
+
}
|
|
15828
|
+
let lastHolders = /* @__PURE__ */ new Map();
|
|
15829
|
+
async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
|
|
15004
15830
|
{
|
|
15005
15831
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
15006
15832
|
const key = runKey(slot);
|
|
@@ -15008,19 +15834,42 @@ function startHost(opts) {
|
|
|
15008
15834
|
const live = runs.get(key);
|
|
15009
15835
|
const tokenId = slotIdentity(slot).tokenId;
|
|
15010
15836
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
15011
|
-
const
|
|
15837
|
+
const holder = holders.get(slot);
|
|
15838
|
+
if (holder) heldHere.add(slot);
|
|
15839
|
+
else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
|
|
15840
|
+
const decided = wakeAction(!!live, Date.now(), seenByServer, { holder, goneAt: goneAt.get(slot) });
|
|
15012
15841
|
if (decided.action === "skip") {
|
|
15013
|
-
|
|
15842
|
+
if (decided.why === "run" && cause === "wake") live?.run.nudge();
|
|
15843
|
+
const name = slotName(slot) ?? slot;
|
|
15844
|
+
opts.log(decided.why === "run" ? `\u25CC ${name} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : decided.why === "holder" ? `\u25B8 ${name} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second` : `\u25CC ${name} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
|
|
15014
15845
|
return;
|
|
15015
15846
|
}
|
|
15847
|
+
const label = slotName(slot) ?? slot;
|
|
15848
|
+
const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
|
|
15016
15849
|
const token = readToken(slot);
|
|
15017
|
-
if (!token)
|
|
15850
|
+
if (!token) {
|
|
15851
|
+
skip("has no token on this machine");
|
|
15852
|
+
return;
|
|
15853
|
+
}
|
|
15018
15854
|
const owner = ownerSession(slot);
|
|
15019
|
-
if (owner && !allowed(owner.cwd, opts.wsDeps))
|
|
15855
|
+
if (owner && !allowed(owner.cwd, opts.wsDeps)) {
|
|
15856
|
+
skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
|
|
15857
|
+
return;
|
|
15858
|
+
}
|
|
15020
15859
|
const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
15021
|
-
if (!workspace)
|
|
15022
|
-
|
|
15023
|
-
|
|
15860
|
+
if (!workspace) {
|
|
15861
|
+
skip("has no granted folder to wake in");
|
|
15862
|
+
return;
|
|
15863
|
+
}
|
|
15864
|
+
const found = await waitingWork(token, owner?.sessionId);
|
|
15865
|
+
if ("skip" in found) {
|
|
15866
|
+
skip(found.skip);
|
|
15867
|
+
return;
|
|
15868
|
+
}
|
|
15869
|
+
if (runs.has(key)) {
|
|
15870
|
+
skip("was started by the wake that read it first");
|
|
15871
|
+
return;
|
|
15872
|
+
}
|
|
15024
15873
|
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
15025
15874
|
const run = runHarness({
|
|
15026
15875
|
harness,
|
|
@@ -15029,17 +15878,20 @@ function startHost(opts) {
|
|
|
15029
15878
|
prompt: "",
|
|
15030
15879
|
token,
|
|
15031
15880
|
...owner ? { resume: owner.sessionId } : {},
|
|
15032
|
-
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
15881
|
+
// A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
|
|
15882
|
+
// a run that is no longer the slot's must not take the live one's place away with it.
|
|
15033
15883
|
onExit: () => {
|
|
15034
|
-
runs
|
|
15884
|
+
evict(runs, key, run);
|
|
15885
|
+
goneAt.set(slot, Date.now());
|
|
15035
15886
|
opts.log(`\u2716 ${label} ended`);
|
|
15036
15887
|
},
|
|
15888
|
+
name: label,
|
|
15037
15889
|
log: log2
|
|
15038
15890
|
});
|
|
15039
15891
|
runs.set(key, { run, label, token });
|
|
15040
15892
|
void heartbeat(void 0, { token }).catch(() => {
|
|
15041
15893
|
});
|
|
15042
|
-
opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014
|
|
15894
|
+
opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 ${found.waiting} was waiting, in ${workspace}${decided.why === "gone" ? " (its process here ended; its last heartbeat was not waited out)" : ""}`);
|
|
15043
15895
|
}
|
|
15044
15896
|
}
|
|
15045
15897
|
const listening = /* @__PURE__ */ new Map();
|
|
@@ -15059,7 +15911,7 @@ function startHost(opts) {
|
|
|
15059
15911
|
joining.add(slot);
|
|
15060
15912
|
try {
|
|
15061
15913
|
const sub = await subscribeWake(
|
|
15062
|
-
() => void
|
|
15914
|
+
() => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
|
|
15063
15915
|
}),
|
|
15064
15916
|
{ token }
|
|
15065
15917
|
);
|
|
@@ -15073,8 +15925,8 @@ function startHost(opts) {
|
|
|
15073
15925
|
}
|
|
15074
15926
|
}
|
|
15075
15927
|
async function sweepSlots() {
|
|
15076
|
-
const
|
|
15077
|
-
for (const slot of listSlots()) await wakeSlot(slot,
|
|
15928
|
+
const seen = await evidence();
|
|
15929
|
+
for (const slot of listSlots()) await wakeSlot(slot, seen);
|
|
15078
15930
|
}
|
|
15079
15931
|
const ACTIVITY_MS = 2e3;
|
|
15080
15932
|
const published = /* @__PURE__ */ new Map();
|
|
@@ -15099,12 +15951,12 @@ function startHost(opts) {
|
|
|
15099
15951
|
};
|
|
15100
15952
|
const publish = () => {
|
|
15101
15953
|
try {
|
|
15102
|
-
|
|
15954
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
15103
15955
|
} catch {
|
|
15104
15956
|
}
|
|
15105
15957
|
};
|
|
15106
|
-
|
|
15107
|
-
const pulse = setInterval(
|
|
15958
|
+
beat2();
|
|
15959
|
+
const pulse = setInterval(beat2, 6e4);
|
|
15108
15960
|
const spawnPoll = setInterval(() => {
|
|
15109
15961
|
void claimAndSpawn();
|
|
15110
15962
|
publish();
|
|
@@ -15130,10 +15982,10 @@ function startHost(opts) {
|
|
|
15130
15982
|
// them live, and a run mid-turn makes it working.
|
|
15131
15983
|
roster: () => {
|
|
15132
15984
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
15133
|
-
const
|
|
15134
|
-
for (const key of listSlots()) if (key !== "Desktop")
|
|
15135
|
-
for (const label of live.keys()) if (!
|
|
15136
|
-
return [...
|
|
15985
|
+
const names2 = /* @__PURE__ */ new Map();
|
|
15986
|
+
for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
|
|
15987
|
+
for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
|
|
15988
|
+
return [...names2.entries()].map(([name, slot]) => {
|
|
15137
15989
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
15138
15990
|
return {
|
|
15139
15991
|
name,
|
|
@@ -15150,7 +16002,7 @@ function startHost(opts) {
|
|
|
15150
16002
|
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
15151
16003
|
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
15152
16004
|
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
15153
|
-
running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
16005
|
+
running: live.has(name) || !!slot && lastHolders.has(slot) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
15154
16006
|
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
15155
16007
|
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
15156
16008
|
working: live.get(name)?.working() ?? false
|
|
@@ -15166,7 +16018,7 @@ function startHost(opts) {
|
|
|
15166
16018
|
streamActivity();
|
|
15167
16019
|
clearInterval(activityTick);
|
|
15168
16020
|
try {
|
|
15169
|
-
|
|
16021
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
15170
16022
|
} catch {
|
|
15171
16023
|
}
|
|
15172
16024
|
}
|
|
@@ -15181,8 +16033,8 @@ function startHost(opts) {
|
|
|
15181
16033
|
var OPEN_STATUSES = ["open", "assigned", "in_progress"];
|
|
15182
16034
|
var DAY_MS = 864e5;
|
|
15183
16035
|
var daysBetween = (from, now) => from ? Math.floor((now - Date.parse(from)) / DAY_MS) : null;
|
|
15184
|
-
function assigneeToken(
|
|
15185
|
-
const ref =
|
|
16036
|
+
function assigneeToken(note2) {
|
|
16037
|
+
const ref = note2.assignee ?? "";
|
|
15186
16038
|
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
15187
16039
|
}
|
|
15188
16040
|
var STOP = /* @__PURE__ */ new Set([
|
|
@@ -15256,23 +16108,23 @@ function overlap(a, b) {
|
|
|
15256
16108
|
return shared / (A.size + B.size - shared);
|
|
15257
16109
|
}
|
|
15258
16110
|
var SUPERSEDE_AT = 0.5;
|
|
15259
|
-
function supersededBy(
|
|
15260
|
-
const mine = `${
|
|
15261
|
-
const at = Date.parse(
|
|
16111
|
+
function supersededBy(note2, all) {
|
|
16112
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
16113
|
+
const at = Date.parse(note2.createdAt);
|
|
15262
16114
|
let best = null;
|
|
15263
16115
|
for (const other of all) {
|
|
15264
|
-
if (other.id ===
|
|
16116
|
+
if (other.id === note2.id) continue;
|
|
15265
16117
|
if (Date.parse(other.createdAt) <= at) continue;
|
|
15266
16118
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15267
16119
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15268
16120
|
}
|
|
15269
16121
|
return best?.note ?? null;
|
|
15270
16122
|
}
|
|
15271
|
-
function doneEcho(
|
|
15272
|
-
const mine = `${
|
|
16123
|
+
function doneEcho(note2, all) {
|
|
16124
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
15273
16125
|
let best = null;
|
|
15274
16126
|
for (const other of all) {
|
|
15275
|
-
if (other.id ===
|
|
16127
|
+
if (other.id === note2.id || other.status !== "done") continue;
|
|
15276
16128
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15277
16129
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15278
16130
|
}
|
|
@@ -15327,8 +16179,8 @@ function dossier(notes, connections, now) {
|
|
|
15327
16179
|
today: new Date(now).toISOString().slice(0, 10)
|
|
15328
16180
|
};
|
|
15329
16181
|
}
|
|
15330
|
-
function affinityWhy(
|
|
15331
|
-
const shared =
|
|
16182
|
+
function affinityWhy(note2, agent2) {
|
|
16183
|
+
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
15332
16184
|
if (shared.length && agent2.recentNotes)
|
|
15333
16185
|
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
15334
16186
|
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
@@ -15337,18 +16189,18 @@ function affinityWhy(note, agent2) {
|
|
|
15337
16189
|
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
15338
16190
|
return null;
|
|
15339
16191
|
}
|
|
15340
|
-
function staleWhy(
|
|
15341
|
-
if (
|
|
15342
|
-
if (
|
|
15343
|
-
if (
|
|
15344
|
-
if (
|
|
15345
|
-
if (
|
|
16192
|
+
function staleWhy(note2) {
|
|
16193
|
+
if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
|
|
16194
|
+
if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
|
|
16195
|
+
if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
|
|
16196
|
+
if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
|
|
16197
|
+
if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
|
|
15346
16198
|
return null;
|
|
15347
16199
|
}
|
|
15348
16200
|
|
|
15349
16201
|
// src/triage/judge.ts
|
|
15350
16202
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
15351
|
-
import { homedir as
|
|
16203
|
+
import { homedir as homedir10 } from "os";
|
|
15352
16204
|
import { delimiter as delimiter3 } from "path";
|
|
15353
16205
|
var PRIVACY = {
|
|
15354
16206
|
ollama: "never leaves this machine",
|
|
@@ -15452,7 +16304,7 @@ async function judgeFor(asked2, deps = {}) {
|
|
|
15452
16304
|
const out = spawnSync2(bin, runner.argv(null), {
|
|
15453
16305
|
input: prompt,
|
|
15454
16306
|
encoding: "utf8",
|
|
15455
|
-
cwd:
|
|
16307
|
+
cwd: homedir10(),
|
|
15456
16308
|
timeout: RUN_TIMEOUT_MS,
|
|
15457
16309
|
maxBuffer: 32 * 1024 * 1024,
|
|
15458
16310
|
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
@@ -15567,22 +16419,22 @@ function toProposal(verdicts, d, meta) {
|
|
|
15567
16419
|
const stale = [];
|
|
15568
16420
|
const assignments = /* @__PURE__ */ new Map();
|
|
15569
16421
|
for (const v of verdicts) {
|
|
15570
|
-
const
|
|
15571
|
-
if (!
|
|
15572
|
-
const item = (why) => ({ noteId:
|
|
16422
|
+
const note2 = byId.get(v.id);
|
|
16423
|
+
if (!note2) continue;
|
|
16424
|
+
const item = (why) => ({ noteId: note2.id, title: note2.title, why });
|
|
15573
16425
|
if (v.verdict === "close") {
|
|
15574
|
-
close.push(item(staleWhy(
|
|
16426
|
+
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
15575
16427
|
continue;
|
|
15576
16428
|
}
|
|
15577
16429
|
if (v.verdict === "stale") {
|
|
15578
|
-
stale.push(item(staleWhy(
|
|
16430
|
+
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
15579
16431
|
continue;
|
|
15580
16432
|
}
|
|
15581
16433
|
if (v.verdict !== "assign" || !v.agent) continue;
|
|
15582
16434
|
const agent2 = agentById.get(v.agent);
|
|
15583
16435
|
if (!agent2) continue;
|
|
15584
16436
|
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
15585
|
-
group.notes.push(item(affinityWhy(
|
|
16437
|
+
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
15586
16438
|
assignments.set(agent2.token, group);
|
|
15587
16439
|
}
|
|
15588
16440
|
return {
|
|
@@ -15750,7 +16602,7 @@ var win = null;
|
|
|
15750
16602
|
function log(message) {
|
|
15751
16603
|
win?.webContents.send("paigy:log", message);
|
|
15752
16604
|
}
|
|
15753
|
-
var ICON =
|
|
16605
|
+
var ICON = join11(here, "..", "assets", "icon.png");
|
|
15754
16606
|
function createWindow() {
|
|
15755
16607
|
win = new BrowserWindow({
|
|
15756
16608
|
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
@@ -15762,9 +16614,9 @@ function createWindow() {
|
|
|
15762
16614
|
minHeight: 520,
|
|
15763
16615
|
icon: ICON,
|
|
15764
16616
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
15765
|
-
webPreferences: { preload:
|
|
16617
|
+
webPreferences: { preload: join11(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
15766
16618
|
});
|
|
15767
|
-
void win.loadFile(
|
|
16619
|
+
void win.loadFile(join11(here, "..", "renderer", "index.html"));
|
|
15768
16620
|
}
|
|
15769
16621
|
var host = null;
|
|
15770
16622
|
app.whenReady().then(() => {
|
|
@@ -15833,7 +16685,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
15833
16685
|
});
|
|
15834
16686
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
15835
16687
|
try {
|
|
15836
|
-
const all =
|
|
16688
|
+
const all = readFileSync6(join11(homedir11(), ".paigy", "host.log"), "utf8").split("\n");
|
|
15837
16689
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
15838
16690
|
return mine.slice(-500).join("\n");
|
|
15839
16691
|
} catch {
|