@paigy/harness 0.3.28 → 0.3.30
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 +16 -5
- package/dist/agy-acp.js +44 -8
- package/dist/cli.js +666 -296
- package/dist/main.js +4917 -4563
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7585,8 +7585,8 @@ var require_webidl = __commonJS({
|
|
|
7585
7585
|
return new TypeError(`${message.header}: ${message.message}`);
|
|
7586
7586
|
};
|
|
7587
7587
|
webidl.errors.conversionFailed = function(context) {
|
|
7588
|
-
const
|
|
7589
|
-
const message = `${context.argument} could not be converted to${
|
|
7588
|
+
const plural3 = context.types.length === 1 ? "" : " one of";
|
|
7589
|
+
const message = `${context.argument} could not be converted to${plural3}: ${context.types.join(", ")}.`;
|
|
7590
7590
|
return webidl.errors.exception({
|
|
7591
7591
|
header: context.prefix,
|
|
7592
7592
|
message
|
|
@@ -15050,9 +15050,9 @@ var require_pluralizer = __commonJS({
|
|
|
15050
15050
|
this: "these"
|
|
15051
15051
|
};
|
|
15052
15052
|
module.exports = class Pluralizer {
|
|
15053
|
-
constructor(singular,
|
|
15053
|
+
constructor(singular, plural3) {
|
|
15054
15054
|
this.singular = singular;
|
|
15055
|
-
this.plural =
|
|
15055
|
+
this.plural = plural3;
|
|
15056
15056
|
}
|
|
15057
15057
|
pluralize(count) {
|
|
15058
15058
|
const one = count === 1;
|
|
@@ -23743,7 +23743,7 @@ function updateSlot(agent2, patch) {
|
|
|
23743
23743
|
}
|
|
23744
23744
|
function slotIdentity(agent2) {
|
|
23745
23745
|
const t = readTokenFile()[agent2];
|
|
23746
|
-
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
|
|
23746
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null };
|
|
23747
23747
|
}
|
|
23748
23748
|
function readToken(agent2 = agentName()) {
|
|
23749
23749
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
@@ -23783,9 +23783,14 @@ async function pollToken(deviceCode) {
|
|
|
23783
23783
|
if (err === "access_denied") throw new Error("Access denied by user. Pairing cancelled.");
|
|
23784
23784
|
throw new Error(`Unexpected error from /api/device/token: ${err}`);
|
|
23785
23785
|
}
|
|
23786
|
-
function ensureAuthed(res) {
|
|
23787
|
-
if (res.status
|
|
23788
|
-
|
|
23786
|
+
async function ensureAuthed(res) {
|
|
23787
|
+
if (res.status !== 401) return res;
|
|
23788
|
+
let body = null;
|
|
23789
|
+
try {
|
|
23790
|
+
body = await res.json();
|
|
23791
|
+
} catch {
|
|
23792
|
+
}
|
|
23793
|
+
throw body?.error === "revoked" ? new RevokedError() : new UnpairedError();
|
|
23789
23794
|
}
|
|
23790
23795
|
function overrideToken(secret) {
|
|
23791
23796
|
tokenOverride = secret;
|
|
@@ -23795,7 +23800,7 @@ async function fail(what, res) {
|
|
|
23795
23800
|
}
|
|
23796
23801
|
async function createGoal(input, opts = {}) {
|
|
23797
23802
|
const token = authToken(opts.token) ?? "";
|
|
23798
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
23803
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
23799
23804
|
method: "POST",
|
|
23800
23805
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
|
|
23801
23806
|
body: JSON.stringify(input)
|
|
@@ -23805,13 +23810,14 @@ async function createGoal(input, opts = {}) {
|
|
|
23805
23810
|
}
|
|
23806
23811
|
async function claimGoal(goalId, opts = {}) {
|
|
23807
23812
|
const token = authToken(opts.token) ?? "";
|
|
23808
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
23813
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
23809
23814
|
if (!res.ok) await fail("claim_goal", res);
|
|
23810
23815
|
return await res.json();
|
|
23811
23816
|
}
|
|
23812
23817
|
async function getGoal(goalId, opts = {}, read = {}) {
|
|
23813
23818
|
const token = authToken(opts.token) ?? "";
|
|
23814
|
-
const
|
|
23819
|
+
const query = [read.history ? "history=1" : "", read.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
23820
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
|
|
23815
23821
|
method: "GET",
|
|
23816
23822
|
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
23817
23823
|
}));
|
|
@@ -23821,12 +23827,12 @@ async function getGoal(goalId, opts = {}, read = {}) {
|
|
|
23821
23827
|
async function updateGoal(goalId, input, opts = {}) {
|
|
23822
23828
|
const token = authToken(opts.token) ?? "";
|
|
23823
23829
|
const operationId = input.operationId ?? randomUUID2();
|
|
23824
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
23830
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
23825
23831
|
if (!res.ok) await fail("update_goal", res);
|
|
23826
23832
|
return await res.json();
|
|
23827
23833
|
}
|
|
23828
23834
|
async function hatch(name, voice = null) {
|
|
23829
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
23835
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
23830
23836
|
method: "POST",
|
|
23831
23837
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
|
|
23832
23838
|
body: JSON.stringify({ name, voice })
|
|
@@ -23845,19 +23851,27 @@ async function whoAmI(opts = {}) {
|
|
|
23845
23851
|
}
|
|
23846
23852
|
}
|
|
23847
23853
|
async function claimSessions(opts = {}) {
|
|
23848
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
23854
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
23849
23855
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23850
23856
|
// the HOST's identity
|
|
23851
23857
|
}));
|
|
23852
23858
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
23853
23859
|
return (await res.json()).sessions;
|
|
23854
23860
|
}
|
|
23861
|
+
async function recordDecision(decision, opts = {}) {
|
|
23862
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
23863
|
+
method: "POST",
|
|
23864
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23865
|
+
body: JSON.stringify(decision)
|
|
23866
|
+
}));
|
|
23867
|
+
if (!res.ok) await fail("record_decision", res);
|
|
23868
|
+
}
|
|
23855
23869
|
async function heartbeat(runtime, opts = {}) {
|
|
23856
23870
|
const body = {
|
|
23857
23871
|
...runtime !== void 0 ? { runtime } : {},
|
|
23858
23872
|
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
23859
23873
|
};
|
|
23860
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
23874
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
23861
23875
|
method: "POST",
|
|
23862
23876
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23863
23877
|
...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
|
|
@@ -23865,7 +23879,7 @@ async function heartbeat(runtime, opts = {}) {
|
|
|
23865
23879
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
23866
23880
|
}
|
|
23867
23881
|
async function registerDelivery(mode, opts = {}) {
|
|
23868
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
23882
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
23869
23883
|
method: "POST",
|
|
23870
23884
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23871
23885
|
body: JSON.stringify({ mode })
|
|
@@ -23874,21 +23888,21 @@ async function registerDelivery(mode, opts = {}) {
|
|
|
23874
23888
|
return await res.json();
|
|
23875
23889
|
}
|
|
23876
23890
|
async function listNotes(opts = {}) {
|
|
23877
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
23891
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
23878
23892
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23879
23893
|
}));
|
|
23880
23894
|
if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
|
|
23881
23895
|
return await res.json();
|
|
23882
23896
|
}
|
|
23883
23897
|
async function listConnections(opts = {}) {
|
|
23884
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
23898
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
23885
23899
|
headers: { authorization: `Bearer ${authToken(opts.token)}`, "x-paigy-model": "goal-entry-v1" }
|
|
23886
23900
|
}));
|
|
23887
23901
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
23888
23902
|
return await res.json();
|
|
23889
23903
|
}
|
|
23890
23904
|
async function submitTriage(run, opts = {}) {
|
|
23891
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
23905
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
23892
23906
|
method: "POST",
|
|
23893
23907
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23894
23908
|
body: JSON.stringify(run)
|
|
@@ -23897,7 +23911,7 @@ async function submitTriage(run, opts = {}) {
|
|
|
23897
23911
|
return await res.json();
|
|
23898
23912
|
}
|
|
23899
23913
|
async function acceptTriage(proposalId, body, opts = {}) {
|
|
23900
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
23914
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
23901
23915
|
method: "POST",
|
|
23902
23916
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23903
23917
|
body: JSON.stringify(body)
|
|
@@ -23952,7 +23966,7 @@ async function contact(input, opts = {}) {
|
|
|
23952
23966
|
ask: a.ask,
|
|
23953
23967
|
options: a.options ?? []
|
|
23954
23968
|
}));
|
|
23955
|
-
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
23969
|
+
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
23956
23970
|
method: "POST",
|
|
23957
23971
|
headers,
|
|
23958
23972
|
signal: opts.signal,
|
|
@@ -23974,15 +23988,17 @@ async function contact(input, opts = {}) {
|
|
|
23974
23988
|
}
|
|
23975
23989
|
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
|
|
23976
23990
|
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
23991
|
+
const joinedCall = deliveries[0]?.joinedCall === true;
|
|
23977
23992
|
const read = async (signal2) => {
|
|
23978
|
-
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
23993
|
+
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
23979
23994
|
if (!res.ok) await fail("read_delivery", res);
|
|
23980
23995
|
const one = await res.json();
|
|
23981
23996
|
return {
|
|
23982
23997
|
...one,
|
|
23983
23998
|
...all.length > 1 ? { deliveries: all } : {},
|
|
23984
23999
|
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
23985
|
-
...joinedCard ? { joinedCard: true } : {}
|
|
24000
|
+
...joinedCard ? { joinedCard: true } : {},
|
|
24001
|
+
...joinedCall ? { joinedCall: true } : {}
|
|
23986
24002
|
};
|
|
23987
24003
|
};
|
|
23988
24004
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
@@ -24004,7 +24020,7 @@ async function contact(input, opts = {}) {
|
|
|
24004
24020
|
}
|
|
24005
24021
|
}
|
|
24006
24022
|
async function checkReplies(opts = {}) {
|
|
24007
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
24023
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
24008
24024
|
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
24009
24025
|
}));
|
|
24010
24026
|
if (!res.ok) await fail("check_replies", res);
|
|
@@ -24170,7 +24186,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24170
24186
|
}
|
|
24171
24187
|
};
|
|
24172
24188
|
}
|
|
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,
|
|
24189
|
+
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, 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, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, 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, 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, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24174
24190
|
var init_dist = __esm({
|
|
24175
24191
|
"../../packages/sdk/dist/index.js"() {
|
|
24176
24192
|
"use strict";
|
|
@@ -24583,18 +24599,18 @@ var init_dist = __esm({
|
|
|
24583
24599
|
parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
24584
24600
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
24585
24601
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
24586
|
-
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014
|
|
24602
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
24587
24603
|
),
|
|
24588
|
-
options: external_exports.array(OptionInputSchema).min(
|
|
24604
|
+
options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
|
|
24589
24605
|
}).strict();
|
|
24590
24606
|
StartContactSchema = external_exports.object({
|
|
24591
24607
|
asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
24592
|
-
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
24608
|
+
waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
24593
24609
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
24594
24610
|
}).strict();
|
|
24595
24611
|
ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
24596
24612
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
24597
|
-
CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read
|
|
24613
|
+
CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
24598
24614
|
CreateGoalSchema = external_exports.object({
|
|
24599
24615
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
24600
24616
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -24632,7 +24648,11 @@ var init_dist = __esm({
|
|
|
24632
24648
|
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
24633
24649
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
24634
24650
|
reviewed: external_exports.literal(true).optional(),
|
|
24635
|
-
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
|
|
24651
|
+
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
|
|
24652
|
+
/** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
|
|
24653
|
+
* (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
|
|
24654
|
+
* by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
|
|
24655
|
+
withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
|
|
24636
24656
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
24637
24657
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
24638
24658
|
operationId: external_exports.string().uuid().optional()
|
|
@@ -24643,10 +24663,14 @@ var init_dist = __esm({
|
|
|
24643
24663
|
goalId: external_exports.string().uuid(),
|
|
24644
24664
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
24645
24665
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
24646
|
-
history: external_exports.boolean().optional()
|
|
24666
|
+
history: external_exports.boolean().optional(),
|
|
24667
|
+
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
24668
|
+
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
24669
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
24670
|
+
diagnose: external_exports.boolean().optional()
|
|
24647
24671
|
}).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.";
|
|
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.`;
|
|
24672
|
+
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.\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.";
|
|
24673
|
+
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. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. 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. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
24650
24674
|
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.";
|
|
24651
24675
|
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.";
|
|
24652
24676
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
@@ -24660,6 +24684,7 @@ var init_dist = __esm({
|
|
|
24660
24684
|
];
|
|
24661
24685
|
AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
24662
24686
|
LIVE_MS = 3 * 6e4;
|
|
24687
|
+
WORKING_MS = 30 * 6e4;
|
|
24663
24688
|
ContextSchema = external_exports.object({
|
|
24664
24689
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
24665
24690
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -24730,7 +24755,7 @@ var init_dist = __esm({
|
|
|
24730
24755
|
points: external_exports.array(external_exports.string()).nullable(),
|
|
24731
24756
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
24732
24757
|
blocking: external_exports.boolean(),
|
|
24733
|
-
/** Reserved (
|
|
24758
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
24734
24759
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
24735
24760
|
deadline: external_exports.string().datetime().nullable().optional()
|
|
24736
24761
|
});
|
|
@@ -24781,7 +24806,7 @@ var init_dist = __esm({
|
|
|
24781
24806
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
24782
24807
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
24783
24808
|
// the ask into topics and gives each one at most three sentences and one question
|
|
24784
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
24809
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
24785
24810
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
24786
24811
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
24787
24812
|
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
@@ -24858,7 +24883,7 @@ var init_dist = __esm({
|
|
|
24858
24883
|
external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
|
|
24859
24884
|
external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
|
|
24860
24885
|
external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
|
|
24861
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
24886
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
24862
24887
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
24863
24888
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
24864
24889
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -24907,7 +24932,7 @@ var init_dist = __esm({
|
|
|
24907
24932
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
24908
24933
|
* a sentence with a hole in it is worse than no sentence. */
|
|
24909
24934
|
note: external_exports.string().optional(),
|
|
24910
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
24935
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
24911
24936
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
24912
24937
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
24913
24938
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -25003,10 +25028,11 @@ var init_dist = __esm({
|
|
|
25003
25028
|
agentId: external_exports.string().optional(),
|
|
25004
25029
|
select: SelectShapeSchema.optional(),
|
|
25005
25030
|
options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
|
|
25006
|
-
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25031
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
25032
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
25033
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
25034
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
25035
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
25010
25036
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
25011
25037
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
25012
25038
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -25041,7 +25067,7 @@ var init_dist = __esm({
|
|
|
25041
25067
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
25042
25068
|
* `createdAt`. */
|
|
25043
25069
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
25044
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
25070
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
25045
25071
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
25046
25072
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
25047
25073
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -25102,7 +25128,7 @@ var init_dist = __esm({
|
|
|
25102
25128
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
25103
25129
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
25104
25130
|
onCall: external_exports.literal(true).optional(),
|
|
25105
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
25131
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
25106
25132
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
25107
25133
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
25108
25134
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -25133,7 +25159,7 @@ var init_dist = __esm({
|
|
|
25133
25159
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
25134
25160
|
),
|
|
25135
25161
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
25136
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
25162
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
25137
25163
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
25138
25164
|
blocking: external_exports.boolean().default(false),
|
|
25139
25165
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -25207,9 +25233,16 @@ var init_dist = __esm({
|
|
|
25207
25233
|
banner: external_exports.boolean(),
|
|
25208
25234
|
push: external_exports.boolean()
|
|
25209
25235
|
}),
|
|
25236
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
25237
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
25238
|
+
* what people read (owner, 2026-09-30). */
|
|
25210
25239
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
25211
|
-
|
|
25212
|
-
|
|
25240
|
+
/** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
|
|
25241
|
+
* reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
|
|
25242
|
+
* PATCHes the whole settings object and would be refused for sending a key we stopped
|
|
25243
|
+
* wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
|
|
25244
|
+
* its column once no installed build still sends it. */
|
|
25245
|
+
silentPush: external_exports.boolean().optional(),
|
|
25213
25246
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
25214
25247
|
improveConsent: external_exports.boolean(),
|
|
25215
25248
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -25220,12 +25253,16 @@ var init_dist = __esm({
|
|
|
25220
25253
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
25221
25254
|
* write, 'hosted' on read (see store.ts). */
|
|
25222
25255
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
25223
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
25256
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
25224
25257
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
25225
25258
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
25226
25259
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
25227
25260
|
* read (see store.ts). */
|
|
25228
25261
|
talk: external_exports.boolean().optional(),
|
|
25262
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
25263
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
25264
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
25265
|
+
callDiagnostics: external_exports.boolean().optional(),
|
|
25229
25266
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
25230
25267
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
25231
25268
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -25234,9 +25271,6 @@ var init_dist = __esm({
|
|
|
25234
25271
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
25235
25272
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
25236
25273
|
voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
|
|
25237
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
25238
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
25239
|
-
pstnCalls: external_exports.boolean().optional(),
|
|
25240
25274
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
25241
25275
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
25242
25276
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -25250,18 +25284,20 @@ var init_dist = __esm({
|
|
|
25250
25284
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
25251
25285
|
broker: BrokerTuningSchema.optional()
|
|
25252
25286
|
});
|
|
25253
|
-
|
|
25287
|
+
HistoryWorkSchema = external_exports.object({
|
|
25254
25288
|
id: external_exports.string(),
|
|
25255
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
25256
|
-
initiator: external_exports.enum(["user", "agent"]),
|
|
25257
25289
|
title: external_exports.string(),
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25290
|
+
state: external_exports.enum(["done", "cancelled"]),
|
|
25291
|
+
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
25292
|
+
assignee: external_exports.string()
|
|
25293
|
+
});
|
|
25294
|
+
HistoryEntrySchema = external_exports.union([
|
|
25295
|
+
external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
|
|
25296
|
+
external_exports.object({ at: external_exports.string(), work: HistoryWorkSchema })
|
|
25297
|
+
]);
|
|
25298
|
+
HistoryPageSchema = external_exports.object({
|
|
25299
|
+
entries: external_exports.array(HistoryEntrySchema),
|
|
25300
|
+
next: external_exports.string().nullable()
|
|
25265
25301
|
});
|
|
25266
25302
|
ACTIVITY_LINES = 2;
|
|
25267
25303
|
ACTIVITY_LINE_MAX = 80;
|
|
@@ -25278,7 +25314,7 @@ var init_dist = __esm({
|
|
|
25278
25314
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
25279
25315
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
25280
25316
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
25281
|
-
*
|
|
25317
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
25282
25318
|
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
25283
25319
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
25284
25320
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -25296,7 +25332,7 @@ var init_dist = __esm({
|
|
|
25296
25332
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
25297
25333
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
25298
25334
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
25299
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
25335
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
25300
25336
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
25301
25337
|
* set once must not overrule a rule they set about one agent. */
|
|
25302
25338
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
@@ -25307,7 +25343,14 @@ var init_dist = __esm({
|
|
|
25307
25343
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
25308
25344
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
25309
25345
|
lastSeenAt: external_exports.string().datetime().nullable().optional(),
|
|
25310
|
-
/**
|
|
25346
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
25347
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
25348
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
25349
|
+
lastWorkedAt: external_exports.string().datetime().nullable().optional(),
|
|
25350
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
25351
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
25352
|
+
oldestReadyAt: external_exports.string().datetime().nullable().optional(),
|
|
25353
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
25311
25354
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
25312
25355
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
25313
25356
|
runtime: external_exports.object({
|
|
@@ -25315,7 +25358,12 @@ var init_dist = __esm({
|
|
|
25315
25358
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
25316
25359
|
version: external_exports.string().optional(),
|
|
25317
25360
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
25318
|
-
workspaces: external_exports.array(external_exports.string()).optional()
|
|
25361
|
+
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
25362
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
25363
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
25364
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
25365
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
25366
|
+
repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
|
|
25319
25367
|
}).optional(),
|
|
25320
25368
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
25321
25369
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -25398,7 +25446,7 @@ var init_dist = __esm({
|
|
|
25398
25446
|
* for a settled question whose reply carried nothing readable. */
|
|
25399
25447
|
answer: external_exports.string().nullable().default(null),
|
|
25400
25448
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
25401
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
25449
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
25402
25450
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
25403
25451
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
25404
25452
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -25445,6 +25493,9 @@ var init_dist = __esm({
|
|
|
25445
25493
|
progressLine: external_exports.string().nullable().optional(),
|
|
25446
25494
|
reviewPending: external_exports.boolean().default(false),
|
|
25447
25495
|
dueAt: external_exports.string().nullable().default(null),
|
|
25496
|
+
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
25497
|
+
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
25498
|
+
finishedAt: external_exports.string().nullable().optional(),
|
|
25448
25499
|
/** The Goal this one was opened under; null at the root. */
|
|
25449
25500
|
parentGoalId: external_exports.string().nullable().default(null),
|
|
25450
25501
|
/** Goals opened under this one — only those the same list holds. */
|
|
@@ -25453,8 +25504,17 @@ var init_dist = __esm({
|
|
|
25453
25504
|
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
25454
25505
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
25455
25506
|
blocked: external_exports.boolean().default(false),
|
|
25456
|
-
/**
|
|
25507
|
+
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
25508
|
+
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
25509
|
+
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
25510
|
+
* question is opened. */
|
|
25457
25511
|
questions: external_exports.array(QueueQuestionSchema).default([]),
|
|
25512
|
+
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
25513
|
+
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
25514
|
+
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
25515
|
+
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
25516
|
+
asked: external_exports.number().optional(),
|
|
25517
|
+
answered: external_exports.number().optional(),
|
|
25458
25518
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
25459
25519
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
25460
25520
|
* not spell an empty list. */
|
|
@@ -25478,7 +25538,19 @@ var init_dist = __esm({
|
|
|
25478
25538
|
said: external_exports.string(),
|
|
25479
25539
|
at: external_exports.string(),
|
|
25480
25540
|
entryId: external_exports.string()
|
|
25481
|
-
}).nullable().optional()
|
|
25541
|
+
}).nullable().optional(),
|
|
25542
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
25543
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
25544
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
25545
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
25546
|
+
* 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
25547
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
25548
|
+
*
|
|
25549
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
25550
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
25551
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
25552
|
+
* Entry with no `about_id` is in neither. */
|
|
25553
|
+
lastPersonAt: external_exports.string().nullable().optional()
|
|
25482
25554
|
});
|
|
25483
25555
|
COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
25484
25556
|
NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -25508,7 +25580,7 @@ var init_dist = __esm({
|
|
|
25508
25580
|
assignee: external_exports.string().nullable(),
|
|
25509
25581
|
/** The request thread minted at assignment; null until assigned. */
|
|
25510
25582
|
parentId: external_exports.string().nullable(),
|
|
25511
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
25583
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
25512
25584
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
25513
25585
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
25514
25586
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -25591,6 +25663,19 @@ var init_dist = __esm({
|
|
|
25591
25663
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
25592
25664
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
25593
25665
|
});
|
|
25666
|
+
HostDecisionSchema = external_exports.object({
|
|
25667
|
+
/** The agent's token id: the row's `recipient`. */
|
|
25668
|
+
agent: external_exports.string().uuid(),
|
|
25669
|
+
decision: external_exports.enum(["stood_back", "took_over"]),
|
|
25670
|
+
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
25671
|
+
goalId: external_exports.string().uuid().nullable().optional(),
|
|
25672
|
+
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
25673
|
+
seenAt: external_exports.string().datetime().nullable().optional(),
|
|
25674
|
+
/** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
|
|
25675
|
+
since: external_exports.string().datetime().nullable().optional(),
|
|
25676
|
+
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
25677
|
+
said: external_exports.string().max(300).optional()
|
|
25678
|
+
});
|
|
25594
25679
|
WakeNudgeSchema = external_exports.object({
|
|
25595
25680
|
kind: external_exports.enum(["reply", "request", "callback"]),
|
|
25596
25681
|
notificationId: external_exports.string().optional(),
|
|
@@ -25635,6 +25720,9 @@ var init_dist = __esm({
|
|
|
25635
25720
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
25636
25721
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
25637
25722
|
workspace: external_exports.string().nullable().optional(),
|
|
25723
|
+
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
25724
|
+
harness: external_exports.enum(["claude", "codex", "agy"]).optional(),
|
|
25725
|
+
session_id: external_exports.string().uuid().optional(),
|
|
25638
25726
|
uik_pub: external_exports.string().nullable().optional()
|
|
25639
25727
|
});
|
|
25640
25728
|
SupportRequestSchema = external_exports.object({
|
|
@@ -25754,6 +25842,29 @@ var init_dist = __esm({
|
|
|
25754
25842
|
computers: external_exports.array(ComputerRowSchema).nullable()
|
|
25755
25843
|
}).nullable()
|
|
25756
25844
|
});
|
|
25845
|
+
CallRecapSchema = external_exports.object({
|
|
25846
|
+
call: external_exports.object({
|
|
25847
|
+
status: external_exports.string(),
|
|
25848
|
+
startedAt: external_exports.string(),
|
|
25849
|
+
durationMs: external_exports.number().nullable(),
|
|
25850
|
+
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
25851
|
+
}),
|
|
25852
|
+
topics: external_exports.array(external_exports.object({
|
|
25853
|
+
goalId: external_exports.string().uuid(),
|
|
25854
|
+
title: external_exports.string(),
|
|
25855
|
+
owner: external_exports.string(),
|
|
25856
|
+
state: external_exports.string(),
|
|
25857
|
+
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
25858
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
25859
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
25860
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
25861
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
25862
|
+
* anything that is not an answer. */
|
|
25863
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional() }))
|
|
25864
|
+
})),
|
|
25865
|
+
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
25866
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
25867
|
+
});
|
|
25757
25868
|
TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
|
|
25758
25869
|
KEY_PATH = join22(homedir22(), ".paigy", "key.json");
|
|
25759
25870
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -25763,6 +25874,13 @@ var init_dist = __esm({
|
|
|
25763
25874
|
this.name = "UnpairedError";
|
|
25764
25875
|
}
|
|
25765
25876
|
};
|
|
25877
|
+
RevokedError = class extends UnpairedError {
|
|
25878
|
+
constructor() {
|
|
25879
|
+
super();
|
|
25880
|
+
this.message = "This identity was revoked (the API answered 401 revoked): its credential is gone for good. Pairing again makes a new identity.";
|
|
25881
|
+
this.name = "RevokedError";
|
|
25882
|
+
}
|
|
25883
|
+
};
|
|
25766
25884
|
ApiError = class extends Error {
|
|
25767
25885
|
status;
|
|
25768
25886
|
body;
|
|
@@ -30358,132 +30476,6 @@ var require_lib = __commonJS({
|
|
|
30358
30476
|
}
|
|
30359
30477
|
});
|
|
30360
30478
|
|
|
30361
|
-
// src/triage/judge.ts
|
|
30362
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
30363
|
-
import { homedir as homedir9 } from "os";
|
|
30364
|
-
import { delimiter as delimiter3 } from "path";
|
|
30365
|
-
function pickOllamaModel(tags) {
|
|
30366
|
-
if (tags.length === 0) return null;
|
|
30367
|
-
const score = (tag) => {
|
|
30368
|
-
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
30369
|
-
return i === -1 ? PREFERRED.length : i;
|
|
30370
|
-
};
|
|
30371
|
-
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
30372
|
-
}
|
|
30373
|
-
function parseOllamaTags(body) {
|
|
30374
|
-
const models = body?.models;
|
|
30375
|
-
if (!Array.isArray(models)) return [];
|
|
30376
|
-
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
30377
|
-
}
|
|
30378
|
-
async function fetchTags(host) {
|
|
30379
|
-
try {
|
|
30380
|
-
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
30381
|
-
if (!res.ok) return null;
|
|
30382
|
-
return parseOllamaTags(await res.json());
|
|
30383
|
-
} catch {
|
|
30384
|
-
return null;
|
|
30385
|
-
}
|
|
30386
|
-
}
|
|
30387
|
-
async function available(deps = {}) {
|
|
30388
|
-
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
30389
|
-
const model = tags ? pickOllamaModel(tags) : null;
|
|
30390
|
-
const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
|
|
30391
|
-
const detected = (deps.detect ?? detectAll)();
|
|
30392
|
-
const harnesses = CATALOG.map((entry) => {
|
|
30393
|
-
const state = detected.find((d) => d.name === entry.name);
|
|
30394
|
-
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
30395
|
-
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
30396
|
-
});
|
|
30397
|
-
return [ollama, ...harnesses];
|
|
30398
|
-
}
|
|
30399
|
-
function refusal(options, asked2) {
|
|
30400
|
-
const lines2 = [
|
|
30401
|
-
asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
|
|
30402
|
-
"",
|
|
30403
|
-
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
30404
|
-
];
|
|
30405
|
-
for (const o of options) {
|
|
30406
|
-
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
30407
|
-
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
30408
|
-
}
|
|
30409
|
-
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
30410
|
-
return lines2.join("\n");
|
|
30411
|
-
}
|
|
30412
|
-
async function judgeFor(asked2, deps = {}) {
|
|
30413
|
-
const options = await available(deps);
|
|
30414
|
-
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
30415
|
-
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
30416
|
-
if (chosen.provider === "ollama") {
|
|
30417
|
-
const model = chosen.model;
|
|
30418
|
-
return {
|
|
30419
|
-
provider: "ollama",
|
|
30420
|
-
model,
|
|
30421
|
-
run: async (prompt) => {
|
|
30422
|
-
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
30423
|
-
method: "POST",
|
|
30424
|
-
headers: { "content-type": "application/json" },
|
|
30425
|
-
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
30426
|
-
// either way. `temperature: 0` because this is a classification, not prose.
|
|
30427
|
-
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
30428
|
-
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
30429
|
-
});
|
|
30430
|
-
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
30431
|
-
return (await res.json()).response ?? "";
|
|
30432
|
-
}
|
|
30433
|
-
};
|
|
30434
|
-
}
|
|
30435
|
-
const runner = RUNNERS[chosen.provider];
|
|
30436
|
-
if (!runner) return { error: refusal(options, asked2) };
|
|
30437
|
-
const bin = which(runner.cli);
|
|
30438
|
-
if (!bin) return { error: refusal(options, asked2) };
|
|
30439
|
-
return {
|
|
30440
|
-
provider: chosen.provider,
|
|
30441
|
-
model: null,
|
|
30442
|
-
run: async (prompt) => {
|
|
30443
|
-
const out = spawnSync2(bin, runner.argv(null), {
|
|
30444
|
-
input: prompt,
|
|
30445
|
-
encoding: "utf8",
|
|
30446
|
-
cwd: homedir9(),
|
|
30447
|
-
timeout: RUN_TIMEOUT_MS,
|
|
30448
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
30449
|
-
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
30450
|
-
});
|
|
30451
|
-
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
30452
|
-
if (out.status !== 0 && !out.stdout?.trim())
|
|
30453
|
-
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
30454
|
-
return out.stdout ?? "";
|
|
30455
|
-
}
|
|
30456
|
-
};
|
|
30457
|
-
}
|
|
30458
|
-
var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
|
|
30459
|
-
var init_judge = __esm({
|
|
30460
|
-
"src/triage/judge.ts"() {
|
|
30461
|
-
"use strict";
|
|
30462
|
-
init_catalog();
|
|
30463
|
-
PRIVACY = {
|
|
30464
|
-
ollama: "never leaves this machine",
|
|
30465
|
-
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
30466
|
-
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
30467
|
-
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
30468
|
-
};
|
|
30469
|
-
OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
30470
|
-
PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
30471
|
-
RUNNERS = {
|
|
30472
|
-
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
30473
|
-
ollama: null,
|
|
30474
|
-
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
30475
|
-
claude: { cli: "claude", argv: () => ["-p"] },
|
|
30476
|
-
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
30477
|
-
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
30478
|
-
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
30479
|
-
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
30480
|
-
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
30481
|
-
agy: { cli: "agy", argv: () => ["-p"] }
|
|
30482
|
-
};
|
|
30483
|
-
RUN_TIMEOUT_MS = 5 * 6e4;
|
|
30484
|
-
}
|
|
30485
|
-
});
|
|
30486
|
-
|
|
30487
30479
|
// src/triage/evidence.ts
|
|
30488
30480
|
function assigneeToken(note2) {
|
|
30489
30481
|
const ref = note2.assignee ?? "";
|
|
@@ -30659,6 +30651,132 @@ var init_evidence = __esm({
|
|
|
30659
30651
|
}
|
|
30660
30652
|
});
|
|
30661
30653
|
|
|
30654
|
+
// src/triage/judge.ts
|
|
30655
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
30656
|
+
import { homedir as homedir5 } from "os";
|
|
30657
|
+
import { delimiter as delimiter3 } from "path";
|
|
30658
|
+
function pickOllamaModel(tags) {
|
|
30659
|
+
if (tags.length === 0) return null;
|
|
30660
|
+
const score = (tag) => {
|
|
30661
|
+
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
30662
|
+
return i === -1 ? PREFERRED.length : i;
|
|
30663
|
+
};
|
|
30664
|
+
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
30665
|
+
}
|
|
30666
|
+
function parseOllamaTags(body) {
|
|
30667
|
+
const models = body?.models;
|
|
30668
|
+
if (!Array.isArray(models)) return [];
|
|
30669
|
+
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
30670
|
+
}
|
|
30671
|
+
async function fetchTags(host) {
|
|
30672
|
+
try {
|
|
30673
|
+
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
30674
|
+
if (!res.ok) return null;
|
|
30675
|
+
return parseOllamaTags(await res.json());
|
|
30676
|
+
} catch {
|
|
30677
|
+
return null;
|
|
30678
|
+
}
|
|
30679
|
+
}
|
|
30680
|
+
async function available(deps = {}) {
|
|
30681
|
+
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
30682
|
+
const model = tags ? pickOllamaModel(tags) : null;
|
|
30683
|
+
const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
|
|
30684
|
+
const detected = (deps.detect ?? detectAll)();
|
|
30685
|
+
const harnesses = CATALOG.map((entry) => {
|
|
30686
|
+
const state = detected.find((d) => d.name === entry.name);
|
|
30687
|
+
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
30688
|
+
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
30689
|
+
});
|
|
30690
|
+
return [ollama, ...harnesses];
|
|
30691
|
+
}
|
|
30692
|
+
function refusal(options, asked2) {
|
|
30693
|
+
const lines2 = [
|
|
30694
|
+
asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
|
|
30695
|
+
"",
|
|
30696
|
+
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
30697
|
+
];
|
|
30698
|
+
for (const o of options) {
|
|
30699
|
+
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
30700
|
+
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
30701
|
+
}
|
|
30702
|
+
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
30703
|
+
return lines2.join("\n");
|
|
30704
|
+
}
|
|
30705
|
+
async function judgeFor(asked2, deps = {}) {
|
|
30706
|
+
const options = await available(deps);
|
|
30707
|
+
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
30708
|
+
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
30709
|
+
if (chosen.provider === "ollama") {
|
|
30710
|
+
const model = chosen.model;
|
|
30711
|
+
return {
|
|
30712
|
+
provider: "ollama",
|
|
30713
|
+
model,
|
|
30714
|
+
run: async (prompt) => {
|
|
30715
|
+
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
30716
|
+
method: "POST",
|
|
30717
|
+
headers: { "content-type": "application/json" },
|
|
30718
|
+
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
30719
|
+
// either way. `temperature: 0` because this is a classification, not prose.
|
|
30720
|
+
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
30721
|
+
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
30722
|
+
});
|
|
30723
|
+
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
30724
|
+
return (await res.json()).response ?? "";
|
|
30725
|
+
}
|
|
30726
|
+
};
|
|
30727
|
+
}
|
|
30728
|
+
const runner = RUNNERS[chosen.provider];
|
|
30729
|
+
if (!runner) return { error: refusal(options, asked2) };
|
|
30730
|
+
const bin = which(runner.cli);
|
|
30731
|
+
if (!bin) return { error: refusal(options, asked2) };
|
|
30732
|
+
return {
|
|
30733
|
+
provider: chosen.provider,
|
|
30734
|
+
model: null,
|
|
30735
|
+
run: async (prompt) => {
|
|
30736
|
+
const out = spawnSync2(bin, runner.argv(null), {
|
|
30737
|
+
input: prompt,
|
|
30738
|
+
encoding: "utf8",
|
|
30739
|
+
cwd: homedir5(),
|
|
30740
|
+
timeout: RUN_TIMEOUT_MS,
|
|
30741
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
30742
|
+
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
30743
|
+
});
|
|
30744
|
+
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
30745
|
+
if (out.status !== 0 && !out.stdout?.trim())
|
|
30746
|
+
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
30747
|
+
return out.stdout ?? "";
|
|
30748
|
+
}
|
|
30749
|
+
};
|
|
30750
|
+
}
|
|
30751
|
+
var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
|
|
30752
|
+
var init_judge = __esm({
|
|
30753
|
+
"src/triage/judge.ts"() {
|
|
30754
|
+
"use strict";
|
|
30755
|
+
init_catalog();
|
|
30756
|
+
PRIVACY = {
|
|
30757
|
+
ollama: "never leaves this machine",
|
|
30758
|
+
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
30759
|
+
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
30760
|
+
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
30761
|
+
};
|
|
30762
|
+
OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
30763
|
+
PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
30764
|
+
RUNNERS = {
|
|
30765
|
+
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
30766
|
+
ollama: null,
|
|
30767
|
+
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
30768
|
+
claude: { cli: "claude", argv: () => ["-p"] },
|
|
30769
|
+
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
30770
|
+
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
30771
|
+
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
30772
|
+
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
30773
|
+
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
30774
|
+
agy: { cli: "agy", argv: () => ["-p"] }
|
|
30775
|
+
};
|
|
30776
|
+
RUN_TIMEOUT_MS = 5 * 6e4;
|
|
30777
|
+
}
|
|
30778
|
+
});
|
|
30779
|
+
|
|
30662
30780
|
// src/triage/verdict.ts
|
|
30663
30781
|
function extractJson(raw) {
|
|
30664
30782
|
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
@@ -32861,18 +32979,18 @@ var AskInputSchema2 = external_exports.object({
|
|
|
32861
32979
|
parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
32862
32980
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
32863
32981
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32864
|
-
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014
|
|
32982
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
32865
32983
|
),
|
|
32866
|
-
options: external_exports.array(OptionInputSchema2).min(
|
|
32984
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
32867
32985
|
}).strict();
|
|
32868
32986
|
var StartContactSchema2 = external_exports.object({
|
|
32869
32987
|
asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
32870
|
-
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
32988
|
+
waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
32871
32989
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
32872
32990
|
}).strict();
|
|
32873
32991
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32874
32992
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32875
|
-
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read
|
|
32993
|
+
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
32876
32994
|
var CreateGoalSchema2 = external_exports.object({
|
|
32877
32995
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32878
32996
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -32910,7 +33028,11 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
32910
33028
|
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
32911
33029
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
32912
33030
|
reviewed: external_exports.literal(true).optional(),
|
|
32913
|
-
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
|
|
33031
|
+
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
|
|
33032
|
+
/** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
|
|
33033
|
+
* (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
|
|
33034
|
+
* by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
|
|
33035
|
+
withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
|
|
32914
33036
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
32915
33037
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
32916
33038
|
operationId: external_exports.string().uuid().optional()
|
|
@@ -32921,10 +33043,14 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
32921
33043
|
goalId: external_exports.string().uuid(),
|
|
32922
33044
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
32923
33045
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
32924
|
-
history: external_exports.boolean().optional()
|
|
33046
|
+
history: external_exports.boolean().optional(),
|
|
33047
|
+
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
33048
|
+
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
33049
|
+
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
33050
|
+
diagnose: external_exports.boolean().optional()
|
|
32925
33051
|
}).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.";
|
|
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.`;
|
|
33052
|
+
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.\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.";
|
|
33053
|
+
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. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. 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. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
32928
33054
|
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.";
|
|
32929
33055
|
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.";
|
|
32930
33056
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -32937,7 +33063,6 @@ var AGENT_TOOLS2 = [
|
|
|
32937
33063
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
32938
33064
|
];
|
|
32939
33065
|
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.";
|
|
32941
33066
|
function entryWords(entry) {
|
|
32942
33067
|
const content = entry.content;
|
|
32943
33068
|
if (content && "sealed" in content) return "";
|
|
@@ -32954,6 +33079,7 @@ function entryWords(entry) {
|
|
|
32954
33079
|
return entry.sources.map((source) => source.text).join("\n");
|
|
32955
33080
|
}
|
|
32956
33081
|
var LIVE_MS2 = 3 * 6e4;
|
|
33082
|
+
var WORKING_MS2 = 30 * 6e4;
|
|
32957
33083
|
var ContextSchema2 = external_exports.object({
|
|
32958
33084
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
32959
33085
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -33024,7 +33150,7 @@ var AttentionSchema2 = external_exports.object({
|
|
|
33024
33150
|
points: external_exports.array(external_exports.string()).nullable(),
|
|
33025
33151
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
33026
33152
|
blocking: external_exports.boolean(),
|
|
33027
|
-
/** Reserved (
|
|
33153
|
+
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
33028
33154
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
33029
33155
|
deadline: external_exports.string().datetime().nullable().optional()
|
|
33030
33156
|
});
|
|
@@ -33075,7 +33201,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
33075
33201
|
* derive agent-side before sealing, so the server only ever shapes plaintext). */
|
|
33076
33202
|
// 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
|
|
33077
33203
|
// the ask into topics and gives each one at most three sentences and one question
|
|
33078
|
-
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
33204
|
+
// (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
33079
33205
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
33080
33206
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
33081
33207
|
ask: external_exports.string().min(1).max(1e4).optional().describe(
|
|
@@ -33152,7 +33278,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
|
|
|
33152
33278
|
external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
|
|
33153
33279
|
external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
|
|
33154
33280
|
external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
|
|
33155
|
-
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
33281
|
+
/** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
|
|
33156
33282
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
33157
33283
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
33158
33284
|
* card the user can reply to — the broker never overrides the user. */
|
|
@@ -33201,7 +33327,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
33201
33327
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
33202
33328
|
* a sentence with a hole in it is worse than no sentence. */
|
|
33203
33329
|
note: external_exports.string().optional(),
|
|
33204
|
-
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
33330
|
+
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
33205
33331
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
33206
33332
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
33207
33333
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
@@ -33297,10 +33423,11 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
33297
33423
|
agentId: external_exports.string().optional(),
|
|
33298
33424
|
select: SelectShapeSchema2.optional(),
|
|
33299
33425
|
options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
|
|
33300
|
-
|
|
33301
|
-
|
|
33302
|
-
|
|
33303
|
-
|
|
33426
|
+
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
33427
|
+
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
33428
|
+
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
33429
|
+
spoken, not where the plan is written, so nothing between the brain and the walk decides
|
|
33430
|
+
anything. A caller's own `beat_s` tuning is what it used to override. */
|
|
33304
33431
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
33305
33432
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
33306
33433
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
@@ -33335,7 +33462,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33335
33462
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
33336
33463
|
* `createdAt`. */
|
|
33337
33464
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
33338
|
-
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
33465
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
33339
33466
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
33340
33467
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
33341
33468
|
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
@@ -33396,7 +33523,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33396
33523
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
33397
33524
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
33398
33525
|
onCall: external_exports.literal(true).optional(),
|
|
33399
|
-
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
33526
|
+
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
33400
33527
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
33401
33528
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
33402
33529
|
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
@@ -33427,7 +33554,7 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
33427
33554
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
33428
33555
|
),
|
|
33429
33556
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
33430
|
-
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
33557
|
+
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
33431
33558
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
33432
33559
|
blocking: external_exports.boolean().default(false),
|
|
33433
33560
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
@@ -33501,9 +33628,16 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33501
33628
|
banner: external_exports.boolean(),
|
|
33502
33629
|
push: external_exports.boolean()
|
|
33503
33630
|
}),
|
|
33631
|
+
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
33632
|
+
* they are an enum on a live column across every account, and the rename is a rename of
|
|
33633
|
+
* what people read (owner, 2026-09-30). */
|
|
33504
33634
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
33505
|
-
|
|
33506
|
-
|
|
33635
|
+
/** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
|
|
33636
|
+
* reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
|
|
33637
|
+
* PATCHes the whole settings object and would be refused for sending a key we stopped
|
|
33638
|
+
* wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
|
|
33639
|
+
* its column once no installed build still sends it. */
|
|
33640
|
+
silentPush: external_exports.boolean().optional(),
|
|
33507
33641
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
33508
33642
|
improveConsent: external_exports.boolean(),
|
|
33509
33643
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -33514,12 +33648,16 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33514
33648
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
33515
33649
|
* write, 'hosted' on read (see store.ts). */
|
|
33516
33650
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
33517
|
-
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
33651
|
+
/** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
|
|
33518
33652
|
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
33519
33653
|
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
33520
33654
|
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
33521
33655
|
* read (see store.ts). */
|
|
33522
33656
|
talk: external_exports.boolean().optional(),
|
|
33657
|
+
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
33658
|
+
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
33659
|
+
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
33660
|
+
callDiagnostics: external_exports.boolean().optional(),
|
|
33523
33661
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
33524
33662
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
33525
33663
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -33528,9 +33666,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33528
33666
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
33529
33667
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
33530
33668
|
voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
|
|
33531
|
-
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
33532
|
-
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
33533
|
-
pstnCalls: external_exports.boolean().optional(),
|
|
33534
33669
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
33535
33670
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
33536
33671
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -33544,18 +33679,20 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33544
33679
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
33545
33680
|
broker: BrokerTuningSchema2.optional()
|
|
33546
33681
|
});
|
|
33547
|
-
var
|
|
33682
|
+
var HistoryWorkSchema2 = external_exports.object({
|
|
33548
33683
|
id: external_exports.string(),
|
|
33549
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
33550
|
-
initiator: external_exports.enum(["user", "agent"]),
|
|
33551
33684
|
title: external_exports.string(),
|
|
33552
|
-
|
|
33553
|
-
|
|
33554
|
-
|
|
33555
|
-
|
|
33556
|
-
|
|
33557
|
-
|
|
33558
|
-
|
|
33685
|
+
state: external_exports.enum(["done", "cancelled"]),
|
|
33686
|
+
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
33687
|
+
assignee: external_exports.string()
|
|
33688
|
+
});
|
|
33689
|
+
var HistoryEntrySchema2 = external_exports.union([
|
|
33690
|
+
external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
|
|
33691
|
+
external_exports.object({ at: external_exports.string(), work: HistoryWorkSchema2 })
|
|
33692
|
+
]);
|
|
33693
|
+
var HistoryPageSchema2 = external_exports.object({
|
|
33694
|
+
entries: external_exports.array(HistoryEntrySchema2),
|
|
33695
|
+
next: external_exports.string().nullable()
|
|
33559
33696
|
});
|
|
33560
33697
|
var ACTIVITY_LINES2 = 2;
|
|
33561
33698
|
var ACTIVITY_LINE_MAX2 = 80;
|
|
@@ -33572,7 +33709,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33572
33709
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
33573
33710
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
33574
33711
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
33575
|
-
*
|
|
33712
|
+
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
33576
33713
|
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
33577
33714
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
33578
33715
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
@@ -33590,7 +33727,7 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33590
33727
|
* user on the agent's own page. null = no ceiling (today's behaviour for every
|
|
33591
33728
|
* connection). Android binds importance to a relationship rather than to each message,
|
|
33592
33729
|
* and that is the thing our roster could not say: "Marlow may always call me; Otto
|
|
33593
|
-
* never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
33730
|
+
* never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
|
|
33594
33731
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
33595
33732
|
* set once must not overrule a rule they set about one agent. */
|
|
33596
33733
|
reach: NotifyLevelSchema2.nullable().optional(),
|
|
@@ -33601,7 +33738,14 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33601
33738
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
33602
33739
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
33603
33740
|
lastSeenAt: external_exports.string().datetime().nullable().optional(),
|
|
33604
|
-
/**
|
|
33741
|
+
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
33742
|
+
* its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
|
|
33743
|
+
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
33744
|
+
lastWorkedAt: external_exports.string().datetime().nullable().optional(),
|
|
33745
|
+
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
33746
|
+
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
33747
|
+
oldestReadyAt: external_exports.string().datetime().nullable().optional(),
|
|
33748
|
+
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
33605
33749
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
33606
33750
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
33607
33751
|
runtime: external_exports.object({
|
|
@@ -33609,7 +33753,12 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33609
33753
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
33610
33754
|
version: external_exports.string().optional(),
|
|
33611
33755
|
harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
|
|
33612
|
-
workspaces: external_exports.array(external_exports.string()).optional()
|
|
33756
|
+
workspaces: external_exports.array(external_exports.string()).optional(),
|
|
33757
|
+
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
33758
|
+
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
33759
|
+
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
33760
|
+
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
33761
|
+
repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
|
|
33613
33762
|
}).optional(),
|
|
33614
33763
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
33615
33764
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -33692,7 +33841,7 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
33692
33841
|
* for a settled question whose reply carried nothing readable. */
|
|
33693
33842
|
answer: external_exports.string().nullable().default(null),
|
|
33694
33843
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
33695
|
-
* an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
|
|
33844
|
+
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
33696
33845
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
33697
33846
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
33698
33847
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
@@ -33739,6 +33888,9 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33739
33888
|
progressLine: external_exports.string().nullable().optional(),
|
|
33740
33889
|
reviewPending: external_exports.boolean().default(false),
|
|
33741
33890
|
dueAt: external_exports.string().nullable().default(null),
|
|
33891
|
+
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
33892
|
+
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
33893
|
+
finishedAt: external_exports.string().nullable().optional(),
|
|
33742
33894
|
/** The Goal this one was opened under; null at the root. */
|
|
33743
33895
|
parentGoalId: external_exports.string().nullable().default(null),
|
|
33744
33896
|
/** Goals opened under this one — only those the same list holds. */
|
|
@@ -33747,8 +33899,17 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33747
33899
|
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
33748
33900
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
33749
33901
|
blocked: external_exports.boolean().default(false),
|
|
33750
|
-
/**
|
|
33902
|
+
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
33903
|
+
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
33904
|
+
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
33905
|
+
* question is opened. */
|
|
33751
33906
|
questions: external_exports.array(QueueQuestionSchema2).default([]),
|
|
33907
|
+
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
33908
|
+
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
33909
|
+
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
33910
|
+
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
33911
|
+
asked: external_exports.number().optional(),
|
|
33912
|
+
answered: external_exports.number().optional(),
|
|
33752
33913
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
33753
33914
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
33754
33915
|
* not spell an empty list. */
|
|
@@ -33772,7 +33933,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33772
33933
|
said: external_exports.string(),
|
|
33773
33934
|
at: external_exports.string(),
|
|
33774
33935
|
entryId: external_exports.string()
|
|
33775
|
-
}).nullable().optional()
|
|
33936
|
+
}).nullable().optional(),
|
|
33937
|
+
/** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
|
|
33938
|
+
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
33939
|
+
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
33940
|
+
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
33941
|
+
* 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
33942
|
+
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
33943
|
+
*
|
|
33944
|
+
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
33945
|
+
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
33946
|
+
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
33947
|
+
* Entry with no `about_id` is in neither. */
|
|
33948
|
+
lastPersonAt: external_exports.string().nullable().optional()
|
|
33776
33949
|
});
|
|
33777
33950
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
33778
33951
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -33802,7 +33975,7 @@ var NoteSchema2 = external_exports.object({
|
|
|
33802
33975
|
assignee: external_exports.string().nullable(),
|
|
33803
33976
|
/** The request thread minted at assignment; null until assigned. */
|
|
33804
33977
|
parentId: external_exports.string().nullable(),
|
|
33805
|
-
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
33978
|
+
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
33806
33979
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
33807
33980
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
33808
33981
|
* a ring at that time); after that it rides like any other. Null = "the very next
|
|
@@ -33883,6 +34056,19 @@ var DeliveryConfigSchema2 = external_exports.object({
|
|
|
33883
34056
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
33884
34057
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
33885
34058
|
});
|
|
34059
|
+
var HostDecisionSchema2 = external_exports.object({
|
|
34060
|
+
/** The agent's token id: the row's `recipient`. */
|
|
34061
|
+
agent: external_exports.string().uuid(),
|
|
34062
|
+
decision: external_exports.enum(["stood_back", "took_over"]),
|
|
34063
|
+
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
34064
|
+
goalId: external_exports.string().uuid().nullable().optional(),
|
|
34065
|
+
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
34066
|
+
seenAt: external_exports.string().datetime().nullable().optional(),
|
|
34067
|
+
/** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
|
|
34068
|
+
since: external_exports.string().datetime().nullable().optional(),
|
|
34069
|
+
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
34070
|
+
said: external_exports.string().max(300).optional()
|
|
34071
|
+
});
|
|
33886
34072
|
var WakeNudgeSchema2 = external_exports.object({
|
|
33887
34073
|
kind: external_exports.enum(["reply", "request", "callback"]),
|
|
33888
34074
|
notificationId: external_exports.string().optional(),
|
|
@@ -33927,6 +34113,9 @@ var DeviceTokenSchema2 = external_exports.object({
|
|
|
33927
34113
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
33928
34114
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
33929
34115
|
workspace: external_exports.string().nullable().optional(),
|
|
34116
|
+
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
34117
|
+
harness: external_exports.enum(["claude", "codex", "agy"]).optional(),
|
|
34118
|
+
session_id: external_exports.string().uuid().optional(),
|
|
33930
34119
|
uik_pub: external_exports.string().nullable().optional()
|
|
33931
34120
|
});
|
|
33932
34121
|
var SupportRequestSchema2 = external_exports.object({
|
|
@@ -34046,6 +34235,29 @@ var SnapshotSchema2 = external_exports.object({
|
|
|
34046
34235
|
computers: external_exports.array(ComputerRowSchema2).nullable()
|
|
34047
34236
|
}).nullable()
|
|
34048
34237
|
});
|
|
34238
|
+
var CallRecapSchema2 = external_exports.object({
|
|
34239
|
+
call: external_exports.object({
|
|
34240
|
+
status: external_exports.string(),
|
|
34241
|
+
startedAt: external_exports.string(),
|
|
34242
|
+
durationMs: external_exports.number().nullable(),
|
|
34243
|
+
agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
|
|
34244
|
+
}),
|
|
34245
|
+
topics: external_exports.array(external_exports.object({
|
|
34246
|
+
goalId: external_exports.string().uuid(),
|
|
34247
|
+
title: external_exports.string(),
|
|
34248
|
+
owner: external_exports.string(),
|
|
34249
|
+
state: external_exports.string(),
|
|
34250
|
+
questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
|
|
34251
|
+
/** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
|
|
34252
|
+
* their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
|
|
34253
|
+
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
34254
|
+
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
34255
|
+
* anything that is not an answer. */
|
|
34256
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional() }))
|
|
34257
|
+
})),
|
|
34258
|
+
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
34259
|
+
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
34260
|
+
});
|
|
34049
34261
|
|
|
34050
34262
|
// src/paigy/activity.ts
|
|
34051
34263
|
function shortenPaths(s) {
|
|
@@ -34146,6 +34358,14 @@ var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on
|
|
|
34146
34358
|
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
34359
|
var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
34148
34360
|
var GATHER_MS = 9e4;
|
|
34361
|
+
function outcome(result, paused) {
|
|
34362
|
+
const r = result ?? {};
|
|
34363
|
+
const who = `${paused.length} paused agent${paused.length === 1 ? "" : "s"}: ${names(paused)}`;
|
|
34364
|
+
if (r.sent === false) return `\u26A0 the limit call did not ring (${who}): it was filed as progress, nobody was notified`;
|
|
34365
|
+
if (r.kind === "call" && typeof r.deliveryId === "string") return `\u260E one call for ${who}, placed (delivery ${r.deliveryId})`;
|
|
34366
|
+
if (typeof r.deliveryId === "string") return `\u26A0 the limit call did not ring (${who}): it went out as a card (delivery ${r.deliveryId})`;
|
|
34367
|
+
return `\u26A0 the limit call did not ring (${who}): no delivery came back`;
|
|
34368
|
+
}
|
|
34149
34369
|
function episodes(opts = {}) {
|
|
34150
34370
|
const gather2 = opts.gatherMs ?? GATHER_MS;
|
|
34151
34371
|
const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
|
|
@@ -34177,8 +34397,10 @@ function episodes(opts = {}) {
|
|
|
34177
34397
|
`Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
|
|
34178
34398
|
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
34399
|
].join(" ");
|
|
34180
|
-
|
|
34181
|
-
|
|
34400
|
+
void call(text).then(
|
|
34401
|
+
(result) => log?.(outcome(result, episode.paused)),
|
|
34402
|
+
(error) => log?.(`\u26A0 could not place the limit call: ${error.message}`)
|
|
34403
|
+
);
|
|
34182
34404
|
}, gather2);
|
|
34183
34405
|
}
|
|
34184
34406
|
};
|
|
@@ -34301,7 +34523,7 @@ ${event.result?.trim() || "No result reported."}`, state, rail, opts);
|
|
|
34301
34523
|
|
|
34302
34524
|
// src/paigy/conversation.ts
|
|
34303
34525
|
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).
|
|
34526
|
+
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).";
|
|
34305
34527
|
var SAFETY_MS = 6e4;
|
|
34306
34528
|
var ASK_BEAT_MS = 5e3;
|
|
34307
34529
|
function beat() {
|
|
@@ -34429,9 +34651,9 @@ async function gather(state, rail, held) {
|
|
|
34429
34651
|
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
34652
|
var says = (h) => h.words.length > 0 || h.via === "assigned";
|
|
34431
34653
|
function whatItIs(h) {
|
|
34432
|
-
const
|
|
34433
|
-
const name = (h.title ??
|
|
34434
|
-
return
|
|
34654
|
+
const outcome2 = h.brief.outcome?.trim() ?? "";
|
|
34655
|
+
const name = (h.title ?? outcome2).trim().split("\n")[0].trim();
|
|
34656
|
+
return outcome2 && outcome2 !== name ? [outcome2] : [];
|
|
34435
34657
|
}
|
|
34436
34658
|
var block = (h) => [
|
|
34437
34659
|
...h.via === "assigned" ? [GIVEN] : [],
|
|
@@ -34558,11 +34780,11 @@ async function look(session, state, rail, opts) {
|
|
|
34558
34780
|
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
34559
34781
|
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
34560
34782
|
}
|
|
34561
|
-
const
|
|
34562
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !==
|
|
34783
|
+
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
34784
|
+
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
34563
34785
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
34564
34786
|
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 = [
|
|
34787
|
+
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
34566
34788
|
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
34567
34789
|
return;
|
|
34568
34790
|
}
|
|
@@ -34748,17 +34970,33 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
34748
34970
|
|
|
34749
34971
|
// src/host.ts
|
|
34750
34972
|
init_dist();
|
|
34751
|
-
init_catalog();
|
|
34752
34973
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
34753
|
-
import { homedir as
|
|
34974
|
+
import { homedir as homedir9 } from "os";
|
|
34754
34975
|
import { join as join9 } from "path";
|
|
34755
34976
|
|
|
34977
|
+
// src/broker.ts
|
|
34978
|
+
init_evidence();
|
|
34979
|
+
init_triage();
|
|
34980
|
+
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
34981
|
+
function ago(iso, now) {
|
|
34982
|
+
const ms = now - Date.parse(iso);
|
|
34983
|
+
if (!Number.isFinite(ms) || ms < 6e4) return "just now";
|
|
34984
|
+
const minutes = Math.floor(ms / 6e4);
|
|
34985
|
+
if (minutes < 60) return `${plural2(minutes, "minute")} ago`;
|
|
34986
|
+
const hours = Math.floor(minutes / 60);
|
|
34987
|
+
if (hours < 24) return `${plural2(hours, "hour")} ago`;
|
|
34988
|
+
return `${plural2(Math.floor(hours / 24), "day")} ago`;
|
|
34989
|
+
}
|
|
34990
|
+
|
|
34991
|
+
// src/host.ts
|
|
34992
|
+
init_catalog();
|
|
34993
|
+
|
|
34756
34994
|
// src/workspaces.ts
|
|
34757
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
34995
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
34758
34996
|
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
34759
|
-
import { homedir as
|
|
34997
|
+
import { homedir as homedir6 } from "os";
|
|
34760
34998
|
function workspacesFile() {
|
|
34761
|
-
return join5(
|
|
34999
|
+
return join5(homedir6(), ".paigy", "workspaces.json");
|
|
34762
35000
|
}
|
|
34763
35001
|
function listWorkspaces(deps) {
|
|
34764
35002
|
try {
|
|
@@ -34769,7 +35007,7 @@ function listWorkspaces(deps) {
|
|
|
34769
35007
|
}
|
|
34770
35008
|
}
|
|
34771
35009
|
function addWorkspace(dir, deps) {
|
|
34772
|
-
const next = resolve2(dir.replace(/^~(?=$|\/)/,
|
|
35010
|
+
const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir6()));
|
|
34773
35011
|
const all = listWorkspaces(deps);
|
|
34774
35012
|
if (!all.includes(next)) {
|
|
34775
35013
|
all.push(next);
|
|
@@ -34779,22 +35017,53 @@ function addWorkspace(dir, deps) {
|
|
|
34779
35017
|
return all;
|
|
34780
35018
|
}
|
|
34781
35019
|
function allowed(cwd, deps) {
|
|
34782
|
-
const target = resolve2(cwd.replace(/^~(?=$|\/)/,
|
|
35020
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir6()));
|
|
34783
35021
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
34784
35022
|
}
|
|
34785
35023
|
function resolveWakeDir(pinned, deps) {
|
|
34786
35024
|
if (pinned && allowed(pinned, deps)) {
|
|
34787
|
-
const dir = resolve2(pinned.replace(/^~(?=$|\/)/,
|
|
35025
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir6()));
|
|
34788
35026
|
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
34789
35027
|
}
|
|
34790
35028
|
return listWorkspaces(deps)[0];
|
|
34791
35029
|
}
|
|
35030
|
+
function originOf(dir) {
|
|
35031
|
+
try {
|
|
35032
|
+
if (!statSync3(join5(dir, ".git")).isDirectory()) return null;
|
|
35033
|
+
const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
|
|
35034
|
+
const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
|
|
35035
|
+
return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
35036
|
+
} catch {
|
|
35037
|
+
return null;
|
|
35038
|
+
}
|
|
35039
|
+
}
|
|
35040
|
+
var MAX_CHILDREN = 300;
|
|
35041
|
+
var MAX_REPOS = 100;
|
|
35042
|
+
function listRepos(deps) {
|
|
35043
|
+
const repos = [];
|
|
35044
|
+
for (const root of listWorkspaces(deps)) {
|
|
35045
|
+
const own = originOf(root);
|
|
35046
|
+
if (own) repos.push({ path: root, remote: own });
|
|
35047
|
+
let children = [];
|
|
35048
|
+
try {
|
|
35049
|
+
children = readdirSync2(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
|
|
35050
|
+
} catch {
|
|
35051
|
+
}
|
|
35052
|
+
for (const name of children) {
|
|
35053
|
+
if (repos.length >= MAX_REPOS) return repos;
|
|
35054
|
+
const path = join5(root, name);
|
|
35055
|
+
const remote = originOf(path);
|
|
35056
|
+
if (remote) repos.push({ path, remote });
|
|
35057
|
+
}
|
|
35058
|
+
}
|
|
35059
|
+
return repos.slice(0, MAX_REPOS);
|
|
35060
|
+
}
|
|
34792
35061
|
|
|
34793
35062
|
// src/holder.ts
|
|
34794
35063
|
init_dist();
|
|
34795
35064
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
34796
|
-
import { readFileSync as readFileSync4, readdirSync as
|
|
34797
|
-
import { homedir as
|
|
35065
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
35066
|
+
import { homedir as homedir7 } from "os";
|
|
34798
35067
|
import { join as join6 } from "path";
|
|
34799
35068
|
function isAlive(pid) {
|
|
34800
35069
|
try {
|
|
@@ -34825,8 +35094,8 @@ function holds(pid, procStart, deps) {
|
|
|
34825
35094
|
return false;
|
|
34826
35095
|
}
|
|
34827
35096
|
function localHolders(deps = {}) {
|
|
34828
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
34829
|
-
const list = deps.list ?? ((d) =>
|
|
35097
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
|
|
35098
|
+
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
34830
35099
|
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
34831
35100
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
34832
35101
|
const held = /* @__PURE__ */ new Map();
|
|
@@ -34854,8 +35123,8 @@ function localHolders(deps = {}) {
|
|
|
34854
35123
|
}
|
|
34855
35124
|
|
|
34856
35125
|
// src/owner.ts
|
|
34857
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as
|
|
34858
|
-
import { homedir as
|
|
35126
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
|
|
35127
|
+
import { homedir as homedir8 } from "os";
|
|
34859
35128
|
import { join as join7 } from "path";
|
|
34860
35129
|
var HEAD_BYTES = 64e3;
|
|
34861
35130
|
function readHead(file) {
|
|
@@ -34872,8 +35141,8 @@ function ownerSession(slot, deps = {}) {
|
|
|
34872
35141
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
34873
35142
|
if (!m) return null;
|
|
34874
35143
|
const prefix = m[1];
|
|
34875
|
-
const root = deps.projectsDir ?? join7(
|
|
34876
|
-
const list = deps.list ?? ((dir) =>
|
|
35144
|
+
const root = deps.projectsDir ?? join7(homedir8(), ".claude", "projects");
|
|
35145
|
+
const list = deps.list ?? ((dir) => readdirSync4(dir));
|
|
34877
35146
|
const head = deps.head ?? readHead;
|
|
34878
35147
|
let folders;
|
|
34879
35148
|
try {
|
|
@@ -34990,34 +35259,51 @@ function selfUpdater(deps) {
|
|
|
34990
35259
|
}
|
|
34991
35260
|
|
|
34992
35261
|
// src/host.ts
|
|
34993
|
-
var HOST_FILE = join9(
|
|
35262
|
+
var HOST_FILE = join9(homedir9(), ".paigy", "host.json");
|
|
35263
|
+
function wakeHarness(slot, saved, claudeTranscript) {
|
|
35264
|
+
if (slot === "Desktop") return void 0;
|
|
35265
|
+
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
35266
|
+
if (claudeTranscript) return "claude";
|
|
35267
|
+
if (slot === "mcp-agent" || slot === "Claude Code") return "claude";
|
|
35268
|
+
if (slot === "codex") return "codex";
|
|
35269
|
+
if (slot === "antigravity") return "agy";
|
|
35270
|
+
return void 0;
|
|
35271
|
+
}
|
|
34994
35272
|
var runKey = (slot) => `slot:${slot}`;
|
|
34995
35273
|
var claimedRun = (sessionId2) => {
|
|
34996
35274
|
const slot = sessionSlot(sessionId2);
|
|
34997
35275
|
return { slot, key: runKey(slot) };
|
|
34998
35276
|
};
|
|
34999
35277
|
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
35278
|
+
var NOTHING_WAITING = "has nothing waiting";
|
|
35000
35279
|
async function waitingWork(token, session) {
|
|
35001
35280
|
let found;
|
|
35002
35281
|
try {
|
|
35003
35282
|
found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
|
|
35004
35283
|
} catch (error) {
|
|
35284
|
+
if (error instanceof RevokedError) return { skip: "was revoked: its credential is gone for good", revoked: true };
|
|
35285
|
+
if (error instanceof ApiError && error.body?.error === "identity_session_mismatch") {
|
|
35286
|
+
return { skip: "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it" };
|
|
35287
|
+
}
|
|
35005
35288
|
return { skip: `could not read what is waiting (${error.message})` };
|
|
35006
35289
|
}
|
|
35007
|
-
const n = found.deliveries.length;
|
|
35008
|
-
if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
|
|
35009
35290
|
const next = found.claimable;
|
|
35010
|
-
|
|
35291
|
+
const about = next ? { goalId: next.goalId, ...next.since ? { since: next.since } : {} } : {};
|
|
35292
|
+
const n = found.deliveries.length;
|
|
35293
|
+
if (n) return { waiting: n === 1 ? "a message" : `${n} messages`, ...about };
|
|
35294
|
+
return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}`, ...about } : { skip: NOTHING_WAITING };
|
|
35011
35295
|
}
|
|
35012
35296
|
function evict(runs, key, run) {
|
|
35013
35297
|
if (runs.get(key)?.run === run) runs.delete(key);
|
|
35014
35298
|
}
|
|
35015
35299
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
35300
|
+
var IDLE_HOLDER_MS = 30 * 6e4;
|
|
35016
35301
|
function wakeAction(live, now, serverSeenAt, here = {}) {
|
|
35017
35302
|
if (live) return { action: "skip", why: "run" };
|
|
35018
35303
|
if (here.holder) return { action: "skip", why: "holder" };
|
|
35019
35304
|
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
|
|
35020
35305
|
if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
|
|
35306
|
+
if (here.movedAt !== void 0 && now - here.movedAt >= IDLE_HOLDER_MS) return { action: "spawn", why: "idle" };
|
|
35021
35307
|
return { action: "skip", why: "presence" };
|
|
35022
35308
|
}
|
|
35023
35309
|
return { action: "spawn", why: "nobody" };
|
|
@@ -35027,9 +35313,45 @@ function startHost(opts) {
|
|
|
35027
35313
|
const asHost = { token: opts.token };
|
|
35028
35314
|
const goneAt = /* @__PURE__ */ new Map();
|
|
35029
35315
|
const heldHere = /* @__PURE__ */ new Set();
|
|
35316
|
+
const revoked = /* @__PURE__ */ new Map();
|
|
35317
|
+
const isRevoked = (slot) => {
|
|
35318
|
+
const dead = revoked.get(slot);
|
|
35319
|
+
if (dead === void 0) return false;
|
|
35320
|
+
if (readToken(slot) === dead) return true;
|
|
35321
|
+
revoked.delete(slot);
|
|
35322
|
+
return false;
|
|
35323
|
+
};
|
|
35324
|
+
function drop(slot, token) {
|
|
35325
|
+
if (revoked.get(slot) === token) return;
|
|
35326
|
+
revoked.set(slot, token);
|
|
35327
|
+
const sub = listening.get(slot);
|
|
35328
|
+
if (sub) {
|
|
35329
|
+
listening.delete(slot);
|
|
35330
|
+
void sub.close().catch(() => {
|
|
35331
|
+
});
|
|
35332
|
+
}
|
|
35333
|
+
opts.log(`\u2716 ${slotName(slot) ?? slot} was revoked \u2014 its credential is gone for good, so this host stops presenting it (dropped from the roster; pairing again makes a new identity)`);
|
|
35334
|
+
}
|
|
35335
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
35336
|
+
function decide(slot, decision, found, seenAt, said) {
|
|
35337
|
+
const agent2 = slotIdentity(slot).tokenId;
|
|
35338
|
+
if (!agent2) return;
|
|
35339
|
+
const goalId = found?.goalId;
|
|
35340
|
+
const key = `${decision}:${goalId ?? ""}`;
|
|
35341
|
+
if (recorded.get(slot) === key) return;
|
|
35342
|
+
recorded.set(slot, key);
|
|
35343
|
+
void recordDecision({
|
|
35344
|
+
agent: agent2,
|
|
35345
|
+
decision,
|
|
35346
|
+
goalId: goalId ?? null,
|
|
35347
|
+
seenAt: seenAt === void 0 ? null : new Date(seenAt).toISOString(),
|
|
35348
|
+
since: found?.since ?? null,
|
|
35349
|
+
said: said.slice(0, 300)
|
|
35350
|
+
}, asHost).catch((e) => opts.log(`\u25CC could not record that ${slotName(slot) ?? slot} ${decision.replace("_", " ")} (${e.message})`));
|
|
35351
|
+
}
|
|
35030
35352
|
const refreshIdentities = async () => {
|
|
35031
35353
|
for (const slot of listSlots()) {
|
|
35032
|
-
if (slot === "Desktop") continue;
|
|
35354
|
+
if (slot === "Desktop" || isRevoked(slot)) continue;
|
|
35033
35355
|
const token = readToken(slot);
|
|
35034
35356
|
if (!token) continue;
|
|
35035
35357
|
const me = await whoAmI({ token }).catch(() => null);
|
|
@@ -35055,7 +35377,9 @@ function startHost(opts) {
|
|
|
35055
35377
|
void heartbeat({
|
|
35056
35378
|
version: VERSION,
|
|
35057
35379
|
harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
|
|
35058
|
-
workspaces: listWorkspaces(opts.wsDeps)
|
|
35380
|
+
workspaces: listWorkspaces(opts.wsDeps),
|
|
35381
|
+
// The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
|
|
35382
|
+
repos: listRepos(opts.wsDeps)
|
|
35059
35383
|
}, asHost).catch(() => {
|
|
35060
35384
|
});
|
|
35061
35385
|
void listenSlots().then(sweepSlots).catch(() => {
|
|
@@ -35067,6 +35391,19 @@ function startHost(opts) {
|
|
|
35067
35391
|
const label = spec.name ?? spec.harness;
|
|
35068
35392
|
const { slot, key } = claimedRun(spec.sessionId);
|
|
35069
35393
|
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
35394
|
+
try {
|
|
35395
|
+
saveToken({
|
|
35396
|
+
access_token: spec.token,
|
|
35397
|
+
name: label,
|
|
35398
|
+
device: null,
|
|
35399
|
+
workspace: spec.workspace,
|
|
35400
|
+
harness: spec.harness,
|
|
35401
|
+
session_id: spec.sessionId
|
|
35402
|
+
}, slot);
|
|
35403
|
+
} catch {
|
|
35404
|
+
log("Cannot persist launch identity/runtime; session was not started. Check local storage and relaunch.");
|
|
35405
|
+
continue;
|
|
35406
|
+
}
|
|
35070
35407
|
const run = runHarness({
|
|
35071
35408
|
harness: spec.harness,
|
|
35072
35409
|
cwd: spec.workspace,
|
|
@@ -35086,14 +35423,9 @@ function startHost(opts) {
|
|
|
35086
35423
|
void heartbeat(void 0, { token: spec.token }).catch(() => {
|
|
35087
35424
|
});
|
|
35088
35425
|
void refreshIdentities();
|
|
35089
|
-
try {
|
|
35090
|
-
saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, slot);
|
|
35091
|
-
} catch {
|
|
35092
|
-
}
|
|
35093
35426
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
35094
35427
|
}
|
|
35095
35428
|
}
|
|
35096
|
-
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
35097
35429
|
async function presenceMap() {
|
|
35098
35430
|
const presence = /* @__PURE__ */ new Map();
|
|
35099
35431
|
try {
|
|
@@ -35114,30 +35446,64 @@ function startHost(opts) {
|
|
|
35114
35446
|
let lastHolders = /* @__PURE__ */ new Map();
|
|
35115
35447
|
async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
|
|
35116
35448
|
{
|
|
35117
|
-
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
35118
35449
|
const key = runKey(slot);
|
|
35119
|
-
if (
|
|
35450
|
+
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
35120
35451
|
const live = runs.get(key);
|
|
35121
35452
|
const tokenId = slotIdentity(slot).tokenId;
|
|
35122
35453
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
35123
35454
|
const holder = holders.get(slot);
|
|
35124
35455
|
if (holder) heldHere.add(slot);
|
|
35125
35456
|
else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
|
|
35126
|
-
const
|
|
35127
|
-
|
|
35457
|
+
const here = { holder, goneAt: goneAt.get(slot) };
|
|
35458
|
+
let decided = wakeAction(!!live, Date.now(), seenByServer, here);
|
|
35459
|
+
const label = slotName(slot) ?? slot;
|
|
35460
|
+
if (decided.action === "skip" && decided.why !== "presence") {
|
|
35128
35461
|
if (decided.why === "run" && cause === "wake") live?.run.nudge();
|
|
35129
|
-
|
|
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`);
|
|
35462
|
+
opts.log(decided.why === "run" ? `\u25CC ${label} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : `\u25B8 ${label} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second`);
|
|
35131
35463
|
return;
|
|
35132
35464
|
}
|
|
35133
|
-
|
|
35134
|
-
|
|
35465
|
+
let taking = "";
|
|
35466
|
+
let found;
|
|
35467
|
+
const skip = (why) => {
|
|
35468
|
+
opts.log(`\u25CC ${label} ${why} \u2014 not spawning${taking}`);
|
|
35469
|
+
if (taking && found) decide(slot, "took_over", found, seenByServer, `${why} \u2014 not spawning${taking}`);
|
|
35470
|
+
};
|
|
35135
35471
|
const token = readToken(slot);
|
|
35136
35472
|
if (!token) {
|
|
35137
35473
|
skip("has no token on this machine");
|
|
35138
35474
|
return;
|
|
35139
35475
|
}
|
|
35140
|
-
const
|
|
35476
|
+
const identity = slotIdentity(slot);
|
|
35477
|
+
const owner = !identity.harness || identity.harness === "claude" ? ownerSession(slot) : null;
|
|
35478
|
+
const read = async () => {
|
|
35479
|
+
const got = await waitingWork(token, identity.sessionId ?? owner?.sessionId);
|
|
35480
|
+
if (!("skip" in got)) return got;
|
|
35481
|
+
if (got.revoked) {
|
|
35482
|
+
drop(slot, token);
|
|
35483
|
+
return null;
|
|
35484
|
+
}
|
|
35485
|
+
skip(got.skip);
|
|
35486
|
+
if (decided.why === "presence" && got.skip !== NOTHING_WAITING) decide(slot, "stood_back", null, seenByServer, got.skip);
|
|
35487
|
+
return null;
|
|
35488
|
+
};
|
|
35489
|
+
if (decided.why === "presence") {
|
|
35490
|
+
found = await read() ?? void 0;
|
|
35491
|
+
if (!found) return;
|
|
35492
|
+
const since = found.since;
|
|
35493
|
+
decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
|
|
35494
|
+
if (decided.action === "skip") {
|
|
35495
|
+
const said = `is working somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; taken over once it sits ${IDLE_HOLDER_MS / 6e4} min untouched` : ", with nothing to say when it last moved"}`;
|
|
35496
|
+
opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
|
|
35497
|
+
decide(slot, "stood_back", found, seenByServer, said);
|
|
35498
|
+
return;
|
|
35499
|
+
}
|
|
35500
|
+
taking = ` (taking over: ${found.waiting} last moved ${ago(since, Date.now())} while something this host cannot see heartbeats as ${label} and has not touched it)`;
|
|
35501
|
+
}
|
|
35502
|
+
const harness = wakeHarness(slot, identity.harness, !!owner);
|
|
35503
|
+
if (!harness) {
|
|
35504
|
+
skip("has no recorded harness; select its runtime before recovery");
|
|
35505
|
+
return;
|
|
35506
|
+
}
|
|
35141
35507
|
if (owner && !allowed(owner.cwd, opts.wsDeps)) {
|
|
35142
35508
|
skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
|
|
35143
35509
|
return;
|
|
@@ -35147,11 +35513,8 @@ function startHost(opts) {
|
|
|
35147
35513
|
skip("has no granted folder to wake in");
|
|
35148
35514
|
return;
|
|
35149
35515
|
}
|
|
35150
|
-
|
|
35151
|
-
if (
|
|
35152
|
-
skip(found.skip);
|
|
35153
|
-
return;
|
|
35154
|
-
}
|
|
35516
|
+
found ??= await read() ?? void 0;
|
|
35517
|
+
if (!found) return;
|
|
35155
35518
|
if (runs.has(key)) {
|
|
35156
35519
|
skip("was started by the wake that read it first");
|
|
35157
35520
|
return;
|
|
@@ -35163,6 +35526,7 @@ function startHost(opts) {
|
|
|
35163
35526
|
mode: "bypass",
|
|
35164
35527
|
prompt: "",
|
|
35165
35528
|
token,
|
|
35529
|
+
...identity.sessionId ? { session: identity.sessionId } : {},
|
|
35166
35530
|
...owner ? { resume: owner.sessionId } : {},
|
|
35167
35531
|
// A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
|
|
35168
35532
|
// a run that is no longer the slot's must not take the live one's place away with it.
|
|
@@ -35177,13 +35541,15 @@ function startHost(opts) {
|
|
|
35177
35541
|
runs.set(key, { run, label, token });
|
|
35178
35542
|
void heartbeat(void 0, { token }).catch(() => {
|
|
35179
35543
|
});
|
|
35180
|
-
|
|
35544
|
+
const woke = `\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)" : taking}`;
|
|
35545
|
+
opts.log(woke);
|
|
35546
|
+
if (taking) decide(slot, "took_over", found, seenByServer, woke);
|
|
35181
35547
|
}
|
|
35182
35548
|
}
|
|
35183
35549
|
const listening = /* @__PURE__ */ new Map();
|
|
35184
35550
|
const joining = /* @__PURE__ */ new Set();
|
|
35185
35551
|
async function listenSlots() {
|
|
35186
|
-
const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot)));
|
|
35552
|
+
const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot) && !isRevoked(slot)));
|
|
35187
35553
|
for (const [slot, sub] of [...listening]) {
|
|
35188
35554
|
if (want.has(slot)) continue;
|
|
35189
35555
|
listening.delete(slot);
|
|
@@ -35204,6 +35570,10 @@ function startHost(opts) {
|
|
|
35204
35570
|
listening.set(slot, sub);
|
|
35205
35571
|
opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
|
|
35206
35572
|
} catch (e) {
|
|
35573
|
+
if (e instanceof RevokedError) {
|
|
35574
|
+
drop(slot, token);
|
|
35575
|
+
continue;
|
|
35576
|
+
}
|
|
35207
35577
|
opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
|
|
35208
35578
|
} finally {
|
|
35209
35579
|
joining.delete(slot);
|
|
@@ -35269,7 +35639,7 @@ function startHost(opts) {
|
|
|
35269
35639
|
roster: () => {
|
|
35270
35640
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
35271
35641
|
const names2 = /* @__PURE__ */ new Map();
|
|
35272
|
-
for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
|
|
35642
|
+
for (const key of listSlots()) if (key !== "Desktop" && !isRevoked(key)) names2.set(slotName(key) ?? key, key);
|
|
35273
35643
|
for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
|
|
35274
35644
|
return [...names2.entries()].map(([name, slot]) => {
|
|
35275
35645
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
@@ -35337,17 +35707,17 @@ function startBridgeInstalls(bridges, deps = {}) {
|
|
|
35337
35707
|
const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
35338
35708
|
const running = bridges.map((b) => {
|
|
35339
35709
|
let proc = null;
|
|
35340
|
-
let
|
|
35710
|
+
let outcome2;
|
|
35341
35711
|
try {
|
|
35342
35712
|
proc = spawn2(b.install);
|
|
35343
|
-
|
|
35713
|
+
outcome2 = proc.exited.then(
|
|
35344
35714
|
(code) => code === 0 ? "installed" : "failed",
|
|
35345
35715
|
() => "failed"
|
|
35346
35716
|
);
|
|
35347
35717
|
} catch {
|
|
35348
|
-
|
|
35718
|
+
outcome2 = Promise.resolve("failed");
|
|
35349
35719
|
}
|
|
35350
|
-
return { ...b, proc, outcome };
|
|
35720
|
+
return { ...b, proc, outcome: outcome2 };
|
|
35351
35721
|
});
|
|
35352
35722
|
return {
|
|
35353
35723
|
started: running.length,
|
|
@@ -35625,7 +35995,7 @@ async function main() {
|
|
|
35625
35995
|
process.exit(1);
|
|
35626
35996
|
}
|
|
35627
35997
|
const cwd = process.cwd();
|
|
35628
|
-
if (!updateSlot(slot, { workspace: cwd })) {
|
|
35998
|
+
if (!updateSlot(slot, { workspace: cwd, session_id: sessionId() })) {
|
|
35629
35999
|
console.error(`Slot "${slot}" isn't in this machine's slot file \u2014 this session was spawned by the host, which already owns its wake dir. Run handoff from the terminal that hatched the identity.`);
|
|
35630
36000
|
process.exit(1);
|
|
35631
36001
|
}
|