@paigy/harness 0.3.16 → 0.3.28

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