@paigy/harness 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +206 -29
- package/dist/main.js +181 -26
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -22743,6 +22743,7 @@ var require_undici = __commonJS({
|
|
|
22743
22743
|
// ../../packages/sdk/dist/index.js
|
|
22744
22744
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
22745
22745
|
import { createHash, randomUUID } from "crypto";
|
|
22746
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
22746
22747
|
import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
22747
22748
|
import { homedir as homedir2 } from "os";
|
|
22748
22749
|
import { join as join2 } from "path";
|
|
@@ -23655,10 +23656,20 @@ function contactSchemaFrom(fields) {
|
|
|
23655
23656
|
),
|
|
23656
23657
|
workId: fields.workId.describe(
|
|
23657
23658
|
"The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
|
|
23659
|
+
),
|
|
23660
|
+
goalId: fields.goalId.describe(
|
|
23661
|
+
"The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
|
|
23662
|
+
),
|
|
23663
|
+
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
23664
|
+
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
23665
|
+
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
23666
|
+
wait: external_exports.string().uuid().optional().describe(
|
|
23667
|
+
"KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
|
|
23658
23668
|
)
|
|
23659
23669
|
});
|
|
23660
23670
|
const out = mcpInputSchema(surface);
|
|
23661
|
-
out.required
|
|
23671
|
+
delete out.required;
|
|
23672
|
+
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
23662
23673
|
return out;
|
|
23663
23674
|
}
|
|
23664
23675
|
function normalizeWaiting(req) {
|
|
@@ -24102,6 +24113,32 @@ async function sealForE2ee(req, token, deps = {}) {
|
|
|
24102
24113
|
}
|
|
24103
24114
|
async function submitNotification(req, opts = {}) {
|
|
24104
24115
|
const token = authToken(opts.token) ?? "";
|
|
24116
|
+
if (req.goalId) {
|
|
24117
|
+
if (req.envelope) throw new Error("Goal-scoped contact does not support E2EE envelopes yet");
|
|
24118
|
+
const shaped = deriveAsk(req);
|
|
24119
|
+
if (!shaped.context || !shaped.select) throw new Error("Goal-scoped contact requires a shaped request or ask");
|
|
24120
|
+
if (shaped.channel === "call" || shaped.urgency === "call") {
|
|
24121
|
+
throw new Error("Goal-scoped call contact is not available yet; use a message contact");
|
|
24122
|
+
}
|
|
24123
|
+
if (shaped.urgency && shaped.urgency !== "inbox") throw new Error("Goal-scoped contact currently supports inbox delivery only");
|
|
24124
|
+
if (shaped.points?.length) throw new Error("Goal-scoped multi-part contact is not available yet; send one decision at a time");
|
|
24125
|
+
const content = [shaped.context.title, ...shaped.context.description].filter(Boolean).join("\n\n");
|
|
24126
|
+
const res2 = ensureAuthed(await reach(`${BACKEND_URL}/api/goals/${encodeURIComponent(req.goalId)}/contact`, {
|
|
24127
|
+
method: "POST",
|
|
24128
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
|
|
24129
|
+
body: JSON.stringify({
|
|
24130
|
+
operationId: randomUUID2(),
|
|
24131
|
+
threadId: shaped.parentId ?? null,
|
|
24132
|
+
content,
|
|
24133
|
+
select: shaped.select,
|
|
24134
|
+
options: shaped.options ?? [],
|
|
24135
|
+
blocking: shaped.blocking,
|
|
24136
|
+
channel: "message"
|
|
24137
|
+
})
|
|
24138
|
+
}));
|
|
24139
|
+
if (!res2.ok) throw new Error(`goal contact failed: ${res2.status} ${await res2.text()}`);
|
|
24140
|
+
return await res2.json();
|
|
24141
|
+
}
|
|
24105
24142
|
const body = await sealForE2ee(req, token);
|
|
24106
24143
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
24107
24144
|
method: "POST",
|
|
@@ -24334,7 +24371,7 @@ async function patchState(path, state, opts, operation) {
|
|
|
24334
24371
|
function setTaskState(notificationId, state, opts = {}) {
|
|
24335
24372
|
return patchState(`/api/notify/${encodeURIComponent(notificationId)}/state`, state, opts, "set_task_state");
|
|
24336
24373
|
}
|
|
24337
|
-
var require2, __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __require2, __commonJS2, __copyProps2, __toESM2, require_nacl_fast, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, SESSION_ID, 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, MISSED_CALL_PLAN, CreateGoalSchema, UpdateGoalSchema, fmtMin, STANDARD_MEANS, CONTACT_DESCRIPTION, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, SetTaskStateSchema, SetWorkStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, CallbackTriggerSchema, ScheduleCallbackSchema, PendingRepliesSchema, NotifyResponseSchema, NotifyPlanUnitSchema, NotifyPlanSchema, UserResponseSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, SnoozeRequestSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, MoveRingSchema, MoveSchema, CreateRequestSchema, HandoffSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, CreateNoteSchema, RecordDecisionSchema, AssignNoteSchema, TriageVerdictSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, StatusSchema, EnvelopeRecipientSchema, EnvelopeHeaderSchema, EnvelopeSchema, SealedAnswerSchema, DeviceCredentialSchema, DeviceRosterSchema, WakeNudgeSchema, PairingStatusSchema, PairingRevealSchema, DeviceCodeRequestSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenRequestSchema, DeviceTokenSchema, DeviceCommitRequestSchema, DevicePeerCommitSchema, SupportRequestSchema, NotificationFeedbackKindSchema, NotificationFeedbackSchema, FeedbackResolutionSchema, FeedbackOutcomeSchema, CONTACT_SCHEMA, import_tweetnacl, import_tweetnacl2, import_tweetnacl3, import_tweetnacl4, import_tweetnacl5, B64, ok, ENVELOPE_VERSION, ENVELOPE_ALG, AGENT_NAME, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, tokenOverride, authToken, sleep3, ReplyLeaseExpiredError, ReplyAckError;
|
|
24374
|
+
var require2, __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __require2, __commonJS2, __copyProps2, __toESM2, require_nacl_fast, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, SESSION_ID, 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, MISSED_CALL_PLAN, CreateGoalSchema, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, fmtMin, STANDARD_MEANS, CONTACT_DESCRIPTION, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, SetTaskStateSchema, SetWorkStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, CallbackTriggerSchema, ScheduleCallbackSchema, PendingRepliesSchema, NotifyResponseSchema, NotifyPlanUnitSchema, NotifyPlanSchema, UserResponseSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, SnoozeRequestSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, CreateRequestSchema, HandoffSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, CreateNoteSchema, RecordDecisionSchema, AssignNoteSchema, TriageVerdictSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, StatusSchema, EnvelopeRecipientSchema, EnvelopeHeaderSchema, EnvelopeSchema, SealedAnswerSchema, DeviceCredentialSchema, DeviceRosterSchema, WakeNudgeSchema, PairingStatusSchema, PairingRevealSchema, DeviceCodeRequestSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenRequestSchema, DeviceTokenSchema, DeviceCommitRequestSchema, DevicePeerCommitSchema, SupportRequestSchema, NotificationFeedbackKindSchema, NotificationFeedbackSchema, FeedbackResolutionSchema, FeedbackOutcomeSchema, CONTACT_SCHEMA, import_tweetnacl, import_tweetnacl2, import_tweetnacl3, import_tweetnacl4, import_tweetnacl5, B64, ok, ENVELOPE_VERSION, ENVELOPE_ALG, AGENT_NAME, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, tokenOverride, authToken, sleep3, ReplyLeaseExpiredError, ReplyAckError;
|
|
24338
24375
|
var init_dist = __esm({
|
|
24339
24376
|
"../../packages/sdk/dist/index.js"() {
|
|
24340
24377
|
"use strict";
|
|
@@ -26996,12 +27033,19 @@ var init_dist = __esm({
|
|
|
26996
27033
|
revision: external_exports.number().int().positive(),
|
|
26997
27034
|
changes: external_exports.object({
|
|
26998
27035
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
26999
|
-
|
|
27036
|
+
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
27037
|
+
parentGoalId: external_exports.string().uuid().nullable().optional(),
|
|
27038
|
+
dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
|
|
27039
|
+
children: external_exports.array(external_exports.object({ outcome: external_exports.string().trim().min(1).max(1e4), ownerParticipant: external_exports.string().trim().min(1), gate: external_exports.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
27040
|
+
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
27000
27041
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
27001
27042
|
reviewed: external_exports.literal(true).optional()
|
|
27002
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
27003
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
27004
|
-
|
|
27043
|
+
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
27044
|
+
reason: external_exports.string().trim().min(1).max(2e3),
|
|
27045
|
+
operationId: external_exports.string().uuid().optional()
|
|
27046
|
+
}).strict();
|
|
27047
|
+
UpdateGoalToolSchema = UpdateGoalSchema.extend({ goalId: external_exports.string().uuid() }).strict();
|
|
27048
|
+
ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
27005
27049
|
fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
27006
27050
|
STANDARD_MEANS = (() => {
|
|
27007
27051
|
const plan = MISSED_CALL_PLAN.backoff_standard;
|
|
@@ -27010,7 +27054,7 @@ var init_dist = __esm({
|
|
|
27010
27054
|
const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
|
|
27011
27055
|
return `Rings again ${list} after the missed call, then leaves it in your inbox`;
|
|
27012
27056
|
})();
|
|
27013
|
-
CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, decisionId? } \u2014
|
|
27057
|
+
CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
27014
27058
|
ContextSchema = external_exports.object({
|
|
27015
27059
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
27016
27060
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -27123,6 +27167,9 @@ var init_dist = __esm({
|
|
|
27123
27167
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
27124
27168
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
27125
27169
|
workId: external_exports.string().uuid().optional(),
|
|
27170
|
+
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
27171
|
+
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
27172
|
+
goalId: external_exports.string().uuid().optional(),
|
|
27126
27173
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
27127
27174
|
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
27128
27175
|
),
|
|
@@ -27200,6 +27247,7 @@ var init_dist = __esm({
|
|
|
27200
27247
|
)
|
|
27201
27248
|
});
|
|
27202
27249
|
NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
27250
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
27203
27251
|
const sealed = !!r.envelope;
|
|
27204
27252
|
if (sealed) {
|
|
27205
27253
|
if (!r.envelope?.context)
|
|
@@ -27364,11 +27412,21 @@ var init_dist = __esm({
|
|
|
27364
27412
|
acts: external_exports.array(IntentSchema).nullable().optional()
|
|
27365
27413
|
})
|
|
27366
27414
|
}),
|
|
27367
|
-
external_exports.object({
|
|
27415
|
+
external_exports.object({
|
|
27416
|
+
type: external_exports.literal("idle"),
|
|
27417
|
+
also: external_exports.array(RideAlongSchema).optional(),
|
|
27418
|
+
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
27419
|
+
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
27420
|
+
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
27421
|
+
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
27422
|
+
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
27423
|
+
inFlight: external_exports.boolean().optional()
|
|
27424
|
+
})
|
|
27368
27425
|
]);
|
|
27369
27426
|
CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
27370
27427
|
ScheduleCallbackSchema = external_exports.object({
|
|
27371
27428
|
parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
|
|
27429
|
+
goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
|
|
27372
27430
|
trigger: CallbackTriggerSchema,
|
|
27373
27431
|
dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
27374
27432
|
note: external_exports.string().optional().describe("What to tell the user when you follow up.")
|
|
@@ -27813,6 +27871,15 @@ var init_dist = __esm({
|
|
|
27813
27871
|
ConnectionSummarySchema = external_exports.object({
|
|
27814
27872
|
/** The connection = the agent's token id (used to address a request). */
|
|
27815
27873
|
id: external_exports.string(),
|
|
27874
|
+
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
27875
|
+
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
27876
|
+
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
27877
|
+
* apps/api/src/tokens/devices-vs-agents-design.md. */
|
|
27878
|
+
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
27879
|
+
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
27880
|
+
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
27881
|
+
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
27882
|
+
mintedByDevice: external_exports.string().nullable().optional(),
|
|
27816
27883
|
device: external_exports.string().nullable(),
|
|
27817
27884
|
/** The agent's display name (the single pairing name). */
|
|
27818
27885
|
name: external_exports.string(),
|
|
@@ -27852,6 +27919,22 @@ var init_dist = __esm({
|
|
|
27852
27919
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
27853
27920
|
managed: external_exports.boolean()
|
|
27854
27921
|
});
|
|
27922
|
+
LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
27923
|
+
AgentLedgerSchema = external_exports.object({
|
|
27924
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
|
|
27925
|
+
/** Its own questions you have not answered. */
|
|
27926
|
+
asks: external_exports.array(LedgerItemSchema),
|
|
27927
|
+
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
27928
|
+
answered: external_exports.array(LedgerItemSchema),
|
|
27929
|
+
/** Requests you sent it that it never took. */
|
|
27930
|
+
requests: external_exports.array(LedgerItemSchema),
|
|
27931
|
+
goals: external_exports.array(external_exports.object({ id: external_exports.string(), outcome: external_exports.string(), state: external_exports.string() })),
|
|
27932
|
+
callbacks: external_exports.array(external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), trigger: external_exports.string(), note: external_exports.string(), dueAt: external_exports.string().nullable() }))
|
|
27933
|
+
});
|
|
27934
|
+
ReassignResultSchema = external_exports.object({
|
|
27935
|
+
moved: external_exports.object({ asks: external_exports.number(), answered: external_exports.number(), requests: external_exports.number(), goals: external_exports.number(), callbacks: external_exports.number() }),
|
|
27936
|
+
parentId: external_exports.string().nullable()
|
|
27937
|
+
});
|
|
27855
27938
|
MoveRingSchema = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
27856
27939
|
MoveSchema = external_exports.object({
|
|
27857
27940
|
id: external_exports.string(),
|
|
@@ -28235,7 +28318,8 @@ var init_dist = __esm({
|
|
|
28235
28318
|
options: NotifyRequestFields.shape.options,
|
|
28236
28319
|
channel: NotifyRequestFields.shape.channel,
|
|
28237
28320
|
parentId: NotifyRequestFields.shape.parentId,
|
|
28238
|
-
workId: NotifyRequestFields.shape.workId
|
|
28321
|
+
workId: NotifyRequestFields.shape.workId,
|
|
28322
|
+
goalId: NotifyRequestFields.shape.goalId
|
|
28239
28323
|
});
|
|
28240
28324
|
import_tweetnacl = __toESM2(require_nacl_fast(), 1);
|
|
28241
28325
|
import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
|
|
@@ -28262,7 +28346,7 @@ var init_dist = __esm({
|
|
|
28262
28346
|
ReplyLeaseExpiredError = class extends Error {
|
|
28263
28347
|
constructor() {
|
|
28264
28348
|
super(
|
|
28265
|
-
"The Paigy reply lease expired before acknowledgement. The answer remains durable;
|
|
28349
|
+
"The Paigy reply lease expired before acknowledgement. The answer remains durable; wait on it again (contact with only { wait: notificationId }) to reclaim it before acting."
|
|
28266
28350
|
);
|
|
28267
28351
|
this.name = "ReplyLeaseExpiredError";
|
|
28268
28352
|
}
|
|
@@ -34853,12 +34937,19 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
34853
34937
|
revision: external_exports.number().int().positive(),
|
|
34854
34938
|
changes: external_exports.object({
|
|
34855
34939
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
34856
|
-
|
|
34940
|
+
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
34941
|
+
parentGoalId: external_exports.string().uuid().nullable().optional(),
|
|
34942
|
+
dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
|
|
34943
|
+
children: external_exports.array(external_exports.object({ outcome: external_exports.string().trim().min(1).max(1e4), ownerParticipant: external_exports.string().trim().min(1), gate: external_exports.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
34944
|
+
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
34857
34945
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
34858
34946
|
reviewed: external_exports.literal(true).optional()
|
|
34859
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
34860
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
34861
|
-
|
|
34947
|
+
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
34948
|
+
reason: external_exports.string().trim().min(1).max(2e3),
|
|
34949
|
+
operationId: external_exports.string().uuid().optional()
|
|
34950
|
+
}).strict();
|
|
34951
|
+
var UpdateGoalToolSchema2 = UpdateGoalSchema2.extend({ goalId: external_exports.string().uuid() }).strict();
|
|
34952
|
+
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
34862
34953
|
var fmtMin2 = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
34863
34954
|
var STANDARD_MEANS2 = (() => {
|
|
34864
34955
|
const plan = MISSED_CALL_PLAN2.backoff_standard;
|
|
@@ -34886,13 +34977,23 @@ function contactSchemaFrom2(fields) {
|
|
|
34886
34977
|
),
|
|
34887
34978
|
workId: fields.workId.describe(
|
|
34888
34979
|
"The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
|
|
34980
|
+
),
|
|
34981
|
+
goalId: fields.goalId.describe(
|
|
34982
|
+
"The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
|
|
34983
|
+
),
|
|
34984
|
+
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
34985
|
+
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
34986
|
+
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
34987
|
+
wait: external_exports.string().uuid().optional().describe(
|
|
34988
|
+
"KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
|
|
34889
34989
|
)
|
|
34890
34990
|
});
|
|
34891
34991
|
const out = mcpInputSchema2(surface);
|
|
34892
|
-
out.required
|
|
34992
|
+
delete out.required;
|
|
34993
|
+
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
34893
34994
|
return out;
|
|
34894
34995
|
}
|
|
34895
|
-
var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, decisionId? } \u2014
|
|
34996
|
+
var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS2}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN2}-${OPTIONS_MAX2} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
34896
34997
|
var ContextSchema2 = external_exports.object({
|
|
34897
34998
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
34898
34999
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -35005,6 +35106,9 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
35005
35106
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
35006
35107
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
35007
35108
|
workId: external_exports.string().uuid().optional(),
|
|
35109
|
+
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
35110
|
+
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
35111
|
+
goalId: external_exports.string().uuid().optional(),
|
|
35008
35112
|
urgency: NotifyLevelSchema2.default("inbox").describe(
|
|
35009
35113
|
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
35010
35114
|
),
|
|
@@ -35082,6 +35186,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
35082
35186
|
)
|
|
35083
35187
|
});
|
|
35084
35188
|
var NotifyRequestSchema2 = NotifyRequestFields2.superRefine((r, ctx) => {
|
|
35189
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
35085
35190
|
const sealed = !!r.envelope;
|
|
35086
35191
|
if (sealed) {
|
|
35087
35192
|
if (!r.envelope?.context)
|
|
@@ -35263,11 +35368,21 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
35263
35368
|
acts: external_exports.array(IntentSchema2).nullable().optional()
|
|
35264
35369
|
})
|
|
35265
35370
|
}),
|
|
35266
|
-
external_exports.object({
|
|
35371
|
+
external_exports.object({
|
|
35372
|
+
type: external_exports.literal("idle"),
|
|
35373
|
+
also: external_exports.array(RideAlongSchema2).optional(),
|
|
35374
|
+
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
35375
|
+
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
35376
|
+
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
35377
|
+
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
35378
|
+
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
35379
|
+
inFlight: external_exports.boolean().optional()
|
|
35380
|
+
})
|
|
35267
35381
|
]);
|
|
35268
35382
|
var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
35269
35383
|
var ScheduleCallbackSchema2 = external_exports.object({
|
|
35270
35384
|
parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
|
|
35385
|
+
goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
|
|
35271
35386
|
trigger: CallbackTriggerSchema2,
|
|
35272
35387
|
dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
35273
35388
|
note: external_exports.string().optional().describe("What to tell the user when you follow up.")
|
|
@@ -35712,6 +35827,15 @@ var AgentActivitySchema2 = external_exports.object({
|
|
|
35712
35827
|
var ConnectionSummarySchema2 = external_exports.object({
|
|
35713
35828
|
/** The connection = the agent's token id (used to address a request). */
|
|
35714
35829
|
id: external_exports.string(),
|
|
35830
|
+
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
35831
|
+
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
35832
|
+
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
35833
|
+
* apps/api/src/tokens/devices-vs-agents-design.md. */
|
|
35834
|
+
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
35835
|
+
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
35836
|
+
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
35837
|
+
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
35838
|
+
mintedByDevice: external_exports.string().nullable().optional(),
|
|
35715
35839
|
device: external_exports.string().nullable(),
|
|
35716
35840
|
/** The agent's display name (the single pairing name). */
|
|
35717
35841
|
name: external_exports.string(),
|
|
@@ -35751,6 +35875,22 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
35751
35875
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
35752
35876
|
managed: external_exports.boolean()
|
|
35753
35877
|
});
|
|
35878
|
+
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
35879
|
+
var AgentLedgerSchema2 = external_exports.object({
|
|
35880
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
|
|
35881
|
+
/** Its own questions you have not answered. */
|
|
35882
|
+
asks: external_exports.array(LedgerItemSchema2),
|
|
35883
|
+
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
35884
|
+
answered: external_exports.array(LedgerItemSchema2),
|
|
35885
|
+
/** Requests you sent it that it never took. */
|
|
35886
|
+
requests: external_exports.array(LedgerItemSchema2),
|
|
35887
|
+
goals: external_exports.array(external_exports.object({ id: external_exports.string(), outcome: external_exports.string(), state: external_exports.string() })),
|
|
35888
|
+
callbacks: external_exports.array(external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), trigger: external_exports.string(), note: external_exports.string(), dueAt: external_exports.string().nullable() }))
|
|
35889
|
+
});
|
|
35890
|
+
var ReassignResultSchema2 = external_exports.object({
|
|
35891
|
+
moved: external_exports.object({ asks: external_exports.number(), answered: external_exports.number(), requests: external_exports.number(), goals: external_exports.number(), callbacks: external_exports.number() }),
|
|
35892
|
+
parentId: external_exports.string().nullable()
|
|
35893
|
+
});
|
|
35754
35894
|
var MoveRingSchema2 = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
35755
35895
|
var MoveSchema2 = external_exports.object({
|
|
35756
35896
|
id: external_exports.string(),
|
|
@@ -36134,7 +36274,8 @@ var CONTACT_SCHEMA2 = contactSchemaFrom2({
|
|
|
36134
36274
|
options: NotifyRequestFields2.shape.options,
|
|
36135
36275
|
channel: NotifyRequestFields2.shape.channel,
|
|
36136
36276
|
parentId: NotifyRequestFields2.shape.parentId,
|
|
36137
|
-
workId: NotifyRequestFields2.shape.workId
|
|
36277
|
+
workId: NotifyRequestFields2.shape.workId,
|
|
36278
|
+
goalId: NotifyRequestFields2.shape.goalId
|
|
36138
36279
|
});
|
|
36139
36280
|
|
|
36140
36281
|
// src/paigy/conversation.ts
|
|
@@ -36881,10 +37022,14 @@ var claimedRun = (sessionId2) => {
|
|
|
36881
37022
|
async function slotHasWaitingWork(token) {
|
|
36882
37023
|
return (await pendingSummary({ token })).unacknowledged > 0;
|
|
36883
37024
|
}
|
|
36884
|
-
|
|
36885
|
-
|
|
36886
|
-
if (
|
|
36887
|
-
|
|
37025
|
+
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
37026
|
+
function wakeAction(live, seenAt, now, serverSeenAt) {
|
|
37027
|
+
if (live) {
|
|
37028
|
+
if (seenAt === void 0) return "skip";
|
|
37029
|
+
return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
|
|
37030
|
+
}
|
|
37031
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
|
|
37032
|
+
return "spawn";
|
|
36888
37033
|
}
|
|
36889
37034
|
function startHost(opts) {
|
|
36890
37035
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -36944,12 +37089,21 @@ function startHost(opts) {
|
|
|
36944
37089
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
36945
37090
|
const wakePrompt = (fresh) => "You were woken because Paigy work is waiting for you. " + (fresh ? "This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. " : "You have been running, so some of this may already be yours \u2014 but not what arrived while you were idle. ") + "Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
|
|
36946
37091
|
async function sweepSlots() {
|
|
37092
|
+
const presence = /* @__PURE__ */ new Map();
|
|
37093
|
+
try {
|
|
37094
|
+
for (const c of (await listConnections(asHost)).items) {
|
|
37095
|
+
if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
|
|
37096
|
+
}
|
|
37097
|
+
} catch {
|
|
37098
|
+
}
|
|
36947
37099
|
for (const slot of listSlots()) {
|
|
36948
37100
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
36949
37101
|
const key = runKey(slot);
|
|
36950
37102
|
if (!harness) continue;
|
|
36951
37103
|
const live = runs.get(key);
|
|
36952
|
-
const
|
|
37104
|
+
const tokenId = slotIdentity(slot).tokenId;
|
|
37105
|
+
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
37106
|
+
const action = wakeAction(!!live, published.get(key)?.movedAt, Date.now(), seenByServer);
|
|
36953
37107
|
if (action === "skip") continue;
|
|
36954
37108
|
const token = readToken(slot);
|
|
36955
37109
|
if (!token) continue;
|
|
@@ -37134,6 +37288,25 @@ function bridgeLine(r) {
|
|
|
37134
37288
|
|
|
37135
37289
|
// src/cli.ts
|
|
37136
37290
|
var STATUS_MARK = { ready: "\u2713", login: "\u25D0", "adapter-missing": "\u25D0", missing: "\u2717" };
|
|
37291
|
+
function printConnectLines(link, userCode) {
|
|
37292
|
+
console.log("Connect this computer from your phone");
|
|
37293
|
+
console.log(` Open ${link}`);
|
|
37294
|
+
console.log(` or scan the QR above \xB7 or type ${userCode} in Paigy \u2192 Devices \u2192 Connect a computer`);
|
|
37295
|
+
}
|
|
37296
|
+
function forwardCodexThreadId() {
|
|
37297
|
+
const cfg = join5(process.env.CODEX_HOME || join5(homedir7(), ".codex"), "config.toml");
|
|
37298
|
+
if (!existsSync5(cfg)) return;
|
|
37299
|
+
const text = readFileSync4(cfg, "utf8");
|
|
37300
|
+
const head = "[mcp_servers.paigy]\n";
|
|
37301
|
+
const at = text.indexOf(head);
|
|
37302
|
+
if (at < 0) return;
|
|
37303
|
+
const block = text.slice(at + head.length, (() => {
|
|
37304
|
+
const n = text.indexOf("\n[", at + head.length);
|
|
37305
|
+
return n < 0 ? text.length : n;
|
|
37306
|
+
})());
|
|
37307
|
+
if (/^\s*env_vars\s*=/m.test(block)) return;
|
|
37308
|
+
writeFileSync4(cfg, text.slice(0, at + head.length) + 'env_vars = ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]\n' + text.slice(at + head.length));
|
|
37309
|
+
}
|
|
37137
37310
|
async function main() {
|
|
37138
37311
|
const parsed = parseArgs(process.argv.slice(2));
|
|
37139
37312
|
if ("error" in parsed) {
|
|
@@ -37209,8 +37382,7 @@ async function main() {
|
|
|
37209
37382
|
if (!live) {
|
|
37210
37383
|
const code = await requestCode(hostname().replace(/\.local$/, ""), void 0, hostname().replace(/\.local$/, ""));
|
|
37211
37384
|
console.log(await import_qrcode.default.toString(code.verification_uri_complete, { type: "terminal", small: true }));
|
|
37212
|
-
|
|
37213
|
-
console.log(`Or scan the QR above, or enter code ${code.user_code} at ${code.verification_uri}`);
|
|
37385
|
+
printConnectLines(code.verification_uri_complete, code.user_code);
|
|
37214
37386
|
const deadline = Date.now() + code.expires_in * 1e3;
|
|
37215
37387
|
let paired = false;
|
|
37216
37388
|
while (!paired && Date.now() < deadline) {
|
|
@@ -37250,15 +37422,21 @@ async function main() {
|
|
|
37250
37422
|
} else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
|
|
37251
37423
|
const TERMINAL_AGENTS = [
|
|
37252
37424
|
{ cli: "claude", name: "Claude Code", wire: "claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
|
|
37253
|
-
|
|
37425
|
+
// Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
|
|
37426
|
+
// a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
|
|
37427
|
+
// written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
|
|
37428
|
+
// all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
|
|
37429
|
+
{ cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
|
|
37254
37430
|
{ cli: "agy", name: "Antigravity" }
|
|
37255
37431
|
];
|
|
37256
37432
|
for (const t of TERMINAL_AGENTS) {
|
|
37257
37433
|
if (!which(t.cli) || !t.wire) continue;
|
|
37258
37434
|
try {
|
|
37259
37435
|
execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
|
|
37436
|
+
if (t.cli === "codex") forwardCodexThreadId();
|
|
37260
37437
|
console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
|
|
37261
|
-
} catch {
|
|
37438
|
+
} catch (e) {
|
|
37439
|
+
console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014 the harness still works; wire it by hand later`);
|
|
37262
37440
|
}
|
|
37263
37441
|
}
|
|
37264
37442
|
if (which("claude")) {
|
|
@@ -37301,8 +37479,7 @@ async function main() {
|
|
|
37301
37479
|
if (existing) console.log("\u26A0 this machine was logged out \u2014 reconnecting it now");
|
|
37302
37480
|
const code = await requestCode(hostname().replace(/\.local$/, ""), void 0, hostname().replace(/\.local$/, ""));
|
|
37303
37481
|
console.log(await import_qrcode.default.toString(code.verification_uri_complete, { type: "terminal", small: true }));
|
|
37304
|
-
|
|
37305
|
-
console.log(`Or scan the QR above, or enter code ${code.user_code} at ${code.verification_uri}`);
|
|
37482
|
+
printConnectLines(code.verification_uri_complete, code.user_code);
|
|
37306
37483
|
const deadline = Date.now() + code.expires_in * 1e3;
|
|
37307
37484
|
while (Date.now() < deadline) {
|
|
37308
37485
|
const token = await pollToken(code.device_code).catch(() => null);
|
package/dist/main.js
CHANGED
|
@@ -8627,6 +8627,7 @@ var coerce = {
|
|
|
8627
8627
|
var NEVER = INVALID;
|
|
8628
8628
|
|
|
8629
8629
|
// ../../packages/sdk/dist/index.js
|
|
8630
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
8630
8631
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
8631
8632
|
import { homedir } from "os";
|
|
8632
8633
|
import { join } from "path";
|
|
@@ -12174,12 +12175,19 @@ var UpdateGoalSchema = external_exports.object({
|
|
|
12174
12175
|
revision: external_exports.number().int().positive(),
|
|
12175
12176
|
changes: external_exports.object({
|
|
12176
12177
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
12177
|
-
|
|
12178
|
+
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
12179
|
+
parentGoalId: external_exports.string().uuid().nullable().optional(),
|
|
12180
|
+
dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
|
|
12181
|
+
children: external_exports.array(external_exports.object({ outcome: external_exports.string().trim().min(1).max(1e4), ownerParticipant: external_exports.string().trim().min(1), gate: external_exports.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
12182
|
+
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
12178
12183
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
12179
12184
|
reviewed: external_exports.literal(true).optional()
|
|
12180
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
12181
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
12182
|
-
|
|
12185
|
+
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
12186
|
+
reason: external_exports.string().trim().min(1).max(2e3),
|
|
12187
|
+
operationId: external_exports.string().uuid().optional()
|
|
12188
|
+
}).strict();
|
|
12189
|
+
var UpdateGoalToolSchema = UpdateGoalSchema.extend({ goalId: external_exports.string().uuid() }).strict();
|
|
12190
|
+
var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
12183
12191
|
var fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
12184
12192
|
var STANDARD_MEANS = (() => {
|
|
12185
12193
|
const plan = MISSED_CALL_PLAN.backoff_standard;
|
|
@@ -12207,13 +12215,23 @@ function contactSchemaFrom(fields) {
|
|
|
12207
12215
|
),
|
|
12208
12216
|
workId: fields.workId.describe(
|
|
12209
12217
|
"The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
|
|
12218
|
+
),
|
|
12219
|
+
goalId: fields.goalId.describe(
|
|
12220
|
+
"The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
|
|
12221
|
+
),
|
|
12222
|
+
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
12223
|
+
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
12224
|
+
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
12225
|
+
wait: external_exports.string().uuid().optional().describe(
|
|
12226
|
+
"KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
|
|
12210
12227
|
)
|
|
12211
12228
|
});
|
|
12212
12229
|
const out = mcpInputSchema(surface);
|
|
12213
|
-
out.required
|
|
12230
|
+
delete out.required;
|
|
12231
|
+
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
12214
12232
|
return out;
|
|
12215
12233
|
}
|
|
12216
|
-
var CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, decisionId? } \u2014
|
|
12234
|
+
var CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
12217
12235
|
var ContextSchema = external_exports.object({
|
|
12218
12236
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
12219
12237
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -12326,6 +12344,9 @@ var NotifyRequestFields = external_exports.object({
|
|
|
12326
12344
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
12327
12345
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
12328
12346
|
workId: external_exports.string().uuid().optional(),
|
|
12347
|
+
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
12348
|
+
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
12349
|
+
goalId: external_exports.string().uuid().optional(),
|
|
12329
12350
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
12330
12351
|
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
12331
12352
|
),
|
|
@@ -12403,6 +12424,7 @@ var NotifyRequestFields = external_exports.object({
|
|
|
12403
12424
|
)
|
|
12404
12425
|
});
|
|
12405
12426
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
12427
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
12406
12428
|
const sealed = !!r.envelope;
|
|
12407
12429
|
if (sealed) {
|
|
12408
12430
|
if (!r.envelope?.context)
|
|
@@ -12622,11 +12644,21 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
12622
12644
|
acts: external_exports.array(IntentSchema).nullable().optional()
|
|
12623
12645
|
})
|
|
12624
12646
|
}),
|
|
12625
|
-
external_exports.object({
|
|
12647
|
+
external_exports.object({
|
|
12648
|
+
type: external_exports.literal("idle"),
|
|
12649
|
+
also: external_exports.array(RideAlongSchema).optional(),
|
|
12650
|
+
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
12651
|
+
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
12652
|
+
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
12653
|
+
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
12654
|
+
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
12655
|
+
inFlight: external_exports.boolean().optional()
|
|
12656
|
+
})
|
|
12626
12657
|
]);
|
|
12627
12658
|
var CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
12628
12659
|
var ScheduleCallbackSchema = external_exports.object({
|
|
12629
12660
|
parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
|
|
12661
|
+
goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
|
|
12630
12662
|
trigger: CallbackTriggerSchema,
|
|
12631
12663
|
dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
12632
12664
|
note: external_exports.string().optional().describe("What to tell the user when you follow up.")
|
|
@@ -13071,6 +13103,15 @@ var AgentActivitySchema = external_exports.object({
|
|
|
13071
13103
|
var ConnectionSummarySchema = external_exports.object({
|
|
13072
13104
|
/** The connection = the agent's token id (used to address a request). */
|
|
13073
13105
|
id: external_exports.string(),
|
|
13106
|
+
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
13107
|
+
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
13108
|
+
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
13109
|
+
* apps/api/src/tokens/devices-vs-agents-design.md. */
|
|
13110
|
+
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
13111
|
+
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
13112
|
+
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
13113
|
+
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
13114
|
+
mintedByDevice: external_exports.string().nullable().optional(),
|
|
13074
13115
|
device: external_exports.string().nullable(),
|
|
13075
13116
|
/** The agent's display name (the single pairing name). */
|
|
13076
13117
|
name: external_exports.string(),
|
|
@@ -13110,6 +13151,22 @@ var ConnectionSummarySchema = external_exports.object({
|
|
|
13110
13151
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
13111
13152
|
managed: external_exports.boolean()
|
|
13112
13153
|
});
|
|
13154
|
+
var LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
13155
|
+
var AgentLedgerSchema = external_exports.object({
|
|
13156
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
|
|
13157
|
+
/** Its own questions you have not answered. */
|
|
13158
|
+
asks: external_exports.array(LedgerItemSchema),
|
|
13159
|
+
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
13160
|
+
answered: external_exports.array(LedgerItemSchema),
|
|
13161
|
+
/** Requests you sent it that it never took. */
|
|
13162
|
+
requests: external_exports.array(LedgerItemSchema),
|
|
13163
|
+
goals: external_exports.array(external_exports.object({ id: external_exports.string(), outcome: external_exports.string(), state: external_exports.string() })),
|
|
13164
|
+
callbacks: external_exports.array(external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), trigger: external_exports.string(), note: external_exports.string(), dueAt: external_exports.string().nullable() }))
|
|
13165
|
+
});
|
|
13166
|
+
var ReassignResultSchema = external_exports.object({
|
|
13167
|
+
moved: external_exports.object({ asks: external_exports.number(), answered: external_exports.number(), requests: external_exports.number(), goals: external_exports.number(), callbacks: external_exports.number() }),
|
|
13168
|
+
parentId: external_exports.string().nullable()
|
|
13169
|
+
});
|
|
13113
13170
|
var MoveRingSchema = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
13114
13171
|
var MoveSchema = external_exports.object({
|
|
13115
13172
|
id: external_exports.string(),
|
|
@@ -13493,7 +13550,8 @@ var CONTACT_SCHEMA = contactSchemaFrom({
|
|
|
13493
13550
|
options: NotifyRequestFields.shape.options,
|
|
13494
13551
|
channel: NotifyRequestFields.shape.channel,
|
|
13495
13552
|
parentId: NotifyRequestFields.shape.parentId,
|
|
13496
|
-
workId: NotifyRequestFields.shape.workId
|
|
13553
|
+
workId: NotifyRequestFields.shape.workId,
|
|
13554
|
+
goalId: NotifyRequestFields.shape.goalId
|
|
13497
13555
|
});
|
|
13498
13556
|
var import_tweetnacl = __toESM2(require_nacl_fast(), 1);
|
|
13499
13557
|
var import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
|
|
@@ -13900,6 +13958,32 @@ async function sealForE2ee(req, token, deps = {}) {
|
|
|
13900
13958
|
}
|
|
13901
13959
|
async function submitNotification(req, opts = {}) {
|
|
13902
13960
|
const token = authToken(opts.token) ?? "";
|
|
13961
|
+
if (req.goalId) {
|
|
13962
|
+
if (req.envelope) throw new Error("Goal-scoped contact does not support E2EE envelopes yet");
|
|
13963
|
+
const shaped = deriveAsk(req);
|
|
13964
|
+
if (!shaped.context || !shaped.select) throw new Error("Goal-scoped contact requires a shaped request or ask");
|
|
13965
|
+
if (shaped.channel === "call" || shaped.urgency === "call") {
|
|
13966
|
+
throw new Error("Goal-scoped call contact is not available yet; use a message contact");
|
|
13967
|
+
}
|
|
13968
|
+
if (shaped.urgency && shaped.urgency !== "inbox") throw new Error("Goal-scoped contact currently supports inbox delivery only");
|
|
13969
|
+
if (shaped.points?.length) throw new Error("Goal-scoped multi-part contact is not available yet; send one decision at a time");
|
|
13970
|
+
const content = [shaped.context.title, ...shaped.context.description].filter(Boolean).join("\n\n");
|
|
13971
|
+
const res2 = ensureAuthed(await reach(`${BACKEND_URL}/api/goals/${encodeURIComponent(req.goalId)}/contact`, {
|
|
13972
|
+
method: "POST",
|
|
13973
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
|
|
13974
|
+
body: JSON.stringify({
|
|
13975
|
+
operationId: randomUUID2(),
|
|
13976
|
+
threadId: shaped.parentId ?? null,
|
|
13977
|
+
content,
|
|
13978
|
+
select: shaped.select,
|
|
13979
|
+
options: shaped.options ?? [],
|
|
13980
|
+
blocking: shaped.blocking,
|
|
13981
|
+
channel: "message"
|
|
13982
|
+
})
|
|
13983
|
+
}));
|
|
13984
|
+
if (!res2.ok) throw new Error(`goal contact failed: ${res2.status} ${await res2.text()}`);
|
|
13985
|
+
return await res2.json();
|
|
13986
|
+
}
|
|
13903
13987
|
const body = await sealForE2ee(req, token);
|
|
13904
13988
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
13905
13989
|
method: "POST",
|
|
@@ -14010,7 +14094,7 @@ var sleep3 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
|
14010
14094
|
var ReplyLeaseExpiredError = class extends Error {
|
|
14011
14095
|
constructor() {
|
|
14012
14096
|
super(
|
|
14013
|
-
"The Paigy reply lease expired before acknowledgement. The answer remains durable;
|
|
14097
|
+
"The Paigy reply lease expired before acknowledgement. The answer remains durable; wait on it again (contact with only { wait: notificationId }) to reclaim it before acting."
|
|
14014
14098
|
);
|
|
14015
14099
|
this.name = "ReplyLeaseExpiredError";
|
|
14016
14100
|
}
|
|
@@ -15623,12 +15707,19 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
15623
15707
|
revision: external_exports.number().int().positive(),
|
|
15624
15708
|
changes: external_exports.object({
|
|
15625
15709
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
15626
|
-
|
|
15710
|
+
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
15711
|
+
parentGoalId: external_exports.string().uuid().nullable().optional(),
|
|
15712
|
+
dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
|
|
15713
|
+
children: external_exports.array(external_exports.object({ outcome: external_exports.string().trim().min(1).max(1e4), ownerParticipant: external_exports.string().trim().min(1), gate: external_exports.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
15714
|
+
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
15627
15715
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
15628
15716
|
reviewed: external_exports.literal(true).optional()
|
|
15629
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
15630
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
15631
|
-
|
|
15717
|
+
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
15718
|
+
reason: external_exports.string().trim().min(1).max(2e3),
|
|
15719
|
+
operationId: external_exports.string().uuid().optional()
|
|
15720
|
+
}).strict();
|
|
15721
|
+
var UpdateGoalToolSchema2 = UpdateGoalSchema2.extend({ goalId: external_exports.string().uuid() }).strict();
|
|
15722
|
+
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
15632
15723
|
var fmtMin2 = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
15633
15724
|
var STANDARD_MEANS2 = (() => {
|
|
15634
15725
|
const plan = MISSED_CALL_PLAN2.backoff_standard;
|
|
@@ -15656,13 +15747,23 @@ function contactSchemaFrom2(fields) {
|
|
|
15656
15747
|
),
|
|
15657
15748
|
workId: fields.workId.describe(
|
|
15658
15749
|
"The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
|
|
15750
|
+
),
|
|
15751
|
+
goalId: fields.goalId.describe(
|
|
15752
|
+
"The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
|
|
15753
|
+
),
|
|
15754
|
+
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
15755
|
+
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
15756
|
+
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
15757
|
+
wait: external_exports.string().uuid().optional().describe(
|
|
15758
|
+
"KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
|
|
15659
15759
|
)
|
|
15660
15760
|
});
|
|
15661
15761
|
const out = mcpInputSchema2(surface);
|
|
15662
|
-
out.required
|
|
15762
|
+
delete out.required;
|
|
15763
|
+
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
15663
15764
|
return out;
|
|
15664
15765
|
}
|
|
15665
|
-
var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, decisionId? } \u2014
|
|
15766
|
+
var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS2}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN2}-${OPTIONS_MAX2} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
15666
15767
|
var ContextSchema2 = external_exports.object({
|
|
15667
15768
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
15668
15769
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -15775,6 +15876,9 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
15775
15876
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
15776
15877
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
15777
15878
|
workId: external_exports.string().uuid().optional(),
|
|
15879
|
+
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
15880
|
+
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
15881
|
+
goalId: external_exports.string().uuid().optional(),
|
|
15778
15882
|
urgency: NotifyLevelSchema2.default("inbox").describe(
|
|
15779
15883
|
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
15780
15884
|
),
|
|
@@ -15852,6 +15956,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
15852
15956
|
)
|
|
15853
15957
|
});
|
|
15854
15958
|
var NotifyRequestSchema2 = NotifyRequestFields2.superRefine((r, ctx) => {
|
|
15959
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
15855
15960
|
const sealed = !!r.envelope;
|
|
15856
15961
|
if (sealed) {
|
|
15857
15962
|
if (!r.envelope?.context)
|
|
@@ -16033,11 +16138,21 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
16033
16138
|
acts: external_exports.array(IntentSchema2).nullable().optional()
|
|
16034
16139
|
})
|
|
16035
16140
|
}),
|
|
16036
|
-
external_exports.object({
|
|
16141
|
+
external_exports.object({
|
|
16142
|
+
type: external_exports.literal("idle"),
|
|
16143
|
+
also: external_exports.array(RideAlongSchema2).optional(),
|
|
16144
|
+
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
16145
|
+
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
16146
|
+
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
16147
|
+
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
16148
|
+
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
16149
|
+
inFlight: external_exports.boolean().optional()
|
|
16150
|
+
})
|
|
16037
16151
|
]);
|
|
16038
16152
|
var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
|
|
16039
16153
|
var ScheduleCallbackSchema2 = external_exports.object({
|
|
16040
16154
|
parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
|
|
16155
|
+
goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
|
|
16041
16156
|
trigger: CallbackTriggerSchema2,
|
|
16042
16157
|
dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
|
|
16043
16158
|
note: external_exports.string().optional().describe("What to tell the user when you follow up.")
|
|
@@ -16482,6 +16597,15 @@ var AgentActivitySchema2 = external_exports.object({
|
|
|
16482
16597
|
var ConnectionSummarySchema2 = external_exports.object({
|
|
16483
16598
|
/** The connection = the agent's token id (used to address a request). */
|
|
16484
16599
|
id: external_exports.string(),
|
|
16600
|
+
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
16601
|
+
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
16602
|
+
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
16603
|
+
* apps/api/src/tokens/devices-vs-agents-design.md. */
|
|
16604
|
+
kind: external_exports.enum(["device", "agent"]).optional(),
|
|
16605
|
+
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
16606
|
+
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
16607
|
+
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
16608
|
+
mintedByDevice: external_exports.string().nullable().optional(),
|
|
16485
16609
|
device: external_exports.string().nullable(),
|
|
16486
16610
|
/** The agent's display name (the single pairing name). */
|
|
16487
16611
|
name: external_exports.string(),
|
|
@@ -16521,6 +16645,22 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
16521
16645
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
16522
16646
|
managed: external_exports.boolean()
|
|
16523
16647
|
});
|
|
16648
|
+
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
16649
|
+
var AgentLedgerSchema2 = external_exports.object({
|
|
16650
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
|
|
16651
|
+
/** Its own questions you have not answered. */
|
|
16652
|
+
asks: external_exports.array(LedgerItemSchema2),
|
|
16653
|
+
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
16654
|
+
answered: external_exports.array(LedgerItemSchema2),
|
|
16655
|
+
/** Requests you sent it that it never took. */
|
|
16656
|
+
requests: external_exports.array(LedgerItemSchema2),
|
|
16657
|
+
goals: external_exports.array(external_exports.object({ id: external_exports.string(), outcome: external_exports.string(), state: external_exports.string() })),
|
|
16658
|
+
callbacks: external_exports.array(external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), trigger: external_exports.string(), note: external_exports.string(), dueAt: external_exports.string().nullable() }))
|
|
16659
|
+
});
|
|
16660
|
+
var ReassignResultSchema2 = external_exports.object({
|
|
16661
|
+
moved: external_exports.object({ asks: external_exports.number(), answered: external_exports.number(), requests: external_exports.number(), goals: external_exports.number(), callbacks: external_exports.number() }),
|
|
16662
|
+
parentId: external_exports.string().nullable()
|
|
16663
|
+
});
|
|
16524
16664
|
var MoveRingSchema2 = external_exports.enum(["home", "travels", "retired", "quarantined"]);
|
|
16525
16665
|
var MoveSchema2 = external_exports.object({
|
|
16526
16666
|
id: external_exports.string(),
|
|
@@ -16904,7 +17044,8 @@ var CONTACT_SCHEMA2 = contactSchemaFrom2({
|
|
|
16904
17044
|
options: NotifyRequestFields2.shape.options,
|
|
16905
17045
|
channel: NotifyRequestFields2.shape.channel,
|
|
16906
17046
|
parentId: NotifyRequestFields2.shape.parentId,
|
|
16907
|
-
workId: NotifyRequestFields2.shape.workId
|
|
17047
|
+
workId: NotifyRequestFields2.shape.workId,
|
|
17048
|
+
goalId: NotifyRequestFields2.shape.goalId
|
|
16908
17049
|
});
|
|
16909
17050
|
|
|
16910
17051
|
// src/paigy/activity.ts
|
|
@@ -17606,10 +17747,14 @@ var claimedRun = (sessionId2) => {
|
|
|
17606
17747
|
async function slotHasWaitingWork(token) {
|
|
17607
17748
|
return (await pendingSummary({ token })).unacknowledged > 0;
|
|
17608
17749
|
}
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
if (
|
|
17612
|
-
|
|
17750
|
+
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
17751
|
+
function wakeAction(live, seenAt, now, serverSeenAt) {
|
|
17752
|
+
if (live) {
|
|
17753
|
+
if (seenAt === void 0) return "skip";
|
|
17754
|
+
return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
|
|
17755
|
+
}
|
|
17756
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
|
|
17757
|
+
return "spawn";
|
|
17613
17758
|
}
|
|
17614
17759
|
function startHost(opts) {
|
|
17615
17760
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -17669,12 +17814,21 @@ function startHost(opts) {
|
|
|
17669
17814
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
17670
17815
|
const wakePrompt = (fresh) => "You were woken because Paigy work is waiting for you. " + (fresh ? "This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. " : "You have been running, so some of this may already be yours \u2014 but not what arrived while you were idle. ") + "Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
|
|
17671
17816
|
async function sweepSlots() {
|
|
17817
|
+
const presence = /* @__PURE__ */ new Map();
|
|
17818
|
+
try {
|
|
17819
|
+
for (const c of (await listConnections(asHost)).items) {
|
|
17820
|
+
if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
|
|
17821
|
+
}
|
|
17822
|
+
} catch {
|
|
17823
|
+
}
|
|
17672
17824
|
for (const slot of listSlots()) {
|
|
17673
17825
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
17674
17826
|
const key = runKey(slot);
|
|
17675
17827
|
if (!harness) continue;
|
|
17676
17828
|
const live = runs.get(key);
|
|
17677
|
-
const
|
|
17829
|
+
const tokenId = slotIdentity(slot).tokenId;
|
|
17830
|
+
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
17831
|
+
const action = wakeAction(!!live, published.get(key)?.movedAt, Date.now(), seenByServer);
|
|
17678
17832
|
if (action === "skip") continue;
|
|
17679
17833
|
const token = readToken(slot);
|
|
17680
17834
|
if (!token) continue;
|
|
@@ -18439,15 +18593,16 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
18439
18593
|
const entry = (host?.roster() ?? readHostState()?.roster ?? []).find((a) => a.name === name);
|
|
18440
18594
|
if (!entry) return null;
|
|
18441
18595
|
const token = entry.slot ? readToken(entry.slot) : "";
|
|
18442
|
-
const pending = token ? await
|
|
18596
|
+
const pending = token ? await pendingSummary({ token }).catch(() => null) : null;
|
|
18443
18597
|
return {
|
|
18444
18598
|
...entry,
|
|
18445
18599
|
pending: pending ? {
|
|
18446
|
-
|
|
18447
|
-
|
|
18600
|
+
// The same two facts, from the counting read: what is waiting, and how much.
|
|
18601
|
+
requests: pending.unacknowledgedItems.slice(0, 5).map((r) => ({
|
|
18602
|
+
line: r.title.slice(0, 140),
|
|
18448
18603
|
at: r.createdAt
|
|
18449
18604
|
})),
|
|
18450
|
-
replies: pending.
|
|
18605
|
+
replies: pending.unacknowledged
|
|
18451
18606
|
} : null
|
|
18452
18607
|
};
|
|
18453
18608
|
});
|