@paigy/harness 0.3.16 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -14
- package/dist/cli.js +1222 -364
- package/dist/main.js +1173 -321
- package/package.json +1 -1
package/dist/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.";
|
|
@@ -25059,7 +25064,13 @@ var init_dist = __esm({
|
|
|
25059
25064
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
25060
25065
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
25061
25066
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
25062
|
-
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional()
|
|
25067
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
25068
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
25069
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
25070
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
25071
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
25072
|
+
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
25073
|
+
options: external_exports.array(OptionSchema).optional()
|
|
25063
25074
|
})).optional(),
|
|
25064
25075
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
25065
25076
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -25455,7 +25466,19 @@ var init_dist = __esm({
|
|
|
25455
25466
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
25456
25467
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
25457
25468
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
25458
|
-
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()
|
|
25459
25482
|
});
|
|
25460
25483
|
COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
25461
25484
|
NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -25633,6 +25656,104 @@ var init_dist = __esm({
|
|
|
25633
25656
|
"other"
|
|
25634
25657
|
// anything else — the note carries it.
|
|
25635
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
|
+
});
|
|
25636
25757
|
TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
|
|
25637
25758
|
KEY_PATH = join22(homedir22(), ".paigy", "key.json");
|
|
25638
25759
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -30239,7 +30360,7 @@ var require_lib = __commonJS({
|
|
|
30239
30360
|
|
|
30240
30361
|
// src/triage/judge.ts
|
|
30241
30362
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
30242
|
-
import { homedir as
|
|
30363
|
+
import { homedir as homedir9 } from "os";
|
|
30243
30364
|
import { delimiter as delimiter3 } from "path";
|
|
30244
30365
|
function pickOllamaModel(tags) {
|
|
30245
30366
|
if (tags.length === 0) return null;
|
|
@@ -30322,7 +30443,7 @@ async function judgeFor(asked2, deps = {}) {
|
|
|
30322
30443
|
const out = spawnSync2(bin, runner.argv(null), {
|
|
30323
30444
|
input: prompt,
|
|
30324
30445
|
encoding: "utf8",
|
|
30325
|
-
cwd:
|
|
30446
|
+
cwd: homedir9(),
|
|
30326
30447
|
timeout: RUN_TIMEOUT_MS,
|
|
30327
30448
|
maxBuffer: 32 * 1024 * 1024,
|
|
30328
30449
|
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
@@ -30364,8 +30485,8 @@ var init_judge = __esm({
|
|
|
30364
30485
|
});
|
|
30365
30486
|
|
|
30366
30487
|
// src/triage/evidence.ts
|
|
30367
|
-
function assigneeToken(
|
|
30368
|
-
const ref =
|
|
30488
|
+
function assigneeToken(note2) {
|
|
30489
|
+
const ref = note2.assignee ?? "";
|
|
30369
30490
|
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
30370
30491
|
}
|
|
30371
30492
|
function words(text) {
|
|
@@ -30393,23 +30514,23 @@ function overlap(a, b) {
|
|
|
30393
30514
|
for (const w of A) if (B.has(w)) shared++;
|
|
30394
30515
|
return shared / (A.size + B.size - shared);
|
|
30395
30516
|
}
|
|
30396
|
-
function supersededBy(
|
|
30397
|
-
const mine = `${
|
|
30398
|
-
const at = Date.parse(
|
|
30517
|
+
function supersededBy(note2, all) {
|
|
30518
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
30519
|
+
const at = Date.parse(note2.createdAt);
|
|
30399
30520
|
let best = null;
|
|
30400
30521
|
for (const other of all) {
|
|
30401
|
-
if (other.id ===
|
|
30522
|
+
if (other.id === note2.id) continue;
|
|
30402
30523
|
if (Date.parse(other.createdAt) <= at) continue;
|
|
30403
30524
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
30404
30525
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
30405
30526
|
}
|
|
30406
30527
|
return best?.note ?? null;
|
|
30407
30528
|
}
|
|
30408
|
-
function doneEcho(
|
|
30409
|
-
const mine = `${
|
|
30529
|
+
function doneEcho(note2, all) {
|
|
30530
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
30410
30531
|
let best = null;
|
|
30411
30532
|
for (const other of all) {
|
|
30412
|
-
if (other.id ===
|
|
30533
|
+
if (other.id === note2.id || other.status !== "done") continue;
|
|
30413
30534
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
30414
30535
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
30415
30536
|
}
|
|
@@ -30463,8 +30584,8 @@ function dossier(notes, connections, now) {
|
|
|
30463
30584
|
today: new Date(now).toISOString().slice(0, 10)
|
|
30464
30585
|
};
|
|
30465
30586
|
}
|
|
30466
|
-
function affinityWhy(
|
|
30467
|
-
const shared =
|
|
30587
|
+
function affinityWhy(note2, agent2) {
|
|
30588
|
+
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
30468
30589
|
if (shared.length && agent2.recentNotes)
|
|
30469
30590
|
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
30470
30591
|
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
@@ -30473,12 +30594,12 @@ function affinityWhy(note, agent2) {
|
|
|
30473
30594
|
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
30474
30595
|
return null;
|
|
30475
30596
|
}
|
|
30476
|
-
function staleWhy(
|
|
30477
|
-
if (
|
|
30478
|
-
if (
|
|
30479
|
-
if (
|
|
30480
|
-
if (
|
|
30481
|
-
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`;
|
|
30482
30603
|
return null;
|
|
30483
30604
|
}
|
|
30484
30605
|
var OPEN_STATUSES, DAY_MS, daysBetween, STOP, SUPERSEDE_AT, RECENT_DAYS;
|
|
@@ -30653,22 +30774,22 @@ function toProposal(verdicts, d, meta) {
|
|
|
30653
30774
|
const stale = [];
|
|
30654
30775
|
const assignments = /* @__PURE__ */ new Map();
|
|
30655
30776
|
for (const v of verdicts) {
|
|
30656
|
-
const
|
|
30657
|
-
if (!
|
|
30658
|
-
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 });
|
|
30659
30780
|
if (v.verdict === "close") {
|
|
30660
|
-
close.push(item(staleWhy(
|
|
30781
|
+
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
30661
30782
|
continue;
|
|
30662
30783
|
}
|
|
30663
30784
|
if (v.verdict === "stale") {
|
|
30664
|
-
stale.push(item(staleWhy(
|
|
30785
|
+
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
30665
30786
|
continue;
|
|
30666
30787
|
}
|
|
30667
30788
|
if (v.verdict !== "assign" || !v.agent) continue;
|
|
30668
30789
|
const agent2 = agentById.get(v.agent);
|
|
30669
30790
|
if (!agent2) continue;
|
|
30670
30791
|
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
30671
|
-
group.notes.push(item(affinityWhy(
|
|
30792
|
+
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
30672
30793
|
assignments.set(agent2.token, group);
|
|
30673
30794
|
}
|
|
30674
30795
|
return {
|
|
@@ -30891,6 +31012,15 @@ function parseArgs(argv) {
|
|
|
30891
31012
|
return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
|
|
30892
31013
|
return args;
|
|
30893
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
|
+
];
|
|
30894
31024
|
var ENABLE_TOOLS_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
30895
31025
|
function onEnter(input, onLine) {
|
|
30896
31026
|
if (!input.isTTY) return () => {
|
|
@@ -30910,10 +31040,10 @@ function onEnter(input, onLine) {
|
|
|
30910
31040
|
init_dist();
|
|
30911
31041
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
30912
31042
|
import { hostname } from "os";
|
|
30913
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
31043
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
30914
31044
|
import { execFile as execFile2, execSync } from "child_process";
|
|
30915
|
-
import { homedir as
|
|
30916
|
-
import { join as
|
|
31045
|
+
import { homedir as homedir10 } from "os";
|
|
31046
|
+
import { join as join10 } from "path";
|
|
30917
31047
|
|
|
30918
31048
|
// src/run.ts
|
|
30919
31049
|
init_dist();
|
|
@@ -30921,29 +31051,34 @@ init_dist();
|
|
|
30921
31051
|
// src/harness/session.ts
|
|
30922
31052
|
import { spawn } from "child_process";
|
|
30923
31053
|
import { existsSync as existsSync3 } from "fs";
|
|
30924
|
-
import { homedir as
|
|
31054
|
+
import { homedir as homedir4 } from "os";
|
|
30925
31055
|
import { resolve, delimiter as delimiter2 } from "path";
|
|
30926
31056
|
|
|
30927
31057
|
// src/harness/acp.ts
|
|
30928
31058
|
var none = { events: [], writes: [] };
|
|
31059
|
+
var NOTHING_TO_RESUME = /* @__PURE__ */ new Set([-32002, -32601]);
|
|
30929
31060
|
function optionFor(options, decision) {
|
|
30930
31061
|
const want = decision.allow ? "allow_once" : "reject_once";
|
|
30931
31062
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
30932
31063
|
}
|
|
30933
31064
|
function createAcpDriver(opts) {
|
|
30934
|
-
return new AcpDriver(opts.cwd, opts.mode, opts.resume);
|
|
31065
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
|
|
30935
31066
|
}
|
|
30936
31067
|
var AcpDriver = class {
|
|
30937
31068
|
/** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
|
|
30938
|
-
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
30939
|
-
|
|
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) {
|
|
30940
31073
|
this.cwd = cwd;
|
|
30941
31074
|
this.mode = mode;
|
|
30942
31075
|
this.resume = resume;
|
|
31076
|
+
this.meta = meta;
|
|
30943
31077
|
}
|
|
30944
31078
|
cwd;
|
|
30945
31079
|
mode;
|
|
30946
31080
|
resume;
|
|
31081
|
+
meta;
|
|
30947
31082
|
nextId = 1;
|
|
30948
31083
|
initId;
|
|
30949
31084
|
sessionNewId;
|
|
@@ -30963,6 +31098,11 @@ var AcpDriver = class {
|
|
|
30963
31098
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
30964
31099
|
text = "";
|
|
30965
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;
|
|
30966
31106
|
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
30967
31107
|
open() {
|
|
30968
31108
|
this.initId = this.nextId++;
|
|
@@ -30994,6 +31134,29 @@ var AcpDriver = class {
|
|
|
30994
31134
|
optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
30995
31135
|
];
|
|
30996
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
|
+
}
|
|
30997
31160
|
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
30998
31161
|
close() {
|
|
30999
31162
|
const writes = [...this.pending.values()].map(
|
|
@@ -31022,20 +31185,26 @@ var AcpDriver = class {
|
|
|
31022
31185
|
if (msg.id === this.initId) {
|
|
31023
31186
|
this.initId = void 0;
|
|
31024
31187
|
if (msg.error) {
|
|
31025
|
-
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}
|
|
31188
|
+
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
|
|
31026
31189
|
}
|
|
31027
31190
|
this.sessionNewId = this.nextId++;
|
|
31028
|
-
return {
|
|
31029
|
-
events: [],
|
|
31030
|
-
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : { id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
|
|
31031
|
-
};
|
|
31191
|
+
return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
|
|
31032
31192
|
}
|
|
31033
31193
|
if (msg.id === this.sessionNewId) {
|
|
31034
31194
|
this.sessionNewId = void 0;
|
|
31195
|
+
const resuming = this.resume !== void 0;
|
|
31196
|
+
if (resuming && msg.error?.code !== void 0 && NOTHING_TO_RESUME.has(msg.error.code)) {
|
|
31197
|
+
this.resume = void 0;
|
|
31198
|
+
this.sessionNewId = this.nextId++;
|
|
31199
|
+
return {
|
|
31200
|
+
events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
|
|
31201
|
+
writes: [frame2(this.opening(this.sessionNewId))]
|
|
31202
|
+
};
|
|
31203
|
+
}
|
|
31035
31204
|
const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
|
|
31036
31205
|
if (!sessionId2) {
|
|
31037
31206
|
return {
|
|
31038
|
-
events: [{ kind: "error", message: `${
|
|
31207
|
+
events: [{ kind: "error", message: `${resuming ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}`, fatal: true }],
|
|
31039
31208
|
writes: []
|
|
31040
31209
|
};
|
|
31041
31210
|
}
|
|
@@ -31054,6 +31223,8 @@ var AcpDriver = class {
|
|
|
31054
31223
|
}
|
|
31055
31224
|
if (msg.id === this.promptId) {
|
|
31056
31225
|
this.promptId = void 0;
|
|
31226
|
+
this.running.clear();
|
|
31227
|
+
this.cancelled = false;
|
|
31057
31228
|
const events = [];
|
|
31058
31229
|
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
31059
31230
|
if (report || this.tools.length) {
|
|
@@ -31085,6 +31256,10 @@ var AcpDriver = class {
|
|
|
31085
31256
|
handleNotification(msg) {
|
|
31086
31257
|
if (msg.method !== "session/update") return none;
|
|
31087
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
|
+
}
|
|
31088
31263
|
switch (update?.sessionUpdate) {
|
|
31089
31264
|
case "agent_message_chunk":
|
|
31090
31265
|
this.text += update.content?.text ?? "";
|
|
@@ -31092,9 +31267,9 @@ var AcpDriver = class {
|
|
|
31092
31267
|
case "tool_call": {
|
|
31093
31268
|
const title = update.title?.trim() || update.kind || "tool";
|
|
31094
31269
|
this.tools.push(title);
|
|
31095
|
-
const
|
|
31270
|
+
const note2 = this.text.slice(this.reportFrom).trim();
|
|
31096
31271
|
this.reportFrom = this.text.length;
|
|
31097
|
-
return { events: [{ kind: "work", tool: title, ...
|
|
31272
|
+
return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
|
|
31098
31273
|
}
|
|
31099
31274
|
default:
|
|
31100
31275
|
return none;
|
|
@@ -31128,6 +31303,12 @@ var AcpDriver = class {
|
|
|
31128
31303
|
writes: []
|
|
31129
31304
|
};
|
|
31130
31305
|
}
|
|
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 };
|
|
31311
|
+
}
|
|
31131
31312
|
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
31132
31313
|
flush() {
|
|
31133
31314
|
if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
|
|
@@ -31147,11 +31328,93 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
31147
31328
|
|
|
31148
31329
|
// src/harness/session.ts
|
|
31149
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
|
|
31150
31400
|
var ADAPTER_BIN = {
|
|
31151
31401
|
claude: "claude-agent-acp",
|
|
31152
31402
|
codex: "codex-acp",
|
|
31153
31403
|
agy: "paigy-agy-acp"
|
|
31154
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;
|
|
31155
31418
|
function splitLines(buffer, chunk) {
|
|
31156
31419
|
const combined = buffer + chunk;
|
|
31157
31420
|
const parts = combined.split("\n");
|
|
@@ -31159,7 +31422,7 @@ function splitLines(buffer, chunk) {
|
|
|
31159
31422
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
31160
31423
|
}
|
|
31161
31424
|
function startSession(opts) {
|
|
31162
|
-
const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/,
|
|
31425
|
+
const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
31163
31426
|
if (!existsSync3(cwd)) {
|
|
31164
31427
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
31165
31428
|
}
|
|
@@ -31173,25 +31436,43 @@ function startSession(opts) {
|
|
|
31173
31436
|
...process.env,
|
|
31174
31437
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
31175
31438
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
31176
|
-
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
|
|
31439
|
+
...opts.resume ?? opts.session ? { PAIGY_SESSION_ID: opts.resume ?? opts.session } : {},
|
|
31177
31440
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
31178
31441
|
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
31179
31442
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
31180
31443
|
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
31181
31444
|
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
31182
|
-
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 } : {}
|
|
31183
31448
|
},
|
|
31184
31449
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31185
31450
|
});
|
|
31451
|
+
let ended2 = false;
|
|
31452
|
+
child.stdin?.on("error", () => {
|
|
31453
|
+
});
|
|
31186
31454
|
const write = (frames) => {
|
|
31455
|
+
if (ended2) return;
|
|
31187
31456
|
for (const f of frames) child.stdin?.write(`${f}
|
|
31188
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);
|
|
31189
31468
|
};
|
|
31190
31469
|
child.stderr?.on("data", (chunk) => {
|
|
31191
31470
|
const text = String(chunk).trim();
|
|
31192
31471
|
if (text) opts.onEvent({ kind: "error", message: text });
|
|
31193
31472
|
});
|
|
31194
31473
|
child.on("exit", (code) => {
|
|
31474
|
+
ended2 = true;
|
|
31475
|
+
clearTimeout(hush);
|
|
31195
31476
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
31196
31477
|
opts.onExit?.();
|
|
31197
31478
|
});
|
|
@@ -31199,16 +31480,18 @@ function startSession(opts) {
|
|
|
31199
31480
|
opts.onEvent({ kind: "error", message: e.message });
|
|
31200
31481
|
opts.onExit?.();
|
|
31201
31482
|
});
|
|
31202
|
-
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
|
+
});
|
|
31203
31489
|
let buffer = "";
|
|
31204
31490
|
child.stdout?.on("data", (chunk) => {
|
|
31491
|
+
listen();
|
|
31205
31492
|
const { lines: lines2, rest } = splitLines(buffer, String(chunk));
|
|
31206
31493
|
buffer = rest;
|
|
31207
|
-
for (const line of lines2)
|
|
31208
|
-
const { events, writes } = driver.handleLine(line);
|
|
31209
|
-
write(writes);
|
|
31210
|
-
for (const event of events) opts.onEvent(event);
|
|
31211
|
-
}
|
|
31494
|
+
for (const line of lines2) apply(driver.handleLine(line));
|
|
31212
31495
|
});
|
|
31213
31496
|
write(driver.open());
|
|
31214
31497
|
return {
|
|
@@ -31220,6 +31503,8 @@ function startSession(opts) {
|
|
|
31220
31503
|
},
|
|
31221
31504
|
stop() {
|
|
31222
31505
|
write(driver.close());
|
|
31506
|
+
ended2 = true;
|
|
31507
|
+
clearTimeout(hush);
|
|
31223
31508
|
child.kill();
|
|
31224
31509
|
}
|
|
31225
31510
|
};
|
|
@@ -32632,8 +32917,13 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
32632
32917
|
}).strict();
|
|
32633
32918
|
var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
32634
32919
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
32635
|
-
var GetGoalSchema2 = external_exports.object({
|
|
32636
|
-
|
|
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.";
|
|
32637
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.`;
|
|
32638
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.";
|
|
32639
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.";
|
|
@@ -32647,6 +32937,7 @@ var AGENT_TOOLS2 = [
|
|
|
32647
32937
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
32648
32938
|
];
|
|
32649
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.";
|
|
32650
32941
|
function entryWords(entry) {
|
|
32651
32942
|
const content = entry.content;
|
|
32652
32943
|
if (content && "sealed" in content) return "";
|
|
@@ -33067,7 +33358,13 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33067
33358
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
33068
33359
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
33069
33360
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
33070
|
-
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
|
|
33361
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
|
|
33362
|
+
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
33363
|
+
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
33364
|
+
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
33365
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
|
|
33366
|
+
* says those, and the list stays small (an `html` is up to 16 KB). */
|
|
33367
|
+
options: external_exports.array(OptionSchema2).optional()
|
|
33071
33368
|
})).optional(),
|
|
33072
33369
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
33073
33370
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -33463,7 +33760,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33463
33760
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
33464
33761
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
33465
33762
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
33466
|
-
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()
|
|
33467
33776
|
});
|
|
33468
33777
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
33469
33778
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -33639,6 +33948,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
33639
33948
|
"other"
|
|
33640
33949
|
// anything else — the note carries it.
|
|
33641
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
|
+
});
|
|
33642
34049
|
|
|
33643
34050
|
// src/paigy/activity.ts
|
|
33644
34051
|
function shortenPaths(s) {
|
|
@@ -33648,8 +34055,8 @@ function shortenPaths(s) {
|
|
|
33648
34055
|
});
|
|
33649
34056
|
}
|
|
33650
34057
|
function workLine(event) {
|
|
33651
|
-
const
|
|
33652
|
-
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, " "));
|
|
33653
34060
|
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
33654
34061
|
}
|
|
33655
34062
|
function pushWork(lines2, line) {
|
|
@@ -33663,127 +34070,131 @@ function sameTail(a, b) {
|
|
|
33663
34070
|
// src/paigy/bridge.ts
|
|
33664
34071
|
init_dist();
|
|
33665
34072
|
|
|
33666
|
-
// src/paigy/
|
|
33667
|
-
|
|
33668
|
-
|
|
33669
|
-
|
|
33670
|
-
|
|
33671
|
-
|
|
33672
|
-
|
|
33673
|
-
|
|
33674
|
-
|
|
33675
|
-
|
|
33676
|
-
|
|
33677
|
-
|
|
33678
|
-
|
|
33679
|
-
|
|
33680
|
-
|
|
33681
|
-
|
|
33682
|
-
|
|
33683
|
-
|
|
33684
|
-
|
|
33685
|
-
|
|
33686
|
-
|
|
33687
|
-
|
|
33688
|
-
|
|
33689
|
-
|
|
33690
|
-
|
|
33691
|
-
/\b(how would you like|how should we|what would you like|what should we)\b/,
|
|
33692
|
-
/\b(please choose|choose one|select one|how do you want)\b/
|
|
33693
|
-
].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) };
|
|
33694
34098
|
}
|
|
33695
|
-
|
|
33696
|
-
|
|
33697
|
-
|
|
33698
|
-
|
|
33699
|
-
|
|
33700
|
-
|
|
33701
|
-
|
|
33702
|
-
|
|
33703
|
-
|
|
33704
|
-
|
|
33705
|
-
|
|
33706
|
-
|
|
33707
|
-
|
|
33708
|
-
|
|
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);
|
|
33709
34115
|
}
|
|
33710
|
-
|
|
33711
|
-
|
|
33712
|
-
|
|
33713
|
-
|
|
33714
|
-
|
|
33715
|
-
|
|
33716
|
-
|
|
33717
|
-
|
|
33718
|
-
const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID4() });
|
|
33719
|
-
brief2 = await rail.claim();
|
|
33720
|
-
if (!brief2.goalId || brief2.revision === void 0) {
|
|
33721
|
-
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
33722
|
-
if (checkCallOnDone(prompt)) state.callOnDone = true;
|
|
33723
|
-
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
33724
|
-
return;
|
|
33725
|
-
}
|
|
33726
|
-
} else {
|
|
33727
|
-
return;
|
|
33728
|
-
}
|
|
33729
|
-
}
|
|
33730
|
-
unheard(brief2, state);
|
|
33731
|
-
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
33732
|
-
else state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
33733
|
-
const outcome = brief2.outcome?.trim() ?? "";
|
|
33734
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
|
|
33735
|
-
const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
|
|
33736
|
-
if (checkCallOnDone(fullText)) state.callOnDone = true;
|
|
33737
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
33738
|
-
return;
|
|
33739
|
-
}
|
|
33740
|
-
const brief = await rail.read(state.goal.id);
|
|
33741
|
-
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
33742
|
-
for (const answer of brief.answers ?? []) {
|
|
33743
|
-
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
33744
|
-
if (!resolve3) continue;
|
|
33745
|
-
state.asks.delete(answer.requestEntryId);
|
|
33746
|
-
resolve3(answer.result);
|
|
33747
|
-
}
|
|
33748
|
-
if (ended(brief.state)) {
|
|
33749
|
-
state.goal = void 0;
|
|
33750
|
-
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
33751
|
-
state.asks?.clear();
|
|
33752
|
-
return;
|
|
33753
|
-
}
|
|
33754
|
-
if (state.asks?.size || !state.resting) return;
|
|
33755
|
-
const words2 = unheard(brief, state);
|
|
33756
|
-
if (words2.length) {
|
|
33757
|
-
const text = words2.join("\n\n");
|
|
33758
|
-
if (checkCallOnDone(text)) state.callOnDone = true;
|
|
33759
|
-
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;
|
|
33760
34124
|
}
|
|
33761
34125
|
}
|
|
33762
|
-
function
|
|
33763
|
-
|
|
33764
|
-
|
|
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) };
|
|
33765
34131
|
}
|
|
33766
|
-
|
|
33767
|
-
const
|
|
33768
|
-
|
|
33769
|
-
|
|
33770
|
-
|
|
33771
|
-
|
|
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);
|
|
33772
34183
|
}
|
|
33773
|
-
|
|
33774
|
-
await (opts.pause ?? wait)();
|
|
33775
|
-
}
|
|
34184
|
+
};
|
|
33776
34185
|
}
|
|
34186
|
+
var LIMITS = episodes();
|
|
33777
34187
|
|
|
33778
34188
|
// src/paigy/bridge.ts
|
|
33779
34189
|
function railFor(opts) {
|
|
33780
34190
|
return {
|
|
33781
|
-
claim: () => claimGoal(
|
|
34191
|
+
claim: (goalId) => claimGoal(goalId, opts),
|
|
33782
34192
|
read: (goalId) => getGoal(goalId, opts),
|
|
33783
34193
|
update: (goalId, input) => updateGoal(goalId, input, opts),
|
|
33784
34194
|
// One read and back: the pump owns the waiting, so a Call must not hold a window here.
|
|
33785
34195
|
contact: (input) => contact(input, { ...opts, waits: false }),
|
|
33786
|
-
create: (input) => createGoal(input, opts)
|
|
34196
|
+
create: (input) => createGoal(input, opts),
|
|
34197
|
+
waiting: () => checkReplies(opts)
|
|
33787
34198
|
};
|
|
33788
34199
|
}
|
|
33789
34200
|
function cancelAsks(state) {
|
|
@@ -33802,27 +34213,13 @@ function decisionFrom(result) {
|
|
|
33802
34213
|
}
|
|
33803
34214
|
return { allow: false, reason: "No approval given" };
|
|
33804
34215
|
}
|
|
33805
|
-
function wordsOf(result) {
|
|
33806
|
-
if (!result || !("kind" in result)) return null;
|
|
33807
|
-
switch (result.kind) {
|
|
33808
|
-
case "text":
|
|
33809
|
-
return result.text.trim() || null;
|
|
33810
|
-
case "confirm":
|
|
33811
|
-
return result.approved ? "yes" : "no";
|
|
33812
|
-
case "one":
|
|
33813
|
-
return result.option.label;
|
|
33814
|
-
case "many":
|
|
33815
|
-
case "rank":
|
|
33816
|
-
return result.options.map((o) => o.label).join(", ");
|
|
33817
|
-
}
|
|
33818
|
-
}
|
|
33819
34216
|
async function ask(state, rail, input) {
|
|
33820
34217
|
if (!state.goal) return null;
|
|
33821
34218
|
try {
|
|
33822
34219
|
const sent = await rail.contact({
|
|
33823
|
-
asks: [{ ask: input.ask, parentId: state.goal.id,
|
|
34220
|
+
asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
|
|
33824
34221
|
waiting: "hard",
|
|
33825
|
-
channel:
|
|
34222
|
+
channel: "call"
|
|
33826
34223
|
});
|
|
33827
34224
|
if ("recorded" in sent) return null;
|
|
33828
34225
|
const entryId = sent.requestEntryIds[0];
|
|
@@ -33833,89 +34230,416 @@ async function ask(state, rail, input) {
|
|
|
33833
34230
|
return null;
|
|
33834
34231
|
}
|
|
33835
34232
|
}
|
|
33836
|
-
function checkCallOnDone(text) {
|
|
33837
|
-
if (!text) return false;
|
|
33838
|
-
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);
|
|
33839
|
-
}
|
|
33840
|
-
function parseQuestionOptions(text) {
|
|
33841
|
-
if (!text) return { question: "" };
|
|
33842
|
-
const trimmed = text.trim();
|
|
33843
|
-
const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
33844
|
-
const optionLines = [];
|
|
33845
|
-
const questionLines = [];
|
|
33846
|
-
for (const line of lines2) {
|
|
33847
|
-
const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
|
|
33848
|
-
if (bulletMatch) {
|
|
33849
|
-
optionLines.push(bulletMatch[1].trim());
|
|
33850
|
-
} else {
|
|
33851
|
-
questionLines.push(line);
|
|
33852
|
-
}
|
|
33853
|
-
}
|
|
33854
|
-
if (optionLines.length >= 2 && optionLines.length <= 6) {
|
|
33855
|
-
return {
|
|
33856
|
-
question: questionLines.join("\n\n").trim() || trimmed,
|
|
33857
|
-
options: optionLines.map((raw) => {
|
|
33858
|
-
const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
|
|
33859
|
-
const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
|
|
33860
|
-
if (colonSplit && colonSplit[1] && colonSplit[2]) {
|
|
33861
|
-
return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
|
|
33862
|
-
}
|
|
33863
|
-
if (parenSplit && parenSplit[1] && parenSplit[2]) {
|
|
33864
|
-
return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
|
|
33865
|
-
}
|
|
33866
|
-
return { label: raw.slice(0, 1e3) };
|
|
33867
|
-
})
|
|
33868
|
-
};
|
|
33869
|
-
}
|
|
33870
|
-
return { question: trimmed };
|
|
33871
|
-
}
|
|
33872
34233
|
async function askPermission(event, state, rail) {
|
|
33873
34234
|
return decisionFrom(await ask(state, rail, {
|
|
33874
34235
|
ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
|
|
33875
34236
|
options: [
|
|
33876
34237
|
{ label: ALLOW, hint: "Permits the agent to run this command" },
|
|
33877
34238
|
{ label: "Deny", hint: "Blocks execution of this command" }
|
|
33878
|
-
]
|
|
33879
|
-
call: true
|
|
33880
|
-
}));
|
|
33881
|
-
}
|
|
33882
|
-
async function askQuestion(question, state, rail, opts) {
|
|
33883
|
-
return wordsOf(await ask(state, rail, {
|
|
33884
|
-
ask: question,
|
|
33885
|
-
call: opts?.call ?? true,
|
|
33886
|
-
options: opts?.options
|
|
34239
|
+
]
|
|
33887
34240
|
}));
|
|
33888
34241
|
}
|
|
33889
34242
|
async function finish(result, state, rail, opts) {
|
|
33890
34243
|
const goal = state.goal;
|
|
33891
34244
|
if (!goal) return;
|
|
33892
|
-
|
|
33893
|
-
|
|
33894
|
-
|
|
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;
|
|
33895
34260
|
try {
|
|
33896
|
-
await rail.
|
|
33897
|
-
const { revision } = await rail.read(goal.id);
|
|
33898
|
-
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." });
|
|
33899
34262
|
} catch (error) {
|
|
33900
|
-
opts?.log?.(`\u26A0
|
|
34263
|
+
opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
|
|
34264
|
+
}
|
|
34265
|
+
}
|
|
34266
|
+
async function cannotWork(report, state, rail, opts) {
|
|
34267
|
+
state.resting = true;
|
|
34268
|
+
try {
|
|
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" });
|
|
34275
|
+
} catch (error) {
|
|
34276
|
+
opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
|
|
33901
34277
|
}
|
|
33902
34278
|
}
|
|
33903
34279
|
async function afterTurn(event, state, rail, opts) {
|
|
33904
|
-
if (
|
|
33905
|
-
|
|
33906
|
-
|
|
33907
|
-
|
|
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;
|
|
33908
34295
|
}
|
|
33909
|
-
|
|
34296
|
+
await finish(event.result, state, rail, opts);
|
|
34297
|
+
state.typed = void 0;
|
|
34298
|
+
state.resting = true;
|
|
34299
|
+
return void 0;
|
|
34300
|
+
}
|
|
33910
34301
|
|
|
33911
|
-
|
|
33912
|
-
|
|
33913
|
-
|
|
33914
|
-
|
|
33915
|
-
|
|
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 }] : [];
|
|
33916
34380
|
});
|
|
33917
|
-
|
|
33918
|
-
|
|
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
|
+
}
|
|
33919
34643
|
}
|
|
33920
34644
|
|
|
33921
34645
|
// src/run.ts
|
|
@@ -33923,11 +34647,34 @@ function runHarness(opts) {
|
|
|
33923
34647
|
let running = true;
|
|
33924
34648
|
let tail = [];
|
|
33925
34649
|
const state = { resting: true };
|
|
34650
|
+
const clock4 = beat();
|
|
33926
34651
|
let session = null;
|
|
33927
|
-
const
|
|
34652
|
+
const speaksAs = opts.resume ?? opts.session;
|
|
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
|
+
}
|
|
33928
34668
|
async function handle(event) {
|
|
33929
34669
|
if (event.kind === "error") {
|
|
33930
34670
|
opts.log(`\u26A0 ${event.message}`);
|
|
34671
|
+
if (event.fatal && running) {
|
|
34672
|
+
opts.log(`\u2716 could not start: ${event.message}`);
|
|
34673
|
+
await cannotWork(`The session could not start, so nothing is working on this yet.
|
|
34674
|
+
|
|
34675
|
+
${event.message}`, state, rail, { log: opts.log });
|
|
34676
|
+
stop2();
|
|
34677
|
+
}
|
|
33931
34678
|
return;
|
|
33932
34679
|
}
|
|
33933
34680
|
if (event.kind === "permission") {
|
|
@@ -33944,6 +34691,7 @@ function runHarness(opts) {
|
|
|
33944
34691
|
return;
|
|
33945
34692
|
}
|
|
33946
34693
|
if (event.kind === "work") {
|
|
34694
|
+
if (state.carriedOn) runningAgain();
|
|
33947
34695
|
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
33948
34696
|
tail = pushWork(tail, workLine(event));
|
|
33949
34697
|
return;
|
|
@@ -33951,20 +34699,10 @@ function runHarness(opts) {
|
|
|
33951
34699
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
33952
34700
|
if (event.kind === "idle") {
|
|
33953
34701
|
tail = [];
|
|
33954
|
-
|
|
33955
|
-
|
|
33956
|
-
|
|
33957
|
-
|
|
33958
|
-
state.resting = false;
|
|
33959
|
-
session.send(local);
|
|
33960
|
-
return;
|
|
33961
|
-
}
|
|
33962
|
-
}
|
|
33963
|
-
const reply = await afterTurn(event, state, rail, { log: opts.log });
|
|
33964
|
-
if (reply && session && running) {
|
|
33965
|
-
opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
|
|
33966
|
-
session.send(reply);
|
|
33967
|
-
}
|
|
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");
|
|
33968
34706
|
}
|
|
33969
34707
|
}
|
|
33970
34708
|
session = startSession({
|
|
@@ -33973,44 +34711,54 @@ function runHarness(opts) {
|
|
|
33973
34711
|
mode: opts.mode,
|
|
33974
34712
|
...opts.token ? { token: opts.token } : {},
|
|
33975
34713
|
...opts.resume ? { resume: opts.resume } : {},
|
|
34714
|
+
...opts.session ? { session: opts.session } : {},
|
|
33976
34715
|
...opts.bin ? { bin: opts.bin } : {},
|
|
33977
|
-
...
|
|
34716
|
+
...inbox ? { inbox: inbox.dir } : {},
|
|
34717
|
+
onExit: () => {
|
|
34718
|
+
inbox?.close();
|
|
34719
|
+
opts.onExit?.();
|
|
34720
|
+
},
|
|
33978
34721
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
33979
34722
|
});
|
|
33980
|
-
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(
|
|
33981
34724
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
33982
34725
|
);
|
|
33983
34726
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
34727
|
+
function stop2() {
|
|
34728
|
+
running = false;
|
|
34729
|
+
clock4.nudge("wake");
|
|
34730
|
+
tail = [];
|
|
34731
|
+
cancelAsks(state);
|
|
34732
|
+
session?.stop();
|
|
34733
|
+
session = null;
|
|
34734
|
+
inbox?.close();
|
|
34735
|
+
opts.log("\u25A0 stopped");
|
|
34736
|
+
}
|
|
33984
34737
|
return {
|
|
33985
34738
|
send(text) {
|
|
33986
34739
|
session?.send(text);
|
|
33987
34740
|
},
|
|
33988
34741
|
working: () => running && state.resting !== true,
|
|
33989
|
-
|
|
33990
|
-
|
|
33991
|
-
|
|
33992
|
-
|
|
33993
|
-
cancelAsks(state);
|
|
33994
|
-
session?.stop();
|
|
33995
|
-
session = null;
|
|
33996
|
-
opts.log("\u25A0 stopped");
|
|
33997
|
-
}
|
|
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"),
|
|
34745
|
+
stop: stop2
|
|
33998
34746
|
};
|
|
33999
34747
|
}
|
|
34000
34748
|
|
|
34001
34749
|
// src/host.ts
|
|
34002
34750
|
init_dist();
|
|
34003
34751
|
init_catalog();
|
|
34004
|
-
import { readFileSync as
|
|
34005
|
-
import { homedir as
|
|
34006
|
-
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";
|
|
34007
34755
|
|
|
34008
34756
|
// src/workspaces.ts
|
|
34009
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
34010
|
-
import { dirname as dirname2, join as
|
|
34011
|
-
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";
|
|
34012
34760
|
function workspacesFile() {
|
|
34013
|
-
return
|
|
34761
|
+
return join5(homedir5(), ".paigy", "workspaces.json");
|
|
34014
34762
|
}
|
|
34015
34763
|
function listWorkspaces(deps) {
|
|
34016
34764
|
try {
|
|
@@ -34021,31 +34769,94 @@ function listWorkspaces(deps) {
|
|
|
34021
34769
|
}
|
|
34022
34770
|
}
|
|
34023
34771
|
function addWorkspace(dir, deps) {
|
|
34024
|
-
const next = resolve2(dir.replace(/^~(?=$|\/)/,
|
|
34772
|
+
const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir5()));
|
|
34025
34773
|
const all = listWorkspaces(deps);
|
|
34026
34774
|
if (!all.includes(next)) {
|
|
34027
34775
|
all.push(next);
|
|
34028
|
-
|
|
34029
|
-
|
|
34776
|
+
mkdirSync3(dirname2(deps.file), { recursive: true });
|
|
34777
|
+
writeFileSync3(deps.file, JSON.stringify(all, null, 2));
|
|
34030
34778
|
}
|
|
34031
34779
|
return all;
|
|
34032
34780
|
}
|
|
34033
34781
|
function allowed(cwd, deps) {
|
|
34034
|
-
const target = resolve2(cwd.replace(/^~(?=$|\/)/,
|
|
34782
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir5()));
|
|
34035
34783
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
34036
34784
|
}
|
|
34037
34785
|
function resolveWakeDir(pinned, deps) {
|
|
34038
34786
|
if (pinned && allowed(pinned, deps)) {
|
|
34039
|
-
const dir = resolve2(pinned.replace(/^~(?=$|\/)/,
|
|
34787
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir5()));
|
|
34040
34788
|
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
34041
34789
|
}
|
|
34042
34790
|
return listWorkspaces(deps)[0];
|
|
34043
34791
|
}
|
|
34044
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
|
+
|
|
34045
34856
|
// src/owner.ts
|
|
34046
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
34047
|
-
import { homedir as
|
|
34048
|
-
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";
|
|
34049
34860
|
var HEAD_BYTES = 64e3;
|
|
34050
34861
|
function readHead(file) {
|
|
34051
34862
|
const fd = openSync2(file, "r");
|
|
@@ -34061,8 +34872,8 @@ function ownerSession(slot, deps = {}) {
|
|
|
34061
34872
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
34062
34873
|
if (!m) return null;
|
|
34063
34874
|
const prefix = m[1];
|
|
34064
|
-
const root = deps.projectsDir ??
|
|
34065
|
-
const list = deps.list ?? ((dir) =>
|
|
34875
|
+
const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
|
|
34876
|
+
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
34066
34877
|
const head = deps.head ?? readHead;
|
|
34067
34878
|
let folders;
|
|
34068
34879
|
try {
|
|
@@ -34073,7 +34884,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34073
34884
|
for (const folder of folders) {
|
|
34074
34885
|
let files;
|
|
34075
34886
|
try {
|
|
34076
|
-
files = list(
|
|
34887
|
+
files = list(join7(root, folder));
|
|
34077
34888
|
} catch {
|
|
34078
34889
|
continue;
|
|
34079
34890
|
}
|
|
@@ -34081,7 +34892,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34081
34892
|
if (!file) continue;
|
|
34082
34893
|
let text;
|
|
34083
34894
|
try {
|
|
34084
|
-
text = head(
|
|
34895
|
+
text = head(join7(root, folder, file));
|
|
34085
34896
|
} catch {
|
|
34086
34897
|
continue;
|
|
34087
34898
|
}
|
|
@@ -34101,7 +34912,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34101
34912
|
// src/update.ts
|
|
34102
34913
|
import { execFile } from "child_process";
|
|
34103
34914
|
import { createRequire } from "module";
|
|
34104
|
-
import { dirname as dirname3, join as
|
|
34915
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
34105
34916
|
import { promisify } from "util";
|
|
34106
34917
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
34107
34918
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -34128,7 +34939,7 @@ async function latestVersion() {
|
|
|
34128
34939
|
}
|
|
34129
34940
|
async function install(version) {
|
|
34130
34941
|
const bin = dirname3(process.execPath);
|
|
34131
|
-
await promisify(execFile)(
|
|
34942
|
+
await promisify(execFile)(join8(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
34132
34943
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
34133
34944
|
timeout: 5 * 6e4
|
|
34134
34945
|
});
|
|
@@ -34179,24 +34990,43 @@ function selfUpdater(deps) {
|
|
|
34179
34990
|
}
|
|
34180
34991
|
|
|
34181
34992
|
// src/host.ts
|
|
34182
|
-
var HOST_FILE =
|
|
34993
|
+
var HOST_FILE = join9(homedir8(), ".paigy", "host.json");
|
|
34183
34994
|
var runKey = (slot) => `slot:${slot}`;
|
|
34184
34995
|
var claimedRun = (sessionId2) => {
|
|
34185
34996
|
const slot = sessionSlot(sessionId2);
|
|
34186
34997
|
return { slot, key: runKey(slot) };
|
|
34187
34998
|
};
|
|
34188
|
-
|
|
34189
|
-
|
|
34999
|
+
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
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);
|
|
34190
35014
|
}
|
|
34191
35015
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
34192
|
-
function wakeAction(live, now, serverSeenAt) {
|
|
35016
|
+
function wakeAction(live, now, serverSeenAt, here = {}) {
|
|
34193
35017
|
if (live) return { action: "skip", why: "run" };
|
|
34194
|
-
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
|
+
}
|
|
34195
35023
|
return { action: "spawn", why: "nobody" };
|
|
34196
35024
|
}
|
|
34197
35025
|
function startHost(opts) {
|
|
34198
35026
|
const runs = /* @__PURE__ */ new Map();
|
|
34199
35027
|
const asHost = { token: opts.token };
|
|
35028
|
+
const goneAt = /* @__PURE__ */ new Map();
|
|
35029
|
+
const heldHere = /* @__PURE__ */ new Set();
|
|
34200
35030
|
const refreshIdentities = async () => {
|
|
34201
35031
|
for (const slot of listSlots()) {
|
|
34202
35032
|
if (slot === "Desktop") continue;
|
|
@@ -34216,7 +35046,7 @@ function startHost(opts) {
|
|
|
34216
35046
|
working: () => [...runs.values()].filter((r) => r.run.working()).length,
|
|
34217
35047
|
log: opts.log
|
|
34218
35048
|
});
|
|
34219
|
-
const
|
|
35049
|
+
const beat2 = () => {
|
|
34220
35050
|
void refreshIdentities();
|
|
34221
35051
|
void update().catch(() => {
|
|
34222
35052
|
});
|
|
@@ -34243,11 +35073,13 @@ function startHost(opts) {
|
|
|
34243
35073
|
mode: "bypass",
|
|
34244
35074
|
prompt: "",
|
|
34245
35075
|
token: spec.token,
|
|
34246
|
-
|
|
35076
|
+
...launchRun(spec),
|
|
34247
35077
|
onExit: () => {
|
|
34248
|
-
runs
|
|
35078
|
+
evict(runs, key, run);
|
|
35079
|
+
goneAt.set(slot, Date.now());
|
|
34249
35080
|
opts.log(`\u2716 ${label} ended`);
|
|
34250
35081
|
},
|
|
35082
|
+
name: label,
|
|
34251
35083
|
log
|
|
34252
35084
|
});
|
|
34253
35085
|
runs.set(key, { run, label, token: spec.token });
|
|
@@ -34274,7 +35106,13 @@ function startHost(opts) {
|
|
|
34274
35106
|
return presence;
|
|
34275
35107
|
}
|
|
34276
35108
|
let lastPresence = /* @__PURE__ */ new Map();
|
|
34277
|
-
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") {
|
|
34278
35116
|
{
|
|
34279
35117
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
34280
35118
|
const key = runKey(slot);
|
|
@@ -34282,19 +35120,42 @@ function startHost(opts) {
|
|
|
34282
35120
|
const live = runs.get(key);
|
|
34283
35121
|
const tokenId = slotIdentity(slot).tokenId;
|
|
34284
35122
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
34285
|
-
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) });
|
|
34286
35127
|
if (decided.action === "skip") {
|
|
34287
|
-
|
|
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`);
|
|
34288
35131
|
return;
|
|
34289
35132
|
}
|
|
35133
|
+
const label = slotName(slot) ?? slot;
|
|
35134
|
+
const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
|
|
34290
35135
|
const token = readToken(slot);
|
|
34291
|
-
if (!token)
|
|
35136
|
+
if (!token) {
|
|
35137
|
+
skip("has no token on this machine");
|
|
35138
|
+
return;
|
|
35139
|
+
}
|
|
34292
35140
|
const owner = ownerSession(slot);
|
|
34293
|
-
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
|
+
}
|
|
34294
35145
|
const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
34295
|
-
if (!workspace)
|
|
34296
|
-
|
|
34297
|
-
|
|
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
|
+
}
|
|
34298
35159
|
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
34299
35160
|
const run = runHarness({
|
|
34300
35161
|
harness,
|
|
@@ -34303,17 +35164,20 @@ function startHost(opts) {
|
|
|
34303
35164
|
prompt: "",
|
|
34304
35165
|
token,
|
|
34305
35166
|
...owner ? { resume: owner.sessionId } : {},
|
|
34306
|
-
// 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.
|
|
34307
35169
|
onExit: () => {
|
|
34308
|
-
runs
|
|
35170
|
+
evict(runs, key, run);
|
|
35171
|
+
goneAt.set(slot, Date.now());
|
|
34309
35172
|
opts.log(`\u2716 ${label} ended`);
|
|
34310
35173
|
},
|
|
35174
|
+
name: label,
|
|
34311
35175
|
log
|
|
34312
35176
|
});
|
|
34313
35177
|
runs.set(key, { run, label, token });
|
|
34314
35178
|
void heartbeat(void 0, { token }).catch(() => {
|
|
34315
35179
|
});
|
|
34316
|
-
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)" : ""}`);
|
|
34317
35181
|
}
|
|
34318
35182
|
}
|
|
34319
35183
|
const listening = /* @__PURE__ */ new Map();
|
|
@@ -34333,7 +35197,7 @@ function startHost(opts) {
|
|
|
34333
35197
|
joining.add(slot);
|
|
34334
35198
|
try {
|
|
34335
35199
|
const sub = await subscribeWake(
|
|
34336
|
-
() => void
|
|
35200
|
+
() => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
|
|
34337
35201
|
}),
|
|
34338
35202
|
{ token }
|
|
34339
35203
|
);
|
|
@@ -34347,8 +35211,8 @@ function startHost(opts) {
|
|
|
34347
35211
|
}
|
|
34348
35212
|
}
|
|
34349
35213
|
async function sweepSlots() {
|
|
34350
|
-
const
|
|
34351
|
-
for (const slot of listSlots()) await wakeSlot(slot,
|
|
35214
|
+
const seen = await evidence();
|
|
35215
|
+
for (const slot of listSlots()) await wakeSlot(slot, seen);
|
|
34352
35216
|
}
|
|
34353
35217
|
const ACTIVITY_MS = 2e3;
|
|
34354
35218
|
const published = /* @__PURE__ */ new Map();
|
|
@@ -34373,12 +35237,12 @@ function startHost(opts) {
|
|
|
34373
35237
|
};
|
|
34374
35238
|
const publish = () => {
|
|
34375
35239
|
try {
|
|
34376
|
-
|
|
35240
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
34377
35241
|
} catch {
|
|
34378
35242
|
}
|
|
34379
35243
|
};
|
|
34380
|
-
|
|
34381
|
-
const pulse = setInterval(
|
|
35244
|
+
beat2();
|
|
35245
|
+
const pulse = setInterval(beat2, 6e4);
|
|
34382
35246
|
const spawnPoll = setInterval(() => {
|
|
34383
35247
|
void claimAndSpawn();
|
|
34384
35248
|
publish();
|
|
@@ -34404,10 +35268,10 @@ function startHost(opts) {
|
|
|
34404
35268
|
// them live, and a run mid-turn makes it working.
|
|
34405
35269
|
roster: () => {
|
|
34406
35270
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
34407
|
-
const
|
|
34408
|
-
for (const key of listSlots()) if (key !== "Desktop")
|
|
34409
|
-
for (const label of live.keys()) if (!
|
|
34410
|
-
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]) => {
|
|
34411
35275
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
34412
35276
|
return {
|
|
34413
35277
|
name,
|
|
@@ -34424,7 +35288,7 @@ function startHost(opts) {
|
|
|
34424
35288
|
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
34425
35289
|
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
34426
35290
|
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
34427
|
-
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,
|
|
34428
35292
|
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
34429
35293
|
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
34430
35294
|
working: live.get(name)?.working() ?? false
|
|
@@ -34440,7 +35304,7 @@ function startHost(opts) {
|
|
|
34440
35304
|
streamActivity();
|
|
34441
35305
|
clearInterval(activityTick);
|
|
34442
35306
|
try {
|
|
34443
|
-
|
|
35307
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
34444
35308
|
} catch {
|
|
34445
35309
|
}
|
|
34446
35310
|
}
|
|
@@ -34545,18 +35409,18 @@ async function pairThisMachine() {
|
|
|
34545
35409
|
}
|
|
34546
35410
|
}
|
|
34547
35411
|
function forwardCodexThreadId() {
|
|
34548
|
-
const cfg =
|
|
35412
|
+
const cfg = join10(process.env.CODEX_HOME || join10(homedir10(), ".codex"), "config.toml");
|
|
34549
35413
|
if (!existsSync5(cfg)) return;
|
|
34550
|
-
const text =
|
|
35414
|
+
const text = readFileSync6(cfg, "utf8");
|
|
34551
35415
|
const head = "[mcp_servers.paigy]\n";
|
|
34552
35416
|
const at = text.indexOf(head);
|
|
34553
35417
|
if (at < 0) return;
|
|
34554
|
-
const
|
|
35418
|
+
const block2 = text.slice(at + head.length, (() => {
|
|
34555
35419
|
const n = text.indexOf("\n[", at + head.length);
|
|
34556
35420
|
return n < 0 ? text.length : n;
|
|
34557
35421
|
})());
|
|
34558
|
-
if (/^\s*env_vars\s*=/m.test(
|
|
34559
|
-
|
|
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));
|
|
34560
35424
|
}
|
|
34561
35425
|
async function main() {
|
|
34562
35426
|
const parsed = parseArgs(process.argv.slice(2));
|
|
@@ -34647,7 +35511,7 @@ async function main() {
|
|
|
34647
35511
|
const wsDeps = { file: workspacesFile() };
|
|
34648
35512
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
34649
35513
|
if (!listWorkspaces(wsDeps).length) {
|
|
34650
|
-
const guess =
|
|
35514
|
+
const guess = join10(homedir10(), "projects");
|
|
34651
35515
|
if (existsSync5(guess)) {
|
|
34652
35516
|
addWorkspace(guess, wsDeps);
|
|
34653
35517
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -34660,23 +35524,14 @@ async function main() {
|
|
|
34660
35524
|
console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
|
|
34661
35525
|
}
|
|
34662
35526
|
} else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
|
|
34663
|
-
const
|
|
34664
|
-
|
|
34665
|
-
// Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
|
|
34666
|
-
// a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
|
|
34667
|
-
// written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
|
|
34668
|
-
// all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
|
|
34669
|
-
{ cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
|
|
34670
|
-
{ cli: "agy", name: "Antigravity" }
|
|
34671
|
-
];
|
|
34672
|
-
for (const t of TERMINAL_AGENTS) {
|
|
34673
|
-
if (!which(t.cli) || !t.wire) continue;
|
|
35527
|
+
for (const t of MCP_WIRES) {
|
|
35528
|
+
if (!which(t.cli)) continue;
|
|
34674
35529
|
try {
|
|
34675
35530
|
execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
|
|
34676
35531
|
if (t.cli === "codex") forwardCodexThreadId();
|
|
34677
35532
|
console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
|
|
34678
35533
|
} catch (e) {
|
|
34679
|
-
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}`);
|
|
34680
35535
|
}
|
|
34681
35536
|
}
|
|
34682
35537
|
if (which("claude")) {
|
|
@@ -34688,12 +35543,12 @@ async function main() {
|
|
|
34688
35543
|
}
|
|
34689
35544
|
}
|
|
34690
35545
|
try {
|
|
34691
|
-
const settings =
|
|
35546
|
+
const settings = join10(homedir10(), ".claude", "settings.json");
|
|
34692
35547
|
if (which("claude") && existsSync5(settings)) {
|
|
34693
|
-
const cfg = JSON.parse(
|
|
35548
|
+
const cfg = JSON.parse(readFileSync6(settings, "utf8"));
|
|
34694
35549
|
if (!cfg["statusLine"]) {
|
|
34695
35550
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
34696
|
-
|
|
35551
|
+
writeFileSync5(settings, JSON.stringify(cfg, null, 2));
|
|
34697
35552
|
console.log("\u2713 statusline wired");
|
|
34698
35553
|
}
|
|
34699
35554
|
}
|
|
@@ -34746,14 +35601,14 @@ async function main() {
|
|
|
34746
35601
|
<key>SoftResourceLimits</key><dict>
|
|
34747
35602
|
<key>NumberOfFiles</key><integer>65536</integer>
|
|
34748
35603
|
</dict>
|
|
34749
|
-
<key>StandardOutPath</key><string>${
|
|
34750
|
-
<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>
|
|
34751
35606
|
</dict></plist>
|
|
34752
35607
|
`;
|
|
34753
|
-
const dir =
|
|
34754
|
-
|
|
34755
|
-
const path =
|
|
34756
|
-
|
|
35608
|
+
const dir = join10(homedir10(), "Library", "LaunchAgents");
|
|
35609
|
+
mkdirSync4(dir, { recursive: true });
|
|
35610
|
+
const path = join10(dir, `${SERVICE_LABEL}.plist`);
|
|
35611
|
+
writeFileSync5(path, plist);
|
|
34757
35612
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
34758
35613
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
34759
35614
|
console.log(" file limit raised to 65536 for the host and every session it spawns");
|
|
@@ -34775,7 +35630,7 @@ async function main() {
|
|
|
34775
35630
|
process.exit(1);
|
|
34776
35631
|
}
|
|
34777
35632
|
const name = slotName(slot) ?? slot;
|
|
34778
|
-
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(), "~")}`);
|
|
34779
35634
|
if (!allowed(cwd, { file: workspacesFile() })) {
|
|
34780
35635
|
console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
|
|
34781
35636
|
console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
|
|
@@ -34799,7 +35654,11 @@ async function main() {
|
|
|
34799
35654
|
if (parsed.hatch) {
|
|
34800
35655
|
overrideToken(readToken("Desktop") || null);
|
|
34801
35656
|
const minted = await hatch(parsed.hatch, parsed.voice ?? null);
|
|
34802
|
-
saveToken({ access_token: minted.token, name: minted.name, device: null }, parsed.slot ?? minted.name);
|
|
35657
|
+
saveToken({ access_token: minted.token, name: minted.name ?? parsed.hatch, device: null }, parsed.slot ?? minted.name ?? parsed.hatch);
|
|
35658
|
+
if (!minted.name) {
|
|
35659
|
+
console.log(`\u2713 hatched, unnamed \u2014 you already have an agent called "${parsed.hatch}". Pick another name in the app's Agents screen.`);
|
|
35660
|
+
return;
|
|
35661
|
+
}
|
|
34803
35662
|
console.log(`\u2713 hatched "${minted.name}" \u2014 it's on your phone's Agents screen now.`);
|
|
34804
35663
|
console.log(` speak as it: PAIGY_AGENT="${minted.name}" <any paigy tool>`);
|
|
34805
35664
|
console.log(` run with it: paigy-harness --identity "${minted.name}" --harness codex "\u2026"`);
|
|
@@ -34863,12 +35722,11 @@ async function main() {
|
|
|
34863
35722
|
const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
|
|
34864
35723
|
if (!said) return null;
|
|
34865
35724
|
return /^(y|yes|ok|allow|approve)/i.test(said) ? { allow: true } : { allow: false, reason: said };
|
|
34866
|
-
}
|
|
34867
|
-
question: (q) => askTerminal(`\u2753 ${q}`)
|
|
35725
|
+
}
|
|
34868
35726
|
},
|
|
34869
35727
|
log: (line) => console.log(line)
|
|
34870
35728
|
});
|
|
34871
|
-
const
|
|
35729
|
+
const beat2 = async () => {
|
|
34872
35730
|
try {
|
|
34873
35731
|
await heartbeat();
|
|
34874
35732
|
signedOut = false;
|
|
@@ -34881,8 +35739,8 @@ async function main() {
|
|
|
34881
35739
|
console.log(" paigy-harness pair");
|
|
34882
35740
|
}
|
|
34883
35741
|
};
|
|
34884
|
-
const pulse = setInterval(() => void
|
|
34885
|
-
void
|
|
35742
|
+
const pulse = setInterval(() => void beat2(), 6e4);
|
|
35743
|
+
void beat2();
|
|
34886
35744
|
pulse.unref?.();
|
|
34887
35745
|
process.on("SIGINT", () => {
|
|
34888
35746
|
run.stop();
|