@paigy/harness 0.3.4 → 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 +88 -18
- package/dist/main.js +92 -21
- 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";
|
|
@@ -23656,6 +23657,9 @@ function contactSchemaFrom(fields) {
|
|
|
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."
|
|
23658
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
|
+
),
|
|
23659
23663
|
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
23660
23664
|
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
23661
23665
|
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
@@ -24109,6 +24113,32 @@ async function sealForE2ee(req, token, deps = {}) {
|
|
|
24109
24113
|
}
|
|
24110
24114
|
async function submitNotification(req, opts = {}) {
|
|
24111
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
|
+
}
|
|
24112
24142
|
const body = await sealForE2ee(req, token);
|
|
24113
24143
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
24114
24144
|
method: "POST",
|
|
@@ -24341,7 +24371,7 @@ async function patchState(path, state, opts, operation) {
|
|
|
24341
24371
|
function setTaskState(notificationId, state, opts = {}) {
|
|
24342
24372
|
return patchState(`/api/notify/${encodeURIComponent(notificationId)}/state`, state, opts, "set_task_state");
|
|
24343
24373
|
}
|
|
24344
|
-
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, 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;
|
|
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;
|
|
24345
24375
|
var init_dist = __esm({
|
|
24346
24376
|
"../../packages/sdk/dist/index.js"() {
|
|
24347
24377
|
"use strict";
|
|
@@ -27003,12 +27033,19 @@ var init_dist = __esm({
|
|
|
27003
27033
|
revision: external_exports.number().int().positive(),
|
|
27004
27034
|
changes: external_exports.object({
|
|
27005
27035
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
27006
|
-
|
|
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(),
|
|
27007
27041
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
27008
27042
|
reviewed: external_exports.literal(true).optional()
|
|
27009
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
27010
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
27011
|
-
|
|
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();
|
|
27012
27049
|
fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
27013
27050
|
STANDARD_MEANS = (() => {
|
|
27014
27051
|
const plan = MISSED_CALL_PLAN.backoff_standard;
|
|
@@ -27017,7 +27054,7 @@ var init_dist = __esm({
|
|
|
27017
27054
|
const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
|
|
27018
27055
|
return `Rings again ${list} after the missed call, then leaves it in your inbox`;
|
|
27019
27056
|
})();
|
|
27020
|
-
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?, 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. 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.`;
|
|
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.`;
|
|
27021
27058
|
ContextSchema = external_exports.object({
|
|
27022
27059
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
27023
27060
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -27130,6 +27167,9 @@ var init_dist = __esm({
|
|
|
27130
27167
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
27131
27168
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
27132
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(),
|
|
27133
27173
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
27134
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."
|
|
27135
27175
|
),
|
|
@@ -27207,6 +27247,7 @@ var init_dist = __esm({
|
|
|
27207
27247
|
)
|
|
27208
27248
|
});
|
|
27209
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" });
|
|
27210
27251
|
const sealed = !!r.envelope;
|
|
27211
27252
|
if (sealed) {
|
|
27212
27253
|
if (!r.envelope?.context)
|
|
@@ -28277,7 +28318,8 @@ var init_dist = __esm({
|
|
|
28277
28318
|
options: NotifyRequestFields.shape.options,
|
|
28278
28319
|
channel: NotifyRequestFields.shape.channel,
|
|
28279
28320
|
parentId: NotifyRequestFields.shape.parentId,
|
|
28280
|
-
workId: NotifyRequestFields.shape.workId
|
|
28321
|
+
workId: NotifyRequestFields.shape.workId,
|
|
28322
|
+
goalId: NotifyRequestFields.shape.goalId
|
|
28281
28323
|
});
|
|
28282
28324
|
import_tweetnacl = __toESM2(require_nacl_fast(), 1);
|
|
28283
28325
|
import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
|
|
@@ -34895,12 +34937,19 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
34895
34937
|
revision: external_exports.number().int().positive(),
|
|
34896
34938
|
changes: external_exports.object({
|
|
34897
34939
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
34898
|
-
|
|
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(),
|
|
34899
34945
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
34900
34946
|
reviewed: external_exports.literal(true).optional()
|
|
34901
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
34902
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
34903
|
-
|
|
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();
|
|
34904
34953
|
var fmtMin2 = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
34905
34954
|
var STANDARD_MEANS2 = (() => {
|
|
34906
34955
|
const plan = MISSED_CALL_PLAN2.backoff_standard;
|
|
@@ -34929,6 +34978,9 @@ function contactSchemaFrom2(fields) {
|
|
|
34929
34978
|
workId: fields.workId.describe(
|
|
34930
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."
|
|
34931
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
|
+
),
|
|
34932
34984
|
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
34933
34985
|
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
34934
34986
|
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
@@ -34941,7 +34993,7 @@ function contactSchemaFrom2(fields) {
|
|
|
34941
34993
|
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
34942
34994
|
return out;
|
|
34943
34995
|
}
|
|
34944
|
-
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?, 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. 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.`;
|
|
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.`;
|
|
34945
34997
|
var ContextSchema2 = external_exports.object({
|
|
34946
34998
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
34947
34999
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -35054,6 +35106,9 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
35054
35106
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
35055
35107
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
35056
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(),
|
|
35057
35112
|
urgency: NotifyLevelSchema2.default("inbox").describe(
|
|
35058
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."
|
|
35059
35114
|
),
|
|
@@ -35131,6 +35186,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
35131
35186
|
)
|
|
35132
35187
|
});
|
|
35133
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" });
|
|
35134
35190
|
const sealed = !!r.envelope;
|
|
35135
35191
|
if (sealed) {
|
|
35136
35192
|
if (!r.envelope?.context)
|
|
@@ -36218,7 +36274,8 @@ var CONTACT_SCHEMA2 = contactSchemaFrom2({
|
|
|
36218
36274
|
options: NotifyRequestFields2.shape.options,
|
|
36219
36275
|
channel: NotifyRequestFields2.shape.channel,
|
|
36220
36276
|
parentId: NotifyRequestFields2.shape.parentId,
|
|
36221
|
-
workId: NotifyRequestFields2.shape.workId
|
|
36277
|
+
workId: NotifyRequestFields2.shape.workId,
|
|
36278
|
+
goalId: NotifyRequestFields2.shape.goalId
|
|
36222
36279
|
});
|
|
36223
36280
|
|
|
36224
36281
|
// src/paigy/conversation.ts
|
|
@@ -36965,10 +37022,14 @@ var claimedRun = (sessionId2) => {
|
|
|
36965
37022
|
async function slotHasWaitingWork(token) {
|
|
36966
37023
|
return (await pendingSummary({ token })).unacknowledged > 0;
|
|
36967
37024
|
}
|
|
36968
|
-
|
|
36969
|
-
|
|
36970
|
-
if (
|
|
36971
|
-
|
|
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";
|
|
36972
37033
|
}
|
|
36973
37034
|
function startHost(opts) {
|
|
36974
37035
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -37028,12 +37089,21 @@ function startHost(opts) {
|
|
|
37028
37089
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
37029
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.";
|
|
37030
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
|
+
}
|
|
37031
37099
|
for (const slot of listSlots()) {
|
|
37032
37100
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
37033
37101
|
const key = runKey(slot);
|
|
37034
37102
|
if (!harness) continue;
|
|
37035
37103
|
const live = runs.get(key);
|
|
37036
|
-
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);
|
|
37037
37107
|
if (action === "skip") continue;
|
|
37038
37108
|
const token = readToken(slot);
|
|
37039
37109
|
if (!token) continue;
|
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;
|
|
@@ -12208,6 +12216,9 @@ function contactSchemaFrom(fields) {
|
|
|
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."
|
|
12210
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
|
+
),
|
|
12211
12222
|
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
12212
12223
|
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
12213
12224
|
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
@@ -12220,7 +12231,7 @@ function contactSchemaFrom(fields) {
|
|
|
12220
12231
|
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
12221
12232
|
return out;
|
|
12222
12233
|
}
|
|
12223
|
-
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?, 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. 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.`;
|
|
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.`;
|
|
12224
12235
|
var ContextSchema = external_exports.object({
|
|
12225
12236
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
12226
12237
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -12333,6 +12344,9 @@ var NotifyRequestFields = external_exports.object({
|
|
|
12333
12344
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
12334
12345
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
12335
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(),
|
|
12336
12350
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
12337
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."
|
|
12338
12352
|
),
|
|
@@ -12410,6 +12424,7 @@ var NotifyRequestFields = external_exports.object({
|
|
|
12410
12424
|
)
|
|
12411
12425
|
});
|
|
12412
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" });
|
|
12413
12428
|
const sealed = !!r.envelope;
|
|
12414
12429
|
if (sealed) {
|
|
12415
12430
|
if (!r.envelope?.context)
|
|
@@ -13535,7 +13550,8 @@ var CONTACT_SCHEMA = contactSchemaFrom({
|
|
|
13535
13550
|
options: NotifyRequestFields.shape.options,
|
|
13536
13551
|
channel: NotifyRequestFields.shape.channel,
|
|
13537
13552
|
parentId: NotifyRequestFields.shape.parentId,
|
|
13538
|
-
workId: NotifyRequestFields.shape.workId
|
|
13553
|
+
workId: NotifyRequestFields.shape.workId,
|
|
13554
|
+
goalId: NotifyRequestFields.shape.goalId
|
|
13539
13555
|
});
|
|
13540
13556
|
var import_tweetnacl = __toESM2(require_nacl_fast(), 1);
|
|
13541
13557
|
var import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
|
|
@@ -13942,6 +13958,32 @@ async function sealForE2ee(req, token, deps = {}) {
|
|
|
13942
13958
|
}
|
|
13943
13959
|
async function submitNotification(req, opts = {}) {
|
|
13944
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
|
+
}
|
|
13945
13987
|
const body = await sealForE2ee(req, token);
|
|
13946
13988
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
|
|
13947
13989
|
method: "POST",
|
|
@@ -15665,12 +15707,19 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
15665
15707
|
revision: external_exports.number().int().positive(),
|
|
15666
15708
|
changes: external_exports.object({
|
|
15667
15709
|
outcome: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
15668
|
-
|
|
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(),
|
|
15669
15715
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
15670
15716
|
reviewed: external_exports.literal(true).optional()
|
|
15671
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
15672
|
-
reason: external_exports.string().trim().min(1).max(2e3)
|
|
15673
|
-
|
|
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();
|
|
15674
15723
|
var fmtMin2 = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
15675
15724
|
var STANDARD_MEANS2 = (() => {
|
|
15676
15725
|
const plan = MISSED_CALL_PLAN2.backoff_standard;
|
|
@@ -15699,6 +15748,9 @@ function contactSchemaFrom2(fields) {
|
|
|
15699
15748
|
workId: fields.workId.describe(
|
|
15700
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."
|
|
15701
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
|
+
),
|
|
15702
15754
|
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
15703
15755
|
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
15704
15756
|
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
@@ -15711,7 +15763,7 @@ function contactSchemaFrom2(fields) {
|
|
|
15711
15763
|
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
15712
15764
|
return out;
|
|
15713
15765
|
}
|
|
15714
|
-
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?, 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. 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.`;
|
|
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.`;
|
|
15715
15767
|
var ContextSchema2 = external_exports.object({
|
|
15716
15768
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
15717
15769
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -15824,6 +15876,9 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
15824
15876
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
15825
15877
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
15826
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(),
|
|
15827
15882
|
urgency: NotifyLevelSchema2.default("inbox").describe(
|
|
15828
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."
|
|
15829
15884
|
),
|
|
@@ -15901,6 +15956,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
15901
15956
|
)
|
|
15902
15957
|
});
|
|
15903
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" });
|
|
15904
15960
|
const sealed = !!r.envelope;
|
|
15905
15961
|
if (sealed) {
|
|
15906
15962
|
if (!r.envelope?.context)
|
|
@@ -16988,7 +17044,8 @@ var CONTACT_SCHEMA2 = contactSchemaFrom2({
|
|
|
16988
17044
|
options: NotifyRequestFields2.shape.options,
|
|
16989
17045
|
channel: NotifyRequestFields2.shape.channel,
|
|
16990
17046
|
parentId: NotifyRequestFields2.shape.parentId,
|
|
16991
|
-
workId: NotifyRequestFields2.shape.workId
|
|
17047
|
+
workId: NotifyRequestFields2.shape.workId,
|
|
17048
|
+
goalId: NotifyRequestFields2.shape.goalId
|
|
16992
17049
|
});
|
|
16993
17050
|
|
|
16994
17051
|
// src/paigy/activity.ts
|
|
@@ -17690,10 +17747,14 @@ var claimedRun = (sessionId2) => {
|
|
|
17690
17747
|
async function slotHasWaitingWork(token) {
|
|
17691
17748
|
return (await pendingSummary({ token })).unacknowledged > 0;
|
|
17692
17749
|
}
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
if (
|
|
17696
|
-
|
|
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";
|
|
17697
17758
|
}
|
|
17698
17759
|
function startHost(opts) {
|
|
17699
17760
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -17753,12 +17814,21 @@ function startHost(opts) {
|
|
|
17753
17814
|
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
17754
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.";
|
|
17755
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
|
+
}
|
|
17756
17824
|
for (const slot of listSlots()) {
|
|
17757
17825
|
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
17758
17826
|
const key = runKey(slot);
|
|
17759
17827
|
if (!harness) continue;
|
|
17760
17828
|
const live = runs.get(key);
|
|
17761
|
-
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);
|
|
17762
17832
|
if (action === "skip") continue;
|
|
17763
17833
|
const token = readToken(slot);
|
|
17764
17834
|
if (!token) continue;
|
|
@@ -18523,15 +18593,16 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
18523
18593
|
const entry = (host?.roster() ?? readHostState()?.roster ?? []).find((a) => a.name === name);
|
|
18524
18594
|
if (!entry) return null;
|
|
18525
18595
|
const token = entry.slot ? readToken(entry.slot) : "";
|
|
18526
|
-
const pending = token ? await
|
|
18596
|
+
const pending = token ? await pendingSummary({ token }).catch(() => null) : null;
|
|
18527
18597
|
return {
|
|
18528
18598
|
...entry,
|
|
18529
18599
|
pending: pending ? {
|
|
18530
|
-
|
|
18531
|
-
|
|
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),
|
|
18532
18603
|
at: r.createdAt
|
|
18533
18604
|
})),
|
|
18534
|
-
replies: pending.
|
|
18605
|
+
replies: pending.unacknowledged
|
|
18535
18606
|
} : null
|
|
18536
18607
|
};
|
|
18537
18608
|
});
|