@paigy/harness 0.3.17 → 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 +1162 -365
- package/dist/main.js +1118 -323
- 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.";
|
|
@@ -10851,7 +10856,19 @@ var QueueItemSchema = external_exports.object({
|
|
|
10851
10856
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
10852
10857
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
10853
10858
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
10854
|
-
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()
|
|
10855
10872
|
});
|
|
10856
10873
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
10857
10874
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -11029,6 +11046,104 @@ var NotificationFeedbackKindSchema = external_exports.enum([
|
|
|
11029
11046
|
"other"
|
|
11030
11047
|
// anything else — the note carries it.
|
|
11031
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
|
+
});
|
|
11032
11147
|
function sessionSlot(sessionId2) {
|
|
11033
11148
|
const id = sessionId2 ?? sessionId();
|
|
11034
11149
|
return `session:${id.slice(0, 8)}`;
|
|
@@ -11175,9 +11290,9 @@ async function claimGoal(goalId, opts = {}) {
|
|
|
11175
11290
|
if (!res.ok) await fail("claim_goal", res);
|
|
11176
11291
|
return await res.json();
|
|
11177
11292
|
}
|
|
11178
|
-
async function getGoal(goalId, opts = {}) {
|
|
11293
|
+
async function getGoal(goalId, opts = {}, read = {}) {
|
|
11179
11294
|
const token = authToken(opts.token) ?? "";
|
|
11180
|
-
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" : ""}`, {
|
|
11181
11296
|
method: "GET",
|
|
11182
11297
|
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
11183
11298
|
}));
|
|
@@ -11553,7 +11668,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
11553
11668
|
|
|
11554
11669
|
// src/main.ts
|
|
11555
11670
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
11556
|
-
import { homedir as
|
|
11671
|
+
import { homedir as homedir11, hostname } from "os";
|
|
11557
11672
|
|
|
11558
11673
|
// src/harness/catalog.ts
|
|
11559
11674
|
import { spawnSync } from "child_process";
|
|
@@ -11646,9 +11761,9 @@ function detectAll(deps = {}) {
|
|
|
11646
11761
|
}
|
|
11647
11762
|
|
|
11648
11763
|
// src/host.ts
|
|
11649
|
-
import { readFileSync as
|
|
11650
|
-
import { homedir as
|
|
11651
|
-
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";
|
|
11652
11767
|
|
|
11653
11768
|
// src/workspaces.ts
|
|
11654
11769
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -11693,10 +11808,72 @@ function resolveWakeDir(pinned, deps) {
|
|
|
11693
11808
|
return listWorkspaces(deps)[0];
|
|
11694
11809
|
}
|
|
11695
11810
|
|
|
11696
|
-
// src/
|
|
11697
|
-
import {
|
|
11811
|
+
// src/holder.ts
|
|
11812
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
11813
|
+
import { readFileSync as readFileSync4, readdirSync } from "fs";
|
|
11698
11814
|
import { homedir as homedir5 } from "os";
|
|
11699
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";
|
|
11700
11877
|
var HEAD_BYTES = 64e3;
|
|
11701
11878
|
function readHead(file) {
|
|
11702
11879
|
const fd = openSync2(file, "r");
|
|
@@ -11712,8 +11889,8 @@ function ownerSession(slot, deps = {}) {
|
|
|
11712
11889
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
11713
11890
|
if (!m) return null;
|
|
11714
11891
|
const prefix = m[1];
|
|
11715
|
-
const root = deps.projectsDir ??
|
|
11716
|
-
const list = deps.list ?? ((dir) =>
|
|
11892
|
+
const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
|
|
11893
|
+
const list = deps.list ?? ((dir) => readdirSync2(dir));
|
|
11717
11894
|
const head = deps.head ?? readHead;
|
|
11718
11895
|
let folders;
|
|
11719
11896
|
try {
|
|
@@ -11724,7 +11901,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11724
11901
|
for (const folder of folders) {
|
|
11725
11902
|
let files;
|
|
11726
11903
|
try {
|
|
11727
|
-
files = list(
|
|
11904
|
+
files = list(join7(root, folder));
|
|
11728
11905
|
} catch {
|
|
11729
11906
|
continue;
|
|
11730
11907
|
}
|
|
@@ -11732,7 +11909,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11732
11909
|
if (!file) continue;
|
|
11733
11910
|
let text;
|
|
11734
11911
|
try {
|
|
11735
|
-
text = head(
|
|
11912
|
+
text = head(join7(root, folder, file));
|
|
11736
11913
|
} catch {
|
|
11737
11914
|
continue;
|
|
11738
11915
|
}
|
|
@@ -13143,8 +13320,13 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
13143
13320
|
}).strict();
|
|
13144
13321
|
var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
13145
13322
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
13146
|
-
var GetGoalSchema2 = external_exports.object({
|
|
13147
|
-
|
|
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.";
|
|
13148
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.`;
|
|
13149
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.";
|
|
13150
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.";
|
|
@@ -13158,6 +13340,7 @@ var AGENT_TOOLS2 = [
|
|
|
13158
13340
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
13159
13341
|
];
|
|
13160
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.";
|
|
13161
13344
|
function entryWords(entry) {
|
|
13162
13345
|
const content = entry.content;
|
|
13163
13346
|
if (content && "sealed" in content) return "";
|
|
@@ -13980,7 +14163,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13980
14163
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
13981
14164
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
13982
14165
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
13983
|
-
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()
|
|
13984
14179
|
});
|
|
13985
14180
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
13986
14181
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -14156,6 +14351,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
14156
14351
|
"other"
|
|
14157
14352
|
// anything else — the note carries it.
|
|
14158
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
|
+
});
|
|
14159
14452
|
|
|
14160
14453
|
// src/paigy/activity.ts
|
|
14161
14454
|
function shortenPaths(s) {
|
|
@@ -14165,8 +14458,8 @@ function shortenPaths(s) {
|
|
|
14165
14458
|
});
|
|
14166
14459
|
}
|
|
14167
14460
|
function workLine(event) {
|
|
14168
|
-
const
|
|
14169
|
-
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, " "));
|
|
14170
14463
|
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
14171
14464
|
}
|
|
14172
14465
|
function pushWork(lines2, line) {
|
|
@@ -14180,7 +14473,7 @@ function sameTail(a, b) {
|
|
|
14180
14473
|
// src/harness/session.ts
|
|
14181
14474
|
import { spawn } from "child_process";
|
|
14182
14475
|
import { existsSync as existsSync4 } from "fs";
|
|
14183
|
-
import { homedir as
|
|
14476
|
+
import { homedir as homedir8 } from "os";
|
|
14184
14477
|
import { resolve as resolve2, delimiter as delimiter2 } from "path";
|
|
14185
14478
|
|
|
14186
14479
|
// src/harness/acp.ts
|
|
@@ -14191,19 +14484,23 @@ function optionFor(options, decision) {
|
|
|
14191
14484
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
14192
14485
|
}
|
|
14193
14486
|
function createAcpDriver(opts) {
|
|
14194
|
-
return new AcpDriver(opts.cwd, opts.mode, opts.resume);
|
|
14487
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
|
|
14195
14488
|
}
|
|
14196
14489
|
var AcpDriver = class {
|
|
14197
14490
|
/** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
|
|
14198
|
-
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
14199
|
-
|
|
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) {
|
|
14200
14495
|
this.cwd = cwd;
|
|
14201
14496
|
this.mode = mode;
|
|
14202
14497
|
this.resume = resume;
|
|
14498
|
+
this.meta = meta;
|
|
14203
14499
|
}
|
|
14204
14500
|
cwd;
|
|
14205
14501
|
mode;
|
|
14206
14502
|
resume;
|
|
14503
|
+
meta;
|
|
14207
14504
|
nextId = 1;
|
|
14208
14505
|
initId;
|
|
14209
14506
|
sessionNewId;
|
|
@@ -14223,6 +14520,11 @@ var AcpDriver = class {
|
|
|
14223
14520
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
14224
14521
|
text = "";
|
|
14225
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;
|
|
14226
14528
|
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
14227
14529
|
open() {
|
|
14228
14530
|
this.initId = this.nextId++;
|
|
@@ -14254,6 +14556,29 @@ var AcpDriver = class {
|
|
|
14254
14556
|
optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
14255
14557
|
];
|
|
14256
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
|
+
}
|
|
14257
14582
|
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
14258
14583
|
close() {
|
|
14259
14584
|
const writes = [...this.pending.values()].map(
|
|
@@ -14285,10 +14610,7 @@ var AcpDriver = class {
|
|
|
14285
14610
|
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
|
|
14286
14611
|
}
|
|
14287
14612
|
this.sessionNewId = this.nextId++;
|
|
14288
|
-
return {
|
|
14289
|
-
events: [],
|
|
14290
|
-
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
|
|
14291
|
-
};
|
|
14613
|
+
return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
|
|
14292
14614
|
}
|
|
14293
14615
|
if (msg.id === this.sessionNewId) {
|
|
14294
14616
|
this.sessionNewId = void 0;
|
|
@@ -14298,7 +14620,7 @@ var AcpDriver = class {
|
|
|
14298
14620
|
this.sessionNewId = this.nextId++;
|
|
14299
14621
|
return {
|
|
14300
14622
|
events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
|
|
14301
|
-
writes: [frame2(this.
|
|
14623
|
+
writes: [frame2(this.opening(this.sessionNewId))]
|
|
14302
14624
|
};
|
|
14303
14625
|
}
|
|
14304
14626
|
const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
|
|
@@ -14323,6 +14645,8 @@ var AcpDriver = class {
|
|
|
14323
14645
|
}
|
|
14324
14646
|
if (msg.id === this.promptId) {
|
|
14325
14647
|
this.promptId = void 0;
|
|
14648
|
+
this.running.clear();
|
|
14649
|
+
this.cancelled = false;
|
|
14326
14650
|
const events = [];
|
|
14327
14651
|
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
14328
14652
|
if (report || this.tools.length) {
|
|
@@ -14354,6 +14678,10 @@ var AcpDriver = class {
|
|
|
14354
14678
|
handleNotification(msg) {
|
|
14355
14679
|
if (msg.method !== "session/update") return none;
|
|
14356
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
|
+
}
|
|
14357
14685
|
switch (update?.sessionUpdate) {
|
|
14358
14686
|
case "agent_message_chunk":
|
|
14359
14687
|
this.text += update.content?.text ?? "";
|
|
@@ -14361,9 +14689,9 @@ var AcpDriver = class {
|
|
|
14361
14689
|
case "tool_call": {
|
|
14362
14690
|
const title = update.title?.trim() || update.kind || "tool";
|
|
14363
14691
|
this.tools.push(title);
|
|
14364
|
-
const
|
|
14692
|
+
const note2 = this.text.slice(this.reportFrom).trim();
|
|
14365
14693
|
this.reportFrom = this.text.length;
|
|
14366
|
-
return { events: [{ kind: "work", tool: title, ...
|
|
14694
|
+
return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
|
|
14367
14695
|
}
|
|
14368
14696
|
default:
|
|
14369
14697
|
return none;
|
|
@@ -14397,8 +14725,11 @@ var AcpDriver = class {
|
|
|
14397
14725
|
writes: []
|
|
14398
14726
|
};
|
|
14399
14727
|
}
|
|
14400
|
-
|
|
14401
|
-
|
|
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 };
|
|
14402
14733
|
}
|
|
14403
14734
|
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
14404
14735
|
flush() {
|
|
@@ -14417,12 +14748,92 @@ var AcpDriver = class {
|
|
|
14417
14748
|
};
|
|
14418
14749
|
var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
14419
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
|
+
|
|
14420
14818
|
// src/harness/session.ts
|
|
14421
14819
|
var ADAPTER_BIN = {
|
|
14422
14820
|
claude: "claude-agent-acp",
|
|
14423
14821
|
codex: "codex-acp",
|
|
14424
14822
|
agy: "paigy-agy-acp"
|
|
14425
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;
|
|
14426
14837
|
function splitLines(buffer, chunk) {
|
|
14427
14838
|
const combined = buffer + chunk;
|
|
14428
14839
|
const parts = combined.split("\n");
|
|
@@ -14430,7 +14841,7 @@ function splitLines(buffer, chunk) {
|
|
|
14430
14841
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
14431
14842
|
}
|
|
14432
14843
|
function startSession(opts) {
|
|
14433
|
-
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/,
|
|
14844
|
+
const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
|
|
14434
14845
|
if (!existsSync4(cwd)) {
|
|
14435
14846
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
14436
14847
|
}
|
|
@@ -14450,7 +14861,9 @@ function startSession(opts) {
|
|
|
14450
14861
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
14451
14862
|
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
14452
14863
|
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
14453
|
-
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 } : {}
|
|
14454
14867
|
},
|
|
14455
14868
|
stdio: ["pipe", "pipe", "pipe"]
|
|
14456
14869
|
});
|
|
@@ -14461,6 +14874,16 @@ function startSession(opts) {
|
|
|
14461
14874
|
if (ended2) return;
|
|
14462
14875
|
for (const f of frames) child.stdin?.write(`${f}
|
|
14463
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);
|
|
14464
14887
|
};
|
|
14465
14888
|
child.stderr?.on("data", (chunk) => {
|
|
14466
14889
|
const text = String(chunk).trim();
|
|
@@ -14468,6 +14891,7 @@ function startSession(opts) {
|
|
|
14468
14891
|
});
|
|
14469
14892
|
child.on("exit", (code) => {
|
|
14470
14893
|
ended2 = true;
|
|
14894
|
+
clearTimeout(hush);
|
|
14471
14895
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
14472
14896
|
opts.onExit?.();
|
|
14473
14897
|
});
|
|
@@ -14475,16 +14899,18 @@ function startSession(opts) {
|
|
|
14475
14899
|
opts.onEvent({ kind: "error", message: e.message });
|
|
14476
14900
|
opts.onExit?.();
|
|
14477
14901
|
});
|
|
14478
|
-
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
|
+
});
|
|
14479
14908
|
let buffer = "";
|
|
14480
14909
|
child.stdout?.on("data", (chunk) => {
|
|
14910
|
+
listen();
|
|
14481
14911
|
const { lines: lines2, rest } = splitLines(buffer, String(chunk));
|
|
14482
14912
|
buffer = rest;
|
|
14483
|
-
for (const line of lines2)
|
|
14484
|
-
const { events, writes } = driver.handleLine(line);
|
|
14485
|
-
write(writes);
|
|
14486
|
-
for (const event of events) opts.onEvent(event);
|
|
14487
|
-
}
|
|
14913
|
+
for (const line of lines2) apply(driver.handleLine(line));
|
|
14488
14914
|
});
|
|
14489
14915
|
write(driver.open());
|
|
14490
14916
|
return {
|
|
@@ -14497,132 +14923,137 @@ function startSession(opts) {
|
|
|
14497
14923
|
stop() {
|
|
14498
14924
|
write(driver.close());
|
|
14499
14925
|
ended2 = true;
|
|
14926
|
+
clearTimeout(hush);
|
|
14500
14927
|
child.kill();
|
|
14501
14928
|
}
|
|
14502
14929
|
};
|
|
14503
14930
|
}
|
|
14504
14931
|
|
|
14505
|
-
// src/paigy/
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
/\b(how would you like|how should we|what would you like|what should we)\b/,
|
|
14531
|
-
/\b(please choose|choose one|select one|how do you want)\b/
|
|
14532
|
-
].some((re) => re.test(tail));
|
|
14533
|
-
}
|
|
14534
|
-
var POLL_MS = 5e3;
|
|
14535
|
-
var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
|
|
14536
|
-
var ended = (state) => state === "done" || state === "cancelled";
|
|
14537
|
-
function unheard(brief, state) {
|
|
14538
|
-
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
14539
|
-
const words2 = [];
|
|
14540
|
-
for (const e of brief.entries ?? []) {
|
|
14541
|
-
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
14542
|
-
seen.add(e.entryId);
|
|
14543
|
-
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
14544
|
-
const text = entryWords(e).trim();
|
|
14545
|
-
if (text) words2.push(text);
|
|
14546
|
-
}
|
|
14547
|
-
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) };
|
|
14548
14957
|
}
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
|
|
14575
|
-
if (checkCallOnDone(fullText)) state.callOnDone = true;
|
|
14576
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
14577
|
-
return;
|
|
14578
|
-
}
|
|
14579
|
-
const brief = await rail.read(state.goal.id);
|
|
14580
|
-
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
14581
|
-
for (const answer of brief.answers ?? []) {
|
|
14582
|
-
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
14583
|
-
if (!resolve3) continue;
|
|
14584
|
-
state.asks.delete(answer.requestEntryId);
|
|
14585
|
-
resolve3(answer.result);
|
|
14586
|
-
}
|
|
14587
|
-
if (ended(brief.state)) {
|
|
14588
|
-
state.goal = void 0;
|
|
14589
|
-
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
14590
|
-
state.asks?.clear();
|
|
14591
|
-
return;
|
|
14592
|
-
}
|
|
14593
|
-
if (state.asks?.size || !state.resting) return;
|
|
14594
|
-
const words2 = unheard(brief, state);
|
|
14595
|
-
if (words2.length) {
|
|
14596
|
-
const text = words2.join("\n\n");
|
|
14597
|
-
if (checkCallOnDone(text)) state.callOnDone = true;
|
|
14598
|
-
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;
|
|
14599
14983
|
}
|
|
14600
14984
|
}
|
|
14601
|
-
function
|
|
14602
|
-
|
|
14603
|
-
|
|
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) };
|
|
14604
14990
|
}
|
|
14605
|
-
|
|
14606
|
-
const
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
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);
|
|
14611
15042
|
}
|
|
14612
|
-
|
|
14613
|
-
await (opts.pause ?? wait)();
|
|
14614
|
-
}
|
|
15043
|
+
};
|
|
14615
15044
|
}
|
|
15045
|
+
var LIMITS = episodes();
|
|
14616
15046
|
|
|
14617
15047
|
// src/paigy/bridge.ts
|
|
14618
15048
|
function railFor(opts) {
|
|
14619
15049
|
return {
|
|
14620
|
-
claim: () => claimGoal(
|
|
15050
|
+
claim: (goalId) => claimGoal(goalId, opts),
|
|
14621
15051
|
read: (goalId) => getGoal(goalId, opts),
|
|
14622
15052
|
update: (goalId, input) => updateGoal(goalId, input, opts),
|
|
14623
15053
|
// One read and back: the pump owns the waiting, so a Call must not hold a window here.
|
|
14624
15054
|
contact: (input) => contact(input, { ...opts, waits: false }),
|
|
14625
|
-
create: (input) => createGoal(input, opts)
|
|
15055
|
+
create: (input) => createGoal(input, opts),
|
|
15056
|
+
waiting: () => checkReplies(opts)
|
|
14626
15057
|
};
|
|
14627
15058
|
}
|
|
14628
15059
|
function cancelAsks(state) {
|
|
@@ -14641,27 +15072,13 @@ function decisionFrom(result) {
|
|
|
14641
15072
|
}
|
|
14642
15073
|
return { allow: false, reason: "No approval given" };
|
|
14643
15074
|
}
|
|
14644
|
-
function wordsOf(result) {
|
|
14645
|
-
if (!result || !("kind" in result)) return null;
|
|
14646
|
-
switch (result.kind) {
|
|
14647
|
-
case "text":
|
|
14648
|
-
return result.text.trim() || null;
|
|
14649
|
-
case "confirm":
|
|
14650
|
-
return result.approved ? "yes" : "no";
|
|
14651
|
-
case "one":
|
|
14652
|
-
return result.option.label;
|
|
14653
|
-
case "many":
|
|
14654
|
-
case "rank":
|
|
14655
|
-
return result.options.map((o) => o.label).join(", ");
|
|
14656
|
-
}
|
|
14657
|
-
}
|
|
14658
15075
|
async function ask(state, rail, input) {
|
|
14659
15076
|
if (!state.goal) return null;
|
|
14660
15077
|
try {
|
|
14661
15078
|
const sent = await rail.contact({
|
|
14662
|
-
asks: [{ ask: input.ask, parentId: state.goal.id,
|
|
15079
|
+
asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
|
|
14663
15080
|
waiting: "hard",
|
|
14664
|
-
channel:
|
|
15081
|
+
channel: "call"
|
|
14665
15082
|
});
|
|
14666
15083
|
if ("recorded" in sent) return null;
|
|
14667
15084
|
const entryId = sent.requestEntryIds[0];
|
|
@@ -14672,106 +15089,416 @@ async function ask(state, rail, input) {
|
|
|
14672
15089
|
return null;
|
|
14673
15090
|
}
|
|
14674
15091
|
}
|
|
14675
|
-
function checkCallOnDone(text) {
|
|
14676
|
-
if (!text) return false;
|
|
14677
|
-
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);
|
|
14678
|
-
}
|
|
14679
|
-
function parseQuestionOptions(text) {
|
|
14680
|
-
if (!text) return { question: "" };
|
|
14681
|
-
const trimmed = text.trim();
|
|
14682
|
-
const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
14683
|
-
const optionLines = [];
|
|
14684
|
-
const questionLines = [];
|
|
14685
|
-
for (const line of lines2) {
|
|
14686
|
-
const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
|
|
14687
|
-
if (bulletMatch) {
|
|
14688
|
-
optionLines.push(bulletMatch[1].trim());
|
|
14689
|
-
} else {
|
|
14690
|
-
questionLines.push(line);
|
|
14691
|
-
}
|
|
14692
|
-
}
|
|
14693
|
-
if (optionLines.length >= 2 && optionLines.length <= 6) {
|
|
14694
|
-
return {
|
|
14695
|
-
question: questionLines.join("\n\n").trim() || trimmed,
|
|
14696
|
-
options: optionLines.map((raw) => {
|
|
14697
|
-
const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
|
|
14698
|
-
const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
|
|
14699
|
-
if (colonSplit && colonSplit[1] && colonSplit[2]) {
|
|
14700
|
-
return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
|
|
14701
|
-
}
|
|
14702
|
-
if (parenSplit && parenSplit[1] && parenSplit[2]) {
|
|
14703
|
-
return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
|
|
14704
|
-
}
|
|
14705
|
-
return { label: raw.slice(0, 1e3) };
|
|
14706
|
-
})
|
|
14707
|
-
};
|
|
14708
|
-
}
|
|
14709
|
-
return { question: trimmed };
|
|
14710
|
-
}
|
|
14711
15092
|
async function askPermission(event, state, rail) {
|
|
14712
15093
|
return decisionFrom(await ask(state, rail, {
|
|
14713
15094
|
ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
|
|
14714
15095
|
options: [
|
|
14715
15096
|
{ label: ALLOW, hint: "Permits the agent to run this command" },
|
|
14716
15097
|
{ label: "Deny", hint: "Blocks execution of this command" }
|
|
14717
|
-
]
|
|
14718
|
-
call: true
|
|
14719
|
-
}));
|
|
14720
|
-
}
|
|
14721
|
-
async function askQuestion(question, state, rail, opts) {
|
|
14722
|
-
return wordsOf(await ask(state, rail, {
|
|
14723
|
-
ask: question,
|
|
14724
|
-
call: opts?.call ?? true,
|
|
14725
|
-
options: opts?.options
|
|
15098
|
+
]
|
|
14726
15099
|
}));
|
|
14727
15100
|
}
|
|
14728
15101
|
async function finish(result, state, rail, opts) {
|
|
14729
15102
|
const goal = state.goal;
|
|
14730
15103
|
if (!goal) return;
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
const channel = opts?.call || state.callOnDone ? "call" : "notification";
|
|
15104
|
+
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
15105
|
+
let revision = goal.revision;
|
|
14734
15106
|
try {
|
|
14735
|
-
await rail.
|
|
14736
|
-
|
|
14737
|
-
|
|
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
|
+
}
|
|
14738
15115
|
} catch (error) {
|
|
14739
|
-
opts?.log?.(`\u26A0
|
|
15116
|
+
opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
|
|
15117
|
+
}
|
|
15118
|
+
if (!progress) return;
|
|
15119
|
+
try {
|
|
15120
|
+
await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
|
|
15121
|
+
} catch (error) {
|
|
15122
|
+
opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
|
|
14740
15123
|
}
|
|
14741
15124
|
}
|
|
14742
|
-
async function
|
|
15125
|
+
async function cannotWork(report, state, rail, opts) {
|
|
15126
|
+
state.resting = true;
|
|
14743
15127
|
try {
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
${reason}`, parentId: goalId }],
|
|
14752
|
-
waiting: "none",
|
|
14753
|
-
channel: "notification"
|
|
14754
|
-
});
|
|
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" });
|
|
14755
15134
|
} catch (error) {
|
|
14756
|
-
opts?.log?.(`\u26A0 could not
|
|
15135
|
+
opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
|
|
14757
15136
|
}
|
|
14758
15137
|
}
|
|
14759
15138
|
async function afterTurn(event, state, rail, opts) {
|
|
14760
|
-
if (
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
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;
|
|
14764
15154
|
}
|
|
14765
|
-
|
|
15155
|
+
await finish(event.result, state, rail, opts);
|
|
15156
|
+
state.typed = void 0;
|
|
15157
|
+
state.resting = true;
|
|
15158
|
+
return void 0;
|
|
15159
|
+
}
|
|
14766
15160
|
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
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 }] : [];
|
|
14772
15239
|
});
|
|
14773
|
-
|
|
14774
|
-
|
|
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
|
+
}
|
|
14775
15502
|
}
|
|
14776
15503
|
|
|
14777
15504
|
// src/run.ts
|
|
@@ -14779,15 +15506,32 @@ function runHarness(opts) {
|
|
|
14779
15506
|
let running = true;
|
|
14780
15507
|
let tail = [];
|
|
14781
15508
|
const state = { resting: true };
|
|
15509
|
+
const clock4 = beat();
|
|
14782
15510
|
let session = null;
|
|
14783
15511
|
const speaksAs = opts.resume ?? opts.session;
|
|
14784
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
|
+
}
|
|
14785
15527
|
async function handle(event) {
|
|
14786
15528
|
if (event.kind === "error") {
|
|
14787
15529
|
opts.log(`\u26A0 ${event.message}`);
|
|
14788
15530
|
if (event.fatal && running) {
|
|
14789
15531
|
opts.log(`\u2716 could not start: ${event.message}`);
|
|
14790
|
-
await
|
|
15532
|
+
await cannotWork(`The session could not start, so nothing is working on this yet.
|
|
15533
|
+
|
|
15534
|
+
${event.message}`, state, rail, { log: opts.log });
|
|
14791
15535
|
stop2();
|
|
14792
15536
|
}
|
|
14793
15537
|
return;
|
|
@@ -14806,6 +15550,7 @@ function runHarness(opts) {
|
|
|
14806
15550
|
return;
|
|
14807
15551
|
}
|
|
14808
15552
|
if (event.kind === "work") {
|
|
15553
|
+
if (state.carriedOn) runningAgain();
|
|
14809
15554
|
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
14810
15555
|
tail = pushWork(tail, workLine(event));
|
|
14811
15556
|
return;
|
|
@@ -14813,20 +15558,10 @@ function runHarness(opts) {
|
|
|
14813
15558
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
14814
15559
|
if (event.kind === "idle") {
|
|
14815
15560
|
tail = [];
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
state.resting = false;
|
|
14821
|
-
session.send(local);
|
|
14822
|
-
return;
|
|
14823
|
-
}
|
|
14824
|
-
}
|
|
14825
|
-
const reply = await afterTurn(event, state, rail, { log: opts.log });
|
|
14826
|
-
if (reply && session && running) {
|
|
14827
|
-
opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
|
|
14828
|
-
session.send(reply);
|
|
14829
|
-
}
|
|
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");
|
|
14830
15565
|
}
|
|
14831
15566
|
}
|
|
14832
15567
|
session = startSession({
|
|
@@ -14837,19 +15572,25 @@ function runHarness(opts) {
|
|
|
14837
15572
|
...opts.resume ? { resume: opts.resume } : {},
|
|
14838
15573
|
...opts.session ? { session: opts.session } : {},
|
|
14839
15574
|
...opts.bin ? { bin: opts.bin } : {},
|
|
14840
|
-
...
|
|
15575
|
+
...inbox ? { inbox: inbox.dir } : {},
|
|
15576
|
+
onExit: () => {
|
|
15577
|
+
inbox?.close();
|
|
15578
|
+
opts.onExit?.();
|
|
15579
|
+
},
|
|
14841
15580
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
14842
15581
|
});
|
|
14843
|
-
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(
|
|
14844
15583
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
14845
15584
|
);
|
|
14846
15585
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
14847
15586
|
function stop2() {
|
|
14848
15587
|
running = false;
|
|
15588
|
+
clock4.nudge("wake");
|
|
14849
15589
|
tail = [];
|
|
14850
15590
|
cancelAsks(state);
|
|
14851
15591
|
session?.stop();
|
|
14852
15592
|
session = null;
|
|
15593
|
+
inbox?.close();
|
|
14853
15594
|
opts.log("\u25A0 stopped");
|
|
14854
15595
|
}
|
|
14855
15596
|
return {
|
|
@@ -14857,7 +15598,9 @@ function runHarness(opts) {
|
|
|
14857
15598
|
session?.send(text);
|
|
14858
15599
|
},
|
|
14859
15600
|
working: () => running && state.resting !== true,
|
|
14860
|
-
|
|
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"),
|
|
14861
15604
|
stop: stop2
|
|
14862
15605
|
};
|
|
14863
15606
|
}
|
|
@@ -14865,7 +15608,7 @@ function runHarness(opts) {
|
|
|
14865
15608
|
// src/update.ts
|
|
14866
15609
|
import { execFile } from "child_process";
|
|
14867
15610
|
import { createRequire } from "module";
|
|
14868
|
-
import { dirname as dirname3, join as
|
|
15611
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
14869
15612
|
import { promisify } from "util";
|
|
14870
15613
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
14871
15614
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -14892,7 +15635,7 @@ async function latestVersion() {
|
|
|
14892
15635
|
}
|
|
14893
15636
|
async function install(version) {
|
|
14894
15637
|
const bin = dirname3(process.execPath);
|
|
14895
|
-
await promisify(execFile)(
|
|
15638
|
+
await promisify(execFile)(join9(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
14896
15639
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
14897
15640
|
timeout: 5 * 6e4
|
|
14898
15641
|
});
|
|
@@ -14943,10 +15686,10 @@ function selfUpdater(deps) {
|
|
|
14943
15686
|
}
|
|
14944
15687
|
|
|
14945
15688
|
// src/host.ts
|
|
14946
|
-
var HOST_FILE =
|
|
15689
|
+
var HOST_FILE = join10(homedir9(), ".paigy", "host.json");
|
|
14947
15690
|
function readHostState() {
|
|
14948
15691
|
try {
|
|
14949
|
-
const s = JSON.parse(
|
|
15692
|
+
const s = JSON.parse(readFileSync5(HOST_FILE, "utf8"));
|
|
14950
15693
|
if (Date.now() - s.at > 2e4) return null;
|
|
14951
15694
|
try {
|
|
14952
15695
|
process.kill(s.pid, 0);
|
|
@@ -14968,18 +15711,36 @@ var claimedRun = (sessionId2) => {
|
|
|
14968
15711
|
return { slot, key: runKey(slot) };
|
|
14969
15712
|
};
|
|
14970
15713
|
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
14971
|
-
async function
|
|
14972
|
-
|
|
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);
|
|
14973
15728
|
}
|
|
14974
15729
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
14975
|
-
function wakeAction(live, now, serverSeenAt) {
|
|
15730
|
+
function wakeAction(live, now, serverSeenAt, here2 = {}) {
|
|
14976
15731
|
if (live) return { action: "skip", why: "run" };
|
|
14977
|
-
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
|
+
}
|
|
14978
15737
|
return { action: "spawn", why: "nobody" };
|
|
14979
15738
|
}
|
|
14980
15739
|
function startHost(opts) {
|
|
14981
15740
|
const runs = /* @__PURE__ */ new Map();
|
|
14982
15741
|
const asHost = { token: opts.token };
|
|
15742
|
+
const goneAt = /* @__PURE__ */ new Map();
|
|
15743
|
+
const heldHere = /* @__PURE__ */ new Set();
|
|
14983
15744
|
const refreshIdentities = async () => {
|
|
14984
15745
|
for (const slot of listSlots()) {
|
|
14985
15746
|
if (slot === "Desktop") continue;
|
|
@@ -14999,7 +15760,7 @@ function startHost(opts) {
|
|
|
14999
15760
|
working: () => [...runs.values()].filter((r) => r.run.working()).length,
|
|
15000
15761
|
log: opts.log
|
|
15001
15762
|
});
|
|
15002
|
-
const
|
|
15763
|
+
const beat2 = () => {
|
|
15003
15764
|
void refreshIdentities();
|
|
15004
15765
|
void update().catch(() => {
|
|
15005
15766
|
});
|
|
@@ -15028,9 +15789,11 @@ function startHost(opts) {
|
|
|
15028
15789
|
token: spec.token,
|
|
15029
15790
|
...launchRun(spec),
|
|
15030
15791
|
onExit: () => {
|
|
15031
|
-
runs
|
|
15792
|
+
evict(runs, key, run);
|
|
15793
|
+
goneAt.set(slot, Date.now());
|
|
15032
15794
|
opts.log(`\u2716 ${label} ended`);
|
|
15033
15795
|
},
|
|
15796
|
+
name: label,
|
|
15034
15797
|
log: log2
|
|
15035
15798
|
});
|
|
15036
15799
|
runs.set(key, { run, label, token: spec.token });
|
|
@@ -15057,7 +15820,13 @@ function startHost(opts) {
|
|
|
15057
15820
|
return presence;
|
|
15058
15821
|
}
|
|
15059
15822
|
let lastPresence = /* @__PURE__ */ new Map();
|
|
15060
|
-
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") {
|
|
15061
15830
|
{
|
|
15062
15831
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
15063
15832
|
const key = runKey(slot);
|
|
@@ -15065,19 +15834,42 @@ function startHost(opts) {
|
|
|
15065
15834
|
const live = runs.get(key);
|
|
15066
15835
|
const tokenId = slotIdentity(slot).tokenId;
|
|
15067
15836
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
15068
|
-
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) });
|
|
15069
15841
|
if (decided.action === "skip") {
|
|
15070
|
-
|
|
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`);
|
|
15071
15845
|
return;
|
|
15072
15846
|
}
|
|
15847
|
+
const label = slotName(slot) ?? slot;
|
|
15848
|
+
const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
|
|
15073
15849
|
const token = readToken(slot);
|
|
15074
|
-
if (!token)
|
|
15850
|
+
if (!token) {
|
|
15851
|
+
skip("has no token on this machine");
|
|
15852
|
+
return;
|
|
15853
|
+
}
|
|
15075
15854
|
const owner = ownerSession(slot);
|
|
15076
|
-
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
|
+
}
|
|
15077
15859
|
const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
15078
|
-
if (!workspace)
|
|
15079
|
-
|
|
15080
|
-
|
|
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
|
+
}
|
|
15081
15873
|
const log2 = (line) => opts.log(`[${label}] ${line}`);
|
|
15082
15874
|
const run = runHarness({
|
|
15083
15875
|
harness,
|
|
@@ -15086,17 +15878,20 @@ function startHost(opts) {
|
|
|
15086
15878
|
prompt: "",
|
|
15087
15879
|
token,
|
|
15088
15880
|
...owner ? { resume: owner.sessionId } : {},
|
|
15089
|
-
// 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.
|
|
15090
15883
|
onExit: () => {
|
|
15091
|
-
runs
|
|
15884
|
+
evict(runs, key, run);
|
|
15885
|
+
goneAt.set(slot, Date.now());
|
|
15092
15886
|
opts.log(`\u2716 ${label} ended`);
|
|
15093
15887
|
},
|
|
15888
|
+
name: label,
|
|
15094
15889
|
log: log2
|
|
15095
15890
|
});
|
|
15096
15891
|
runs.set(key, { run, label, token });
|
|
15097
15892
|
void heartbeat(void 0, { token }).catch(() => {
|
|
15098
15893
|
});
|
|
15099
|
-
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)" : ""}`);
|
|
15100
15895
|
}
|
|
15101
15896
|
}
|
|
15102
15897
|
const listening = /* @__PURE__ */ new Map();
|
|
@@ -15116,7 +15911,7 @@ function startHost(opts) {
|
|
|
15116
15911
|
joining.add(slot);
|
|
15117
15912
|
try {
|
|
15118
15913
|
const sub = await subscribeWake(
|
|
15119
|
-
() => void
|
|
15914
|
+
() => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
|
|
15120
15915
|
}),
|
|
15121
15916
|
{ token }
|
|
15122
15917
|
);
|
|
@@ -15130,8 +15925,8 @@ function startHost(opts) {
|
|
|
15130
15925
|
}
|
|
15131
15926
|
}
|
|
15132
15927
|
async function sweepSlots() {
|
|
15133
|
-
const
|
|
15134
|
-
for (const slot of listSlots()) await wakeSlot(slot,
|
|
15928
|
+
const seen = await evidence();
|
|
15929
|
+
for (const slot of listSlots()) await wakeSlot(slot, seen);
|
|
15135
15930
|
}
|
|
15136
15931
|
const ACTIVITY_MS = 2e3;
|
|
15137
15932
|
const published = /* @__PURE__ */ new Map();
|
|
@@ -15156,12 +15951,12 @@ function startHost(opts) {
|
|
|
15156
15951
|
};
|
|
15157
15952
|
const publish = () => {
|
|
15158
15953
|
try {
|
|
15159
|
-
|
|
15954
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
15160
15955
|
} catch {
|
|
15161
15956
|
}
|
|
15162
15957
|
};
|
|
15163
|
-
|
|
15164
|
-
const pulse = setInterval(
|
|
15958
|
+
beat2();
|
|
15959
|
+
const pulse = setInterval(beat2, 6e4);
|
|
15165
15960
|
const spawnPoll = setInterval(() => {
|
|
15166
15961
|
void claimAndSpawn();
|
|
15167
15962
|
publish();
|
|
@@ -15187,10 +15982,10 @@ function startHost(opts) {
|
|
|
15187
15982
|
// them live, and a run mid-turn makes it working.
|
|
15188
15983
|
roster: () => {
|
|
15189
15984
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
15190
|
-
const
|
|
15191
|
-
for (const key of listSlots()) if (key !== "Desktop")
|
|
15192
|
-
for (const label of live.keys()) if (!
|
|
15193
|
-
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]) => {
|
|
15194
15989
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
15195
15990
|
return {
|
|
15196
15991
|
name,
|
|
@@ -15207,7 +16002,7 @@ function startHost(opts) {
|
|
|
15207
16002
|
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
15208
16003
|
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
15209
16004
|
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
15210
|
-
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,
|
|
15211
16006
|
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
15212
16007
|
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
15213
16008
|
working: live.get(name)?.working() ?? false
|
|
@@ -15223,7 +16018,7 @@ function startHost(opts) {
|
|
|
15223
16018
|
streamActivity();
|
|
15224
16019
|
clearInterval(activityTick);
|
|
15225
16020
|
try {
|
|
15226
|
-
|
|
16021
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
15227
16022
|
} catch {
|
|
15228
16023
|
}
|
|
15229
16024
|
}
|
|
@@ -15238,8 +16033,8 @@ function startHost(opts) {
|
|
|
15238
16033
|
var OPEN_STATUSES = ["open", "assigned", "in_progress"];
|
|
15239
16034
|
var DAY_MS = 864e5;
|
|
15240
16035
|
var daysBetween = (from, now) => from ? Math.floor((now - Date.parse(from)) / DAY_MS) : null;
|
|
15241
|
-
function assigneeToken(
|
|
15242
|
-
const ref =
|
|
16036
|
+
function assigneeToken(note2) {
|
|
16037
|
+
const ref = note2.assignee ?? "";
|
|
15243
16038
|
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
15244
16039
|
}
|
|
15245
16040
|
var STOP = /* @__PURE__ */ new Set([
|
|
@@ -15313,23 +16108,23 @@ function overlap(a, b) {
|
|
|
15313
16108
|
return shared / (A.size + B.size - shared);
|
|
15314
16109
|
}
|
|
15315
16110
|
var SUPERSEDE_AT = 0.5;
|
|
15316
|
-
function supersededBy(
|
|
15317
|
-
const mine = `${
|
|
15318
|
-
const at = Date.parse(
|
|
16111
|
+
function supersededBy(note2, all) {
|
|
16112
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
16113
|
+
const at = Date.parse(note2.createdAt);
|
|
15319
16114
|
let best = null;
|
|
15320
16115
|
for (const other of all) {
|
|
15321
|
-
if (other.id ===
|
|
16116
|
+
if (other.id === note2.id) continue;
|
|
15322
16117
|
if (Date.parse(other.createdAt) <= at) continue;
|
|
15323
16118
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15324
16119
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15325
16120
|
}
|
|
15326
16121
|
return best?.note ?? null;
|
|
15327
16122
|
}
|
|
15328
|
-
function doneEcho(
|
|
15329
|
-
const mine = `${
|
|
16123
|
+
function doneEcho(note2, all) {
|
|
16124
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
15330
16125
|
let best = null;
|
|
15331
16126
|
for (const other of all) {
|
|
15332
|
-
if (other.id ===
|
|
16127
|
+
if (other.id === note2.id || other.status !== "done") continue;
|
|
15333
16128
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
15334
16129
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
15335
16130
|
}
|
|
@@ -15384,8 +16179,8 @@ function dossier(notes, connections, now) {
|
|
|
15384
16179
|
today: new Date(now).toISOString().slice(0, 10)
|
|
15385
16180
|
};
|
|
15386
16181
|
}
|
|
15387
|
-
function affinityWhy(
|
|
15388
|
-
const shared =
|
|
16182
|
+
function affinityWhy(note2, agent2) {
|
|
16183
|
+
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
15389
16184
|
if (shared.length && agent2.recentNotes)
|
|
15390
16185
|
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
15391
16186
|
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
@@ -15394,18 +16189,18 @@ function affinityWhy(note, agent2) {
|
|
|
15394
16189
|
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
15395
16190
|
return null;
|
|
15396
16191
|
}
|
|
15397
|
-
function staleWhy(
|
|
15398
|
-
if (
|
|
15399
|
-
if (
|
|
15400
|
-
if (
|
|
15401
|
-
if (
|
|
15402
|
-
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`;
|
|
15403
16198
|
return null;
|
|
15404
16199
|
}
|
|
15405
16200
|
|
|
15406
16201
|
// src/triage/judge.ts
|
|
15407
16202
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
15408
|
-
import { homedir as
|
|
16203
|
+
import { homedir as homedir10 } from "os";
|
|
15409
16204
|
import { delimiter as delimiter3 } from "path";
|
|
15410
16205
|
var PRIVACY = {
|
|
15411
16206
|
ollama: "never leaves this machine",
|
|
@@ -15509,7 +16304,7 @@ async function judgeFor(asked2, deps = {}) {
|
|
|
15509
16304
|
const out = spawnSync2(bin, runner.argv(null), {
|
|
15510
16305
|
input: prompt,
|
|
15511
16306
|
encoding: "utf8",
|
|
15512
|
-
cwd:
|
|
16307
|
+
cwd: homedir10(),
|
|
15513
16308
|
timeout: RUN_TIMEOUT_MS,
|
|
15514
16309
|
maxBuffer: 32 * 1024 * 1024,
|
|
15515
16310
|
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
@@ -15624,22 +16419,22 @@ function toProposal(verdicts, d, meta) {
|
|
|
15624
16419
|
const stale = [];
|
|
15625
16420
|
const assignments = /* @__PURE__ */ new Map();
|
|
15626
16421
|
for (const v of verdicts) {
|
|
15627
|
-
const
|
|
15628
|
-
if (!
|
|
15629
|
-
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 });
|
|
15630
16425
|
if (v.verdict === "close") {
|
|
15631
|
-
close.push(item(staleWhy(
|
|
16426
|
+
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
15632
16427
|
continue;
|
|
15633
16428
|
}
|
|
15634
16429
|
if (v.verdict === "stale") {
|
|
15635
|
-
stale.push(item(staleWhy(
|
|
16430
|
+
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
15636
16431
|
continue;
|
|
15637
16432
|
}
|
|
15638
16433
|
if (v.verdict !== "assign" || !v.agent) continue;
|
|
15639
16434
|
const agent2 = agentById.get(v.agent);
|
|
15640
16435
|
if (!agent2) continue;
|
|
15641
16436
|
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
15642
|
-
group.notes.push(item(affinityWhy(
|
|
16437
|
+
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
15643
16438
|
assignments.set(agent2.token, group);
|
|
15644
16439
|
}
|
|
15645
16440
|
return {
|
|
@@ -15807,7 +16602,7 @@ var win = null;
|
|
|
15807
16602
|
function log(message) {
|
|
15808
16603
|
win?.webContents.send("paigy:log", message);
|
|
15809
16604
|
}
|
|
15810
|
-
var ICON =
|
|
16605
|
+
var ICON = join11(here, "..", "assets", "icon.png");
|
|
15811
16606
|
function createWindow() {
|
|
15812
16607
|
win = new BrowserWindow({
|
|
15813
16608
|
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
@@ -15819,9 +16614,9 @@ function createWindow() {
|
|
|
15819
16614
|
minHeight: 520,
|
|
15820
16615
|
icon: ICON,
|
|
15821
16616
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
15822
|
-
webPreferences: { preload:
|
|
16617
|
+
webPreferences: { preload: join11(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
15823
16618
|
});
|
|
15824
|
-
void win.loadFile(
|
|
16619
|
+
void win.loadFile(join11(here, "..", "renderer", "index.html"));
|
|
15825
16620
|
}
|
|
15826
16621
|
var host = null;
|
|
15827
16622
|
app.whenReady().then(() => {
|
|
@@ -15890,7 +16685,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
15890
16685
|
});
|
|
15891
16686
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
15892
16687
|
try {
|
|
15893
|
-
const all =
|
|
16688
|
+
const all = readFileSync6(join11(homedir11(), ".paigy", "host.log"), "utf8").split("\n");
|
|
15894
16689
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
15895
16690
|
return mine.slice(-500).join("\n");
|
|
15896
16691
|
} catch {
|