@paigy/harness 0.3.15 → 0.3.16

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.
Files changed (3) hide show
  1. package/dist/cli.js +226 -122
  2. package/dist/main.js +226 -122
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -23937,6 +23937,7 @@ async function contact(input, opts = {}) {
23937
23937
  const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
23938
23938
  let deliveryId;
23939
23939
  let deliveries = [];
23940
+ let recorded = [];
23940
23941
  if ("deliveryId" in parsed) {
23941
23942
  deliveryId = parsed.deliveryId;
23942
23943
  } else {
@@ -23965,16 +23966,24 @@ async function contact(input, opts = {}) {
23965
23966
  if (!res.ok) await fail("batch contact", res);
23966
23967
  const receipt = await res.json();
23967
23968
  deliveries = receipt.deliveries ?? [];
23969
+ recorded = receipt.recorded ?? [];
23970
+ if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
23968
23971
  if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
23969
23972
  deliveryId = deliveries[0]?.deliveryId ?? "";
23970
23973
  if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
23971
23974
  }
23972
- const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
23975
+ const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
23976
+ const joinedCard = deliveries[0]?.joinedCard === true;
23973
23977
  const read = async (signal2) => {
23974
23978
  const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23975
23979
  if (!res.ok) await fail("read_delivery", res);
23976
23980
  const one = await res.json();
23977
- return all.length > 1 ? { ...one, deliveries: all } : one;
23981
+ return {
23982
+ ...one,
23983
+ ...all.length > 1 ? { deliveries: all } : {},
23984
+ ...recorded.length ? { recorded, notSent: NOT_SENT } : {},
23985
+ ...joinedCard ? { joinedCard: true } : {}
23986
+ };
23978
23987
  };
23979
23988
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
23980
23989
  if (opts.waits === false) return read(opts.signal);
@@ -24002,45 +24011,105 @@ async function checkReplies(opts = {}) {
24002
24011
  return await res.json();
24003
24012
  }
24004
24013
  function frame(line, topic, event, payload) {
24014
+ const ref = String(++line.ref);
24005
24015
  try {
24006
- line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
24016
+ line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
24007
24017
  } catch {
24008
24018
  }
24019
+ return ref;
24009
24020
  }
24010
24021
  function stop(line) {
24011
24022
  line.open = false;
24023
+ line.beat = void 0;
24012
24024
  clearInterval(line.heart);
24013
24025
  clearTimeout(line.retry);
24026
+ for (const t of line.topics.values()) {
24027
+ clearTimeout(t.timer);
24028
+ t.joined = false;
24029
+ t.ref = void 0;
24030
+ }
24031
+ }
24032
+ function join3(line, name, t) {
24033
+ clearTimeout(t.timer);
24034
+ t.joined = false;
24035
+ t.ref = frame(line, `realtime:${name}`, "phx_join", JOIN);
24036
+ t.timer = setTimeout(() => rejoin(line, name, t), JOIN_TIMEOUT_MS);
24037
+ }
24038
+ function rejoin(line, name, t) {
24039
+ clearTimeout(t.timer);
24040
+ t.joined = false;
24041
+ t.ref = void 0;
24042
+ if (!line.open || line.topics.get(name) !== t) return;
24043
+ t.timer = setTimeout(() => {
24044
+ if (line.open && line.topics.get(name) === t) join3(line, name, t);
24045
+ }, RETRY_MS[Math.min(t.attempt++, RETRY_MS.length - 1)]);
24046
+ }
24047
+ function joined(t) {
24048
+ clearTimeout(t.timer);
24049
+ t.joined = true;
24050
+ t.attempt = 0;
24051
+ for (const l of t.listeners) l.onJoined?.(++l.joins);
24014
24052
  }
24015
24053
  function connect(line) {
24016
24054
  const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
24017
24055
  const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
24056
+ let gone = false;
24057
+ const again = () => {
24058
+ if (gone) return;
24059
+ gone = true;
24060
+ stop(line);
24061
+ if (!line.topics.size) return;
24062
+ line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
24063
+ };
24018
24064
  ws.addEventListener("open", () => {
24065
+ if (gone) return;
24019
24066
  line.open = true;
24020
24067
  line.attempt = 0;
24021
- for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
24022
- line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
24068
+ for (const [name, t] of line.topics) join3(line, name, t);
24069
+ line.heart = setInterval(() => {
24070
+ if (line.beat) {
24071
+ try {
24072
+ ws.close();
24073
+ } catch {
24074
+ }
24075
+ again();
24076
+ return;
24077
+ }
24078
+ line.beat = frame(line, "phoenix", "heartbeat", {});
24079
+ }, HEARTBEAT_MS);
24023
24080
  });
24024
24081
  ws.addEventListener("message", (e) => {
24082
+ if (gone) return;
24025
24083
  let m;
24026
24084
  try {
24027
24085
  m = JSON.parse(String(e.data));
24028
24086
  } catch {
24029
24087
  return;
24030
24088
  }
24031
- if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
24032
- for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
24089
+ if (m.topic === "phoenix") {
24090
+ if (m.event === "phx_reply" && m.ref === line.beat) line.beat = void 0;
24091
+ return;
24092
+ }
24093
+ if (!m.topic?.startsWith("realtime:")) return;
24094
+ const name = m.topic.slice("realtime:".length);
24095
+ const t = line.topics.get(name);
24096
+ if (!t) return;
24097
+ if (m.event === "phx_reply") {
24098
+ if (!t.ref || m.ref !== t.ref) return;
24099
+ if (m.payload?.status === "ok") joined(t);
24100
+ else rejoin(line, name, t);
24101
+ return;
24102
+ }
24103
+ if (m.event === "phx_error" || m.event === "phx_close") {
24104
+ if (m.ref && m.ref !== t.ref) return;
24105
+ rejoin(line, name, t);
24106
+ return;
24107
+ }
24108
+ if (m.event !== "broadcast") return;
24109
+ for (const l of t.listeners) {
24033
24110
  if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
24034
24111
  }
24035
24112
  });
24036
- let gone = false;
24037
- const again = () => {
24038
- if (gone) return;
24039
- gone = true;
24040
- stop(line);
24041
- if (!line.topics.size) return;
24042
- line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
24043
- };
24044
24113
  ws.addEventListener("close", again);
24045
24114
  ws.addEventListener("error", again);
24046
24115
  }
@@ -24048,17 +24117,22 @@ function subscribeRealtime(args) {
24048
24117
  const key = `${args.url} ${args.anonKey}`;
24049
24118
  let line = lines.get(key);
24050
24119
  if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
24051
- let heard = line.topics.get(args.topic);
24052
- if (!heard) {
24053
- line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
24054
- if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
24055
- }
24056
- const me = { event: args.event, onMessage: args.onMessage };
24057
- heard.add(me);
24120
+ let t = line.topics.get(args.topic);
24121
+ if (!t) {
24122
+ line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
24123
+ if (line.open) join3(line, args.topic, t);
24124
+ }
24125
+ const topic = t;
24126
+ const me = { event: args.event, onMessage: args.onMessage, onJoined: args.onJoined, joins: 0 };
24127
+ topic.listeners.add(me);
24128
+ if (topic.joined && me.onJoined) queueMicrotask(() => {
24129
+ if (topic.joined && topic.listeners.has(me)) me.onJoined?.(++me.joins);
24130
+ });
24058
24131
  if (!line.ws) connect(line);
24059
24132
  return {
24060
24133
  close: () => {
24061
- if (!heard.delete(me) || heard.size) return;
24134
+ if (!topic.listeners.delete(me) || topic.listeners.size) return;
24135
+ clearTimeout(topic.timer);
24062
24136
  line.topics.delete(args.topic);
24063
24137
  if (line.topics.size) {
24064
24138
  if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
@@ -24074,7 +24148,7 @@ function subscribeRealtime(args) {
24074
24148
  };
24075
24149
  }
24076
24150
  async function subscribeWake(onNudge, opts = {}) {
24077
- const { release = true, ...client } = opts;
24151
+ const { release = true, onJoined, ...client } = opts;
24078
24152
  const cfg = await registerDelivery("self_hosted", client);
24079
24153
  const { url, anonKey } = cfg.realtime;
24080
24154
  const channel = wakeChannel(cfg.tokenId);
@@ -24083,7 +24157,8 @@ async function subscribeWake(onNudge, opts = {}) {
24083
24157
  anonKey,
24084
24158
  topic: channel,
24085
24159
  event: WAKE_EVENT,
24086
- onMessage: (payload) => onNudge(payload ?? {})
24160
+ onMessage: (payload) => onNudge(payload ?? {}),
24161
+ onJoined
24087
24162
  });
24088
24163
  return {
24089
24164
  tokenId: cfg.tokenId,
@@ -24095,7 +24170,7 @@ async function subscribeWake(onNudge, opts = {}) {
24095
24170
  }
24096
24171
  };
24097
24172
  }
24098
- var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, HEARTBEAT_MS, RETRY_MS, JOIN, lines;
24173
+ var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24099
24174
  var init_dist = __esm({
24100
24175
  "../../packages/sdk/dist/index.js"() {
24101
24176
  "use strict";
@@ -24508,7 +24583,7 @@ var init_dist = __esm({
24508
24583
  parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
24509
24584
  repo: external_exports.string().optional().describe("Optional repository context."),
24510
24585
  ask: external_exports.string().trim().min(1).max(1e4).describe(
24511
- "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
24586
+ "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
24512
24587
  ),
24513
24588
  options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
24514
24589
  }).strict();
@@ -24519,7 +24594,7 @@ var init_dist = __esm({
24519
24594
  }).strict();
24520
24595
  ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
24521
24596
  CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
24522
- CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
24597
+ CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
24523
24598
  CreateGoalSchema = external_exports.object({
24524
24599
  outcome: external_exports.string().trim().min(1).max(1e4),
24525
24600
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -24566,9 +24641,9 @@ var init_dist = __esm({
24566
24641
  ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
24567
24642
  GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
24568
24643
  GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
24569
- UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
24570
- CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
24571
- CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
24644
+ UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
24645
+ CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
24646
+ CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
24572
24647
  CheckRepliesSchema = external_exports.object({}).strict();
24573
24648
  AGENT_TOOLS = [
24574
24649
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
@@ -24898,9 +24973,9 @@ var init_dist = __esm({
24898
24973
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
24899
24974
  * the planner declares duplicates instead of asking them twice. */
24900
24975
  coveredIds: external_exports.array(external_exports.string()).optional(),
24901
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
24902
- * line went out on 2026-07-28 and the caller could not answer it at all. */
24903
- info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
24976
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
24977
+ * (owner, 2026-09-25), and a count here refused whole plans. */
24978
+ info: external_exports.array(external_exports.string().min(1)).default([]),
24904
24979
  question: external_exports.string().min(1).nullable(),
24905
24980
  /** True on the one turn carrying the agent's own declared question. */
24906
24981
  asks: external_exports.boolean().optional(),
@@ -24935,32 +25010,6 @@ var init_dist = __esm({
24935
25010
  CLAIM_STALE_MS = 30 * 6e4;
24936
25011
  InboxItemSchema = external_exports.object({
24937
25012
  id: external_exports.string(),
24938
- /** The conversation thread + connection this item lives on. Present on the replied
24939
- * detail — they power History's "Continue" / "New session from this" (#57/#251). */
24940
- parentId: external_exports.string().optional(),
24941
- /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
24942
- * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
24943
- * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
24944
- * accumulates every message an agent ever sent, so grouping by it renders a day of
24945
- * unrelated updates as a single "12-part request". Absent on rows written before the
24946
- * `asks` table, and on anything that never went through `notify` — both fall back to the
24947
- * thread, which is what the client did for all rows until now. */
24948
- askId: external_exports.string().optional(),
24949
- /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
24950
- * shares one `created_at` to the microsecond, so without it the author's order is
24951
- * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
24952
- * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
24953
- * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
24954
- seq: external_exports.number().int().optional(),
24955
- /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
24956
- * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
24957
- * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
24958
- * screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
24959
- * only evidence of itself. How big an arrival is, is a fact about the arrival, so the
24960
- * server that can still see every row states it. Absent on any row with no `askId`: a
24961
- * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
24962
- * the one part of its own arrival a single row cannot answer. */
24963
- units: external_exports.number().int().positive().optional(),
24964
25013
  tokenId: external_exports.string().optional(),
24965
25014
  status: NotifyStatusSchema,
24966
25015
  context: ContextSchema,
@@ -25005,7 +25054,12 @@ var init_dist = __esm({
25005
25054
  entryId: external_exports.string(),
25006
25055
  title: external_exports.string(),
25007
25056
  state: external_exports.enum(["open", "answered"]),
25008
- answer: external_exports.string().nullable()
25057
+ answer: external_exports.string().nullable(),
25058
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
25059
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
25060
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
25061
+ * author is not an agent this account holds (unpaired since, or a person). */
25062
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional()
25009
25063
  })).optional(),
25010
25064
  visuals: external_exports.array(VisualSchema).optional(),
25011
25065
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -25023,10 +25077,20 @@ var init_dist = __esm({
25023
25077
  * agent (held in the inbox until the agent replies on the thread). */
25024
25078
  turn: external_exports.enum(["you", "agent"]).default("you"),
25025
25079
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
25026
- * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
25027
- * client may still flag a stall by age. Drives the inbox error badge + Retry. */
25080
+ * the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
25081
+ * error badge + Retry. */
25028
25082
  error: external_exports.string().optional(),
25083
+ /** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
25084
+ * with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
25085
+ * this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
25086
+ * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
25087
+ cold: external_exports.string().datetime().optional(),
25029
25088
  clarifies: external_exports.string().optional(),
25089
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
25090
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
25091
+ * off the same open list the card came from, so it clears when the Call does. A card is the
25092
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
25093
+ onCall: external_exports.literal(true).optional(),
25030
25094
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
25031
25095
  * declined, and what the ladder will do next — read off the cron's own row, never computed
25032
25096
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -25075,7 +25139,10 @@ var init_dist = __esm({
25075
25139
  goalIds: external_exports.array(external_exports.string()),
25076
25140
  decisionNeedId: external_exports.string().optional(),
25077
25141
  sealed: external_exports.boolean(),
25078
- goalState: external_exports.string().optional()
25142
+ goalState: external_exports.string().optional(),
25143
+ /** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
25144
+ * and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
25145
+ goalTitle: external_exports.string().optional()
25079
25146
  }).optional()
25080
25147
  });
25081
25148
  APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
@@ -25174,7 +25241,6 @@ var init_dist = __esm({
25174
25241
  });
25175
25242
  HistoryItemSchema = external_exports.object({
25176
25243
  id: external_exports.string(),
25177
- parentId: external_exports.string(),
25178
25244
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
25179
25245
  initiator: external_exports.enum(["user", "agent"]),
25180
25246
  title: external_exports.string(),
@@ -25333,6 +25399,24 @@ var init_dist = __esm({
25333
25399
  * gate at all. */
25334
25400
  blocked: external_exports.boolean().default(false)
25335
25401
  });
25402
+ QueueReplySchema = external_exports.object({
25403
+ /** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
25404
+ * id) — so the phone can tell a reply it just sent from one the queue already carries, and the
25405
+ * walk can name it in its zoom. */
25406
+ id: external_exports.string(),
25407
+ /** The Goal the note is on. */
25408
+ goalId: external_exports.string(),
25409
+ /** What the note said. */
25410
+ note: external_exports.string(),
25411
+ /** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
25412
+ deliveryId: external_exports.string(),
25413
+ requestEntryId: external_exports.string(),
25414
+ askedAt: external_exports.string(),
25415
+ /** When the person last replied — the window's start. */
25416
+ repliedAt: external_exports.string(),
25417
+ /** The person's latest words about it; null when there is nothing readable in them. */
25418
+ reply: external_exports.string().nullable()
25419
+ });
25336
25420
  QueueItemSchema = external_exports.object({
25337
25421
  id: external_exports.string(),
25338
25422
  /** One-line headline — the first sentence of the outcome. */
@@ -25360,6 +25444,10 @@ var init_dist = __esm({
25360
25444
  blocked: external_exports.boolean().default(false),
25361
25445
  /** Every decision need on it, open or settled — the page decides which to show. */
25362
25446
  questions: external_exports.array(QueueQuestionSchema).default([]),
25447
+ /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
25448
+ * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
25449
+ * not spell an empty list. */
25450
+ replies: external_exports.array(QueueReplySchema).optional(),
25363
25451
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
25364
25452
  repo: external_exports.string().nullable().optional(),
25365
25453
  createdAt: external_exports.string(),
@@ -25572,9 +25660,11 @@ var init_dist = __esm({
25572
25660
  tokenOverride = null;
25573
25661
  authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
25574
25662
  AWAIT_WINDOW_MS = 45e3;
25663
+ NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
25575
25664
  HEARTBEAT_MS = 25e3;
25576
25665
  RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
25577
25666
  JOIN = { config: { broadcast: { self: false } } };
25667
+ JOIN_TIMEOUT_MS = 1e4;
25578
25668
  lines = /* @__PURE__ */ new Map();
25579
25669
  }
25580
25670
  });
@@ -30823,7 +30913,7 @@ import { hostname } from "os";
30823
30913
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
30824
30914
  import { execFile as execFile2, execSync } from "child_process";
30825
30915
  import { homedir as homedir8 } from "os";
30826
- import { join as join7 } from "path";
30916
+ import { join as join8 } from "path";
30827
30917
 
30828
30918
  // src/run.ts
30829
30919
  init_dist();
@@ -32486,7 +32576,7 @@ var AskInputSchema2 = external_exports.object({
32486
32576
  parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
32487
32577
  repo: external_exports.string().optional().describe("Optional repository context."),
32488
32578
  ask: external_exports.string().trim().min(1).max(1e4).describe(
32489
- "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
32579
+ "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
32490
32580
  ),
32491
32581
  options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
32492
32582
  }).strict();
@@ -32497,7 +32587,7 @@ var StartContactSchema2 = external_exports.object({
32497
32587
  }).strict();
32498
32588
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
32499
32589
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
32500
- var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
32590
+ var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
32501
32591
  var CreateGoalSchema2 = external_exports.object({
32502
32592
  outcome: external_exports.string().trim().min(1).max(1e4),
32503
32593
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -32544,9 +32634,9 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
32544
32634
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
32545
32635
  var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
32546
32636
  var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
32547
- var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
32548
- var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
32549
- var CHECK_REPLIES_DESCRIPTION2 = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
32637
+ var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
32638
+ var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
32639
+ var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
32550
32640
  var CheckRepliesSchema2 = external_exports.object({}).strict();
32551
32641
  var AGENT_TOOLS2 = [
32552
32642
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
@@ -32891,9 +32981,9 @@ var AgendaTurnSchema2 = external_exports.object({
32891
32981
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
32892
32982
  * the planner declares duplicates instead of asking them twice. */
32893
32983
  coveredIds: external_exports.array(external_exports.string()).optional(),
32894
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
32895
- * line went out on 2026-07-28 and the caller could not answer it at all. */
32896
- info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
32984
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
32985
+ * (owner, 2026-09-25), and a count here refused whole plans. */
32986
+ info: external_exports.array(external_exports.string().min(1)).default([]),
32897
32987
  question: external_exports.string().min(1).nullable(),
32898
32988
  /** True on the one turn carrying the agent's own declared question. */
32899
32989
  asks: external_exports.boolean().optional(),
@@ -32928,32 +33018,6 @@ var AgendaTurnSchema2 = external_exports.object({
32928
33018
  var CLAIM_STALE_MS2 = 30 * 6e4;
32929
33019
  var InboxItemSchema2 = external_exports.object({
32930
33020
  id: external_exports.string(),
32931
- /** The conversation thread + connection this item lives on. Present on the replied
32932
- * detail — they power History's "Continue" / "New session from this" (#57/#251). */
32933
- parentId: external_exports.string().optional(),
32934
- /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
32935
- * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
32936
- * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
32937
- * accumulates every message an agent ever sent, so grouping by it renders a day of
32938
- * unrelated updates as a single "12-part request". Absent on rows written before the
32939
- * `asks` table, and on anything that never went through `notify` — both fall back to the
32940
- * thread, which is what the client did for all rows until now. */
32941
- askId: external_exports.string().optional(),
32942
- /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
32943
- * shares one `created_at` to the microsecond, so without it the author's order is
32944
- * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
32945
- * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
32946
- * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
32947
- seq: external_exports.number().int().optional(),
32948
- /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
32949
- * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
32950
- * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
32951
- * screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
32952
- * only evidence of itself. How big an arrival is, is a fact about the arrival, so the
32953
- * server that can still see every row states it. Absent on any row with no `askId`: a
32954
- * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
32955
- * the one part of its own arrival a single row cannot answer. */
32956
- units: external_exports.number().int().positive().optional(),
32957
33021
  tokenId: external_exports.string().optional(),
32958
33022
  status: NotifyStatusSchema2,
32959
33023
  context: ContextSchema2,
@@ -32998,7 +33062,12 @@ var InboxItemSchema2 = external_exports.object({
32998
33062
  entryId: external_exports.string(),
32999
33063
  title: external_exports.string(),
33000
33064
  state: external_exports.enum(["open", "answered"]),
33001
- answer: external_exports.string().nullable()
33065
+ answer: external_exports.string().nullable(),
33066
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
33067
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
33068
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
33069
+ * author is not an agent this account holds (unpaired since, or a person). */
33070
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
33002
33071
  })).optional(),
33003
33072
  visuals: external_exports.array(VisualSchema2).optional(),
33004
33073
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -33016,10 +33085,20 @@ var InboxItemSchema2 = external_exports.object({
33016
33085
  * agent (held in the inbox until the agent replies on the thread). */
33017
33086
  turn: external_exports.enum(["you", "agent"]).default("you"),
33018
33087
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
33019
- * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
33020
- * client may still flag a stall by age. Drives the inbox error badge + Retry. */
33088
+ * the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
33089
+ * error badge + Retry. */
33021
33090
  error: external_exports.string().optional(),
33091
+ /** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
33092
+ * with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
33093
+ * this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
33094
+ * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
33095
+ cold: external_exports.string().datetime().optional(),
33022
33096
  clarifies: external_exports.string().optional(),
33097
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
33098
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
33099
+ * off the same open list the card came from, so it clears when the Call does. A card is the
33100
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
33101
+ onCall: external_exports.literal(true).optional(),
33023
33102
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
33024
33103
  * declined, and what the ladder will do next — read off the cron's own row, never computed
33025
33104
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -33068,7 +33147,10 @@ var InboxItemSchema2 = external_exports.object({
33068
33147
  goalIds: external_exports.array(external_exports.string()),
33069
33148
  decisionNeedId: external_exports.string().optional(),
33070
33149
  sealed: external_exports.boolean(),
33071
- goalState: external_exports.string().optional()
33150
+ goalState: external_exports.string().optional(),
33151
+ /** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
33152
+ * and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
33153
+ goalTitle: external_exports.string().optional()
33072
33154
  }).optional()
33073
33155
  });
33074
33156
  var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
@@ -33167,7 +33249,6 @@ var UserSettingsSchema2 = external_exports.object({
33167
33249
  });
33168
33250
  var HistoryItemSchema2 = external_exports.object({
33169
33251
  id: external_exports.string(),
33170
- parentId: external_exports.string(),
33171
33252
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
33172
33253
  initiator: external_exports.enum(["user", "agent"]),
33173
33254
  title: external_exports.string(),
@@ -33326,6 +33407,24 @@ var QueueQuestionSchema2 = external_exports.object({
33326
33407
  * gate at all. */
33327
33408
  blocked: external_exports.boolean().default(false)
33328
33409
  });
33410
+ var QueueReplySchema2 = external_exports.object({
33411
+ /** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
33412
+ * id) — so the phone can tell a reply it just sent from one the queue already carries, and the
33413
+ * walk can name it in its zoom. */
33414
+ id: external_exports.string(),
33415
+ /** The Goal the note is on. */
33416
+ goalId: external_exports.string(),
33417
+ /** What the note said. */
33418
+ note: external_exports.string(),
33419
+ /** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
33420
+ deliveryId: external_exports.string(),
33421
+ requestEntryId: external_exports.string(),
33422
+ askedAt: external_exports.string(),
33423
+ /** When the person last replied — the window's start. */
33424
+ repliedAt: external_exports.string(),
33425
+ /** The person's latest words about it; null when there is nothing readable in them. */
33426
+ reply: external_exports.string().nullable()
33427
+ });
33329
33428
  var QueueItemSchema2 = external_exports.object({
33330
33429
  id: external_exports.string(),
33331
33430
  /** One-line headline — the first sentence of the outcome. */
@@ -33353,6 +33452,10 @@ var QueueItemSchema2 = external_exports.object({
33353
33452
  blocked: external_exports.boolean().default(false),
33354
33453
  /** Every decision need on it, open or settled — the page decides which to show. */
33355
33454
  questions: external_exports.array(QueueQuestionSchema2).default([]),
33455
+ /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
33456
+ * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
33457
+ * not spell an empty list. */
33458
+ replies: external_exports.array(QueueReplySchema2).optional(),
33356
33459
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
33357
33460
  repo: external_exports.string().nullable().optional(),
33358
33461
  createdAt: external_exports.string(),
@@ -33721,6 +33824,7 @@ async function ask(state, rail, input) {
33721
33824
  waiting: "hard",
33722
33825
  channel: input.call ? "call" : "notification"
33723
33826
  });
33827
+ if ("recorded" in sent) return null;
33724
33828
  const entryId = sent.requestEntryIds[0];
33725
33829
  if (!entryId) return null;
33726
33830
  (state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
@@ -33899,14 +34003,14 @@ init_dist();
33899
34003
  init_catalog();
33900
34004
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
33901
34005
  import { homedir as homedir6 } from "os";
33902
- import { join as join6 } from "path";
34006
+ import { join as join7 } from "path";
33903
34007
 
33904
34008
  // src/workspaces.ts
33905
34009
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
33906
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
34010
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
33907
34011
  import { homedir as homedir4 } from "os";
33908
34012
  function workspacesFile() {
33909
- return join3(homedir4(), ".paigy", "workspaces.json");
34013
+ return join4(homedir4(), ".paigy", "workspaces.json");
33910
34014
  }
33911
34015
  function listWorkspaces(deps) {
33912
34016
  try {
@@ -33941,7 +34045,7 @@ function resolveWakeDir(pinned, deps) {
33941
34045
  // src/owner.ts
33942
34046
  import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
33943
34047
  import { homedir as homedir5 } from "os";
33944
- import { join as join4 } from "path";
34048
+ import { join as join5 } from "path";
33945
34049
  var HEAD_BYTES = 64e3;
33946
34050
  function readHead(file) {
33947
34051
  const fd = openSync2(file, "r");
@@ -33957,7 +34061,7 @@ function ownerSession(slot, deps = {}) {
33957
34061
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
33958
34062
  if (!m) return null;
33959
34063
  const prefix = m[1];
33960
- const root = deps.projectsDir ?? join4(homedir5(), ".claude", "projects");
34064
+ const root = deps.projectsDir ?? join5(homedir5(), ".claude", "projects");
33961
34065
  const list = deps.list ?? ((dir) => readdirSync(dir));
33962
34066
  const head = deps.head ?? readHead;
33963
34067
  let folders;
@@ -33969,7 +34073,7 @@ function ownerSession(slot, deps = {}) {
33969
34073
  for (const folder of folders) {
33970
34074
  let files;
33971
34075
  try {
33972
- files = list(join4(root, folder));
34076
+ files = list(join5(root, folder));
33973
34077
  } catch {
33974
34078
  continue;
33975
34079
  }
@@ -33977,7 +34081,7 @@ function ownerSession(slot, deps = {}) {
33977
34081
  if (!file) continue;
33978
34082
  let text;
33979
34083
  try {
33980
- text = head(join4(root, folder, file));
34084
+ text = head(join5(root, folder, file));
33981
34085
  } catch {
33982
34086
  continue;
33983
34087
  }
@@ -33997,7 +34101,7 @@ function ownerSession(slot, deps = {}) {
33997
34101
  // src/update.ts
33998
34102
  import { execFile } from "child_process";
33999
34103
  import { createRequire } from "module";
34000
- import { dirname as dirname3, join as join5 } from "path";
34104
+ import { dirname as dirname3, join as join6 } from "path";
34001
34105
  import { promisify } from "util";
34002
34106
  var SERVICE_LABEL = "ai.paigy.harness";
34003
34107
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -34024,7 +34128,7 @@ async function latestVersion() {
34024
34128
  }
34025
34129
  async function install(version) {
34026
34130
  const bin = dirname3(process.execPath);
34027
- await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34131
+ await promisify(execFile)(join6(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34028
34132
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
34029
34133
  timeout: 5 * 6e4
34030
34134
  });
@@ -34075,7 +34179,7 @@ function selfUpdater(deps) {
34075
34179
  }
34076
34180
 
34077
34181
  // src/host.ts
34078
- var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
34182
+ var HOST_FILE = join7(homedir6(), ".paigy", "host.json");
34079
34183
  var runKey = (slot) => `slot:${slot}`;
34080
34184
  var claimedRun = (sessionId2) => {
34081
34185
  const slot = sessionSlot(sessionId2);
@@ -34441,7 +34545,7 @@ async function pairThisMachine() {
34441
34545
  }
34442
34546
  }
34443
34547
  function forwardCodexThreadId() {
34444
- const cfg = join7(process.env.CODEX_HOME || join7(homedir8(), ".codex"), "config.toml");
34548
+ const cfg = join8(process.env.CODEX_HOME || join8(homedir8(), ".codex"), "config.toml");
34445
34549
  if (!existsSync5(cfg)) return;
34446
34550
  const text = readFileSync5(cfg, "utf8");
34447
34551
  const head = "[mcp_servers.paigy]\n";
@@ -34543,7 +34647,7 @@ async function main() {
34543
34647
  const wsDeps = { file: workspacesFile() };
34544
34648
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
34545
34649
  if (!listWorkspaces(wsDeps).length) {
34546
- const guess = join7(homedir8(), "projects");
34650
+ const guess = join8(homedir8(), "projects");
34547
34651
  if (existsSync5(guess)) {
34548
34652
  addWorkspace(guess, wsDeps);
34549
34653
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -34584,7 +34688,7 @@ async function main() {
34584
34688
  }
34585
34689
  }
34586
34690
  try {
34587
- const settings = join7(homedir8(), ".claude", "settings.json");
34691
+ const settings = join8(homedir8(), ".claude", "settings.json");
34588
34692
  if (which("claude") && existsSync5(settings)) {
34589
34693
  const cfg = JSON.parse(readFileSync5(settings, "utf8"));
34590
34694
  if (!cfg["statusLine"]) {
@@ -34642,13 +34746,13 @@ async function main() {
34642
34746
  <key>SoftResourceLimits</key><dict>
34643
34747
  <key>NumberOfFiles</key><integer>65536</integer>
34644
34748
  </dict>
34645
- <key>StandardOutPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
34646
- <key>StandardErrorPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
34749
+ <key>StandardOutPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34750
+ <key>StandardErrorPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34647
34751
  </dict></plist>
34648
34752
  `;
34649
- const dir = join7(homedir8(), "Library", "LaunchAgents");
34753
+ const dir = join8(homedir8(), "Library", "LaunchAgents");
34650
34754
  mkdirSync3(dir, { recursive: true });
34651
- const path = join7(dir, `${SERVICE_LABEL}.plist`);
34755
+ const path = join8(dir, `${SERVICE_LABEL}.plist`);
34652
34756
  writeFileSync4(path, plist);
34653
34757
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
34654
34758
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);