@paigy/harness 0.3.37 → 0.3.39
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/dist/cli.js +428 -70
- package/dist/main.js +2694 -2345
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -298,8 +298,8 @@ var init_ZodError = __esm({
|
|
|
298
298
|
"not_finite"
|
|
299
299
|
]);
|
|
300
300
|
quotelessJson = (obj) => {
|
|
301
|
-
const
|
|
302
|
-
return
|
|
301
|
+
const json3 = JSON.stringify(obj, null, 2);
|
|
302
|
+
return json3.replace(/"([^"]+)":/g, "$1:");
|
|
303
303
|
};
|
|
304
304
|
ZodError = class _ZodError extends Error {
|
|
305
305
|
get errors() {
|
|
@@ -22777,10 +22777,47 @@ function claudeConversation() {
|
|
|
22777
22777
|
if (!envSession("CLAUDE_CODE_SESSION_ID") || process.env.CLAUDE_CODE_CHILD_SESSION) return void 0;
|
|
22778
22778
|
const dir = envSession("CLAUDE_CONFIG_DIR") ?? join2(homedir2(), ".claude");
|
|
22779
22779
|
for (const pid of ancestors()) {
|
|
22780
|
+
let text;
|
|
22780
22781
|
try {
|
|
22781
|
-
|
|
22782
|
-
return typeof sessionId2 === "string" && sessionId2.trim() ? sessionId2 : void 0;
|
|
22782
|
+
text = readFileSync(join2(dir, "sessions", `${pid}.json`), "utf8");
|
|
22783
22783
|
} catch {
|
|
22784
|
+
continue;
|
|
22785
|
+
}
|
|
22786
|
+
const record = parseSessionRecord(text);
|
|
22787
|
+
if (!record) continue;
|
|
22788
|
+
return typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId : void 0;
|
|
22789
|
+
}
|
|
22790
|
+
return void 0;
|
|
22791
|
+
}
|
|
22792
|
+
function parseSessionRecord(text) {
|
|
22793
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
22794
|
+
try {
|
|
22795
|
+
return object(JSON.parse(text));
|
|
22796
|
+
} catch {
|
|
22797
|
+
}
|
|
22798
|
+
let depth = 0;
|
|
22799
|
+
let start = -1;
|
|
22800
|
+
let quoted = false;
|
|
22801
|
+
let escaped = false;
|
|
22802
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
22803
|
+
const c = text[i];
|
|
22804
|
+
if (quoted) {
|
|
22805
|
+
if (escaped) escaped = false;
|
|
22806
|
+
else if (c === "\\") escaped = true;
|
|
22807
|
+
else if (c === '"') quoted = false;
|
|
22808
|
+
} else if (c === '"') quoted = true;
|
|
22809
|
+
else if (c === "{") {
|
|
22810
|
+
if (depth === 0) start = i;
|
|
22811
|
+
depth += 1;
|
|
22812
|
+
} else if (c === "}" && depth > 0) {
|
|
22813
|
+
depth -= 1;
|
|
22814
|
+
if (depth === 0) {
|
|
22815
|
+
try {
|
|
22816
|
+
return object(JSON.parse(text.slice(start, i + 1)));
|
|
22817
|
+
} catch {
|
|
22818
|
+
return void 0;
|
|
22819
|
+
}
|
|
22820
|
+
}
|
|
22784
22821
|
}
|
|
22785
22822
|
}
|
|
22786
22823
|
return void 0;
|
|
@@ -23887,14 +23924,18 @@ async function hatch(name, voice = null, opts = {}) {
|
|
|
23887
23924
|
}
|
|
23888
23925
|
async function whoAmI(opts = {}) {
|
|
23889
23926
|
try {
|
|
23890
|
-
|
|
23891
|
-
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23892
|
-
});
|
|
23893
|
-
return res.ok ? await res.json() : null;
|
|
23927
|
+
return await readIdentity(opts);
|
|
23894
23928
|
} catch {
|
|
23895
23929
|
return null;
|
|
23896
23930
|
}
|
|
23897
23931
|
}
|
|
23932
|
+
async function readIdentity(opts = {}) {
|
|
23933
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/identity`, {
|
|
23934
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23935
|
+
}));
|
|
23936
|
+
if (!res.ok) await fail("read_identity", res);
|
|
23937
|
+
return await res.json();
|
|
23938
|
+
}
|
|
23898
23939
|
async function claimSessions(opts = {}) {
|
|
23899
23940
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
23900
23941
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
@@ -24254,7 +24295,7 @@ ${block2}`;
|
|
|
24254
24295
|
}
|
|
24255
24296
|
return text.slice(0, start) + updated + text.slice(end);
|
|
24256
24297
|
}
|
|
24257
|
-
var import_undici, require2, CODEX_ENV, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, CLIENT, 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, OPTION_LABEL_MAX, OPTION_HINT_MAX, OptionSchema, OptionInputSchema, NIGHT, ASK_MAX, UNITS_MAX, UNIT_TITLE_MAX, UNIT_BODY_MAX, UnitInputSchema, questionHandle, NodeRef, BlockedAction, reason, actionFits, ACTION_FITS, userExplicitlyRequested, units, UpdateInputSchema, questionFields, refineQuestion, QuestionInputSchema, QuestionWireSchema, AnswerInputSchema, QuestionDependencySchema, sendFields, ContactSchema, sendsAnything, ContactWireSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, UpdateGoalSchema, changedGoal, goalTitle, GOAL_OUTCOME_MAX, goalOutcome, CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange, read, GoalChangeSchema, editNames, ManageGoalsSchema, ManageGoalsToolSchema, MANAGE_GOALS_DESCRIPTION, GetGoalSchema, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, CheckActivitySchema, FEEDBACK_TEXT_MAX, SendFeedbackSchema, SEND_FEEDBACK_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, id, key, participant, SourceSchema, ResultRefSchema, NodeRefSchema, DependencySchema, goalState, existingGoalChanges, BrainGoalChangeSchema, option, QuestionChangeSchema, lessonScope, LessonChangeSchema, BrainNextSchema, messageBase, BrainMessageSchema, BrainSearchSchema, BrainResultSchema, str, strs, CompactResultSchema, COMPACT_RESULT_JSON_SCHEMA, str2, strs2, TalkerReplySchema, TALKER_REPLY_JSON_SCHEMA, str3, strs3, FilerReplySchema, FILER_REPLY_JSON_SCHEMA, ExplainerReplySchema, EXPLAINER_REPLY_JSON_SCHEMA, DISTILLER_DEFECTS, DistillerReplySchema, DISTILLER_REPLY_JSON_SCHEMA, TIDY_AUDIENCES, TidyReplySchema, TIDY_REPLY_JSON_SCHEMA, LIVE_MS, WORKING_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,
|
|
24298
|
+
var import_undici, require2, CODEX_ENV, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, CLIENT, 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, OPTION_LABEL_MAX, OPTION_HINT_MAX, OptionSchema, OptionInputSchema, NIGHT, ASKING_RULES, ASK_MAX, UNITS_MAX, UNIT_TITLE_MAX, UNIT_BODY_MAX, UnitInputSchema, questionHandle, NodeRef, BlockedAction, reason, actionFits, ACTION_FITS, userExplicitlyRequested, units, UpdateInputSchema, questionFields, refineQuestion, QuestionInputSchema, QuestionWireSchema, AnswerInputSchema, QuestionDependencySchema, sendFields, ContactSchema, sendsAnything, ContactWireSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, UpdateGoalSchema, changedGoal, goalTitle, GOAL_OUTCOME_MAX, goalOutcome, CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange, read, GoalChangeSchema, editNames, ManageGoalsSchema, ManageGoalsToolSchema, MANAGE_GOALS_DESCRIPTION, GetGoalSchema, GetDeliverySchema, GET_DELIVERY_DESCRIPTION, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, ConfigureIdentitySchema, CONFIGURE_IDENTITY_DESCRIPTION, CheckActivitySchema, FEEDBACK_TEXT_MAX, SendFeedbackSchema, SEND_FEEDBACK_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, id, key, participant, SourceSchema, ResultRefSchema, NodeRefSchema, DependencySchema, goalState, existingGoalChanges, BrainGoalChangeSchema, option, QuestionChangeSchema, lessonScope, LessonChangeSchema, BrainNextSchema, messageBase, BrainMessageSchema, BrainSearchSchema, BrainResultSchema, str, strs, CompactResultSchema, COMPACT_RESULT_JSON_SCHEMA, str2, strs2, TalkerReplySchema, TALKER_REPLY_JSON_SCHEMA, str3, strs3, FilerReplySchema, FILER_REPLY_JSON_SCHEMA, ExplainerReplySchema, EXPLAINER_REPLY_JSON_SCHEMA, DISTILLER_DEFECTS, DistillerReplySchema, DISTILLER_REPLY_JSON_SCHEMA, TIDY_AUDIENCES, TidyReplySchema, TIDY_REPLY_JSON_SCHEMA, LIVE_MS, WORKING_MS, WORKING_TIER_MS, RECENT_TIER_MS, json, clock, HeardEventSchema, HeardCursorSchema, HeardPageSchema, MAX_EVENTS, MAX_BYTES, batchBytes, HeardBatchSchema, 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, clock2, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, LessonStateSchema, LessonViewSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, QueueDeltaSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, NOT_SENT, APPLIED, DEMOTED, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24258
24299
|
var init_dist = __esm({
|
|
24259
24300
|
"../../packages/sdk/dist/index.js"() {
|
|
24260
24301
|
"use strict";
|
|
@@ -24270,6 +24311,7 @@ var init_dist = __esm({
|
|
|
24270
24311
|
init_zod();
|
|
24271
24312
|
init_zod();
|
|
24272
24313
|
init_zod();
|
|
24314
|
+
init_zod();
|
|
24273
24315
|
import_undici = __toESM(require_undici(), 1);
|
|
24274
24316
|
require2 = __sdkCreateRequire(import.meta.url);
|
|
24275
24317
|
CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
|
|
@@ -24678,6 +24720,16 @@ var init_dist = __esm({
|
|
|
24678
24720
|
imgUrl: OptionSchema.shape.image
|
|
24679
24721
|
}).strict();
|
|
24680
24722
|
NIGHT = { from: 23, to: 7 };
|
|
24723
|
+
ASKING_RULES = `HOW TO ASK:
|
|
24724
|
+
1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
24725
|
+
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
24726
|
+
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
24727
|
+
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
|
|
24728
|
+
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
24729
|
+
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
24730
|
+
7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
|
|
24731
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
|
|
24732
|
+
9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
|
|
24681
24733
|
ASK_MAX = 250;
|
|
24682
24734
|
UNITS_MAX = 12;
|
|
24683
24735
|
UNIT_TITLE_MAX = 50;
|
|
@@ -24705,8 +24757,49 @@ var init_dist = __esm({
|
|
|
24705
24757
|
message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
24706
24758
|
goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
|
|
24707
24759
|
units,
|
|
24760
|
+
// A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
|
|
24761
|
+
// until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
|
|
24762
|
+
// still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
|
|
24763
|
+
// tap lands as a contribution, the person's own words on the Goal, because there is no Question
|
|
24764
|
+
// for it to answer.
|
|
24765
|
+
//
|
|
24766
|
+
// AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
|
|
24767
|
+
// acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
|
|
24768
|
+
// it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
|
|
24769
|
+
// work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
|
|
24770
|
+
// call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
|
|
24771
|
+
// tested yet*. The words are worth writing because the agent reads them back.
|
|
24772
|
+
//
|
|
24773
|
+
// ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
|
|
24774
|
+
// write up to four of them: "just on the cards, but it should be more like just they should only
|
|
24775
|
+
// get one basically string value that they can enter that replaces got it"). A report has exactly
|
|
24776
|
+
// one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
|
|
24777
|
+
// `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
|
|
24778
|
+
// MECHANISM). It rides the door as the report's single stored option (`storedOption`,
|
|
24779
|
+
// `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
|
|
24780
|
+
reply: external_exports.string().trim().min(1).describe(
|
|
24781
|
+
`What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
|
|
24782
|
+
).optional(),
|
|
24708
24783
|
userExplicitlyRequested
|
|
24709
|
-
}).strict().superRefine((u, ctx) =>
|
|
24784
|
+
}).strict().superRefine((u, ctx) => {
|
|
24785
|
+
if (u.userExplicitlyRequested && !u.reply) {
|
|
24786
|
+
ctx.addIssue({
|
|
24787
|
+
code: external_exports.ZodIssueCode.custom,
|
|
24788
|
+
path: ["reply"],
|
|
24789
|
+
params: { refusal: "reply_required" },
|
|
24790
|
+
message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
|
|
24791
|
+
});
|
|
24792
|
+
}
|
|
24793
|
+
refuseOverCaps(u.message, u.units, "message", ctx);
|
|
24794
|
+
if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
|
|
24795
|
+
ctx.addIssue({
|
|
24796
|
+
code: external_exports.ZodIssueCode.custom,
|
|
24797
|
+
path: ["reply"],
|
|
24798
|
+
params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX },
|
|
24799
|
+
message: `reply_too_long: the reply is ${u.reply.length} characters; the cap is ${OPTION_LABEL_MAX}. It is a button's words, so say it in a breath.`
|
|
24800
|
+
});
|
|
24801
|
+
}
|
|
24802
|
+
});
|
|
24710
24803
|
questionFields = {
|
|
24711
24804
|
question: external_exports.string().trim().min(1).describe(
|
|
24712
24805
|
`ONE question, in at most ${ASK_MAX} characters, and only what is needed to answer it; the context goes in units. Several questions are several objects, one each: an answer settles the one question it was given, so a person who answers the part of a bundled question that interested them settles nothing and is asked again. Separate questions cost nothing: ANY contact for a person already on a call JOINS that call, so several arrive as one call.`
|
|
@@ -24784,9 +24877,7 @@ var init_dist = __esm({
|
|
|
24784
24877
|
questions: external_exports.array(QuestionWireSchema).min(1).max(20).optional()
|
|
24785
24878
|
}).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
24786
24879
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
24787
|
-
CONTACT_DESCRIPTION =
|
|
24788
|
-
|
|
24789
|
-
QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
|
|
24880
|
+
CONTACT_DESCRIPTION = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES + `
|
|
24790
24881
|
|
|
24791
24882
|
CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX} characters; everything else it needs goes in 'units', up to ${UNITS_MAX} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX} characters and every body at most ${UNIT_BODY_MAX}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
|
|
24792
24883
|
|
|
@@ -24895,6 +24986,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
24895
24986
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
24896
24987
|
diagnose: external_exports.boolean().optional()
|
|
24897
24988
|
}).strict();
|
|
24989
|
+
GetDeliverySchema = external_exports.object({
|
|
24990
|
+
deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
|
|
24991
|
+
}).strict();
|
|
24992
|
+
GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
24898
24993
|
GET_GOAL_DESCRIPTION = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): 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. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. 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. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
24899
24994
|
SearchToolSchema = external_exports.object({
|
|
24900
24995
|
query: external_exports.string().trim().min(1).max(500),
|
|
@@ -24903,6 +24998,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
24903
24998
|
limit: external_exports.number().int().min(1).max(20).optional()
|
|
24904
24999
|
}).strict();
|
|
24905
25000
|
SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and replies -- the person's answers, acknowledgements and deferrals (type answer; found by their summary, by the question or report they reply to, or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, the replies to its items, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (a reply carries its kind -- answered, acknowledged or deferred -- whether it is closed (a deferral is not), its summary in under ten words, its question or report, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
25001
|
+
ConfigureIdentitySchema = external_exports.object({}).strict();
|
|
25002
|
+
CONFIGURE_IDENTITY_DESCRIPTION = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
|
|
24906
25003
|
CheckActivitySchema = external_exports.object({}).strict();
|
|
24907
25004
|
FEEDBACK_TEXT_MAX = 5e4;
|
|
24908
25005
|
SendFeedbackSchema = external_exports.object({
|
|
@@ -24912,8 +25009,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
24912
25009
|
}).strict();
|
|
24913
25010
|
SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
|
|
24914
25011
|
AGENT_TOOLS = [
|
|
25012
|
+
{ name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION, inputSchema: mcpInputSchema(ConfigureIdentitySchema) },
|
|
24915
25013
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
24916
25014
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
25015
|
+
{ name: "get_delivery", description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
|
|
24917
25016
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
24918
25017
|
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
24919
25018
|
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
@@ -25144,8 +25243,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
25144
25243
|
// something the person answered or asked for did not save, was refused or dropped
|
|
25145
25244
|
"ignored",
|
|
25146
25245
|
// the person asked or said something and was never answered (the critic's ignored_a_question)
|
|
25147
|
-
"promised"
|
|
25246
|
+
"promised",
|
|
25148
25247
|
// said it did or would do something, and nothing shows it done (the critic's broken_promise)
|
|
25248
|
+
"dropped"
|
|
25249
|
+
// something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
|
|
25149
25250
|
];
|
|
25150
25251
|
DistillerReplySchema = external_exports.object({
|
|
25151
25252
|
defects: external_exports.array(external_exports.object({
|
|
@@ -25176,6 +25277,44 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
25176
25277
|
);
|
|
25177
25278
|
LIVE_MS = 3 * 6e4;
|
|
25178
25279
|
WORKING_MS = 60 * 6e4;
|
|
25280
|
+
WORKING_TIER_MS = 2 * 60 * 6e4;
|
|
25281
|
+
RECENT_TIER_MS = 24 * 60 * 6e4;
|
|
25282
|
+
json = external_exports.lazy(() => external_exports.union([
|
|
25283
|
+
external_exports.string(),
|
|
25284
|
+
external_exports.number().finite(),
|
|
25285
|
+
external_exports.boolean(),
|
|
25286
|
+
external_exports.null(),
|
|
25287
|
+
external_exports.array(json),
|
|
25288
|
+
external_exports.record(json)
|
|
25289
|
+
]));
|
|
25290
|
+
clock = external_exports.number().int().nonnegative().safe();
|
|
25291
|
+
HeardEventSchema = external_exports.object({
|
|
25292
|
+
eventId: external_exports.string().uuid(),
|
|
25293
|
+
source: external_exports.enum(["phone", "bot"]),
|
|
25294
|
+
sequence: clock,
|
|
25295
|
+
kind: external_exports.string().min(1),
|
|
25296
|
+
phoneAtMs: clock.optional(),
|
|
25297
|
+
botAtMs: clock.optional(),
|
|
25298
|
+
payload: external_exports.record(json)
|
|
25299
|
+
}).strict();
|
|
25300
|
+
HeardCursorSchema = HeardEventSchema.pick({ source: true, sequence: true, eventId: true });
|
|
25301
|
+
HeardPageSchema = external_exports.object({
|
|
25302
|
+
events: external_exports.array(HeardEventSchema).max(200),
|
|
25303
|
+
next: HeardCursorSchema.nullable()
|
|
25304
|
+
}).strict();
|
|
25305
|
+
MAX_EVENTS = 50;
|
|
25306
|
+
MAX_BYTES = 8e4;
|
|
25307
|
+
batchBytes = (events) => {
|
|
25308
|
+
let bytes = 0;
|
|
25309
|
+
for (const char of JSON.stringify({ events })) {
|
|
25310
|
+
const point = char.codePointAt(0);
|
|
25311
|
+
bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
|
|
25312
|
+
}
|
|
25313
|
+
return bytes;
|
|
25314
|
+
};
|
|
25315
|
+
HeardBatchSchema = external_exports.object({
|
|
25316
|
+
events: external_exports.array(HeardEventSchema).min(1).max(MAX_EVENTS)
|
|
25317
|
+
}).strict().refine((value) => batchBytes(value.events) <= MAX_BYTES, "heard_batch_too_large");
|
|
25179
25318
|
ContextSchema = external_exports.object({
|
|
25180
25319
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
25181
25320
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -25520,6 +25659,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
25520
25659
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
25521
25660
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
25522
25661
|
agentId: external_exports.string().optional(),
|
|
25662
|
+
/** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
|
|
25663
|
+
* the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
|
|
25664
|
+
* question turn — the talker owns re-asking and the waits, so `question` stays null. */
|
|
25665
|
+
onTable: external_exports.boolean().optional(),
|
|
25523
25666
|
select: SelectShapeSchema.optional(),
|
|
25524
25667
|
options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
|
|
25525
25668
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
@@ -25594,9 +25737,16 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
25594
25737
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
25595
25738
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
25596
25739
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
25597
|
-
* never re-sent as a second card to be seen mid-call. Words-only options are
|
|
25598
|
-
|
|
25599
|
-
|
|
25740
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
25741
|
+
options: external_exports.array(OptionSchema).optional(),
|
|
25742
|
+
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
25743
|
+
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
25744
|
+
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
25745
|
+
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
25746
|
+
* preview layout for any `options`, and must see exactly what it did. */
|
|
25747
|
+
choices: external_exports.array(OptionSchema).optional(),
|
|
25748
|
+
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
25749
|
+
select: external_exports.enum(["one", "many", "rank"]).optional()
|
|
25600
25750
|
})).optional(),
|
|
25601
25751
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
25602
25752
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -25721,8 +25871,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
25721
25871
|
"inbox",
|
|
25722
25872
|
"dismiss"
|
|
25723
25873
|
]);
|
|
25724
|
-
|
|
25725
|
-
QUIET = ` Nothing rings from ${
|
|
25874
|
+
clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
25875
|
+
QUIET = ` Nothing rings from ${clock2(NIGHT.from)} to ${clock2(NIGHT.to)} your time; the count waits for morning.`;
|
|
25726
25876
|
BrokerTuningSchema = external_exports.object({
|
|
25727
25877
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
25728
25878
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -26033,7 +26183,17 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
26033
26183
|
from: external_exports.enum(["person", "agent"]),
|
|
26034
26184
|
said: external_exports.string(),
|
|
26035
26185
|
at: external_exports.string(),
|
|
26036
|
-
entryId: external_exports.string()
|
|
26186
|
+
entryId: external_exports.string(),
|
|
26187
|
+
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
26188
|
+
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
26189
|
+
* none was. The Work row reads it before `said`. */
|
|
26190
|
+
summary: external_exports.string().optional(),
|
|
26191
|
+
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
26192
|
+
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
26193
|
+
titles: external_exports.array(external_exports.string()).optional(),
|
|
26194
|
+
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
26195
|
+
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
26196
|
+
line: external_exports.string().optional()
|
|
26037
26197
|
}).nullable().optional(),
|
|
26038
26198
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
26039
26199
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -26370,6 +26530,9 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
26370
26530
|
title: external_exports.string(),
|
|
26371
26531
|
owner: external_exports.string(),
|
|
26372
26532
|
state: external_exports.string(),
|
|
26533
|
+
/** Whether the call put it to the person. An item it carried and never said has no row on the
|
|
26534
|
+
* review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
|
|
26535
|
+
presented: external_exports.boolean(),
|
|
26373
26536
|
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
26374
26537
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
26375
26538
|
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
@@ -26377,10 +26540,36 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
|
|
|
26377
26540
|
* neither wrote one. */
|
|
26378
26541
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
26379
26542
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
26380
|
-
|
|
26543
|
+
/** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
|
|
26544
|
+
* however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
|
|
26545
|
+
* said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
|
|
26546
|
+
lines: external_exports.array(external_exports.object({
|
|
26547
|
+
entryId: external_exports.string().uuid(),
|
|
26548
|
+
words: external_exports.string(),
|
|
26549
|
+
summary: external_exports.string().optional(),
|
|
26550
|
+
about: external_exports.string().nullable().optional(),
|
|
26551
|
+
reply: external_exports.string().nullable().optional(),
|
|
26552
|
+
replyKind: external_exports.string().nullable().optional(),
|
|
26553
|
+
at: external_exports.array(external_exports.number()).optional()
|
|
26554
|
+
}))
|
|
26381
26555
|
})),
|
|
26382
26556
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
26383
|
-
|
|
26557
|
+
/** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
|
|
26558
|
+
* (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
|
|
26559
|
+
* it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
|
|
26560
|
+
* person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
|
|
26561
|
+
* theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
|
|
26562
|
+
* BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
|
|
26563
|
+
said: external_exports.array(external_exports.object({
|
|
26564
|
+
lineId: external_exports.string().uuid(),
|
|
26565
|
+
speaker: external_exports.enum(["user", "agent"]),
|
|
26566
|
+
agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
|
|
26567
|
+
words: external_exports.string(),
|
|
26568
|
+
atMs: external_exports.number()
|
|
26569
|
+
})),
|
|
26570
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
|
|
26571
|
+
/** Last stored position on this page when another exists; absent before paging shipped. */
|
|
26572
|
+
next: external_exports.number().int().nonnegative().nullable().optional()
|
|
26384
26573
|
});
|
|
26385
26574
|
TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
|
|
26386
26575
|
KEY_PATH = join22(homedir22(), ".paigy", "key.json");
|
|
@@ -31309,11 +31498,11 @@ function extractJson(raw) {
|
|
|
31309
31498
|
return null;
|
|
31310
31499
|
}
|
|
31311
31500
|
function parseVerdicts(raw, knownIds, knownAgents) {
|
|
31312
|
-
const
|
|
31313
|
-
if (!
|
|
31501
|
+
const json3 = extractJson(raw);
|
|
31502
|
+
if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
|
|
31314
31503
|
let parsed;
|
|
31315
31504
|
try {
|
|
31316
|
-
parsed = JSON.parse(
|
|
31505
|
+
parsed = JSON.parse(json3);
|
|
31317
31506
|
} catch (e) {
|
|
31318
31507
|
return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
|
|
31319
31508
|
}
|
|
@@ -31559,13 +31748,17 @@ var init_triage = __esm({
|
|
|
31559
31748
|
init_catalog();
|
|
31560
31749
|
|
|
31561
31750
|
// src/cli-args.ts
|
|
31751
|
+
init_zod();
|
|
31562
31752
|
var TRIAGE_VIA = ["ollama", "claude", "codex", "agy"];
|
|
31563
31753
|
var USAGE = "usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | enable-tools [--scope user|project] | hatch NAME [--voice KEY] [--slot SLOT] | launch NAME [--under GOAL] [--voice KEY] [--harness \u2026] [--mode \u2026] [--cwd DIR] BRIEF\u2026 | handoff | triage [--via ollama|claude|codex|agy] [--json] [--apply [--yes-close] [--yes-stale] [--yes-assign]] | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--grace SECONDS] [--doctor] PROMPT\u2026";
|
|
31564
31754
|
function parseArgs(argv) {
|
|
31565
31755
|
const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, help: false, host: false, pair: false, service: false, setup: false, enableTools: false, scope: "user", grant: [], hatch: null, voice: null, slot: null, launch: null, under: null, graceSeconds: 90, prompt: "", handoff: false, triage: false, via: null, json: false, apply: false, yesClose: false, yesStale: false, yesAssign: false };
|
|
31566
31756
|
const words2 = [];
|
|
31757
|
+
let commandPosition = true;
|
|
31567
31758
|
for (let i = 0; i < argv.length; i++) {
|
|
31568
31759
|
const arg = argv[i];
|
|
31760
|
+
const command = commandPosition && !!arg && !arg.startsWith("-");
|
|
31761
|
+
if (command) commandPosition = false;
|
|
31569
31762
|
const next = () => argv[++i];
|
|
31570
31763
|
if (arg === "--harness") {
|
|
31571
31764
|
const v = next();
|
|
@@ -31581,7 +31774,7 @@ function parseArgs(argv) {
|
|
|
31581
31774
|
args.cwd = v;
|
|
31582
31775
|
} else if (arg === "--doctor") {
|
|
31583
31776
|
args.doctor = true;
|
|
31584
|
-
} else if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
31777
|
+
} else if (arg === "-h" || arg === "--help" || command && arg === "help") {
|
|
31585
31778
|
args.help = true;
|
|
31586
31779
|
} else if (arg === "--scope") {
|
|
31587
31780
|
const v = next();
|
|
@@ -31591,21 +31784,21 @@ function parseArgs(argv) {
|
|
|
31591
31784
|
const v = Number(next());
|
|
31592
31785
|
if (!Number.isFinite(v) || v < 0) return { error: "--grace needs seconds (0 = straight to phone)" };
|
|
31593
31786
|
args.graceSeconds = v;
|
|
31594
|
-
} else if (arg === "host") {
|
|
31787
|
+
} else if (command && arg === "host") {
|
|
31595
31788
|
args.host = true;
|
|
31596
|
-
} else if (arg === "pair") {
|
|
31789
|
+
} else if (command && arg === "pair") {
|
|
31597
31790
|
args.pair = true;
|
|
31598
|
-
} else if (arg === "setup") {
|
|
31791
|
+
} else if (command && arg === "setup") {
|
|
31599
31792
|
args.setup = true;
|
|
31600
|
-
} else if (arg === "service") {
|
|
31793
|
+
} else if (command && arg === "service") {
|
|
31601
31794
|
args.service = true;
|
|
31602
31795
|
} else if (arg === "--grant") {
|
|
31603
31796
|
const v = next();
|
|
31604
31797
|
if (!v) return { error: "--grant needs a folder to allow" };
|
|
31605
31798
|
args.grant.push(v);
|
|
31606
|
-
} else if (arg === "handoff") {
|
|
31799
|
+
} else if (command && arg === "handoff") {
|
|
31607
31800
|
args.handoff = true;
|
|
31608
|
-
} else if (arg === "triage") {
|
|
31801
|
+
} else if (command && arg === "triage") {
|
|
31609
31802
|
args.triage = true;
|
|
31610
31803
|
} else if (arg === "--via") {
|
|
31611
31804
|
const v = next();
|
|
@@ -31622,9 +31815,9 @@ function parseArgs(argv) {
|
|
|
31622
31815
|
args.yesStale = true;
|
|
31623
31816
|
} else if (arg === "--yes-assign") {
|
|
31624
31817
|
args.yesAssign = true;
|
|
31625
|
-
} else if (arg === "enable-tools") {
|
|
31818
|
+
} else if (command && arg === "enable-tools") {
|
|
31626
31819
|
args.enableTools = true;
|
|
31627
|
-
} else if (arg === "hatch") {
|
|
31820
|
+
} else if (command && arg === "hatch") {
|
|
31628
31821
|
const v = next();
|
|
31629
31822
|
if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
|
|
31630
31823
|
args.hatch = v;
|
|
@@ -31634,12 +31827,14 @@ function parseArgs(argv) {
|
|
|
31634
31827
|
args.slot = next() ?? null;
|
|
31635
31828
|
} else if (arg === "--identity") {
|
|
31636
31829
|
return { error: '--identity is gone: start a helper with `paigy-harness launch NAME "brief"`, which gives it its own session' };
|
|
31637
|
-
} else if (arg === "launch") {
|
|
31830
|
+
} else if (command && arg === "launch") {
|
|
31638
31831
|
const v = next();
|
|
31639
31832
|
if (!v) return { error: 'launch needs a name: paigy-harness launch "Deploy check" "brief"' };
|
|
31640
31833
|
args.launch = v;
|
|
31641
31834
|
} else if (arg === "--under") {
|
|
31642
|
-
|
|
31835
|
+
const v = next();
|
|
31836
|
+
if (!v || !external_exports.string().uuid().safeParse(v).success) return { error: "--under needs a Goal UUID" };
|
|
31837
|
+
args.under = v;
|
|
31643
31838
|
} else if (arg?.startsWith("--")) {
|
|
31644
31839
|
return { error: `unknown flag ${arg}` };
|
|
31645
31840
|
} else if (arg) {
|
|
@@ -31648,6 +31843,7 @@ function parseArgs(argv) {
|
|
|
31648
31843
|
}
|
|
31649
31844
|
args.prompt = words2.join(" ");
|
|
31650
31845
|
if (args.help) return args;
|
|
31846
|
+
if (args.under && !args.launch) return { error: "--under requires launch NAME" };
|
|
31651
31847
|
if (args.launch && !args.prompt) return { error: 'launch needs a brief: paigy-harness launch "Deploy check" "brief"' };
|
|
31652
31848
|
if (!args.doctor && !args.handoff && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.enableTools && !args.triage && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / enable-tools / hatch NAME / handoff / triage)" };
|
|
31653
31849
|
if ((args.yesClose || args.yesStale || args.yesAssign) && !args.apply)
|
|
@@ -33475,6 +33671,7 @@ init_zod();
|
|
|
33475
33671
|
init_zod();
|
|
33476
33672
|
init_zod();
|
|
33477
33673
|
init_zod();
|
|
33674
|
+
init_zod();
|
|
33478
33675
|
var OPTIONS_MIN2 = 2;
|
|
33479
33676
|
var OPTIONS_MAX2 = 6;
|
|
33480
33677
|
var OPTION_LABEL_MAX2 = 50;
|
|
@@ -33504,6 +33701,16 @@ var OptionInputSchema2 = external_exports.object({
|
|
|
33504
33701
|
imgUrl: OptionSchema2.shape.image
|
|
33505
33702
|
}).strict();
|
|
33506
33703
|
var NIGHT2 = { from: 23, to: 7 };
|
|
33704
|
+
var ASKING_RULES2 = `HOW TO ASK:
|
|
33705
|
+
1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
|
|
33706
|
+
2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
|
|
33707
|
+
3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
|
|
33708
|
+
4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
|
|
33709
|
+
5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
33710
|
+
6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
|
|
33711
|
+
7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
|
|
33712
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
|
|
33713
|
+
9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
|
|
33507
33714
|
function draft20202(node) {
|
|
33508
33715
|
if (Array.isArray(node)) return node.map(draft20202);
|
|
33509
33716
|
if (node && typeof node === "object") {
|
|
@@ -33601,8 +33808,49 @@ var UpdateInputSchema2 = external_exports.object({
|
|
|
33601
33808
|
message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX2} characters; the context goes in units.`),
|
|
33602
33809
|
goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
|
|
33603
33810
|
units: units2,
|
|
33811
|
+
// A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
|
|
33812
|
+
// until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
|
|
33813
|
+
// still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
|
|
33814
|
+
// tap lands as a contribution, the person's own words on the Goal, because there is no Question
|
|
33815
|
+
// for it to answer.
|
|
33816
|
+
//
|
|
33817
|
+
// AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
|
|
33818
|
+
// acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
|
|
33819
|
+
// it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
|
|
33820
|
+
// work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
|
|
33821
|
+
// call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
|
|
33822
|
+
// tested yet*. The words are worth writing because the agent reads them back.
|
|
33823
|
+
//
|
|
33824
|
+
// ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
|
|
33825
|
+
// write up to four of them: "just on the cards, but it should be more like just they should only
|
|
33826
|
+
// get one basically string value that they can enter that replaces got it"). A report has exactly
|
|
33827
|
+
// one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
|
|
33828
|
+
// `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
|
|
33829
|
+
// MECHANISM). It rides the door as the report's single stored option (`storedOption`,
|
|
33830
|
+
// `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
|
|
33831
|
+
reply: external_exports.string().trim().min(1).describe(
|
|
33832
|
+
`What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX2} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
|
|
33833
|
+
).optional(),
|
|
33604
33834
|
userExplicitlyRequested: userExplicitlyRequested2
|
|
33605
|
-
}).strict().superRefine((u, ctx) =>
|
|
33835
|
+
}).strict().superRefine((u, ctx) => {
|
|
33836
|
+
if (u.userExplicitlyRequested && !u.reply) {
|
|
33837
|
+
ctx.addIssue({
|
|
33838
|
+
code: external_exports.ZodIssueCode.custom,
|
|
33839
|
+
path: ["reply"],
|
|
33840
|
+
params: { refusal: "reply_required" },
|
|
33841
|
+
message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
|
|
33842
|
+
});
|
|
33843
|
+
}
|
|
33844
|
+
refuseOverCaps2(u.message, u.units, "message", ctx);
|
|
33845
|
+
if (u.reply && u.reply.length > OPTION_LABEL_MAX2) {
|
|
33846
|
+
ctx.addIssue({
|
|
33847
|
+
code: external_exports.ZodIssueCode.custom,
|
|
33848
|
+
path: ["reply"],
|
|
33849
|
+
params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX2 },
|
|
33850
|
+
message: `reply_too_long: the reply is ${u.reply.length} characters; the cap is ${OPTION_LABEL_MAX2}. It is a button's words, so say it in a breath.`
|
|
33851
|
+
});
|
|
33852
|
+
}
|
|
33853
|
+
});
|
|
33606
33854
|
var questionFields2 = {
|
|
33607
33855
|
question: external_exports.string().trim().min(1).describe(
|
|
33608
33856
|
`ONE question, in at most ${ASK_MAX2} characters, and only what is needed to answer it; the context goes in units. Several questions are several objects, one each: an answer settles the one question it was given, so a person who answers the part of a bundled question that interested them settles nothing and is asked again. Separate questions cost nothing: ANY contact for a person already on a call JOINS that call, so several arrive as one call.`
|
|
@@ -33680,9 +33928,7 @@ var ContactWireSchema2 = external_exports.object({
|
|
|
33680
33928
|
questions: external_exports.array(QuestionWireSchema2).min(1).max(20).optional()
|
|
33681
33929
|
}).strict().refine(sendsAnything2, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
33682
33930
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
33683
|
-
var CONTACT_DESCRIPTION2 =
|
|
33684
|
-
|
|
33685
|
-
QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
|
|
33931
|
+
var CONTACT_DESCRIPTION2 = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES2 + `
|
|
33686
33932
|
|
|
33687
33933
|
CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX2} characters; everything else it needs goes in 'units', up to ${UNITS_MAX2} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX2} characters and every body at most ${UNIT_BODY_MAX2}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
|
|
33688
33934
|
|
|
@@ -33791,6 +34037,10 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
33791
34037
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
33792
34038
|
diagnose: external_exports.boolean().optional()
|
|
33793
34039
|
}).strict();
|
|
34040
|
+
var GetDeliverySchema2 = external_exports.object({
|
|
34041
|
+
deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
|
|
34042
|
+
}).strict();
|
|
34043
|
+
var GET_DELIVERY_DESCRIPTION2 = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
33794
34044
|
var GET_GOAL_DESCRIPTION2 = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): 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. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. 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. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
33795
34045
|
var SearchToolSchema2 = external_exports.object({
|
|
33796
34046
|
query: external_exports.string().trim().min(1).max(500),
|
|
@@ -33799,6 +34049,8 @@ var SearchToolSchema2 = external_exports.object({
|
|
|
33799
34049
|
limit: external_exports.number().int().min(1).max(20).optional()
|
|
33800
34050
|
}).strict();
|
|
33801
34051
|
var SEARCH_DESCRIPTION2 = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and replies -- the person's answers, acknowledgements and deferrals (type answer; found by their summary, by the question or report they reply to, or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, the replies to its items, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (a reply carries its kind -- answered, acknowledged or deferred -- whether it is closed (a deferral is not), its summary in under ten words, its question or report, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
34052
|
+
var ConfigureIdentitySchema2 = external_exports.object({}).strict();
|
|
34053
|
+
var CONFIGURE_IDENTITY_DESCRIPTION2 = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
|
|
33802
34054
|
var CheckActivitySchema2 = external_exports.object({}).strict();
|
|
33803
34055
|
var FEEDBACK_TEXT_MAX2 = 5e4;
|
|
33804
34056
|
var SendFeedbackSchema2 = external_exports.object({
|
|
@@ -33808,8 +34060,10 @@ var SendFeedbackSchema2 = external_exports.object({
|
|
|
33808
34060
|
}).strict();
|
|
33809
34061
|
var SEND_FEEDBACK_DESCRIPTION2 = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
|
|
33810
34062
|
var AGENT_TOOLS2 = [
|
|
34063
|
+
{ name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION2, inputSchema: mcpInputSchema2(ConfigureIdentitySchema2) },
|
|
33811
34064
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
33812
34065
|
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(ManageGoalsToolSchema2) },
|
|
34066
|
+
{ name: "get_delivery", description: GET_DELIVERY_DESCRIPTION2, inputSchema: mcpInputSchema2(GetDeliverySchema2) },
|
|
33813
34067
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
|
|
33814
34068
|
{ name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) },
|
|
33815
34069
|
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckActivitySchema2) },
|
|
@@ -34040,8 +34294,10 @@ var DISTILLER_DEFECTS2 = [
|
|
|
34040
34294
|
// something the person answered or asked for did not save, was refused or dropped
|
|
34041
34295
|
"ignored",
|
|
34042
34296
|
// the person asked or said something and was never answered (the critic's ignored_a_question)
|
|
34043
|
-
"promised"
|
|
34297
|
+
"promised",
|
|
34044
34298
|
// said it did or would do something, and nothing shows it done (the critic's broken_promise)
|
|
34299
|
+
"dropped"
|
|
34300
|
+
// something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
|
|
34045
34301
|
];
|
|
34046
34302
|
var DistillerReplySchema2 = external_exports.object({
|
|
34047
34303
|
defects: external_exports.array(external_exports.object({
|
|
@@ -34089,6 +34345,45 @@ function entryWords(entry) {
|
|
|
34089
34345
|
}
|
|
34090
34346
|
var LIVE_MS2 = 3 * 6e4;
|
|
34091
34347
|
var WORKING_MS2 = 60 * 6e4;
|
|
34348
|
+
var isWorking = (c, now) => !!c.lastWorkedAt && now - Date.parse(c.lastWorkedAt) < WORKING_MS2;
|
|
34349
|
+
var WORKING_TIER_MS2 = 2 * 60 * 6e4;
|
|
34350
|
+
var RECENT_TIER_MS2 = 24 * 60 * 6e4;
|
|
34351
|
+
var json2 = external_exports.lazy(() => external_exports.union([
|
|
34352
|
+
external_exports.string(),
|
|
34353
|
+
external_exports.number().finite(),
|
|
34354
|
+
external_exports.boolean(),
|
|
34355
|
+
external_exports.null(),
|
|
34356
|
+
external_exports.array(json2),
|
|
34357
|
+
external_exports.record(json2)
|
|
34358
|
+
]));
|
|
34359
|
+
var clock3 = external_exports.number().int().nonnegative().safe();
|
|
34360
|
+
var HeardEventSchema2 = external_exports.object({
|
|
34361
|
+
eventId: external_exports.string().uuid(),
|
|
34362
|
+
source: external_exports.enum(["phone", "bot"]),
|
|
34363
|
+
sequence: clock3,
|
|
34364
|
+
kind: external_exports.string().min(1),
|
|
34365
|
+
phoneAtMs: clock3.optional(),
|
|
34366
|
+
botAtMs: clock3.optional(),
|
|
34367
|
+
payload: external_exports.record(json2)
|
|
34368
|
+
}).strict();
|
|
34369
|
+
var HeardCursorSchema2 = HeardEventSchema2.pick({ source: true, sequence: true, eventId: true });
|
|
34370
|
+
var HeardPageSchema2 = external_exports.object({
|
|
34371
|
+
events: external_exports.array(HeardEventSchema2).max(200),
|
|
34372
|
+
next: HeardCursorSchema2.nullable()
|
|
34373
|
+
}).strict();
|
|
34374
|
+
var MAX_EVENTS2 = 50;
|
|
34375
|
+
var MAX_BYTES2 = 8e4;
|
|
34376
|
+
var batchBytes2 = (events) => {
|
|
34377
|
+
let bytes = 0;
|
|
34378
|
+
for (const char of JSON.stringify({ events })) {
|
|
34379
|
+
const point = char.codePointAt(0);
|
|
34380
|
+
bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
|
|
34381
|
+
}
|
|
34382
|
+
return bytes;
|
|
34383
|
+
};
|
|
34384
|
+
var HeardBatchSchema2 = external_exports.object({
|
|
34385
|
+
events: external_exports.array(HeardEventSchema2).min(1).max(MAX_EVENTS2)
|
|
34386
|
+
}).strict().refine((value) => batchBytes2(value.events) <= MAX_BYTES2, "heard_batch_too_large");
|
|
34092
34387
|
var ContextSchema2 = external_exports.object({
|
|
34093
34388
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
34094
34389
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -34433,6 +34728,10 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
34433
34728
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
34434
34729
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
34435
34730
|
agentId: external_exports.string().optional(),
|
|
34731
|
+
/** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
|
|
34732
|
+
* the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
|
|
34733
|
+
* question turn — the talker owns re-asking and the waits, so `question` stays null. */
|
|
34734
|
+
onTable: external_exports.boolean().optional(),
|
|
34436
34735
|
select: SelectShapeSchema2.optional(),
|
|
34437
34736
|
options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
|
|
34438
34737
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
@@ -34507,9 +34806,16 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
34507
34806
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
34508
34807
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
34509
34808
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
34510
|
-
* never re-sent as a second card to be seen mid-call. Words-only options are
|
|
34511
|
-
|
|
34512
|
-
|
|
34809
|
+
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
34810
|
+
options: external_exports.array(OptionSchema2).optional(),
|
|
34811
|
+
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
34812
|
+
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
34813
|
+
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
34814
|
+
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
34815
|
+
* preview layout for any `options`, and must see exactly what it did. */
|
|
34816
|
+
choices: external_exports.array(OptionSchema2).optional(),
|
|
34817
|
+
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
34818
|
+
select: external_exports.enum(["one", "many", "rank"]).optional()
|
|
34513
34819
|
})).optional(),
|
|
34514
34820
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
34515
34821
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -34634,8 +34940,8 @@ var MissedCallSchema2 = external_exports.enum([
|
|
|
34634
34940
|
"inbox",
|
|
34635
34941
|
"dismiss"
|
|
34636
34942
|
]);
|
|
34637
|
-
var
|
|
34638
|
-
var QUIET2 = ` Nothing rings from ${
|
|
34943
|
+
var clock22 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
34944
|
+
var QUIET2 = ` Nothing rings from ${clock22(NIGHT2.from)} to ${clock22(NIGHT2.to)} your time; the count waits for morning.`;
|
|
34639
34945
|
var BrokerTuningSchema2 = external_exports.object({
|
|
34640
34946
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
34641
34947
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -34946,7 +35252,17 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
34946
35252
|
from: external_exports.enum(["person", "agent"]),
|
|
34947
35253
|
said: external_exports.string(),
|
|
34948
35254
|
at: external_exports.string(),
|
|
34949
|
-
entryId: external_exports.string()
|
|
35255
|
+
entryId: external_exports.string(),
|
|
35256
|
+
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
35257
|
+
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
35258
|
+
* none was. The Work row reads it before `said`. */
|
|
35259
|
+
summary: external_exports.string().optional(),
|
|
35260
|
+
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
35261
|
+
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
35262
|
+
titles: external_exports.array(external_exports.string()).optional(),
|
|
35263
|
+
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
35264
|
+
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
35265
|
+
line: external_exports.string().optional()
|
|
34950
35266
|
}).nullable().optional(),
|
|
34951
35267
|
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
34952
35268
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
@@ -35281,6 +35597,9 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
35281
35597
|
title: external_exports.string(),
|
|
35282
35598
|
owner: external_exports.string(),
|
|
35283
35599
|
state: external_exports.string(),
|
|
35600
|
+
/** Whether the call put it to the person. An item it carried and never said has no row on the
|
|
35601
|
+
* review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
|
|
35602
|
+
presented: external_exports.boolean(),
|
|
35284
35603
|
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
35285
35604
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
35286
35605
|
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
@@ -35288,10 +35607,36 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
35288
35607
|
* neither wrote one. */
|
|
35289
35608
|
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
35290
35609
|
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
35291
|
-
|
|
35610
|
+
/** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
|
|
35611
|
+
* however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
|
|
35612
|
+
* said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
|
|
35613
|
+
lines: external_exports.array(external_exports.object({
|
|
35614
|
+
entryId: external_exports.string().uuid(),
|
|
35615
|
+
words: external_exports.string(),
|
|
35616
|
+
summary: external_exports.string().optional(),
|
|
35617
|
+
about: external_exports.string().nullable().optional(),
|
|
35618
|
+
reply: external_exports.string().nullable().optional(),
|
|
35619
|
+
replyKind: external_exports.string().nullable().optional(),
|
|
35620
|
+
at: external_exports.array(external_exports.number()).optional()
|
|
35621
|
+
}))
|
|
35292
35622
|
})),
|
|
35293
35623
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
35294
|
-
|
|
35624
|
+
/** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
|
|
35625
|
+
* (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
|
|
35626
|
+
* it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
|
|
35627
|
+
* person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
|
|
35628
|
+
* theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
|
|
35629
|
+
* BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
|
|
35630
|
+
said: external_exports.array(external_exports.object({
|
|
35631
|
+
lineId: external_exports.string().uuid(),
|
|
35632
|
+
speaker: external_exports.enum(["user", "agent"]),
|
|
35633
|
+
agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
|
|
35634
|
+
words: external_exports.string(),
|
|
35635
|
+
atMs: external_exports.number()
|
|
35636
|
+
})),
|
|
35637
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
|
|
35638
|
+
/** Last stored position on this page when another exists; absent before paging shipped. */
|
|
35639
|
+
next: external_exports.number().int().nonnegative().nullable().optional()
|
|
35295
35640
|
});
|
|
35296
35641
|
|
|
35297
35642
|
// src/paigy/activity.ts
|
|
@@ -35365,9 +35710,9 @@ function limitIn(text, now) {
|
|
|
35365
35710
|
if (!line) return null;
|
|
35366
35711
|
const reset = resetOf(line, now);
|
|
35367
35712
|
if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
|
|
35368
|
-
return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ??
|
|
35713
|
+
return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock4(reset) };
|
|
35369
35714
|
}
|
|
35370
|
-
return { until: now + RETRY_MS2, resets:
|
|
35715
|
+
return { until: now + RETRY_MS2, resets: clock4(now + RETRY_MS2) };
|
|
35371
35716
|
}
|
|
35372
35717
|
function resetOf(line, now) {
|
|
35373
35718
|
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);
|
|
@@ -35411,7 +35756,7 @@ function wallToEpoch(year, month, day, hour, minute, zone) {
|
|
|
35411
35756
|
const first = wall - offset(wall);
|
|
35412
35757
|
return wall - offset(first);
|
|
35413
35758
|
}
|
|
35414
|
-
function
|
|
35759
|
+
function clock4(t) {
|
|
35415
35760
|
return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
|
|
35416
35761
|
}
|
|
35417
35762
|
var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
|
|
@@ -35455,7 +35800,7 @@ function episodes(opts = {}) {
|
|
|
35455
35800
|
episode.called = true;
|
|
35456
35801
|
const rest = episode.paused.filter((n) => !episode.running.includes(n));
|
|
35457
35802
|
const text = [
|
|
35458
|
-
`Claude's usage limit paused ${names(episode.paused)} from ${
|
|
35803
|
+
`Claude's usage limit paused ${names(episode.paused)} from ${clock4(episode.since)}.`,
|
|
35459
35804
|
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.`
|
|
35460
35805
|
].join(" ");
|
|
35461
35806
|
void call(text).then(
|
|
@@ -35948,7 +36293,7 @@ function send2(session, state, text) {
|
|
|
35948
36293
|
session.send(text);
|
|
35949
36294
|
}
|
|
35950
36295
|
async function pump(session, state, rail, opts) {
|
|
35951
|
-
const
|
|
36296
|
+
const clock5 = opts.beat ?? beat();
|
|
35952
36297
|
const now = opts.now ?? Date.now;
|
|
35953
36298
|
const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
|
|
35954
36299
|
const tally = { noted: 0, seen: 0, announced: 0 };
|
|
@@ -35980,7 +36325,7 @@ async function pump(session, state, rail, opts) {
|
|
|
35980
36325
|
counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
|
|
35981
36326
|
since = now();
|
|
35982
36327
|
}
|
|
35983
|
-
cause = await
|
|
36328
|
+
cause = await clock5.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
|
|
35984
36329
|
if (!await opts.onIdle()) return;
|
|
35985
36330
|
}
|
|
35986
36331
|
}
|
|
@@ -35990,7 +36335,7 @@ function runHarness(opts) {
|
|
|
35990
36335
|
let running = true;
|
|
35991
36336
|
let tail = [];
|
|
35992
36337
|
const state = { resting: true };
|
|
35993
|
-
const
|
|
36338
|
+
const clock5 = beat();
|
|
35994
36339
|
let session = null;
|
|
35995
36340
|
const speaksAs = opts.session ?? opts.resume ?? sessionId();
|
|
35996
36341
|
const instance = randomUUID4();
|
|
@@ -36051,7 +36396,7 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
36051
36396
|
stop2();
|
|
36052
36397
|
return;
|
|
36053
36398
|
} else if (carried) runningAgain();
|
|
36054
|
-
|
|
36399
|
+
clock5.nudge("rest");
|
|
36055
36400
|
}
|
|
36056
36401
|
}
|
|
36057
36402
|
session = startSession({
|
|
@@ -36070,13 +36415,13 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
36070
36415
|
},
|
|
36071
36416
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
36072
36417
|
});
|
|
36073
|
-
void pump(session, state, rail, { onIdle: () => running, beat:
|
|
36418
|
+
void pump(session, state, rail, { onIdle: () => running, beat: clock5, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
|
|
36074
36419
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
36075
36420
|
);
|
|
36076
36421
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
36077
36422
|
function stop2() {
|
|
36078
36423
|
running = false;
|
|
36079
|
-
|
|
36424
|
+
clock5.nudge("wake");
|
|
36080
36425
|
tail = [];
|
|
36081
36426
|
cancelAsks(state);
|
|
36082
36427
|
session?.stop();
|
|
@@ -36091,7 +36436,7 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
36091
36436
|
working: () => running && state.resting !== true,
|
|
36092
36437
|
// Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
|
|
36093
36438
|
tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
|
|
36094
|
-
nudge: () =>
|
|
36439
|
+
nudge: () => clock5.nudge("wake"),
|
|
36095
36440
|
stop: stop2
|
|
36096
36441
|
};
|
|
36097
36442
|
}
|
|
@@ -36245,10 +36590,14 @@ function localHolderPids(deps = {}) {
|
|
|
36245
36590
|
if (!/^\d+\.json$/.test(file)) continue;
|
|
36246
36591
|
let row;
|
|
36247
36592
|
try {
|
|
36248
|
-
row =
|
|
36593
|
+
row = parseSessionRecord(read3(join7(dir, file)));
|
|
36249
36594
|
} catch {
|
|
36250
36595
|
continue;
|
|
36251
36596
|
}
|
|
36597
|
+
if (!row) {
|
|
36598
|
+
deps.unreadable?.(file);
|
|
36599
|
+
continue;
|
|
36600
|
+
}
|
|
36252
36601
|
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
36253
36602
|
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
36254
36603
|
if (!holds(pid, row.procStart, check)) continue;
|
|
@@ -36290,10 +36639,14 @@ function localHolders(deps = {}) {
|
|
|
36290
36639
|
if (!/^\d+\.json$/.test(file)) continue;
|
|
36291
36640
|
let row;
|
|
36292
36641
|
try {
|
|
36293
|
-
row =
|
|
36642
|
+
row = parseSessionRecord(read3(join7(dir, file)));
|
|
36294
36643
|
} catch {
|
|
36295
36644
|
continue;
|
|
36296
36645
|
}
|
|
36646
|
+
if (!row) {
|
|
36647
|
+
deps.unreadable?.(file);
|
|
36648
|
+
continue;
|
|
36649
|
+
}
|
|
36297
36650
|
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
36298
36651
|
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
36299
36652
|
const slot = sessionSlot(row.sessionId);
|
|
@@ -36544,13 +36897,12 @@ function evict(runs, key3, run) {
|
|
|
36544
36897
|
if (runs.get(key3)?.run === run) runs.delete(key3);
|
|
36545
36898
|
}
|
|
36546
36899
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
36547
|
-
var IDLE_HOLDER_MS = 30 * 6e4;
|
|
36548
36900
|
function wakeAction(live, now, serverSeenAt, here = {}) {
|
|
36549
36901
|
if (live) return { action: "skip", why: "run" };
|
|
36550
36902
|
if (here.holder) return { action: "skip", why: "holder" };
|
|
36551
36903
|
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
|
|
36552
36904
|
if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
|
|
36553
|
-
if (here.movedAt !== void 0 && now - here.movedAt >=
|
|
36905
|
+
if (here.movedAt !== void 0 && now - here.movedAt >= WORKING_MS2 && !isWorking(here, now)) return { action: "spawn", why: "idle" };
|
|
36554
36906
|
return { action: "skip", why: "presence" };
|
|
36555
36907
|
}
|
|
36556
36908
|
return { action: "spawn", why: "nobody" };
|
|
@@ -36705,7 +37057,7 @@ function startHost(opts) {
|
|
|
36705
37057
|
const presence = /* @__PURE__ */ new Map();
|
|
36706
37058
|
try {
|
|
36707
37059
|
for (const c of (await listConnections(asHost)).items) {
|
|
36708
|
-
|
|
37060
|
+
presence.set(c.id, c);
|
|
36709
37061
|
}
|
|
36710
37062
|
lastPresence = presence;
|
|
36711
37063
|
} catch {
|
|
@@ -36715,9 +37067,14 @@ function startHost(opts) {
|
|
|
36715
37067
|
let lastPresence = /* @__PURE__ */ new Map();
|
|
36716
37068
|
async function evidence() {
|
|
36717
37069
|
const presence = await presenceMap();
|
|
36718
|
-
lastHolders = localHolders()
|
|
37070
|
+
lastHolders = localHolders({ unreadable: (file) => {
|
|
37071
|
+
if (unreadable.has(file)) return;
|
|
37072
|
+
unreadable.add(file);
|
|
37073
|
+
opts.log(`\u25CC session record ${file} has no object in it \u2014 the process it names reads as not running here`);
|
|
37074
|
+
} });
|
|
36719
37075
|
return { presence, holders: lastHolders };
|
|
36720
37076
|
}
|
|
37077
|
+
const unreadable = /* @__PURE__ */ new Set();
|
|
36721
37078
|
let lastHolders = /* @__PURE__ */ new Map();
|
|
36722
37079
|
async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
|
|
36723
37080
|
{
|
|
@@ -36733,11 +37090,12 @@ function startHost(opts) {
|
|
|
36733
37090
|
}
|
|
36734
37091
|
}
|
|
36735
37092
|
const tokenId = slotIdentity(slot).tokenId;
|
|
36736
|
-
const
|
|
37093
|
+
const connection = tokenId ? presence.get(tokenId) : void 0;
|
|
37094
|
+
const seenByServer = connection?.lastSeenAt ? Date.parse(connection.lastSeenAt) : void 0;
|
|
36737
37095
|
const holder = holders.get(slot);
|
|
36738
37096
|
if (holder) heldHere.add(slot);
|
|
36739
37097
|
else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
|
|
36740
|
-
const here = { holder, goneAt: goneAt.get(slot) };
|
|
37098
|
+
const here = { holder, goneAt: goneAt.get(slot), lastWorkedAt: connection?.lastWorkedAt };
|
|
36741
37099
|
let decided = wakeAction(!!live, Date.now(), seenByServer, here);
|
|
36742
37100
|
const label = slotName(slot) ?? slot;
|
|
36743
37101
|
if (decided.action === "skip" && decided.why !== "presence") {
|
|
@@ -36780,7 +37138,7 @@ function startHost(opts) {
|
|
|
36780
37138
|
const since = found.since;
|
|
36781
37139
|
decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
|
|
36782
37140
|
if (decided.action === "skip") {
|
|
36783
|
-
const said = `is
|
|
37141
|
+
const said = `is connected somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; takeover requires ${WORKING_MS2 / 6e4} min without own activity or waiting-work movement` : ", with nothing to say when it last moved"}`;
|
|
36784
37142
|
opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
|
|
36785
37143
|
decide(slot, "stood_back", found, seenByServer, said);
|
|
36786
37144
|
return;
|
|
@@ -36949,7 +37307,7 @@ function startHost(opts) {
|
|
|
36949
37307
|
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
36950
37308
|
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
36951
37309
|
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
36952
|
-
running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - (lastPresence.get(id3.tokenId) ??
|
|
37310
|
+
running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - Date.parse(lastPresence.get(id3.tokenId)?.lastSeenAt ?? "") < PRESENCE_FRESH_MS,
|
|
36953
37311
|
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
36954
37312
|
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
36955
37313
|
working: live.get(name)?.working() ?? false
|