@paigy/harness 0.3.17 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -14
- package/dist/cli.js +1162 -365
- package/dist/main.js +1118 -323
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16098,13 +16098,13 @@ var require_headers = __commonJS({
|
|
|
16098
16098
|
return this.#headersList[kHeadersSortedMap];
|
|
16099
16099
|
}
|
|
16100
16100
|
const headers = [];
|
|
16101
|
-
const
|
|
16101
|
+
const names2 = this.#headersList.toSortedArray();
|
|
16102
16102
|
const cookies = this.#headersList.cookies;
|
|
16103
16103
|
if (cookies === null || cookies.length === 1) {
|
|
16104
|
-
return this.#headersList[kHeadersSortedMap] =
|
|
16104
|
+
return this.#headersList[kHeadersSortedMap] = names2;
|
|
16105
16105
|
}
|
|
16106
|
-
for (let i = 0; i <
|
|
16107
|
-
const { 0: name, 1: value } =
|
|
16106
|
+
for (let i = 0; i < names2.length; ++i) {
|
|
16107
|
+
const { 0: name, 1: value } = names2[i];
|
|
16108
16108
|
if (name === "set-cookie") {
|
|
16109
16109
|
for (let j = 0; j < cookies.length; ++j) {
|
|
16110
16110
|
headers.push([name, cookies[j]]);
|
|
@@ -23809,9 +23809,9 @@ async function claimGoal(goalId, opts = {}) {
|
|
|
23809
23809
|
if (!res.ok) await fail("claim_goal", res);
|
|
23810
23810
|
return await res.json();
|
|
23811
23811
|
}
|
|
23812
|
-
async function getGoal(goalId, opts = {}) {
|
|
23812
|
+
async function getGoal(goalId, opts = {}, read = {}) {
|
|
23813
23813
|
const token = authToken(opts.token) ?? "";
|
|
23814
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
|
|
23814
|
+
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
|
|
23815
23815
|
method: "GET",
|
|
23816
23816
|
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
23817
23817
|
}));
|
|
@@ -24170,7 +24170,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24170
24170
|
}
|
|
24171
24171
|
};
|
|
24172
24172
|
}
|
|
24173
|
-
var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24173
|
+
var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24174
24174
|
var init_dist = __esm({
|
|
24175
24175
|
"../../packages/sdk/dist/index.js"() {
|
|
24176
24176
|
"use strict";
|
|
@@ -24639,8 +24639,13 @@ var init_dist = __esm({
|
|
|
24639
24639
|
}).strict();
|
|
24640
24640
|
UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
24641
24641
|
ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
24642
|
-
GetGoalSchema = external_exports.object({
|
|
24643
|
-
|
|
24642
|
+
GetGoalSchema = external_exports.object({
|
|
24643
|
+
goalId: external_exports.string().uuid(),
|
|
24644
|
+
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
24645
|
+
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
24646
|
+
history: external_exports.boolean().optional()
|
|
24647
|
+
}).strict();
|
|
24648
|
+
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.";
|
|
24644
24649
|
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.`;
|
|
24645
24650
|
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.";
|
|
24646
24651
|
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.";
|
|
@@ -25461,7 +25466,19 @@ var init_dist = __esm({
|
|
|
25461
25466
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
25462
25467
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
25463
25468
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
25464
|
-
lastProgressAt: external_exports.string().nullable().optional()
|
|
25469
|
+
lastProgressAt: external_exports.string().nullable().optional(),
|
|
25470
|
+
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
25471
|
+
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
25472
|
+
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
25473
|
+
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
25474
|
+
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
25475
|
+
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
25476
|
+
latest: external_exports.object({
|
|
25477
|
+
from: external_exports.enum(["person", "agent"]),
|
|
25478
|
+
said: external_exports.string(),
|
|
25479
|
+
at: external_exports.string(),
|
|
25480
|
+
entryId: external_exports.string()
|
|
25481
|
+
}).nullable().optional()
|
|
25465
25482
|
});
|
|
25466
25483
|
COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
25467
25484
|
NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -25639,6 +25656,104 @@ var init_dist = __esm({
|
|
|
25639
25656
|
"other"
|
|
25640
25657
|
// anything else — the note carries it.
|
|
25641
25658
|
]);
|
|
25659
|
+
SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
25660
|
+
QuestionRowSchema = external_exports.object({
|
|
25661
|
+
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
25662
|
+
id: external_exports.string(),
|
|
25663
|
+
deliveryId: external_exports.string(),
|
|
25664
|
+
entryId: external_exports.string(),
|
|
25665
|
+
/** The decision it waits on; null for an update, which asks nothing. */
|
|
25666
|
+
needId: external_exports.string().nullable(),
|
|
25667
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
25668
|
+
/** The name of the work it is about, when the read could word it. */
|
|
25669
|
+
goalTitle: external_exports.string().optional(),
|
|
25670
|
+
tokenId: external_exports.string().optional(),
|
|
25671
|
+
name: external_exports.string(),
|
|
25672
|
+
title: external_exports.string(),
|
|
25673
|
+
body: external_exports.string(),
|
|
25674
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
|
|
25675
|
+
options: external_exports.array(SlimOptionSchema),
|
|
25676
|
+
hasPreview: external_exports.boolean(),
|
|
25677
|
+
blocking: external_exports.boolean(),
|
|
25678
|
+
askedAt: external_exports.string().datetime(),
|
|
25679
|
+
ring: InboxItemSchema.shape.ring,
|
|
25680
|
+
onCall: external_exports.literal(true).optional(),
|
|
25681
|
+
sealed: external_exports.boolean()
|
|
25682
|
+
});
|
|
25683
|
+
WorkStateSchema = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
25684
|
+
WorkRowSchema = external_exports.object({
|
|
25685
|
+
id: external_exports.string(),
|
|
25686
|
+
parentId: external_exports.string().nullable(),
|
|
25687
|
+
title: external_exports.string(),
|
|
25688
|
+
/** Straight off the Goal. */
|
|
25689
|
+
state: WorkStateSchema,
|
|
25690
|
+
owner: external_exports.string().nullable(),
|
|
25691
|
+
revision: external_exports.number().int(),
|
|
25692
|
+
/** Open questions on it, counted to 100. */
|
|
25693
|
+
waiting: external_exports.number().int(),
|
|
25694
|
+
/** Held by a gate on work that is not done. */
|
|
25695
|
+
blocked: external_exports.boolean(),
|
|
25696
|
+
lastProgressAt: external_exports.string().datetime().nullable(),
|
|
25697
|
+
/** The line written for its newest progress note, else that note's first words. */
|
|
25698
|
+
line: external_exports.string().nullable(),
|
|
25699
|
+
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
25700
|
+
children: external_exports.number().int(),
|
|
25701
|
+
createdAt: external_exports.string().datetime(),
|
|
25702
|
+
updatedAt: external_exports.string().datetime(),
|
|
25703
|
+
/** When anything at or under it last moved — the order the list is in. */
|
|
25704
|
+
activeAt: external_exports.string().datetime(),
|
|
25705
|
+
/** A sealed outcome has no title here; the work's page opens it. */
|
|
25706
|
+
sealed: external_exports.boolean()
|
|
25707
|
+
});
|
|
25708
|
+
ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
25709
|
+
AgentRowSchema = ComputerRowSchema.extend({
|
|
25710
|
+
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
25711
|
+
asking: external_exports.number().int().nullable(),
|
|
25712
|
+
oldestAskAt: external_exports.string().datetime().nullable(),
|
|
25713
|
+
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
25714
|
+
* many in all among the account's 200 most recently active agent-held live Goals
|
|
25715
|
+
* (`agent_holds`); null when that read failed. */
|
|
25716
|
+
holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
|
|
25717
|
+
held: external_exports.number().int().nullable(),
|
|
25718
|
+
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
25719
|
+
* while none has, or when that read failed. */
|
|
25720
|
+
cold: external_exports.string().datetime().nullable(),
|
|
25721
|
+
/** The newest line of its working log, and when the harness saw it. */
|
|
25722
|
+
line: external_exports.string().nullable(),
|
|
25723
|
+
lineAt: external_exports.string().datetime().nullable()
|
|
25724
|
+
});
|
|
25725
|
+
SnapshotSchema = external_exports.object({
|
|
25726
|
+
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
25727
|
+
at: external_exports.string().datetime(),
|
|
25728
|
+
questions: external_exports.object({
|
|
25729
|
+
/** The newest 30 open cards, questions before updates. */
|
|
25730
|
+
items: external_exports.array(QuestionRowSchema),
|
|
25731
|
+
/** Every open question, and apart from them every update, and what was put off. */
|
|
25732
|
+
total: external_exports.number().int(),
|
|
25733
|
+
updates: external_exports.number().int(),
|
|
25734
|
+
putOff: external_exports.number().int()
|
|
25735
|
+
}).nullable(),
|
|
25736
|
+
agents: external_exports.object({
|
|
25737
|
+
/** Up to 60, most recently seen first. */
|
|
25738
|
+
items: external_exports.array(AgentRowSchema),
|
|
25739
|
+
more: external_exports.boolean()
|
|
25740
|
+
}).nullable(),
|
|
25741
|
+
work: external_exports.object({
|
|
25742
|
+
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
25743
|
+
items: external_exports.array(WorkRowSchema),
|
|
25744
|
+
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
25745
|
+
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
25746
|
+
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
25747
|
+
* is `ready`; Done is `done` or `cancelled`. */
|
|
25748
|
+
counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
|
|
25749
|
+
}).nullable(),
|
|
25750
|
+
you: external_exports.object({
|
|
25751
|
+
settings: UserSettingsSchema,
|
|
25752
|
+
callable: external_exports.boolean(),
|
|
25753
|
+
/** Up to 20 paired computers; null when the roster read failed. */
|
|
25754
|
+
computers: external_exports.array(ComputerRowSchema).nullable()
|
|
25755
|
+
}).nullable()
|
|
25756
|
+
});
|
|
25642
25757
|
TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
|
|
25643
25758
|
KEY_PATH = join22(homedir22(), ".paigy", "key.json");
|
|
25644
25759
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -30245,7 +30360,7 @@ var require_lib = __commonJS({
|
|
|
30245
30360
|
|
|
30246
30361
|
// src/triage/judge.ts
|
|
30247
30362
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
30248
|
-
import { homedir as
|
|
30363
|
+
import { homedir as homedir9 } from "os";
|
|
30249
30364
|
import { delimiter as delimiter3 } from "path";
|
|
30250
30365
|
function pickOllamaModel(tags) {
|
|
30251
30366
|
if (tags.length === 0) return null;
|
|
@@ -30328,7 +30443,7 @@ async function judgeFor(asked2, deps = {}) {
|
|
|
30328
30443
|
const out = spawnSync2(bin, runner.argv(null), {
|
|
30329
30444
|
input: prompt,
|
|
30330
30445
|
encoding: "utf8",
|
|
30331
|
-
cwd:
|
|
30446
|
+
cwd: homedir9(),
|
|
30332
30447
|
timeout: RUN_TIMEOUT_MS,
|
|
30333
30448
|
maxBuffer: 32 * 1024 * 1024,
|
|
30334
30449
|
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
@@ -30370,8 +30485,8 @@ var init_judge = __esm({
|
|
|
30370
30485
|
});
|
|
30371
30486
|
|
|
30372
30487
|
// src/triage/evidence.ts
|
|
30373
|
-
function assigneeToken(
|
|
30374
|
-
const ref =
|
|
30488
|
+
function assigneeToken(note2) {
|
|
30489
|
+
const ref = note2.assignee ?? "";
|
|
30375
30490
|
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
30376
30491
|
}
|
|
30377
30492
|
function words(text) {
|
|
@@ -30399,23 +30514,23 @@ function overlap(a, b) {
|
|
|
30399
30514
|
for (const w of A) if (B.has(w)) shared++;
|
|
30400
30515
|
return shared / (A.size + B.size - shared);
|
|
30401
30516
|
}
|
|
30402
|
-
function supersededBy(
|
|
30403
|
-
const mine = `${
|
|
30404
|
-
const at = Date.parse(
|
|
30517
|
+
function supersededBy(note2, all) {
|
|
30518
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
30519
|
+
const at = Date.parse(note2.createdAt);
|
|
30405
30520
|
let best = null;
|
|
30406
30521
|
for (const other of all) {
|
|
30407
|
-
if (other.id ===
|
|
30522
|
+
if (other.id === note2.id) continue;
|
|
30408
30523
|
if (Date.parse(other.createdAt) <= at) continue;
|
|
30409
30524
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
30410
30525
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
30411
30526
|
}
|
|
30412
30527
|
return best?.note ?? null;
|
|
30413
30528
|
}
|
|
30414
|
-
function doneEcho(
|
|
30415
|
-
const mine = `${
|
|
30529
|
+
function doneEcho(note2, all) {
|
|
30530
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
30416
30531
|
let best = null;
|
|
30417
30532
|
for (const other of all) {
|
|
30418
|
-
if (other.id ===
|
|
30533
|
+
if (other.id === note2.id || other.status !== "done") continue;
|
|
30419
30534
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
30420
30535
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
30421
30536
|
}
|
|
@@ -30469,8 +30584,8 @@ function dossier(notes, connections, now) {
|
|
|
30469
30584
|
today: new Date(now).toISOString().slice(0, 10)
|
|
30470
30585
|
};
|
|
30471
30586
|
}
|
|
30472
|
-
function affinityWhy(
|
|
30473
|
-
const shared =
|
|
30587
|
+
function affinityWhy(note2, agent2) {
|
|
30588
|
+
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
30474
30589
|
if (shared.length && agent2.recentNotes)
|
|
30475
30590
|
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
30476
30591
|
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
@@ -30479,12 +30594,12 @@ function affinityWhy(note, agent2) {
|
|
|
30479
30594
|
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
30480
30595
|
return null;
|
|
30481
30596
|
}
|
|
30482
|
-
function staleWhy(
|
|
30483
|
-
if (
|
|
30484
|
-
if (
|
|
30485
|
-
if (
|
|
30486
|
-
if (
|
|
30487
|
-
if (
|
|
30597
|
+
function staleWhy(note2) {
|
|
30598
|
+
if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
|
|
30599
|
+
if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
|
|
30600
|
+
if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
|
|
30601
|
+
if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
|
|
30602
|
+
if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
|
|
30488
30603
|
return null;
|
|
30489
30604
|
}
|
|
30490
30605
|
var OPEN_STATUSES, DAY_MS, daysBetween, STOP, SUPERSEDE_AT, RECENT_DAYS;
|
|
@@ -30659,22 +30774,22 @@ function toProposal(verdicts, d, meta) {
|
|
|
30659
30774
|
const stale = [];
|
|
30660
30775
|
const assignments = /* @__PURE__ */ new Map();
|
|
30661
30776
|
for (const v of verdicts) {
|
|
30662
|
-
const
|
|
30663
|
-
if (!
|
|
30664
|
-
const item = (why) => ({ noteId:
|
|
30777
|
+
const note2 = byId.get(v.id);
|
|
30778
|
+
if (!note2) continue;
|
|
30779
|
+
const item = (why) => ({ noteId: note2.id, title: note2.title, why });
|
|
30665
30780
|
if (v.verdict === "close") {
|
|
30666
|
-
close.push(item(staleWhy(
|
|
30781
|
+
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
30667
30782
|
continue;
|
|
30668
30783
|
}
|
|
30669
30784
|
if (v.verdict === "stale") {
|
|
30670
|
-
stale.push(item(staleWhy(
|
|
30785
|
+
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
30671
30786
|
continue;
|
|
30672
30787
|
}
|
|
30673
30788
|
if (v.verdict !== "assign" || !v.agent) continue;
|
|
30674
30789
|
const agent2 = agentById.get(v.agent);
|
|
30675
30790
|
if (!agent2) continue;
|
|
30676
30791
|
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
30677
|
-
group.notes.push(item(affinityWhy(
|
|
30792
|
+
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
30678
30793
|
assignments.set(agent2.token, group);
|
|
30679
30794
|
}
|
|
30680
30795
|
return {
|
|
@@ -30897,6 +31012,15 @@ function parseArgs(argv) {
|
|
|
30897
31012
|
return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
|
|
30898
31013
|
return args;
|
|
30899
31014
|
}
|
|
31015
|
+
var MCP_WIRES = [
|
|
31016
|
+
{ cli: "claude", name: "Claude Code", wire: "claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
|
|
31017
|
+
// Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
|
|
31018
|
+
// a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
|
|
31019
|
+
// written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
|
|
31020
|
+
// all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
|
|
31021
|
+
{ cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
|
|
31022
|
+
{ cli: "agy", name: "Antigravity", wire: "agy mcp list | grep -q '^paigy ' || agy mcp add paigy -- npx -y @paigy/mcp@latest" }
|
|
31023
|
+
];
|
|
30900
31024
|
var ENABLE_TOOLS_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
30901
31025
|
function onEnter(input, onLine) {
|
|
30902
31026
|
if (!input.isTTY) return () => {
|
|
@@ -30916,10 +31040,10 @@ function onEnter(input, onLine) {
|
|
|
30916
31040
|
init_dist();
|
|
30917
31041
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
30918
31042
|
import { hostname } from "os";
|
|
30919
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
31043
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
30920
31044
|
import { execFile as execFile2, execSync } from "child_process";
|
|
30921
|
-
import { homedir as
|
|
30922
|
-
import { join as
|
|
31045
|
+
import { homedir as homedir10 } from "os";
|
|
31046
|
+
import { join as join10 } from "path";
|
|
30923
31047
|
|
|
30924
31048
|
// src/run.ts
|
|
30925
31049
|
init_dist();
|
|
@@ -30927,7 +31051,7 @@ init_dist();
|
|
|
30927
31051
|
// src/harness/session.ts
|
|
30928
31052
|
import { spawn } from "child_process";
|
|
30929
31053
|
import { existsSync as existsSync3 } from "fs";
|
|
30930
|
-
import { homedir as
|
|
31054
|
+
import { homedir as homedir4 } from "os";
|
|
30931
31055
|
import { resolve, delimiter as delimiter2 } from "path";
|
|
30932
31056
|
|
|
30933
31057
|
// src/harness/acp.ts
|
|
@@ -30938,19 +31062,23 @@ function optionFor(options, decision) {
|
|
|
30938
31062
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
30939
31063
|
}
|
|
30940
31064
|
function createAcpDriver(opts) {
|
|
30941
|
-
return new AcpDriver(opts.cwd, opts.mode, opts.resume);
|
|
31065
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
|
|
30942
31066
|
}
|
|
30943
31067
|
var AcpDriver = class {
|
|
30944
31068
|
/** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
|
|
30945
|
-
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
30946
|
-
|
|
31069
|
+
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
31070
|
+
* `meta`: the adapter's own `_meta` on the frame that opens the session — where a switch only
|
|
31071
|
+
* one adapter understands rides (`session.ts`, a Claude session's tools). */
|
|
31072
|
+
constructor(cwd, mode, resume, meta) {
|
|
30947
31073
|
this.cwd = cwd;
|
|
30948
31074
|
this.mode = mode;
|
|
30949
31075
|
this.resume = resume;
|
|
31076
|
+
this.meta = meta;
|
|
30950
31077
|
}
|
|
30951
31078
|
cwd;
|
|
30952
31079
|
mode;
|
|
30953
31080
|
resume;
|
|
31081
|
+
meta;
|
|
30954
31082
|
nextId = 1;
|
|
30955
31083
|
initId;
|
|
30956
31084
|
sessionNewId;
|
|
@@ -30970,6 +31098,11 @@ var AcpDriver = class {
|
|
|
30970
31098
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
30971
31099
|
text = "";
|
|
30972
31100
|
tools = [];
|
|
31101
|
+
/** Tool calls opened and not yet reported `completed` or `failed`, by `toolCallId` — whoever
|
|
31102
|
+
* started them. A running tool is the one long silence that is still a working turn (`quiet`). */
|
|
31103
|
+
running = /* @__PURE__ */ new Set();
|
|
31104
|
+
/** The prompt in flight was already cancelled for silence: once is the whole remedy. */
|
|
31105
|
+
cancelled = false;
|
|
30973
31106
|
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
30974
31107
|
open() {
|
|
30975
31108
|
this.initId = this.nextId++;
|
|
@@ -31001,6 +31134,29 @@ var AcpDriver = class {
|
|
|
31001
31134
|
optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
31002
31135
|
];
|
|
31003
31136
|
}
|
|
31137
|
+
/**
|
|
31138
|
+
* THE ANSWER THAT NEVER COMES (2026-09-26, session "mute"). A turn ends when the adapter answers
|
|
31139
|
+
* its prompt, and nothing else ends one. claude-agent-acp can fold a prompt into a turn the agent
|
|
31140
|
+
* started on its own and never answer it (agentclientprotocol/claude-agent-acp#1145): the prompt
|
|
31141
|
+
* stayed in flight, no `idle` came, the pump never saw the agent rest, and the session was deaf
|
|
31142
|
+
* for twelve hours with its heartbeat alive.
|
|
31143
|
+
*
|
|
31144
|
+
* The caller's clock says nothing has crossed the pipe for `ms`; this says whether that silence
|
|
31145
|
+
* is still a turn. A running tool is (a ten-minute build streams nothing), and so is a permission
|
|
31146
|
+
* waiting on the person. Anything else, with a prompt in flight, is a turn that is over without
|
|
31147
|
+
* its answer, and the remedy is the protocol's own settle: `session/cancel`, which the adapter
|
|
31148
|
+
* answers `stopReason: "cancelled"` through the ordinary response path above — a turn that
|
|
31149
|
+
* ended, not one that failed. Once per prompt, and said as a crumb: a silent recovery is
|
|
31150
|
+
* indistinguishable from a working turn.
|
|
31151
|
+
*/
|
|
31152
|
+
quiet(ms) {
|
|
31153
|
+
if (this.promptId === void 0 || this.cancelled || this.running.size || this.pending.size) return none;
|
|
31154
|
+
this.cancelled = true;
|
|
31155
|
+
return {
|
|
31156
|
+
events: [{ kind: "error", message: `turn settled: no reply after ${Math.round(ms / 1e3)}s quiet with no tool running \u2014 sent session/cancel` }],
|
|
31157
|
+
writes: [frame2({ method: "session/cancel", params: { sessionId: this.sessionId } })]
|
|
31158
|
+
};
|
|
31159
|
+
}
|
|
31004
31160
|
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
31005
31161
|
close() {
|
|
31006
31162
|
const writes = [...this.pending.values()].map(
|
|
@@ -31032,10 +31188,7 @@ var AcpDriver = class {
|
|
|
31032
31188
|
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
|
|
31033
31189
|
}
|
|
31034
31190
|
this.sessionNewId = this.nextId++;
|
|
31035
|
-
return {
|
|
31036
|
-
events: [],
|
|
31037
|
-
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
|
|
31038
|
-
};
|
|
31191
|
+
return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
|
|
31039
31192
|
}
|
|
31040
31193
|
if (msg.id === this.sessionNewId) {
|
|
31041
31194
|
this.sessionNewId = void 0;
|
|
@@ -31045,7 +31198,7 @@ var AcpDriver = class {
|
|
|
31045
31198
|
this.sessionNewId = this.nextId++;
|
|
31046
31199
|
return {
|
|
31047
31200
|
events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
|
|
31048
|
-
writes: [frame2(this.
|
|
31201
|
+
writes: [frame2(this.opening(this.sessionNewId))]
|
|
31049
31202
|
};
|
|
31050
31203
|
}
|
|
31051
31204
|
const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
|
|
@@ -31070,6 +31223,8 @@ var AcpDriver = class {
|
|
|
31070
31223
|
}
|
|
31071
31224
|
if (msg.id === this.promptId) {
|
|
31072
31225
|
this.promptId = void 0;
|
|
31226
|
+
this.running.clear();
|
|
31227
|
+
this.cancelled = false;
|
|
31073
31228
|
const events = [];
|
|
31074
31229
|
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
31075
31230
|
if (report || this.tools.length) {
|
|
@@ -31101,6 +31256,10 @@ var AcpDriver = class {
|
|
|
31101
31256
|
handleNotification(msg) {
|
|
31102
31257
|
if (msg.method !== "session/update") return none;
|
|
31103
31258
|
const update = msg.params?.update;
|
|
31259
|
+
if (update?.toolCallId) {
|
|
31260
|
+
if (update.status === "completed" || update.status === "failed") this.running.delete(update.toolCallId);
|
|
31261
|
+
else if (update.sessionUpdate === "tool_call") this.running.add(update.toolCallId);
|
|
31262
|
+
}
|
|
31104
31263
|
switch (update?.sessionUpdate) {
|
|
31105
31264
|
case "agent_message_chunk":
|
|
31106
31265
|
this.text += update.content?.text ?? "";
|
|
@@ -31108,9 +31267,9 @@ var AcpDriver = class {
|
|
|
31108
31267
|
case "tool_call": {
|
|
31109
31268
|
const title = update.title?.trim() || update.kind || "tool";
|
|
31110
31269
|
this.tools.push(title);
|
|
31111
|
-
const
|
|
31270
|
+
const note2 = this.text.slice(this.reportFrom).trim();
|
|
31112
31271
|
this.reportFrom = this.text.length;
|
|
31113
|
-
return { events: [{ kind: "work", tool: title, ...
|
|
31272
|
+
return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
|
|
31114
31273
|
}
|
|
31115
31274
|
default:
|
|
31116
31275
|
return none;
|
|
@@ -31144,8 +31303,11 @@ var AcpDriver = class {
|
|
|
31144
31303
|
writes: []
|
|
31145
31304
|
};
|
|
31146
31305
|
}
|
|
31147
|
-
|
|
31148
|
-
|
|
31306
|
+
/** The frame that opens the conversation: a resume of `resume` while there is one to try,
|
|
31307
|
+
* else a new session. */
|
|
31308
|
+
opening(id) {
|
|
31309
|
+
const params = { cwd: this.cwd, mcpServers: [], ...this.meta ? { _meta: this.meta } : {} };
|
|
31310
|
+
return this.resume ? { id, method: "session/resume", params: { sessionId: this.resume, ...params } } : { id, method: "session/new", params };
|
|
31149
31311
|
}
|
|
31150
31312
|
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
31151
31313
|
flush() {
|
|
@@ -31166,11 +31328,93 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
31166
31328
|
|
|
31167
31329
|
// src/harness/session.ts
|
|
31168
31330
|
init_catalog();
|
|
31331
|
+
|
|
31332
|
+
// src/paigy/inbox.ts
|
|
31333
|
+
import { mkdirSync as mkdirSync2, readdirSync, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
31334
|
+
import { join as join4 } from "path";
|
|
31335
|
+
import { homedir as homedir3 } from "os";
|
|
31336
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
31337
|
+
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"`;
|
|
31338
|
+
var missing = (error) => error.code === "ENOENT";
|
|
31339
|
+
function openInbox(dir) {
|
|
31340
|
+
mkdirSync2(dir, { recursive: true });
|
|
31341
|
+
let written = 0;
|
|
31342
|
+
const receipt = (name) => join4(dir, "taken", `${name}.note`);
|
|
31343
|
+
const taken = (name) => {
|
|
31344
|
+
try {
|
|
31345
|
+
return statSync2(receipt(name)).mtimeMs;
|
|
31346
|
+
} catch (error) {
|
|
31347
|
+
if (missing(error)) return null;
|
|
31348
|
+
throw error;
|
|
31349
|
+
}
|
|
31350
|
+
};
|
|
31351
|
+
return {
|
|
31352
|
+
dir,
|
|
31353
|
+
put(text) {
|
|
31354
|
+
const name = `${Date.now()}-${String(++written).padStart(6, "0")}`;
|
|
31355
|
+
const tmp = join4(dir, `${name}.tmp`);
|
|
31356
|
+
writeFileSync2(tmp, JSON.stringify(text).slice(1, -1));
|
|
31357
|
+
renameSync(tmp, join4(dir, `${name}.note`));
|
|
31358
|
+
return name;
|
|
31359
|
+
},
|
|
31360
|
+
taken,
|
|
31361
|
+
settle(name) {
|
|
31362
|
+
try {
|
|
31363
|
+
renameSync(join4(dir, `${name}.note`), join4(dir, `${name}.back`));
|
|
31364
|
+
rmSync2(join4(dir, `${name}.back`), { force: true });
|
|
31365
|
+
return "retracted";
|
|
31366
|
+
} catch (error) {
|
|
31367
|
+
if (!missing(error)) throw error;
|
|
31368
|
+
}
|
|
31369
|
+
const seen = taken(name) !== null;
|
|
31370
|
+
rmSync2(receipt(name), { force: true });
|
|
31371
|
+
return seen ? "taken" : "retracted";
|
|
31372
|
+
},
|
|
31373
|
+
close() {
|
|
31374
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
31375
|
+
}
|
|
31376
|
+
};
|
|
31377
|
+
}
|
|
31378
|
+
var INBOX_ROOT = join4(homedir3(), ".paigy", "inbox");
|
|
31379
|
+
var alive = (pid) => {
|
|
31380
|
+
try {
|
|
31381
|
+
process.kill(pid, 0);
|
|
31382
|
+
return true;
|
|
31383
|
+
} catch (error) {
|
|
31384
|
+
return error.code === "EPERM";
|
|
31385
|
+
}
|
|
31386
|
+
};
|
|
31387
|
+
function inboxFor(root = INBOX_ROOT) {
|
|
31388
|
+
try {
|
|
31389
|
+
for (const name of readdirSync(root)) {
|
|
31390
|
+
const pid = Number(name.split("-")[0]);
|
|
31391
|
+
if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join4(root, name), { recursive: true, force: true });
|
|
31392
|
+
}
|
|
31393
|
+
} catch (error) {
|
|
31394
|
+
if (!missing(error)) throw error;
|
|
31395
|
+
}
|
|
31396
|
+
return openInbox(join4(root, `${process.pid}-${randomUUID4().slice(0, 8)}`));
|
|
31397
|
+
}
|
|
31398
|
+
|
|
31399
|
+
// src/harness/session.ts
|
|
31169
31400
|
var ADAPTER_BIN = {
|
|
31170
31401
|
claude: "claude-agent-acp",
|
|
31171
31402
|
codex: "codex-acp",
|
|
31172
31403
|
agy: "paigy-agy-acp"
|
|
31173
31404
|
};
|
|
31405
|
+
var CLAUDE_ONE_WAY_IN = {
|
|
31406
|
+
env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
|
|
31407
|
+
disallowedTools: ["Monitor", "ScheduleWakeup", "CronCreate", "Workflow"]
|
|
31408
|
+
};
|
|
31409
|
+
var CLAUDE_META = {
|
|
31410
|
+
claudeCode: {
|
|
31411
|
+
options: {
|
|
31412
|
+
disallowedTools: CLAUDE_ONE_WAY_IN.disallowedTools,
|
|
31413
|
+
settings: { hooks: { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: MIDTURN_HOOK }] }] } }
|
|
31414
|
+
}
|
|
31415
|
+
}
|
|
31416
|
+
};
|
|
31417
|
+
var QUIET_MS = 15 * 6e4;
|
|
31174
31418
|
function splitLines(buffer, chunk) {
|
|
31175
31419
|
const combined = buffer + chunk;
|
|
31176
31420
|
const parts = combined.split("\n");
|
|
@@ -31178,7 +31422,7 @@ function splitLines(buffer, chunk) {
|
|
|
31178
31422
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
31179
31423
|
}
|
|
31180
31424
|
function startSession(opts) {
|
|
31181
|
-
const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/,
|
|
31425
|
+
const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
31182
31426
|
if (!existsSync3(cwd)) {
|
|
31183
31427
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
31184
31428
|
}
|
|
@@ -31198,7 +31442,9 @@ function startSession(opts) {
|
|
|
31198
31442
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
31199
31443
|
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
31200
31444
|
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
31201
|
-
PAIGY_HARNESS: "1"
|
|
31445
|
+
PAIGY_HARNESS: "1",
|
|
31446
|
+
...opts.harness === "claude" ? CLAUDE_ONE_WAY_IN.env : {},
|
|
31447
|
+
...opts.harness === "claude" && opts.inbox ? { PAIGY_INBOX: opts.inbox } : {}
|
|
31202
31448
|
},
|
|
31203
31449
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31204
31450
|
});
|
|
@@ -31209,6 +31455,16 @@ function startSession(opts) {
|
|
|
31209
31455
|
if (ended2) return;
|
|
31210
31456
|
for (const f of frames) child.stdin?.write(`${f}
|
|
31211
31457
|
`);
|
|
31458
|
+
if (frames.length) listen();
|
|
31459
|
+
};
|
|
31460
|
+
const apply = ({ events, writes }) => {
|
|
31461
|
+
write(writes);
|
|
31462
|
+
for (const event of events) opts.onEvent(event);
|
|
31463
|
+
};
|
|
31464
|
+
let hush;
|
|
31465
|
+
const listen = () => {
|
|
31466
|
+
clearTimeout(hush);
|
|
31467
|
+
if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
|
|
31212
31468
|
};
|
|
31213
31469
|
child.stderr?.on("data", (chunk) => {
|
|
31214
31470
|
const text = String(chunk).trim();
|
|
@@ -31216,6 +31472,7 @@ function startSession(opts) {
|
|
|
31216
31472
|
});
|
|
31217
31473
|
child.on("exit", (code) => {
|
|
31218
31474
|
ended2 = true;
|
|
31475
|
+
clearTimeout(hush);
|
|
31219
31476
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
31220
31477
|
opts.onExit?.();
|
|
31221
31478
|
});
|
|
@@ -31223,16 +31480,18 @@ function startSession(opts) {
|
|
|
31223
31480
|
opts.onEvent({ kind: "error", message: e.message });
|
|
31224
31481
|
opts.onExit?.();
|
|
31225
31482
|
});
|
|
31226
|
-
const driver = createAcpDriver({
|
|
31483
|
+
const driver = createAcpDriver({
|
|
31484
|
+
cwd,
|
|
31485
|
+
mode: opts.mode,
|
|
31486
|
+
...opts.resume ? { resume: opts.resume } : {},
|
|
31487
|
+
...opts.harness === "claude" ? { meta: CLAUDE_META } : {}
|
|
31488
|
+
});
|
|
31227
31489
|
let buffer = "";
|
|
31228
31490
|
child.stdout?.on("data", (chunk) => {
|
|
31491
|
+
listen();
|
|
31229
31492
|
const { lines: lines2, rest } = splitLines(buffer, String(chunk));
|
|
31230
31493
|
buffer = rest;
|
|
31231
|
-
for (const line of lines2)
|
|
31232
|
-
const { events, writes } = driver.handleLine(line);
|
|
31233
|
-
write(writes);
|
|
31234
|
-
for (const event of events) opts.onEvent(event);
|
|
31235
|
-
}
|
|
31494
|
+
for (const line of lines2) apply(driver.handleLine(line));
|
|
31236
31495
|
});
|
|
31237
31496
|
write(driver.open());
|
|
31238
31497
|
return {
|
|
@@ -31245,6 +31504,7 @@ function startSession(opts) {
|
|
|
31245
31504
|
stop() {
|
|
31246
31505
|
write(driver.close());
|
|
31247
31506
|
ended2 = true;
|
|
31507
|
+
clearTimeout(hush);
|
|
31248
31508
|
child.kill();
|
|
31249
31509
|
}
|
|
31250
31510
|
};
|
|
@@ -32657,8 +32917,13 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
32657
32917
|
}).strict();
|
|
32658
32918
|
var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
32659
32919
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
32660
|
-
var GetGoalSchema2 = external_exports.object({
|
|
32661
|
-
|
|
32920
|
+
var GetGoalSchema2 = external_exports.object({
|
|
32921
|
+
goalId: external_exports.string().uuid(),
|
|
32922
|
+
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
32923
|
+
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
32924
|
+
history: external_exports.boolean().optional()
|
|
32925
|
+
}).strict();
|
|
32926
|
+
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.";
|
|
32662
32927
|
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.`;
|
|
32663
32928
|
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.";
|
|
32664
32929
|
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.";
|
|
@@ -32672,6 +32937,7 @@ var AGENT_TOOLS2 = [
|
|
|
32672
32937
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
32673
32938
|
];
|
|
32674
32939
|
var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
|
|
32940
|
+
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.";
|
|
32675
32941
|
function entryWords(entry) {
|
|
32676
32942
|
const content = entry.content;
|
|
32677
32943
|
if (content && "sealed" in content) return "";
|
|
@@ -33494,7 +33760,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33494
33760
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
33495
33761
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
33496
33762
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
33497
|
-
lastProgressAt: external_exports.string().nullable().optional()
|
|
33763
|
+
lastProgressAt: external_exports.string().nullable().optional(),
|
|
33764
|
+
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
33765
|
+
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
33766
|
+
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
33767
|
+
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
33768
|
+
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
33769
|
+
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
33770
|
+
latest: external_exports.object({
|
|
33771
|
+
from: external_exports.enum(["person", "agent"]),
|
|
33772
|
+
said: external_exports.string(),
|
|
33773
|
+
at: external_exports.string(),
|
|
33774
|
+
entryId: external_exports.string()
|
|
33775
|
+
}).nullable().optional()
|
|
33498
33776
|
});
|
|
33499
33777
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
33500
33778
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -33670,6 +33948,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
33670
33948
|
"other"
|
|
33671
33949
|
// anything else — the note carries it.
|
|
33672
33950
|
]);
|
|
33951
|
+
var SlimOptionSchema2 = OptionSchema2.omit({ html: true });
|
|
33952
|
+
var QuestionRowSchema2 = external_exports.object({
|
|
33953
|
+
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
33954
|
+
id: external_exports.string(),
|
|
33955
|
+
deliveryId: external_exports.string(),
|
|
33956
|
+
entryId: external_exports.string(),
|
|
33957
|
+
/** The decision it waits on; null for an update, which asks nothing. */
|
|
33958
|
+
needId: external_exports.string().nullable(),
|
|
33959
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
33960
|
+
/** The name of the work it is about, when the read could word it. */
|
|
33961
|
+
goalTitle: external_exports.string().optional(),
|
|
33962
|
+
tokenId: external_exports.string().optional(),
|
|
33963
|
+
name: external_exports.string(),
|
|
33964
|
+
title: external_exports.string(),
|
|
33965
|
+
body: external_exports.string(),
|
|
33966
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
|
|
33967
|
+
options: external_exports.array(SlimOptionSchema2),
|
|
33968
|
+
hasPreview: external_exports.boolean(),
|
|
33969
|
+
blocking: external_exports.boolean(),
|
|
33970
|
+
askedAt: external_exports.string().datetime(),
|
|
33971
|
+
ring: InboxItemSchema2.shape.ring,
|
|
33972
|
+
onCall: external_exports.literal(true).optional(),
|
|
33973
|
+
sealed: external_exports.boolean()
|
|
33974
|
+
});
|
|
33975
|
+
var WorkStateSchema2 = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
33976
|
+
var WorkRowSchema2 = external_exports.object({
|
|
33977
|
+
id: external_exports.string(),
|
|
33978
|
+
parentId: external_exports.string().nullable(),
|
|
33979
|
+
title: external_exports.string(),
|
|
33980
|
+
/** Straight off the Goal. */
|
|
33981
|
+
state: WorkStateSchema2,
|
|
33982
|
+
owner: external_exports.string().nullable(),
|
|
33983
|
+
revision: external_exports.number().int(),
|
|
33984
|
+
/** Open questions on it, counted to 100. */
|
|
33985
|
+
waiting: external_exports.number().int(),
|
|
33986
|
+
/** Held by a gate on work that is not done. */
|
|
33987
|
+
blocked: external_exports.boolean(),
|
|
33988
|
+
lastProgressAt: external_exports.string().datetime().nullable(),
|
|
33989
|
+
/** The line written for its newest progress note, else that note's first words. */
|
|
33990
|
+
line: external_exports.string().nullable(),
|
|
33991
|
+
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
33992
|
+
children: external_exports.number().int(),
|
|
33993
|
+
createdAt: external_exports.string().datetime(),
|
|
33994
|
+
updatedAt: external_exports.string().datetime(),
|
|
33995
|
+
/** When anything at or under it last moved — the order the list is in. */
|
|
33996
|
+
activeAt: external_exports.string().datetime(),
|
|
33997
|
+
/** A sealed outcome has no title here; the work's page opens it. */
|
|
33998
|
+
sealed: external_exports.boolean()
|
|
33999
|
+
});
|
|
34000
|
+
var ComputerRowSchema2 = ConnectionSummarySchema2.omit({ activity: true });
|
|
34001
|
+
var AgentRowSchema2 = ComputerRowSchema2.extend({
|
|
34002
|
+
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
34003
|
+
asking: external_exports.number().int().nullable(),
|
|
34004
|
+
oldestAskAt: external_exports.string().datetime().nullable(),
|
|
34005
|
+
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
34006
|
+
* many in all among the account's 200 most recently active agent-held live Goals
|
|
34007
|
+
* (`agent_holds`); null when that read failed. */
|
|
34008
|
+
holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
|
|
34009
|
+
held: external_exports.number().int().nullable(),
|
|
34010
|
+
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
34011
|
+
* while none has, or when that read failed. */
|
|
34012
|
+
cold: external_exports.string().datetime().nullable(),
|
|
34013
|
+
/** The newest line of its working log, and when the harness saw it. */
|
|
34014
|
+
line: external_exports.string().nullable(),
|
|
34015
|
+
lineAt: external_exports.string().datetime().nullable()
|
|
34016
|
+
});
|
|
34017
|
+
var SnapshotSchema2 = external_exports.object({
|
|
34018
|
+
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
34019
|
+
at: external_exports.string().datetime(),
|
|
34020
|
+
questions: external_exports.object({
|
|
34021
|
+
/** The newest 30 open cards, questions before updates. */
|
|
34022
|
+
items: external_exports.array(QuestionRowSchema2),
|
|
34023
|
+
/** Every open question, and apart from them every update, and what was put off. */
|
|
34024
|
+
total: external_exports.number().int(),
|
|
34025
|
+
updates: external_exports.number().int(),
|
|
34026
|
+
putOff: external_exports.number().int()
|
|
34027
|
+
}).nullable(),
|
|
34028
|
+
agents: external_exports.object({
|
|
34029
|
+
/** Up to 60, most recently seen first. */
|
|
34030
|
+
items: external_exports.array(AgentRowSchema2),
|
|
34031
|
+
more: external_exports.boolean()
|
|
34032
|
+
}).nullable(),
|
|
34033
|
+
work: external_exports.object({
|
|
34034
|
+
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
34035
|
+
items: external_exports.array(WorkRowSchema2),
|
|
34036
|
+
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
34037
|
+
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
34038
|
+
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
34039
|
+
* is `ready`; Done is `done` or `cancelled`. */
|
|
34040
|
+
counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
|
|
34041
|
+
}).nullable(),
|
|
34042
|
+
you: external_exports.object({
|
|
34043
|
+
settings: UserSettingsSchema2,
|
|
34044
|
+
callable: external_exports.boolean(),
|
|
34045
|
+
/** Up to 20 paired computers; null when the roster read failed. */
|
|
34046
|
+
computers: external_exports.array(ComputerRowSchema2).nullable()
|
|
34047
|
+
}).nullable()
|
|
34048
|
+
});
|
|
33673
34049
|
|
|
33674
34050
|
// src/paigy/activity.ts
|
|
33675
34051
|
function shortenPaths(s) {
|
|
@@ -33679,8 +34055,8 @@ function shortenPaths(s) {
|
|
|
33679
34055
|
});
|
|
33680
34056
|
}
|
|
33681
34057
|
function workLine(event) {
|
|
33682
|
-
const
|
|
33683
|
-
const line = shortenPaths([event.tool.trim(),
|
|
34058
|
+
const note2 = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
34059
|
+
const line = shortenPaths([event.tool.trim(), note2.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
33684
34060
|
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
33685
34061
|
}
|
|
33686
34062
|
function pushWork(lines2, line) {
|
|
@@ -33694,127 +34070,131 @@ function sameTail(a, b) {
|
|
|
33694
34070
|
// src/paigy/bridge.ts
|
|
33695
34071
|
init_dist();
|
|
33696
34072
|
|
|
33697
|
-
// src/paigy/
|
|
33698
|
-
|
|
33699
|
-
|
|
33700
|
-
|
|
33701
|
-
|
|
33702
|
-
|
|
33703
|
-
|
|
33704
|
-
|
|
33705
|
-
|
|
33706
|
-
|
|
33707
|
-
|
|
33708
|
-
|
|
33709
|
-
|
|
33710
|
-
|
|
33711
|
-
|
|
33712
|
-
|
|
33713
|
-
|
|
33714
|
-
|
|
33715
|
-
|
|
33716
|
-
|
|
33717
|
-
|
|
33718
|
-
|
|
33719
|
-
|
|
33720
|
-
|
|
33721
|
-
|
|
33722
|
-
/\b(how would you like|how should we|what would you like|what should we)\b/,
|
|
33723
|
-
/\b(please choose|choose one|select one|how do you want)\b/
|
|
33724
|
-
].some((re) => re.test(tail));
|
|
34073
|
+
// src/paigy/limit.ts
|
|
34074
|
+
var LIMIT_PREFIXES = [
|
|
34075
|
+
"You've hit your",
|
|
34076
|
+
"You've reached your",
|
|
34077
|
+
"You're out of usage credits",
|
|
34078
|
+
"Your org is out of usage",
|
|
34079
|
+
"Your seat type doesn't include usage",
|
|
34080
|
+
"Your usage allocation has been disabled by your admin",
|
|
34081
|
+
"Your group's usage limit is set to $0",
|
|
34082
|
+
"requires usage credits",
|
|
34083
|
+
"You're out of extra usage",
|
|
34084
|
+
"Your seat type doesn't include extra usage"
|
|
34085
|
+
];
|
|
34086
|
+
var RETRY_MS2 = 15 * 6e4;
|
|
34087
|
+
var MARGIN_MS = 6e4;
|
|
34088
|
+
var FARTHEST_MS = 8 * 24 * 60 * 6e4;
|
|
34089
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
34090
|
+
function limitIn(text, now) {
|
|
34091
|
+
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)));
|
|
34092
|
+
if (!line) return null;
|
|
34093
|
+
const reset = resetOf(line, now);
|
|
34094
|
+
if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
|
|
34095
|
+
return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
|
|
34096
|
+
}
|
|
34097
|
+
return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
|
|
33725
34098
|
}
|
|
33726
|
-
|
|
33727
|
-
|
|
33728
|
-
|
|
33729
|
-
|
|
33730
|
-
|
|
33731
|
-
|
|
33732
|
-
|
|
33733
|
-
|
|
33734
|
-
|
|
33735
|
-
|
|
33736
|
-
|
|
33737
|
-
|
|
33738
|
-
|
|
33739
|
-
|
|
34099
|
+
function resetOf(line, now) {
|
|
34100
|
+
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);
|
|
34101
|
+
if (!m) return null;
|
|
34102
|
+
const zone = zoneOr(m[6]);
|
|
34103
|
+
let hour = Number(m[3]) % 12;
|
|
34104
|
+
if (m[5].toLowerCase() === "pm") hour += 12;
|
|
34105
|
+
const minute = Number(m[4] ?? 0);
|
|
34106
|
+
const today = partsAt(now, zone);
|
|
34107
|
+
if (m[1]) {
|
|
34108
|
+
const month = MONTHS.indexOf(m[1].toLowerCase());
|
|
34109
|
+
if (month < 0) return null;
|
|
34110
|
+
const at2 = wallToEpoch(today.year, month, Number(m[2]), hour, minute, zone);
|
|
34111
|
+
return at2 > now ? at2 : wallToEpoch(today.year + 1, month, Number(m[2]), hour, minute, zone);
|
|
34112
|
+
}
|
|
34113
|
+
const at = wallToEpoch(today.year, today.month, today.day, hour, minute, zone);
|
|
34114
|
+
return at > now ? at : wallToEpoch(today.year, today.month, today.day + 1, hour, minute, zone);
|
|
33740
34115
|
}
|
|
33741
|
-
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
33745
|
-
|
|
33746
|
-
|
|
33747
|
-
|
|
33748
|
-
|
|
33749
|
-
const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID4() });
|
|
33750
|
-
brief2 = await rail.claim();
|
|
33751
|
-
if (!brief2.goalId || brief2.revision === void 0) {
|
|
33752
|
-
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
33753
|
-
if (checkCallOnDone(prompt)) state.callOnDone = true;
|
|
33754
|
-
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
33755
|
-
return;
|
|
33756
|
-
}
|
|
33757
|
-
} else {
|
|
33758
|
-
return;
|
|
33759
|
-
}
|
|
33760
|
-
}
|
|
33761
|
-
unheard(brief2, state);
|
|
33762
|
-
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
33763
|
-
else state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
33764
|
-
const outcome = brief2.outcome?.trim() ?? "";
|
|
33765
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
|
|
33766
|
-
const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
|
|
33767
|
-
if (checkCallOnDone(fullText)) state.callOnDone = true;
|
|
33768
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
33769
|
-
return;
|
|
33770
|
-
}
|
|
33771
|
-
const brief = await rail.read(state.goal.id);
|
|
33772
|
-
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
33773
|
-
for (const answer of brief.answers ?? []) {
|
|
33774
|
-
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
33775
|
-
if (!resolve3) continue;
|
|
33776
|
-
state.asks.delete(answer.requestEntryId);
|
|
33777
|
-
resolve3(answer.result);
|
|
33778
|
-
}
|
|
33779
|
-
if (ended(brief.state)) {
|
|
33780
|
-
state.goal = void 0;
|
|
33781
|
-
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
33782
|
-
state.asks?.clear();
|
|
33783
|
-
return;
|
|
33784
|
-
}
|
|
33785
|
-
if (state.asks?.size || !state.resting) return;
|
|
33786
|
-
const words2 = unheard(brief, state);
|
|
33787
|
-
if (words2.length) {
|
|
33788
|
-
const text = words2.join("\n\n");
|
|
33789
|
-
if (checkCallOnDone(text)) state.callOnDone = true;
|
|
33790
|
-
send2(session, state, text);
|
|
34116
|
+
function zoneOr(zone) {
|
|
34117
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
34118
|
+
if (!zone) return local;
|
|
34119
|
+
try {
|
|
34120
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone.trim() });
|
|
34121
|
+
return zone.trim();
|
|
34122
|
+
} catch {
|
|
34123
|
+
return local;
|
|
33791
34124
|
}
|
|
33792
34125
|
}
|
|
33793
|
-
function
|
|
33794
|
-
|
|
33795
|
-
|
|
34126
|
+
function partsAt(t, zone) {
|
|
34127
|
+
const p = Object.fromEntries(
|
|
34128
|
+
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])
|
|
34129
|
+
);
|
|
34130
|
+
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) };
|
|
33796
34131
|
}
|
|
33797
|
-
|
|
33798
|
-
const
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
34132
|
+
function wallToEpoch(year, month, day, hour, minute, zone) {
|
|
34133
|
+
const wall = Date.UTC(year, month, day, hour, minute);
|
|
34134
|
+
const offset = (t) => {
|
|
34135
|
+
const p = partsAt(t, zone);
|
|
34136
|
+
return Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(t / 1e3) * 1e3;
|
|
34137
|
+
};
|
|
34138
|
+
const first = wall - offset(wall);
|
|
34139
|
+
return wall - offset(first);
|
|
34140
|
+
}
|
|
34141
|
+
function clock3(t) {
|
|
34142
|
+
return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
|
|
34143
|
+
}
|
|
34144
|
+
var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
|
|
34145
|
+
var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on where you stopped.";
|
|
34146
|
+
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:";
|
|
34147
|
+
var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
34148
|
+
var GATHER_MS = 9e4;
|
|
34149
|
+
function episodes(opts = {}) {
|
|
34150
|
+
const gather2 = opts.gatherMs ?? GATHER_MS;
|
|
34151
|
+
const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
|
|
34152
|
+
let open = null;
|
|
34153
|
+
return {
|
|
34154
|
+
paused(name, now) {
|
|
34155
|
+
if (!open || open.called) open = { since: now, paused: [], running: [], called: false, scheduled: false };
|
|
34156
|
+
if (!open.paused.includes(name)) open.paused.push(name);
|
|
34157
|
+
open.running = open.running.filter((n) => n !== name);
|
|
34158
|
+
},
|
|
34159
|
+
running(name, call, log) {
|
|
34160
|
+
const episode = open;
|
|
34161
|
+
if (!episode || !episode.paused.includes(name)) return;
|
|
34162
|
+
if (episode.called) {
|
|
34163
|
+
log?.(`\u25B6 ${name} is running again after the limit (the call already went out)`);
|
|
34164
|
+
return;
|
|
34165
|
+
}
|
|
34166
|
+
if (!episode.running.includes(name)) episode.running.push(name);
|
|
34167
|
+
if (episode.scheduled) return;
|
|
34168
|
+
episode.scheduled = true;
|
|
34169
|
+
later(() => {
|
|
34170
|
+
if (!episode.running.length) {
|
|
34171
|
+
episode.scheduled = false;
|
|
34172
|
+
return;
|
|
34173
|
+
}
|
|
34174
|
+
episode.called = true;
|
|
34175
|
+
const rest = episode.paused.filter((n) => !episode.running.includes(n));
|
|
34176
|
+
const text = [
|
|
34177
|
+
`Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
|
|
34178
|
+
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.`
|
|
34179
|
+
].join(" ");
|
|
34180
|
+
log?.(`\u260E one call for ${episode.paused.length} paused agent${episode.paused.length === 1 ? "" : "s"}: ${names(episode.paused)}`);
|
|
34181
|
+
void call(text).catch((error) => log?.(`\u26A0 could not place the limit call: ${error.message}`));
|
|
34182
|
+
}, gather2);
|
|
33803
34183
|
}
|
|
33804
|
-
|
|
33805
|
-
await (opts.pause ?? wait)();
|
|
33806
|
-
}
|
|
34184
|
+
};
|
|
33807
34185
|
}
|
|
34186
|
+
var LIMITS = episodes();
|
|
33808
34187
|
|
|
33809
34188
|
// src/paigy/bridge.ts
|
|
33810
34189
|
function railFor(opts) {
|
|
33811
34190
|
return {
|
|
33812
|
-
claim: () => claimGoal(
|
|
34191
|
+
claim: (goalId) => claimGoal(goalId, opts),
|
|
33813
34192
|
read: (goalId) => getGoal(goalId, opts),
|
|
33814
34193
|
update: (goalId, input) => updateGoal(goalId, input, opts),
|
|
33815
34194
|
// One read and back: the pump owns the waiting, so a Call must not hold a window here.
|
|
33816
34195
|
contact: (input) => contact(input, { ...opts, waits: false }),
|
|
33817
|
-
create: (input) => createGoal(input, opts)
|
|
34196
|
+
create: (input) => createGoal(input, opts),
|
|
34197
|
+
waiting: () => checkReplies(opts)
|
|
33818
34198
|
};
|
|
33819
34199
|
}
|
|
33820
34200
|
function cancelAsks(state) {
|
|
@@ -33833,27 +34213,13 @@ function decisionFrom(result) {
|
|
|
33833
34213
|
}
|
|
33834
34214
|
return { allow: false, reason: "No approval given" };
|
|
33835
34215
|
}
|
|
33836
|
-
function wordsOf(result) {
|
|
33837
|
-
if (!result || !("kind" in result)) return null;
|
|
33838
|
-
switch (result.kind) {
|
|
33839
|
-
case "text":
|
|
33840
|
-
return result.text.trim() || null;
|
|
33841
|
-
case "confirm":
|
|
33842
|
-
return result.approved ? "yes" : "no";
|
|
33843
|
-
case "one":
|
|
33844
|
-
return result.option.label;
|
|
33845
|
-
case "many":
|
|
33846
|
-
case "rank":
|
|
33847
|
-
return result.options.map((o) => o.label).join(", ");
|
|
33848
|
-
}
|
|
33849
|
-
}
|
|
33850
34216
|
async function ask(state, rail, input) {
|
|
33851
34217
|
if (!state.goal) return null;
|
|
33852
34218
|
try {
|
|
33853
34219
|
const sent = await rail.contact({
|
|
33854
|
-
asks: [{ ask: input.ask, parentId: state.goal.id,
|
|
34220
|
+
asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
|
|
33855
34221
|
waiting: "hard",
|
|
33856
|
-
channel:
|
|
34222
|
+
channel: "call"
|
|
33857
34223
|
});
|
|
33858
34224
|
if ("recorded" in sent) return null;
|
|
33859
34225
|
const entryId = sent.requestEntryIds[0];
|
|
@@ -33864,106 +34230,416 @@ async function ask(state, rail, input) {
|
|
|
33864
34230
|
return null;
|
|
33865
34231
|
}
|
|
33866
34232
|
}
|
|
33867
|
-
function checkCallOnDone(text) {
|
|
33868
|
-
if (!text) return false;
|
|
33869
|
-
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);
|
|
33870
|
-
}
|
|
33871
|
-
function parseQuestionOptions(text) {
|
|
33872
|
-
if (!text) return { question: "" };
|
|
33873
|
-
const trimmed = text.trim();
|
|
33874
|
-
const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
33875
|
-
const optionLines = [];
|
|
33876
|
-
const questionLines = [];
|
|
33877
|
-
for (const line of lines2) {
|
|
33878
|
-
const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
|
|
33879
|
-
if (bulletMatch) {
|
|
33880
|
-
optionLines.push(bulletMatch[1].trim());
|
|
33881
|
-
} else {
|
|
33882
|
-
questionLines.push(line);
|
|
33883
|
-
}
|
|
33884
|
-
}
|
|
33885
|
-
if (optionLines.length >= 2 && optionLines.length <= 6) {
|
|
33886
|
-
return {
|
|
33887
|
-
question: questionLines.join("\n\n").trim() || trimmed,
|
|
33888
|
-
options: optionLines.map((raw) => {
|
|
33889
|
-
const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
|
|
33890
|
-
const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
|
|
33891
|
-
if (colonSplit && colonSplit[1] && colonSplit[2]) {
|
|
33892
|
-
return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
|
|
33893
|
-
}
|
|
33894
|
-
if (parenSplit && parenSplit[1] && parenSplit[2]) {
|
|
33895
|
-
return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
|
|
33896
|
-
}
|
|
33897
|
-
return { label: raw.slice(0, 1e3) };
|
|
33898
|
-
})
|
|
33899
|
-
};
|
|
33900
|
-
}
|
|
33901
|
-
return { question: trimmed };
|
|
33902
|
-
}
|
|
33903
34233
|
async function askPermission(event, state, rail) {
|
|
33904
34234
|
return decisionFrom(await ask(state, rail, {
|
|
33905
34235
|
ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
|
|
33906
34236
|
options: [
|
|
33907
34237
|
{ label: ALLOW, hint: "Permits the agent to run this command" },
|
|
33908
34238
|
{ label: "Deny", hint: "Blocks execution of this command" }
|
|
33909
|
-
]
|
|
33910
|
-
call: true
|
|
33911
|
-
}));
|
|
33912
|
-
}
|
|
33913
|
-
async function askQuestion(question, state, rail, opts) {
|
|
33914
|
-
return wordsOf(await ask(state, rail, {
|
|
33915
|
-
ask: question,
|
|
33916
|
-
call: opts?.call ?? true,
|
|
33917
|
-
options: opts?.options
|
|
34239
|
+
]
|
|
33918
34240
|
}));
|
|
33919
34241
|
}
|
|
33920
34242
|
async function finish(result, state, rail, opts) {
|
|
33921
34243
|
const goal = state.goal;
|
|
33922
34244
|
if (!goal) return;
|
|
33923
|
-
|
|
33924
|
-
|
|
33925
|
-
|
|
34245
|
+
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
34246
|
+
let revision = goal.revision;
|
|
34247
|
+
try {
|
|
34248
|
+
const held = await rail.claim(goal.id);
|
|
34249
|
+
revision = held.revision ?? revision;
|
|
34250
|
+
const owed = state.noted?.some((b) => b.goals.includes(goal.id));
|
|
34251
|
+
if (!held.openDecisionNeedIds?.length && !owed) {
|
|
34252
|
+
await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
|
|
34253
|
+
state.goal = void 0;
|
|
34254
|
+
return;
|
|
34255
|
+
}
|
|
34256
|
+
} catch (error) {
|
|
34257
|
+
opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
|
|
34258
|
+
}
|
|
34259
|
+
if (!progress) return;
|
|
33926
34260
|
try {
|
|
33927
|
-
await rail.
|
|
33928
|
-
const { revision } = await rail.read(goal.id);
|
|
33929
|
-
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." });
|
|
34261
|
+
await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
|
|
33930
34262
|
} catch (error) {
|
|
33931
|
-
opts?.log?.(`\u26A0
|
|
34263
|
+
opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
|
|
33932
34264
|
}
|
|
33933
34265
|
}
|
|
33934
|
-
async function
|
|
34266
|
+
async function cannotWork(report, state, rail, opts) {
|
|
34267
|
+
state.resting = true;
|
|
33935
34268
|
try {
|
|
33936
|
-
|
|
33937
|
-
|
|
33938
|
-
|
|
33939
|
-
|
|
33940
|
-
|
|
33941
|
-
|
|
33942
|
-
|
|
33943
|
-
${reason}`, parentId: goalId }],
|
|
33944
|
-
waiting: "none",
|
|
33945
|
-
channel: "notification"
|
|
33946
|
-
});
|
|
34269
|
+
if (!state.goal) {
|
|
34270
|
+
const claimed = await rail.claim();
|
|
34271
|
+
if (!claimed.goalId || claimed.revision === void 0) return;
|
|
34272
|
+
state.goal = { id: claimed.goalId, revision: claimed.revision };
|
|
34273
|
+
}
|
|
34274
|
+
await rail.contact({ asks: [{ ask: report, parentId: state.goal.id }], waiting: "none", channel: "notification" });
|
|
33947
34275
|
} catch (error) {
|
|
33948
|
-
opts?.log?.(`\u26A0 could not
|
|
34276
|
+
opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
|
|
33949
34277
|
}
|
|
33950
34278
|
}
|
|
33951
34279
|
async function afterTurn(event, state, rail, opts) {
|
|
33952
|
-
if (
|
|
33953
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
34280
|
+
if (event.failed) {
|
|
34281
|
+
const limit = limitIn(event.result, (opts?.now ?? Date.now)());
|
|
34282
|
+
if (limit) {
|
|
34283
|
+
state.paused = limit;
|
|
34284
|
+
if (state.typed) state.owed = state.typed;
|
|
34285
|
+
state.typed = void 0;
|
|
34286
|
+
state.carriedOn = false;
|
|
34287
|
+
state.resting = true;
|
|
34288
|
+
opts?.log?.(`\u23F8 paused by the usage limit until ${limit.resets}: nothing is typed in until then, and it carries on by itself`);
|
|
34289
|
+
return limit;
|
|
34290
|
+
}
|
|
34291
|
+
await cannotWork(`The agent stopped without finishing.
|
|
34292
|
+
|
|
34293
|
+
${event.result?.trim() || "No result reported."}`, state, rail, opts);
|
|
34294
|
+
return void 0;
|
|
33956
34295
|
}
|
|
33957
|
-
|
|
34296
|
+
await finish(event.result, state, rail, opts);
|
|
34297
|
+
state.typed = void 0;
|
|
34298
|
+
state.resting = true;
|
|
34299
|
+
return void 0;
|
|
34300
|
+
}
|
|
33958
34301
|
|
|
33959
|
-
|
|
33960
|
-
|
|
33961
|
-
|
|
33962
|
-
|
|
33963
|
-
|
|
34302
|
+
// src/paigy/conversation.ts
|
|
34303
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
34304
|
+
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;
|
|
34305
|
+
var SAFETY_MS = 6e4;
|
|
34306
|
+
var ASK_BEAT_MS = 5e3;
|
|
34307
|
+
function beat() {
|
|
34308
|
+
let pending = null;
|
|
34309
|
+
let wakeUp = null;
|
|
34310
|
+
return {
|
|
34311
|
+
nudge(cause) {
|
|
34312
|
+
if (wakeUp) {
|
|
34313
|
+
const w = wakeUp;
|
|
34314
|
+
wakeUp = null;
|
|
34315
|
+
w(cause);
|
|
34316
|
+
} else pending ??= cause;
|
|
34317
|
+
},
|
|
34318
|
+
next(ms) {
|
|
34319
|
+
if (pending) {
|
|
34320
|
+
const cause = pending;
|
|
34321
|
+
pending = null;
|
|
34322
|
+
return Promise.resolve(cause);
|
|
34323
|
+
}
|
|
34324
|
+
return new Promise((resolve3) => {
|
|
34325
|
+
const timer = setTimeout(() => {
|
|
34326
|
+
wakeUp = null;
|
|
34327
|
+
resolve3("safety");
|
|
34328
|
+
}, ms);
|
|
34329
|
+
wakeUp = (cause) => {
|
|
34330
|
+
clearTimeout(timer);
|
|
34331
|
+
resolve3(cause);
|
|
34332
|
+
};
|
|
34333
|
+
});
|
|
34334
|
+
}
|
|
34335
|
+
};
|
|
34336
|
+
}
|
|
34337
|
+
var BEAT_REPORT_MS = 10 * 6e4;
|
|
34338
|
+
var ended = (state) => state === "done" || state === "cancelled";
|
|
34339
|
+
function unheard(brief, state, since = null, after = -1) {
|
|
34340
|
+
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
34341
|
+
const words2 = [];
|
|
34342
|
+
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
34343
|
+
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
34344
|
+
if (i <= after) {
|
|
34345
|
+
seen.add(e.entryId);
|
|
34346
|
+
continue;
|
|
34347
|
+
}
|
|
34348
|
+
if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
|
|
34349
|
+
seen.add(e.entryId);
|
|
34350
|
+
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
34351
|
+
const text = entryWords(e).trim();
|
|
34352
|
+
if (text) words2.push(text);
|
|
34353
|
+
}
|
|
34354
|
+
return words2;
|
|
34355
|
+
}
|
|
34356
|
+
function sinceTheWork(brief) {
|
|
34357
|
+
const entries = brief.entries ?? [];
|
|
34358
|
+
let after = -1;
|
|
34359
|
+
for (const [i, e] of entries.entries()) if (!e.authorParticipant.startsWith("human:")) after = i;
|
|
34360
|
+
const last = entries[after];
|
|
34361
|
+
const handed = last && brief.ownerParticipant && last.authorParticipant !== brief.ownerParticipant;
|
|
34362
|
+
const note2 = handed ? entryWords(last).trim() : "";
|
|
34363
|
+
return { after, ...note2 ? { note: note2 } : {} };
|
|
34364
|
+
}
|
|
34365
|
+
var baseline = (goal, subtree) => ({
|
|
34366
|
+
goal,
|
|
34367
|
+
goals: new Map((subtree ?? []).map((g) => [g.goalId, { since: g.updatedAt ?? null, at: g.updatedAt ?? null }]))
|
|
34368
|
+
});
|
|
34369
|
+
function movedBelow(held, state) {
|
|
34370
|
+
const goals = state.below.goals;
|
|
34371
|
+
return (held.subtree ?? []).flatMap((g) => {
|
|
34372
|
+
const known = goals.get(g.goalId);
|
|
34373
|
+
const stamp = g.updatedAt ?? null;
|
|
34374
|
+
if (!known) return [{ goal: g, since: null }];
|
|
34375
|
+
if (known.at === null) {
|
|
34376
|
+
known.since = known.at = stamp;
|
|
34377
|
+
return [];
|
|
34378
|
+
}
|
|
34379
|
+
return stamp !== null && stamp !== known.at ? [{ goal: g, since: known.since }] : [];
|
|
33964
34380
|
});
|
|
33965
|
-
|
|
33966
|
-
|
|
34381
|
+
}
|
|
34382
|
+
function named(goalId, title, brief) {
|
|
34383
|
+
const line = (title ?? brief.outcome ?? "").trim().split("\n")[0].trim();
|
|
34384
|
+
const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
34385
|
+
return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
|
|
34386
|
+
}
|
|
34387
|
+
async function gather(state, rail, held) {
|
|
34388
|
+
const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
|
|
34389
|
+
const look2 = (goalId, via, title, since) => {
|
|
34390
|
+
if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
|
|
34391
|
+
};
|
|
34392
|
+
const waiting = await rail.waiting().catch(() => null);
|
|
34393
|
+
const listed = /* @__PURE__ */ new Set();
|
|
34394
|
+
const announced = state.announced ??= /* @__PURE__ */ new Set();
|
|
34395
|
+
if (waiting) {
|
|
34396
|
+
for (const id of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id)) announced.delete(id);
|
|
34397
|
+
}
|
|
34398
|
+
for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
|
|
34399
|
+
for (const g of waiting?.review ?? []) {
|
|
34400
|
+
listed.add(g.goalId);
|
|
34401
|
+
look2(g.goalId, "waiting work", g.title);
|
|
34402
|
+
}
|
|
34403
|
+
for (const d of waiting?.deliveries ?? []) for (const id of d.goalIds) {
|
|
34404
|
+
listed.add(id);
|
|
34405
|
+
look2(id, "waiting work");
|
|
34406
|
+
}
|
|
34407
|
+
const moved = movedBelow(held, state);
|
|
34408
|
+
for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
|
|
34409
|
+
const heard = [];
|
|
34410
|
+
const read = /* @__PURE__ */ new Set();
|
|
34411
|
+
for (const [goalId, { since, ...found }] of looks) {
|
|
34412
|
+
let brief = found.brief;
|
|
34413
|
+
if (!brief) {
|
|
34414
|
+
try {
|
|
34415
|
+
brief = await rail.read(goalId);
|
|
34416
|
+
} catch (error) {
|
|
34417
|
+
if (error.status === 404) read.add(goalId);
|
|
34418
|
+
continue;
|
|
34419
|
+
}
|
|
34420
|
+
}
|
|
34421
|
+
read.add(goalId);
|
|
34422
|
+
const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
|
|
34423
|
+
if (found.via === "assigned") announced.add(goalId);
|
|
34424
|
+
heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
|
|
34425
|
+
}
|
|
34426
|
+
for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
34427
|
+
return { heard, listed };
|
|
34428
|
+
}
|
|
34429
|
+
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.";
|
|
34430
|
+
var says = (h) => h.words.length > 0 || h.via === "assigned";
|
|
34431
|
+
function whatItIs(h) {
|
|
34432
|
+
const outcome = h.brief.outcome?.trim() ?? "";
|
|
34433
|
+
const name = (h.title ?? outcome).trim().split("\n")[0].trim();
|
|
34434
|
+
return outcome && outcome !== name ? [outcome] : [];
|
|
34435
|
+
}
|
|
34436
|
+
var block = (h) => [
|
|
34437
|
+
...h.via === "assigned" ? [GIVEN] : [],
|
|
34438
|
+
named(h.goalId, h.title, h.brief),
|
|
34439
|
+
...h.via === "assigned" ? whatItIs(h) : [],
|
|
34440
|
+
...h.note ? [`The last note on it, from the agent before you: ${h.note}`] : [],
|
|
34441
|
+
...h.words
|
|
34442
|
+
].join("\n\n");
|
|
34443
|
+
var short = (ids) => ids.map((id) => id.slice(0, 8)).join(", ");
|
|
34444
|
+
var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
|
|
34445
|
+
async function hear(session, state, rail, held, opts) {
|
|
34446
|
+
const log = opts?.log;
|
|
34447
|
+
const { heard, listed } = await gather(state, rail, held);
|
|
34448
|
+
const said = heard.filter(says);
|
|
34449
|
+
if (said.length) {
|
|
34450
|
+
send2(session, state, said.map(block).join("\n\n"));
|
|
34451
|
+
const worded = said.filter((h) => h.words.length);
|
|
34452
|
+
if (worded.length) log?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
|
|
34453
|
+
const goals = given(said);
|
|
34454
|
+
if (goals.length) {
|
|
34455
|
+
log?.(`\u25B8 announced assigned ${short(goals)} (via rest)`);
|
|
34456
|
+
if (opts?.tally) opts.tally.announced += goals.length;
|
|
34457
|
+
}
|
|
34458
|
+
}
|
|
34459
|
+
for (const h of heard) if (listed.has(h.goalId)) await acknowledge(h.goalId, h.brief, state, rail, log);
|
|
34460
|
+
}
|
|
34461
|
+
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.";
|
|
34462
|
+
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.";
|
|
34463
|
+
async function note(state, rail, held, inbox, how) {
|
|
34464
|
+
const { heard } = await gather(state, rail, held);
|
|
34465
|
+
const said = heard.filter(says);
|
|
34466
|
+
if (!said.length) return 0;
|
|
34467
|
+
const words2 = said.map(block).join("\n\n");
|
|
34468
|
+
const goals = said.map((h) => h.goalId);
|
|
34469
|
+
const assigned = given(said);
|
|
34470
|
+
if (how.tally) how.tally.announced += assigned.length;
|
|
34471
|
+
let name = "";
|
|
34472
|
+
try {
|
|
34473
|
+
name = inbox.put(`${NOTED}
|
|
34474
|
+
|
|
34475
|
+
${words2}`);
|
|
34476
|
+
how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
|
|
34477
|
+
if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
|
|
34478
|
+
} catch (error) {
|
|
34479
|
+
how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
|
|
34480
|
+
}
|
|
34481
|
+
(state.noted ??= []).push({ name, goals, words: words2, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
|
|
34482
|
+
return 1;
|
|
34483
|
+
}
|
|
34484
|
+
function receipts(state, inbox, log) {
|
|
34485
|
+
let seen = 0;
|
|
34486
|
+
for (const b of state.noted ?? []) {
|
|
34487
|
+
if (b.seen !== void 0 || !b.name) continue;
|
|
34488
|
+
const at = inbox.taken(b.name);
|
|
34489
|
+
if (at === null) continue;
|
|
34490
|
+
b.seen = at;
|
|
34491
|
+
seen++;
|
|
34492
|
+
log?.(`\u2713 seen mid-turn: ${short(b.goals)} ${Math.max(0, Math.round((at - b.at) / 1e3))}s after the ${b.cause}`);
|
|
34493
|
+
}
|
|
34494
|
+
return seen;
|
|
34495
|
+
}
|
|
34496
|
+
function settle(session, state, inbox, log) {
|
|
34497
|
+
const batches = state.noted ?? [];
|
|
34498
|
+
state.noted = [];
|
|
34499
|
+
const seen = [];
|
|
34500
|
+
const unseen = [];
|
|
34501
|
+
for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
|
|
34502
|
+
send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
|
|
34503
|
+
log?.(`\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)`);
|
|
34504
|
+
}
|
|
34505
|
+
async function acknowledge(goalId, brief, state, rail, log) {
|
|
34506
|
+
const acked = state.acked ??= /* @__PURE__ */ new Map();
|
|
34507
|
+
if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
|
|
34508
|
+
acked.set(goalId, brief.revision);
|
|
34509
|
+
try {
|
|
34510
|
+
const claimed = await rail.claim(goalId);
|
|
34511
|
+
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." });
|
|
34512
|
+
acked.set(goalId, done.revision);
|
|
34513
|
+
if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
|
|
34514
|
+
} catch (error) {
|
|
34515
|
+
log?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${error.message} \u2014 its words are typed in; it stays listed until acknowledged`);
|
|
34516
|
+
}
|
|
34517
|
+
}
|
|
34518
|
+
async function step(session, state, rail, opts) {
|
|
34519
|
+
if (state.paused) {
|
|
34520
|
+
if ((opts?.now ?? Date.now)() < state.paused.until) {
|
|
34521
|
+
if (opts?.tally) opts.tally.paused = (opts.tally.paused ?? 0) + 1;
|
|
34522
|
+
return;
|
|
34523
|
+
}
|
|
34524
|
+
opts?.log?.(`\u25B6 resumed after the limit (it reset at ${state.paused.resets}): told to carry on, with what arrived meanwhile`);
|
|
34525
|
+
state.paused = void 0;
|
|
34526
|
+
state.carryOn = true;
|
|
34527
|
+
}
|
|
34528
|
+
await look(session, state, rail, opts);
|
|
34529
|
+
if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
|
|
34530
|
+
}
|
|
34531
|
+
async function look(session, state, rail, opts) {
|
|
34532
|
+
if (opts?.inbox) {
|
|
34533
|
+
const seen = receipts(state, opts.inbox, opts.log);
|
|
34534
|
+
if (opts.tally) opts.tally.seen += seen;
|
|
34535
|
+
}
|
|
34536
|
+
if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
|
|
34537
|
+
if (!state.goal) {
|
|
34538
|
+
if (!state.resting) return;
|
|
34539
|
+
let brief2 = await rail.claim();
|
|
34540
|
+
if (!brief2.goalId || brief2.revision === void 0) {
|
|
34541
|
+
if (opts?.initialPrompt?.trim()) {
|
|
34542
|
+
const prompt = opts.initialPrompt.trim();
|
|
34543
|
+
opts.initialPrompt = void 0;
|
|
34544
|
+
const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID5() });
|
|
34545
|
+
brief2 = await rail.claim();
|
|
34546
|
+
if (!brief2.goalId || brief2.revision === void 0) {
|
|
34547
|
+
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
34548
|
+
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
34549
|
+
return;
|
|
34550
|
+
}
|
|
34551
|
+
} else {
|
|
34552
|
+
return;
|
|
34553
|
+
}
|
|
34554
|
+
}
|
|
34555
|
+
unheard(brief2, state);
|
|
34556
|
+
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
34557
|
+
else {
|
|
34558
|
+
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
34559
|
+
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
34560
|
+
}
|
|
34561
|
+
const outcome = brief2.outcome?.trim() ?? "";
|
|
34562
|
+
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
|
|
34563
|
+
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
34564
|
+
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
34565
|
+
const fullText = [outcome, earlier, ...said].filter(Boolean).join("\n\n");
|
|
34566
|
+
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
34567
|
+
return;
|
|
34568
|
+
}
|
|
34569
|
+
const brief = await rail.read(state.goal.id);
|
|
34570
|
+
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
34571
|
+
for (const answer of brief.answers ?? []) {
|
|
34572
|
+
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
34573
|
+
if (!resolve3) continue;
|
|
34574
|
+
state.asks.delete(answer.requestEntryId);
|
|
34575
|
+
resolve3(answer.result);
|
|
34576
|
+
}
|
|
34577
|
+
if (ended(brief.state)) {
|
|
34578
|
+
state.goal = void 0;
|
|
34579
|
+
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
34580
|
+
state.asks?.clear();
|
|
34581
|
+
return;
|
|
34582
|
+
}
|
|
34583
|
+
if (state.below?.goal !== state.goal.id) state.below = baseline(state.goal.id, brief.subtree);
|
|
34584
|
+
if (state.asks?.size) return;
|
|
34585
|
+
if (!state.resting) {
|
|
34586
|
+
if (!opts?.inbox) return;
|
|
34587
|
+
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 } : {} });
|
|
34588
|
+
if (opts.tally) opts.tally.noted += noted;
|
|
34589
|
+
return;
|
|
34590
|
+
}
|
|
34591
|
+
await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
|
|
34592
|
+
}
|
|
34593
|
+
function send2(session, state, text) {
|
|
34594
|
+
state.resting = false;
|
|
34595
|
+
let words2 = text;
|
|
34596
|
+
if (state.carryOn) {
|
|
34597
|
+
state.carryOn = false;
|
|
34598
|
+
state.carriedOn = true;
|
|
34599
|
+
const owed = state.owed;
|
|
34600
|
+
state.owed = void 0;
|
|
34601
|
+
words2 = [owed, text].filter(Boolean).join("\n\n");
|
|
34602
|
+
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
34603
|
+
}
|
|
34604
|
+
state.typed = words2 || void 0;
|
|
34605
|
+
session.send(text);
|
|
34606
|
+
}
|
|
34607
|
+
async function pump(session, state, rail, opts) {
|
|
34608
|
+
const clock4 = opts.beat ?? beat();
|
|
34609
|
+
const now = opts.now ?? Date.now;
|
|
34610
|
+
const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
|
|
34611
|
+
const tally = { noted: 0, seen: 0, announced: 0 };
|
|
34612
|
+
const stepOpts = {
|
|
34613
|
+
...opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : {},
|
|
34614
|
+
...opts.log ? { log: opts.log } : {},
|
|
34615
|
+
...opts.inbox ? { inbox: opts.inbox } : {},
|
|
34616
|
+
now,
|
|
34617
|
+
tally
|
|
34618
|
+
};
|
|
34619
|
+
let since = now();
|
|
34620
|
+
let cause = "start";
|
|
34621
|
+
for (; ; ) {
|
|
34622
|
+
if (cause === "wake" && !state.resting && !state.asks?.size && !opts.inbox) counts.skipped++;
|
|
34623
|
+
else {
|
|
34624
|
+
if (cause !== "start") counts[cause]++;
|
|
34625
|
+
try {
|
|
34626
|
+
stepOpts.cause = cause;
|
|
34627
|
+
await step(session, state, rail, stepOpts);
|
|
34628
|
+
} catch {
|
|
34629
|
+
}
|
|
34630
|
+
}
|
|
34631
|
+
if (!await opts.onIdle()) return;
|
|
34632
|
+
if (now() - since >= BEAT_REPORT_MS) {
|
|
34633
|
+
const read = counts.wake + counts.rest + counts.safety;
|
|
34634
|
+
if (read || counts.skipped || tally.noted || tally.seen || tally.announced) {
|
|
34635
|
+
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` : ""}`);
|
|
34636
|
+
}
|
|
34637
|
+
counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
|
|
34638
|
+
since = now();
|
|
34639
|
+
}
|
|
34640
|
+
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);
|
|
34641
|
+
if (!await opts.onIdle()) return;
|
|
34642
|
+
}
|
|
33967
34643
|
}
|
|
33968
34644
|
|
|
33969
34645
|
// src/run.ts
|
|
@@ -33971,15 +34647,32 @@ function runHarness(opts) {
|
|
|
33971
34647
|
let running = true;
|
|
33972
34648
|
let tail = [];
|
|
33973
34649
|
const state = { resting: true };
|
|
34650
|
+
const clock4 = beat();
|
|
33974
34651
|
let session = null;
|
|
33975
34652
|
const speaksAs = opts.resume ?? opts.session;
|
|
33976
34653
|
const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
|
|
34654
|
+
let inbox;
|
|
34655
|
+
if (opts.harness === "claude") {
|
|
34656
|
+
try {
|
|
34657
|
+
inbox = inboxFor();
|
|
34658
|
+
} catch (error) {
|
|
34659
|
+
opts.log(`\u26A0 no inbox (${error.message}): what the person says mid-turn waits for the turn to end`);
|
|
34660
|
+
}
|
|
34661
|
+
}
|
|
34662
|
+
const name = opts.name ?? "The agent";
|
|
34663
|
+
function runningAgain() {
|
|
34664
|
+
state.carriedOn = false;
|
|
34665
|
+
opts.log("\u25B6 running again after the limit");
|
|
34666
|
+
LIMITS.running(name, (text) => rail.contact({ asks: [{ ask: text, ...state.goal ? { parentId: state.goal.id } : {} }], waiting: "none", channel: "call" }), opts.log);
|
|
34667
|
+
}
|
|
33977
34668
|
async function handle(event) {
|
|
33978
34669
|
if (event.kind === "error") {
|
|
33979
34670
|
opts.log(`\u26A0 ${event.message}`);
|
|
33980
34671
|
if (event.fatal && running) {
|
|
33981
34672
|
opts.log(`\u2716 could not start: ${event.message}`);
|
|
33982
|
-
await
|
|
34673
|
+
await cannotWork(`The session could not start, so nothing is working on this yet.
|
|
34674
|
+
|
|
34675
|
+
${event.message}`, state, rail, { log: opts.log });
|
|
33983
34676
|
stop2();
|
|
33984
34677
|
}
|
|
33985
34678
|
return;
|
|
@@ -33998,6 +34691,7 @@ function runHarness(opts) {
|
|
|
33998
34691
|
return;
|
|
33999
34692
|
}
|
|
34000
34693
|
if (event.kind === "work") {
|
|
34694
|
+
if (state.carriedOn) runningAgain();
|
|
34001
34695
|
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
34002
34696
|
tail = pushWork(tail, workLine(event));
|
|
34003
34697
|
return;
|
|
@@ -34005,20 +34699,10 @@ function runHarness(opts) {
|
|
|
34005
34699
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
34006
34700
|
if (event.kind === "idle") {
|
|
34007
34701
|
tail = [];
|
|
34008
|
-
|
|
34009
|
-
|
|
34010
|
-
|
|
34011
|
-
|
|
34012
|
-
state.resting = false;
|
|
34013
|
-
session.send(local);
|
|
34014
|
-
return;
|
|
34015
|
-
}
|
|
34016
|
-
}
|
|
34017
|
-
const reply = await afterTurn(event, state, rail, { log: opts.log });
|
|
34018
|
-
if (reply && session && running) {
|
|
34019
|
-
opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
|
|
34020
|
-
session.send(reply);
|
|
34021
|
-
}
|
|
34702
|
+
const carried = state.carriedOn && !event.failed;
|
|
34703
|
+
if (await afterTurn(event, state, rail, { log: opts.log })) LIMITS.paused(name, Date.now());
|
|
34704
|
+
else if (carried) runningAgain();
|
|
34705
|
+
clock4.nudge("rest");
|
|
34022
34706
|
}
|
|
34023
34707
|
}
|
|
34024
34708
|
session = startSession({
|
|
@@ -34029,19 +34713,25 @@ function runHarness(opts) {
|
|
|
34029
34713
|
...opts.resume ? { resume: opts.resume } : {},
|
|
34030
34714
|
...opts.session ? { session: opts.session } : {},
|
|
34031
34715
|
...opts.bin ? { bin: opts.bin } : {},
|
|
34032
|
-
...
|
|
34716
|
+
...inbox ? { inbox: inbox.dir } : {},
|
|
34717
|
+
onExit: () => {
|
|
34718
|
+
inbox?.close();
|
|
34719
|
+
opts.onExit?.();
|
|
34720
|
+
},
|
|
34033
34721
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
34034
34722
|
});
|
|
34035
|
-
void pump(session, state, rail, { onIdle: () => running, initialPrompt: opts.prompt }).catch(
|
|
34723
|
+
void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
|
|
34036
34724
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
34037
34725
|
);
|
|
34038
34726
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
34039
34727
|
function stop2() {
|
|
34040
34728
|
running = false;
|
|
34729
|
+
clock4.nudge("wake");
|
|
34041
34730
|
tail = [];
|
|
34042
34731
|
cancelAsks(state);
|
|
34043
34732
|
session?.stop();
|
|
34044
34733
|
session = null;
|
|
34734
|
+
inbox?.close();
|
|
34045
34735
|
opts.log("\u25A0 stopped");
|
|
34046
34736
|
}
|
|
34047
34737
|
return {
|
|
@@ -34049,7 +34739,9 @@ function runHarness(opts) {
|
|
|
34049
34739
|
session?.send(text);
|
|
34050
34740
|
},
|
|
34051
34741
|
working: () => running && state.resting !== true,
|
|
34052
|
-
|
|
34742
|
+
// Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
|
|
34743
|
+
tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
|
|
34744
|
+
nudge: () => clock4.nudge("wake"),
|
|
34053
34745
|
stop: stop2
|
|
34054
34746
|
};
|
|
34055
34747
|
}
|
|
@@ -34057,16 +34749,16 @@ function runHarness(opts) {
|
|
|
34057
34749
|
// src/host.ts
|
|
34058
34750
|
init_dist();
|
|
34059
34751
|
init_catalog();
|
|
34060
|
-
import { readFileSync as
|
|
34061
|
-
import { homedir as
|
|
34062
|
-
import { join as
|
|
34752
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
34753
|
+
import { homedir as homedir8 } from "os";
|
|
34754
|
+
import { join as join9 } from "path";
|
|
34063
34755
|
|
|
34064
34756
|
// src/workspaces.ts
|
|
34065
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
34066
|
-
import { dirname as dirname2, join as
|
|
34067
|
-
import { homedir as
|
|
34757
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
34758
|
+
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
34759
|
+
import { homedir as homedir5 } from "os";
|
|
34068
34760
|
function workspacesFile() {
|
|
34069
|
-
return
|
|
34761
|
+
return join5(homedir5(), ".paigy", "workspaces.json");
|
|
34070
34762
|
}
|
|
34071
34763
|
function listWorkspaces(deps) {
|
|
34072
34764
|
try {
|
|
@@ -34077,31 +34769,94 @@ function listWorkspaces(deps) {
|
|
|
34077
34769
|
}
|
|
34078
34770
|
}
|
|
34079
34771
|
function addWorkspace(dir, deps) {
|
|
34080
|
-
const next = resolve2(dir.replace(/^~(?=$|\/)/,
|
|
34772
|
+
const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir5()));
|
|
34081
34773
|
const all = listWorkspaces(deps);
|
|
34082
34774
|
if (!all.includes(next)) {
|
|
34083
34775
|
all.push(next);
|
|
34084
|
-
|
|
34085
|
-
|
|
34776
|
+
mkdirSync3(dirname2(deps.file), { recursive: true });
|
|
34777
|
+
writeFileSync3(deps.file, JSON.stringify(all, null, 2));
|
|
34086
34778
|
}
|
|
34087
34779
|
return all;
|
|
34088
34780
|
}
|
|
34089
34781
|
function allowed(cwd, deps) {
|
|
34090
|
-
const target = resolve2(cwd.replace(/^~(?=$|\/)/,
|
|
34782
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir5()));
|
|
34091
34783
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
34092
34784
|
}
|
|
34093
34785
|
function resolveWakeDir(pinned, deps) {
|
|
34094
34786
|
if (pinned && allowed(pinned, deps)) {
|
|
34095
|
-
const dir = resolve2(pinned.replace(/^~(?=$|\/)/,
|
|
34787
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir5()));
|
|
34096
34788
|
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
34097
34789
|
}
|
|
34098
34790
|
return listWorkspaces(deps)[0];
|
|
34099
34791
|
}
|
|
34100
34792
|
|
|
34793
|
+
// src/holder.ts
|
|
34794
|
+
init_dist();
|
|
34795
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
34796
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
34797
|
+
import { homedir as homedir6 } from "os";
|
|
34798
|
+
import { join as join6 } from "path";
|
|
34799
|
+
function isAlive(pid) {
|
|
34800
|
+
try {
|
|
34801
|
+
process.kill(pid, 0);
|
|
34802
|
+
return true;
|
|
34803
|
+
} catch (e) {
|
|
34804
|
+
return e.code === "EPERM";
|
|
34805
|
+
}
|
|
34806
|
+
}
|
|
34807
|
+
function psStart(pid, zone) {
|
|
34808
|
+
try {
|
|
34809
|
+
const env = zone === "UTC" ? { ...process.env, TZ: "UTC", LC_ALL: "C" } : { ...process.env, LC_ALL: "C" };
|
|
34810
|
+
return execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
|
|
34811
|
+
} catch {
|
|
34812
|
+
return null;
|
|
34813
|
+
}
|
|
34814
|
+
}
|
|
34815
|
+
var squash = (s) => s.trim().replace(/\s+/g, " ");
|
|
34816
|
+
function holds(pid, procStart, deps) {
|
|
34817
|
+
if (!deps.alive(pid)) return false;
|
|
34818
|
+
if (typeof procStart !== "string" || !procStart.trim()) return true;
|
|
34819
|
+
const want = squash(procStart);
|
|
34820
|
+
for (const zone of ["UTC", "local"]) {
|
|
34821
|
+
const got = deps.started(pid, zone);
|
|
34822
|
+
if (got === null) return true;
|
|
34823
|
+
if (squash(got) === want) return true;
|
|
34824
|
+
}
|
|
34825
|
+
return false;
|
|
34826
|
+
}
|
|
34827
|
+
function localHolders(deps = {}) {
|
|
34828
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
|
|
34829
|
+
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
34830
|
+
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
34831
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
34832
|
+
const held = /* @__PURE__ */ new Map();
|
|
34833
|
+
let files;
|
|
34834
|
+
try {
|
|
34835
|
+
files = list(dir);
|
|
34836
|
+
} catch {
|
|
34837
|
+
return held;
|
|
34838
|
+
}
|
|
34839
|
+
for (const file of files) {
|
|
34840
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
34841
|
+
let row;
|
|
34842
|
+
try {
|
|
34843
|
+
row = JSON.parse(read(join6(dir, file)));
|
|
34844
|
+
} catch {
|
|
34845
|
+
continue;
|
|
34846
|
+
}
|
|
34847
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
34848
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
34849
|
+
const slot = sessionSlot(row.sessionId);
|
|
34850
|
+
if (held.has(slot)) continue;
|
|
34851
|
+
if (holds(pid, row.procStart, check)) held.set(slot, { pid });
|
|
34852
|
+
}
|
|
34853
|
+
return held;
|
|
34854
|
+
}
|
|
34855
|
+
|
|
34101
34856
|
// src/owner.ts
|
|
34102
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
34103
|
-
import { homedir as
|
|
34104
|
-
import { join as
|
|
34857
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
34858
|
+
import { homedir as homedir7 } from "os";
|
|
34859
|
+
import { join as join7 } from "path";
|
|
34105
34860
|
var HEAD_BYTES = 64e3;
|
|
34106
34861
|
function readHead(file) {
|
|
34107
34862
|
const fd = openSync2(file, "r");
|
|
@@ -34117,8 +34872,8 @@ function ownerSession(slot, deps = {}) {
|
|
|
34117
34872
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
34118
34873
|
if (!m) return null;
|
|
34119
34874
|
const prefix = m[1];
|
|
34120
|
-
const root = deps.projectsDir ??
|
|
34121
|
-
const list = deps.list ?? ((dir) =>
|
|
34875
|
+
const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
|
|
34876
|
+
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
34122
34877
|
const head = deps.head ?? readHead;
|
|
34123
34878
|
let folders;
|
|
34124
34879
|
try {
|
|
@@ -34129,7 +34884,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34129
34884
|
for (const folder of folders) {
|
|
34130
34885
|
let files;
|
|
34131
34886
|
try {
|
|
34132
|
-
files = list(
|
|
34887
|
+
files = list(join7(root, folder));
|
|
34133
34888
|
} catch {
|
|
34134
34889
|
continue;
|
|
34135
34890
|
}
|
|
@@ -34137,7 +34892,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34137
34892
|
if (!file) continue;
|
|
34138
34893
|
let text;
|
|
34139
34894
|
try {
|
|
34140
|
-
text = head(
|
|
34895
|
+
text = head(join7(root, folder, file));
|
|
34141
34896
|
} catch {
|
|
34142
34897
|
continue;
|
|
34143
34898
|
}
|
|
@@ -34157,7 +34912,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34157
34912
|
// src/update.ts
|
|
34158
34913
|
import { execFile } from "child_process";
|
|
34159
34914
|
import { createRequire } from "module";
|
|
34160
|
-
import { dirname as dirname3, join as
|
|
34915
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
34161
34916
|
import { promisify } from "util";
|
|
34162
34917
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
34163
34918
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -34184,7 +34939,7 @@ async function latestVersion() {
|
|
|
34184
34939
|
}
|
|
34185
34940
|
async function install(version) {
|
|
34186
34941
|
const bin = dirname3(process.execPath);
|
|
34187
|
-
await promisify(execFile)(
|
|
34942
|
+
await promisify(execFile)(join8(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
34188
34943
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
34189
34944
|
timeout: 5 * 6e4
|
|
34190
34945
|
});
|
|
@@ -34235,25 +34990,43 @@ function selfUpdater(deps) {
|
|
|
34235
34990
|
}
|
|
34236
34991
|
|
|
34237
34992
|
// src/host.ts
|
|
34238
|
-
var HOST_FILE =
|
|
34993
|
+
var HOST_FILE = join9(homedir8(), ".paigy", "host.json");
|
|
34239
34994
|
var runKey = (slot) => `slot:${slot}`;
|
|
34240
34995
|
var claimedRun = (sessionId2) => {
|
|
34241
34996
|
const slot = sessionSlot(sessionId2);
|
|
34242
34997
|
return { slot, key: runKey(slot) };
|
|
34243
34998
|
};
|
|
34244
34999
|
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
34245
|
-
async function
|
|
34246
|
-
|
|
35000
|
+
async function waitingWork(token, session) {
|
|
35001
|
+
let found;
|
|
35002
|
+
try {
|
|
35003
|
+
found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
|
|
35004
|
+
} catch (error) {
|
|
35005
|
+
return { skip: `could not read what is waiting (${error.message})` };
|
|
35006
|
+
}
|
|
35007
|
+
const n = found.deliveries.length;
|
|
35008
|
+
if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
|
|
35009
|
+
const next = found.claimable;
|
|
35010
|
+
return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}` } : { skip: "has nothing waiting" };
|
|
35011
|
+
}
|
|
35012
|
+
function evict(runs, key, run) {
|
|
35013
|
+
if (runs.get(key)?.run === run) runs.delete(key);
|
|
34247
35014
|
}
|
|
34248
35015
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
34249
|
-
function wakeAction(live, now, serverSeenAt) {
|
|
35016
|
+
function wakeAction(live, now, serverSeenAt, here = {}) {
|
|
34250
35017
|
if (live) return { action: "skip", why: "run" };
|
|
34251
|
-
if (
|
|
35018
|
+
if (here.holder) return { action: "skip", why: "holder" };
|
|
35019
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
|
|
35020
|
+
if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
|
|
35021
|
+
return { action: "skip", why: "presence" };
|
|
35022
|
+
}
|
|
34252
35023
|
return { action: "spawn", why: "nobody" };
|
|
34253
35024
|
}
|
|
34254
35025
|
function startHost(opts) {
|
|
34255
35026
|
const runs = /* @__PURE__ */ new Map();
|
|
34256
35027
|
const asHost = { token: opts.token };
|
|
35028
|
+
const goneAt = /* @__PURE__ */ new Map();
|
|
35029
|
+
const heldHere = /* @__PURE__ */ new Set();
|
|
34257
35030
|
const refreshIdentities = async () => {
|
|
34258
35031
|
for (const slot of listSlots()) {
|
|
34259
35032
|
if (slot === "Desktop") continue;
|
|
@@ -34273,7 +35046,7 @@ function startHost(opts) {
|
|
|
34273
35046
|
working: () => [...runs.values()].filter((r) => r.run.working()).length,
|
|
34274
35047
|
log: opts.log
|
|
34275
35048
|
});
|
|
34276
|
-
const
|
|
35049
|
+
const beat2 = () => {
|
|
34277
35050
|
void refreshIdentities();
|
|
34278
35051
|
void update().catch(() => {
|
|
34279
35052
|
});
|
|
@@ -34302,9 +35075,11 @@ function startHost(opts) {
|
|
|
34302
35075
|
token: spec.token,
|
|
34303
35076
|
...launchRun(spec),
|
|
34304
35077
|
onExit: () => {
|
|
34305
|
-
runs
|
|
35078
|
+
evict(runs, key, run);
|
|
35079
|
+
goneAt.set(slot, Date.now());
|
|
34306
35080
|
opts.log(`\u2716 ${label} ended`);
|
|
34307
35081
|
},
|
|
35082
|
+
name: label,
|
|
34308
35083
|
log
|
|
34309
35084
|
});
|
|
34310
35085
|
runs.set(key, { run, label, token: spec.token });
|
|
@@ -34331,7 +35106,13 @@ function startHost(opts) {
|
|
|
34331
35106
|
return presence;
|
|
34332
35107
|
}
|
|
34333
35108
|
let lastPresence = /* @__PURE__ */ new Map();
|
|
34334
|
-
async function
|
|
35109
|
+
async function evidence() {
|
|
35110
|
+
const presence = await presenceMap();
|
|
35111
|
+
lastHolders = localHolders();
|
|
35112
|
+
return { presence, holders: lastHolders };
|
|
35113
|
+
}
|
|
35114
|
+
let lastHolders = /* @__PURE__ */ new Map();
|
|
35115
|
+
async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
|
|
34335
35116
|
{
|
|
34336
35117
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
34337
35118
|
const key = runKey(slot);
|
|
@@ -34339,19 +35120,42 @@ function startHost(opts) {
|
|
|
34339
35120
|
const live = runs.get(key);
|
|
34340
35121
|
const tokenId = slotIdentity(slot).tokenId;
|
|
34341
35122
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
34342
|
-
const
|
|
35123
|
+
const holder = holders.get(slot);
|
|
35124
|
+
if (holder) heldHere.add(slot);
|
|
35125
|
+
else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
|
|
35126
|
+
const decided = wakeAction(!!live, Date.now(), seenByServer, { holder, goneAt: goneAt.get(slot) });
|
|
34343
35127
|
if (decided.action === "skip") {
|
|
34344
|
-
|
|
35128
|
+
if (decided.why === "run" && cause === "wake") live?.run.nudge();
|
|
35129
|
+
const name = slotName(slot) ?? slot;
|
|
35130
|
+
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`);
|
|
34345
35131
|
return;
|
|
34346
35132
|
}
|
|
35133
|
+
const label = slotName(slot) ?? slot;
|
|
35134
|
+
const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
|
|
34347
35135
|
const token = readToken(slot);
|
|
34348
|
-
if (!token)
|
|
35136
|
+
if (!token) {
|
|
35137
|
+
skip("has no token on this machine");
|
|
35138
|
+
return;
|
|
35139
|
+
}
|
|
34349
35140
|
const owner = ownerSession(slot);
|
|
34350
|
-
if (owner && !allowed(owner.cwd, opts.wsDeps))
|
|
35141
|
+
if (owner && !allowed(owner.cwd, opts.wsDeps)) {
|
|
35142
|
+
skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
|
|
35143
|
+
return;
|
|
35144
|
+
}
|
|
34351
35145
|
const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
34352
|
-
if (!workspace)
|
|
34353
|
-
|
|
34354
|
-
|
|
35146
|
+
if (!workspace) {
|
|
35147
|
+
skip("has no granted folder to wake in");
|
|
35148
|
+
return;
|
|
35149
|
+
}
|
|
35150
|
+
const found = await waitingWork(token, owner?.sessionId);
|
|
35151
|
+
if ("skip" in found) {
|
|
35152
|
+
skip(found.skip);
|
|
35153
|
+
return;
|
|
35154
|
+
}
|
|
35155
|
+
if (runs.has(key)) {
|
|
35156
|
+
skip("was started by the wake that read it first");
|
|
35157
|
+
return;
|
|
35158
|
+
}
|
|
34355
35159
|
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
34356
35160
|
const run = runHarness({
|
|
34357
35161
|
harness,
|
|
@@ -34360,17 +35164,20 @@ function startHost(opts) {
|
|
|
34360
35164
|
prompt: "",
|
|
34361
35165
|
token,
|
|
34362
35166
|
...owner ? { resume: owner.sessionId } : {},
|
|
34363
|
-
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
35167
|
+
// A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
|
|
35168
|
+
// a run that is no longer the slot's must not take the live one's place away with it.
|
|
34364
35169
|
onExit: () => {
|
|
34365
|
-
runs
|
|
35170
|
+
evict(runs, key, run);
|
|
35171
|
+
goneAt.set(slot, Date.now());
|
|
34366
35172
|
opts.log(`\u2716 ${label} ended`);
|
|
34367
35173
|
},
|
|
35174
|
+
name: label,
|
|
34368
35175
|
log
|
|
34369
35176
|
});
|
|
34370
35177
|
runs.set(key, { run, label, token });
|
|
34371
35178
|
void heartbeat(void 0, { token }).catch(() => {
|
|
34372
35179
|
});
|
|
34373
|
-
opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014
|
|
35180
|
+
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)" : ""}`);
|
|
34374
35181
|
}
|
|
34375
35182
|
}
|
|
34376
35183
|
const listening = /* @__PURE__ */ new Map();
|
|
@@ -34390,7 +35197,7 @@ function startHost(opts) {
|
|
|
34390
35197
|
joining.add(slot);
|
|
34391
35198
|
try {
|
|
34392
35199
|
const sub = await subscribeWake(
|
|
34393
|
-
() => void
|
|
35200
|
+
() => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
|
|
34394
35201
|
}),
|
|
34395
35202
|
{ token }
|
|
34396
35203
|
);
|
|
@@ -34404,8 +35211,8 @@ function startHost(opts) {
|
|
|
34404
35211
|
}
|
|
34405
35212
|
}
|
|
34406
35213
|
async function sweepSlots() {
|
|
34407
|
-
const
|
|
34408
|
-
for (const slot of listSlots()) await wakeSlot(slot,
|
|
35214
|
+
const seen = await evidence();
|
|
35215
|
+
for (const slot of listSlots()) await wakeSlot(slot, seen);
|
|
34409
35216
|
}
|
|
34410
35217
|
const ACTIVITY_MS = 2e3;
|
|
34411
35218
|
const published = /* @__PURE__ */ new Map();
|
|
@@ -34430,12 +35237,12 @@ function startHost(opts) {
|
|
|
34430
35237
|
};
|
|
34431
35238
|
const publish = () => {
|
|
34432
35239
|
try {
|
|
34433
|
-
|
|
35240
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
34434
35241
|
} catch {
|
|
34435
35242
|
}
|
|
34436
35243
|
};
|
|
34437
|
-
|
|
34438
|
-
const pulse = setInterval(
|
|
35244
|
+
beat2();
|
|
35245
|
+
const pulse = setInterval(beat2, 6e4);
|
|
34439
35246
|
const spawnPoll = setInterval(() => {
|
|
34440
35247
|
void claimAndSpawn();
|
|
34441
35248
|
publish();
|
|
@@ -34461,10 +35268,10 @@ function startHost(opts) {
|
|
|
34461
35268
|
// them live, and a run mid-turn makes it working.
|
|
34462
35269
|
roster: () => {
|
|
34463
35270
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
34464
|
-
const
|
|
34465
|
-
for (const key of listSlots()) if (key !== "Desktop")
|
|
34466
|
-
for (const label of live.keys()) if (!
|
|
34467
|
-
return [...
|
|
35271
|
+
const names2 = /* @__PURE__ */ new Map();
|
|
35272
|
+
for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
|
|
35273
|
+
for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
|
|
35274
|
+
return [...names2.entries()].map(([name, slot]) => {
|
|
34468
35275
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
34469
35276
|
return {
|
|
34470
35277
|
name,
|
|
@@ -34481,7 +35288,7 @@ function startHost(opts) {
|
|
|
34481
35288
|
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
34482
35289
|
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
34483
35290
|
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
34484
|
-
running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
35291
|
+
running: live.has(name) || !!slot && lastHolders.has(slot) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
34485
35292
|
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
34486
35293
|
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
34487
35294
|
working: live.get(name)?.working() ?? false
|
|
@@ -34497,7 +35304,7 @@ function startHost(opts) {
|
|
|
34497
35304
|
streamActivity();
|
|
34498
35305
|
clearInterval(activityTick);
|
|
34499
35306
|
try {
|
|
34500
|
-
|
|
35307
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
34501
35308
|
} catch {
|
|
34502
35309
|
}
|
|
34503
35310
|
}
|
|
@@ -34602,18 +35409,18 @@ async function pairThisMachine() {
|
|
|
34602
35409
|
}
|
|
34603
35410
|
}
|
|
34604
35411
|
function forwardCodexThreadId() {
|
|
34605
|
-
const cfg =
|
|
35412
|
+
const cfg = join10(process.env.CODEX_HOME || join10(homedir10(), ".codex"), "config.toml");
|
|
34606
35413
|
if (!existsSync5(cfg)) return;
|
|
34607
|
-
const text =
|
|
35414
|
+
const text = readFileSync6(cfg, "utf8");
|
|
34608
35415
|
const head = "[mcp_servers.paigy]\n";
|
|
34609
35416
|
const at = text.indexOf(head);
|
|
34610
35417
|
if (at < 0) return;
|
|
34611
|
-
const
|
|
35418
|
+
const block2 = text.slice(at + head.length, (() => {
|
|
34612
35419
|
const n = text.indexOf("\n[", at + head.length);
|
|
34613
35420
|
return n < 0 ? text.length : n;
|
|
34614
35421
|
})());
|
|
34615
|
-
if (/^\s*env_vars\s*=/m.test(
|
|
34616
|
-
|
|
35422
|
+
if (/^\s*env_vars\s*=/m.test(block2)) return;
|
|
35423
|
+
writeFileSync5(cfg, text.slice(0, at + head.length) + 'env_vars = ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]\n' + text.slice(at + head.length));
|
|
34617
35424
|
}
|
|
34618
35425
|
async function main() {
|
|
34619
35426
|
const parsed = parseArgs(process.argv.slice(2));
|
|
@@ -34704,7 +35511,7 @@ async function main() {
|
|
|
34704
35511
|
const wsDeps = { file: workspacesFile() };
|
|
34705
35512
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
34706
35513
|
if (!listWorkspaces(wsDeps).length) {
|
|
34707
|
-
const guess =
|
|
35514
|
+
const guess = join10(homedir10(), "projects");
|
|
34708
35515
|
if (existsSync5(guess)) {
|
|
34709
35516
|
addWorkspace(guess, wsDeps);
|
|
34710
35517
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -34717,23 +35524,14 @@ async function main() {
|
|
|
34717
35524
|
console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
|
|
34718
35525
|
}
|
|
34719
35526
|
} else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
|
|
34720
|
-
const
|
|
34721
|
-
|
|
34722
|
-
// Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
|
|
34723
|
-
// a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
|
|
34724
|
-
// written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
|
|
34725
|
-
// all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
|
|
34726
|
-
{ cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
|
|
34727
|
-
{ cli: "agy", name: "Antigravity" }
|
|
34728
|
-
];
|
|
34729
|
-
for (const t of TERMINAL_AGENTS) {
|
|
34730
|
-
if (!which(t.cli) || !t.wire) continue;
|
|
35527
|
+
for (const t of MCP_WIRES) {
|
|
35528
|
+
if (!which(t.cli)) continue;
|
|
34731
35529
|
try {
|
|
34732
35530
|
execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
|
|
34733
35531
|
if (t.cli === "codex") forwardCodexThreadId();
|
|
34734
35532
|
console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
|
|
34735
35533
|
} catch (e) {
|
|
34736
|
-
console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014
|
|
35534
|
+
console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014 its agents cannot reach you until it is: ${t.wire}`);
|
|
34737
35535
|
}
|
|
34738
35536
|
}
|
|
34739
35537
|
if (which("claude")) {
|
|
@@ -34745,12 +35543,12 @@ async function main() {
|
|
|
34745
35543
|
}
|
|
34746
35544
|
}
|
|
34747
35545
|
try {
|
|
34748
|
-
const settings =
|
|
35546
|
+
const settings = join10(homedir10(), ".claude", "settings.json");
|
|
34749
35547
|
if (which("claude") && existsSync5(settings)) {
|
|
34750
|
-
const cfg = JSON.parse(
|
|
35548
|
+
const cfg = JSON.parse(readFileSync6(settings, "utf8"));
|
|
34751
35549
|
if (!cfg["statusLine"]) {
|
|
34752
35550
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
34753
|
-
|
|
35551
|
+
writeFileSync5(settings, JSON.stringify(cfg, null, 2));
|
|
34754
35552
|
console.log("\u2713 statusline wired");
|
|
34755
35553
|
}
|
|
34756
35554
|
}
|
|
@@ -34803,14 +35601,14 @@ async function main() {
|
|
|
34803
35601
|
<key>SoftResourceLimits</key><dict>
|
|
34804
35602
|
<key>NumberOfFiles</key><integer>65536</integer>
|
|
34805
35603
|
</dict>
|
|
34806
|
-
<key>StandardOutPath</key><string>${
|
|
34807
|
-
<key>StandardErrorPath</key><string>${
|
|
35604
|
+
<key>StandardOutPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</string>
|
|
35605
|
+
<key>StandardErrorPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</string>
|
|
34808
35606
|
</dict></plist>
|
|
34809
35607
|
`;
|
|
34810
|
-
const dir =
|
|
34811
|
-
|
|
34812
|
-
const path =
|
|
34813
|
-
|
|
35608
|
+
const dir = join10(homedir10(), "Library", "LaunchAgents");
|
|
35609
|
+
mkdirSync4(dir, { recursive: true });
|
|
35610
|
+
const path = join10(dir, `${SERVICE_LABEL}.plist`);
|
|
35611
|
+
writeFileSync5(path, plist);
|
|
34814
35612
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
34815
35613
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
34816
35614
|
console.log(" file limit raised to 65536 for the host and every session it spawns");
|
|
@@ -34832,7 +35630,7 @@ async function main() {
|
|
|
34832
35630
|
process.exit(1);
|
|
34833
35631
|
}
|
|
34834
35632
|
const name = slotName(slot) ?? slot;
|
|
34835
|
-
console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(
|
|
35633
|
+
console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir10(), "~")}`);
|
|
34836
35634
|
if (!allowed(cwd, { file: workspacesFile() })) {
|
|
34837
35635
|
console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
|
|
34838
35636
|
console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
|
|
@@ -34924,12 +35722,11 @@ async function main() {
|
|
|
34924
35722
|
const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
|
|
34925
35723
|
if (!said) return null;
|
|
34926
35724
|
return /^(y|yes|ok|allow|approve)/i.test(said) ? { allow: true } : { allow: false, reason: said };
|
|
34927
|
-
}
|
|
34928
|
-
question: (q) => askTerminal(`\u2753 ${q}`)
|
|
35725
|
+
}
|
|
34929
35726
|
},
|
|
34930
35727
|
log: (line) => console.log(line)
|
|
34931
35728
|
});
|
|
34932
|
-
const
|
|
35729
|
+
const beat2 = async () => {
|
|
34933
35730
|
try {
|
|
34934
35731
|
await heartbeat();
|
|
34935
35732
|
signedOut = false;
|
|
@@ -34942,8 +35739,8 @@ async function main() {
|
|
|
34942
35739
|
console.log(" paigy-harness pair");
|
|
34943
35740
|
}
|
|
34944
35741
|
};
|
|
34945
|
-
const pulse = setInterval(() => void
|
|
34946
|
-
void
|
|
35742
|
+
const pulse = setInterval(() => void beat2(), 6e4);
|
|
35743
|
+
void beat2();
|
|
34947
35744
|
pulse.unref?.();
|
|
34948
35745
|
process.on("SIGINT", () => {
|
|
34949
35746
|
run.stop();
|