@paigy/harness 0.3.15 → 0.3.17

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 +302 -137
  2. package/dist/main.js +297 -136
  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,18 @@ 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(),
25063
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
25064
+ * its need offers, exactly as its own card carries them, present only when one of them has a
25065
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
25066
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
25067
+ * says those, and the list stays small (an `html` is up to 16 KB). */
25068
+ options: external_exports.array(OptionSchema).optional()
25009
25069
  })).optional(),
25010
25070
  visuals: external_exports.array(VisualSchema).optional(),
25011
25071
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -25023,10 +25083,20 @@ var init_dist = __esm({
25023
25083
  * agent (held in the inbox until the agent replies on the thread). */
25024
25084
  turn: external_exports.enum(["you", "agent"]).default("you"),
25025
25085
  /** 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. */
25086
+ * the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
25087
+ * error badge + Retry. */
25028
25088
  error: external_exports.string().optional(),
25089
+ /** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
25090
+ * with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
25091
+ * this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
25092
+ * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
25093
+ cold: external_exports.string().datetime().optional(),
25029
25094
  clarifies: external_exports.string().optional(),
25095
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
25096
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
25097
+ * off the same open list the card came from, so it clears when the Call does. A card is the
25098
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
25099
+ onCall: external_exports.literal(true).optional(),
25030
25100
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
25031
25101
  * declined, and what the ladder will do next — read off the cron's own row, never computed
25032
25102
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -25075,7 +25145,10 @@ var init_dist = __esm({
25075
25145
  goalIds: external_exports.array(external_exports.string()),
25076
25146
  decisionNeedId: external_exports.string().optional(),
25077
25147
  sealed: external_exports.boolean(),
25078
- goalState: external_exports.string().optional()
25148
+ goalState: external_exports.string().optional(),
25149
+ /** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
25150
+ * and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
25151
+ goalTitle: external_exports.string().optional()
25079
25152
  }).optional()
25080
25153
  });
25081
25154
  APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
@@ -25174,7 +25247,6 @@ var init_dist = __esm({
25174
25247
  });
25175
25248
  HistoryItemSchema = external_exports.object({
25176
25249
  id: external_exports.string(),
25177
- parentId: external_exports.string(),
25178
25250
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
25179
25251
  initiator: external_exports.enum(["user", "agent"]),
25180
25252
  title: external_exports.string(),
@@ -25333,6 +25405,24 @@ var init_dist = __esm({
25333
25405
  * gate at all. */
25334
25406
  blocked: external_exports.boolean().default(false)
25335
25407
  });
25408
+ QueueReplySchema = external_exports.object({
25409
+ /** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
25410
+ * id) — so the phone can tell a reply it just sent from one the queue already carries, and the
25411
+ * walk can name it in its zoom. */
25412
+ id: external_exports.string(),
25413
+ /** The Goal the note is on. */
25414
+ goalId: external_exports.string(),
25415
+ /** What the note said. */
25416
+ note: external_exports.string(),
25417
+ /** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
25418
+ deliveryId: external_exports.string(),
25419
+ requestEntryId: external_exports.string(),
25420
+ askedAt: external_exports.string(),
25421
+ /** When the person last replied — the window's start. */
25422
+ repliedAt: external_exports.string(),
25423
+ /** The person's latest words about it; null when there is nothing readable in them. */
25424
+ reply: external_exports.string().nullable()
25425
+ });
25336
25426
  QueueItemSchema = external_exports.object({
25337
25427
  id: external_exports.string(),
25338
25428
  /** One-line headline — the first sentence of the outcome. */
@@ -25360,6 +25450,10 @@ var init_dist = __esm({
25360
25450
  blocked: external_exports.boolean().default(false),
25361
25451
  /** Every decision need on it, open or settled — the page decides which to show. */
25362
25452
  questions: external_exports.array(QueueQuestionSchema).default([]),
25453
+ /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
25454
+ * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
25455
+ * not spell an empty list. */
25456
+ replies: external_exports.array(QueueReplySchema).optional(),
25363
25457
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
25364
25458
  repo: external_exports.string().nullable().optional(),
25365
25459
  createdAt: external_exports.string(),
@@ -25572,9 +25666,11 @@ var init_dist = __esm({
25572
25666
  tokenOverride = null;
25573
25667
  authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
25574
25668
  AWAIT_WINDOW_MS = 45e3;
25669
+ 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
25670
  HEARTBEAT_MS = 25e3;
25576
25671
  RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
25577
25672
  JOIN = { config: { broadcast: { self: false } } };
25673
+ JOIN_TIMEOUT_MS = 1e4;
25578
25674
  lines = /* @__PURE__ */ new Map();
25579
25675
  }
25580
25676
  });
@@ -30823,7 +30919,7 @@ import { hostname } from "os";
30823
30919
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
30824
30920
  import { execFile as execFile2, execSync } from "child_process";
30825
30921
  import { homedir as homedir8 } from "os";
30826
- import { join as join7 } from "path";
30922
+ import { join as join8 } from "path";
30827
30923
 
30828
30924
  // src/run.ts
30829
30925
  init_dist();
@@ -30836,6 +30932,7 @@ import { resolve, delimiter as delimiter2 } from "path";
30836
30932
 
30837
30933
  // src/harness/acp.ts
30838
30934
  var none = { events: [], writes: [] };
30935
+ var NOTHING_TO_RESUME = /* @__PURE__ */ new Set([-32002, -32601]);
30839
30936
  function optionFor(options, decision) {
30840
30937
  const want = decision.allow ? "allow_once" : "reject_once";
30841
30938
  return options.find((o) => o.kind === want)?.optionId ?? null;
@@ -30932,20 +31029,29 @@ var AcpDriver = class {
30932
31029
  if (msg.id === this.initId) {
30933
31030
  this.initId = void 0;
30934
31031
  if (msg.error) {
30935
- return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
31032
+ return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
30936
31033
  }
30937
31034
  this.sessionNewId = this.nextId++;
30938
31035
  return {
30939
31036
  events: [],
30940
- writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : { id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
31037
+ writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
30941
31038
  };
30942
31039
  }
30943
31040
  if (msg.id === this.sessionNewId) {
30944
31041
  this.sessionNewId = void 0;
31042
+ const resuming = this.resume !== void 0;
31043
+ if (resuming && msg.error?.code !== void 0 && NOTHING_TO_RESUME.has(msg.error.code)) {
31044
+ this.resume = void 0;
31045
+ this.sessionNewId = this.nextId++;
31046
+ return {
31047
+ events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
31048
+ writes: [frame2(this.sessionNew(this.sessionNewId))]
31049
+ };
31050
+ }
30945
31051
  const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
30946
31052
  if (!sessionId2) {
30947
31053
  return {
30948
- events: [{ kind: "error", message: `${this.resume ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}` }],
31054
+ events: [{ kind: "error", message: `${resuming ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}`, fatal: true }],
30949
31055
  writes: []
30950
31056
  };
30951
31057
  }
@@ -31038,6 +31144,9 @@ var AcpDriver = class {
31038
31144
  writes: []
31039
31145
  };
31040
31146
  }
31147
+ sessionNew(id) {
31148
+ return { id, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } };
31149
+ }
31041
31150
  /** Send the queued prompts as one turn, if the agent can take one right now. */
31042
31151
  flush() {
31043
31152
  if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
@@ -31083,7 +31192,7 @@ function startSession(opts) {
31083
31192
  ...process.env,
31084
31193
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
31085
31194
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
31086
- ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
31195
+ ...opts.resume ?? opts.session ? { PAIGY_SESSION_ID: opts.resume ?? opts.session } : {},
31087
31196
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
31088
31197
  // bridge's pump typing answers in, so nothing inside it may start a second delivery path.
31089
31198
  // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
@@ -31093,7 +31202,11 @@ function startSession(opts) {
31093
31202
  },
31094
31203
  stdio: ["pipe", "pipe", "pipe"]
31095
31204
  });
31205
+ let ended2 = false;
31206
+ child.stdin?.on("error", () => {
31207
+ });
31096
31208
  const write = (frames) => {
31209
+ if (ended2) return;
31097
31210
  for (const f of frames) child.stdin?.write(`${f}
31098
31211
  `);
31099
31212
  };
@@ -31102,6 +31215,7 @@ function startSession(opts) {
31102
31215
  if (text) opts.onEvent({ kind: "error", message: text });
31103
31216
  });
31104
31217
  child.on("exit", (code) => {
31218
+ ended2 = true;
31105
31219
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
31106
31220
  opts.onExit?.();
31107
31221
  });
@@ -31130,6 +31244,7 @@ function startSession(opts) {
31130
31244
  },
31131
31245
  stop() {
31132
31246
  write(driver.close());
31247
+ ended2 = true;
31133
31248
  child.kill();
31134
31249
  }
31135
31250
  };
@@ -32486,7 +32601,7 @@ var AskInputSchema2 = external_exports.object({
32486
32601
  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
32602
  repo: external_exports.string().optional().describe("Optional repository context."),
32488
32603
  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."
32604
+ "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
32605
  ),
32491
32606
  options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
32492
32607
  }).strict();
@@ -32497,7 +32612,7 @@ var StartContactSchema2 = external_exports.object({
32497
32612
  }).strict();
32498
32613
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
32499
32614
  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.";
32615
+ 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
32616
  var CreateGoalSchema2 = external_exports.object({
32502
32617
  outcome: external_exports.string().trim().min(1).max(1e4),
32503
32618
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -32544,9 +32659,9 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
32544
32659
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
32545
32660
  var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
32546
32661
  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.";
32662
+ 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.`;
32663
+ 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.";
32664
+ 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
32665
  var CheckRepliesSchema2 = external_exports.object({}).strict();
32551
32666
  var AGENT_TOOLS2 = [
32552
32667
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
@@ -32891,9 +33006,9 @@ var AgendaTurnSchema2 = external_exports.object({
32891
33006
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
32892
33007
  * the planner declares duplicates instead of asking them twice. */
32893
33008
  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([]),
33009
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
33010
+ * (owner, 2026-09-25), and a count here refused whole plans. */
33011
+ info: external_exports.array(external_exports.string().min(1)).default([]),
32897
33012
  question: external_exports.string().min(1).nullable(),
32898
33013
  /** True on the one turn carrying the agent's own declared question. */
32899
33014
  asks: external_exports.boolean().optional(),
@@ -32928,32 +33043,6 @@ var AgendaTurnSchema2 = external_exports.object({
32928
33043
  var CLAIM_STALE_MS2 = 30 * 6e4;
32929
33044
  var InboxItemSchema2 = external_exports.object({
32930
33045
  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
33046
  tokenId: external_exports.string().optional(),
32958
33047
  status: NotifyStatusSchema2,
32959
33048
  context: ContextSchema2,
@@ -32998,7 +33087,18 @@ var InboxItemSchema2 = external_exports.object({
32998
33087
  entryId: external_exports.string(),
32999
33088
  title: external_exports.string(),
33000
33089
  state: external_exports.enum(["open", "answered"]),
33001
- answer: external_exports.string().nullable()
33090
+ answer: external_exports.string().nullable(),
33091
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
33092
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
33093
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
33094
+ * author is not an agent this account holds (unpaired since, or a person). */
33095
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
33096
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
33097
+ * its need offers, exactly as its own card carries them, present only when one of them has a
33098
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
33099
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
33100
+ * says those, and the list stays small (an `html` is up to 16 KB). */
33101
+ options: external_exports.array(OptionSchema2).optional()
33002
33102
  })).optional(),
33003
33103
  visuals: external_exports.array(VisualSchema2).optional(),
33004
33104
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -33016,10 +33116,20 @@ var InboxItemSchema2 = external_exports.object({
33016
33116
  * agent (held in the inbox until the agent replies on the thread). */
33017
33117
  turn: external_exports.enum(["you", "agent"]).default("you"),
33018
33118
  /** 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. */
33119
+ * the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
33120
+ * error badge + Retry. */
33021
33121
  error: external_exports.string().optional(),
33122
+ /** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
33123
+ * with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
33124
+ * this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
33125
+ * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
33126
+ cold: external_exports.string().datetime().optional(),
33022
33127
  clarifies: external_exports.string().optional(),
33128
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
33129
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
33130
+ * off the same open list the card came from, so it clears when the Call does. A card is the
33131
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
33132
+ onCall: external_exports.literal(true).optional(),
33023
33133
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
33024
33134
  * declined, and what the ladder will do next — read off the cron's own row, never computed
33025
33135
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -33068,7 +33178,10 @@ var InboxItemSchema2 = external_exports.object({
33068
33178
  goalIds: external_exports.array(external_exports.string()),
33069
33179
  decisionNeedId: external_exports.string().optional(),
33070
33180
  sealed: external_exports.boolean(),
33071
- goalState: external_exports.string().optional()
33181
+ goalState: external_exports.string().optional(),
33182
+ /** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
33183
+ * and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
33184
+ goalTitle: external_exports.string().optional()
33072
33185
  }).optional()
33073
33186
  });
33074
33187
  var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
@@ -33167,7 +33280,6 @@ var UserSettingsSchema2 = external_exports.object({
33167
33280
  });
33168
33281
  var HistoryItemSchema2 = external_exports.object({
33169
33282
  id: external_exports.string(),
33170
- parentId: external_exports.string(),
33171
33283
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
33172
33284
  initiator: external_exports.enum(["user", "agent"]),
33173
33285
  title: external_exports.string(),
@@ -33326,6 +33438,24 @@ var QueueQuestionSchema2 = external_exports.object({
33326
33438
  * gate at all. */
33327
33439
  blocked: external_exports.boolean().default(false)
33328
33440
  });
33441
+ var QueueReplySchema2 = external_exports.object({
33442
+ /** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
33443
+ * id) — so the phone can tell a reply it just sent from one the queue already carries, and the
33444
+ * walk can name it in its zoom. */
33445
+ id: external_exports.string(),
33446
+ /** The Goal the note is on. */
33447
+ goalId: external_exports.string(),
33448
+ /** What the note said. */
33449
+ note: external_exports.string(),
33450
+ /** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
33451
+ deliveryId: external_exports.string(),
33452
+ requestEntryId: external_exports.string(),
33453
+ askedAt: external_exports.string(),
33454
+ /** When the person last replied — the window's start. */
33455
+ repliedAt: external_exports.string(),
33456
+ /** The person's latest words about it; null when there is nothing readable in them. */
33457
+ reply: external_exports.string().nullable()
33458
+ });
33329
33459
  var QueueItemSchema2 = external_exports.object({
33330
33460
  id: external_exports.string(),
33331
33461
  /** One-line headline — the first sentence of the outcome. */
@@ -33353,6 +33483,10 @@ var QueueItemSchema2 = external_exports.object({
33353
33483
  blocked: external_exports.boolean().default(false),
33354
33484
  /** Every decision need on it, open or settled — the page decides which to show. */
33355
33485
  questions: external_exports.array(QueueQuestionSchema2).default([]),
33486
+ /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
33487
+ * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
33488
+ * not spell an empty list. */
33489
+ replies: external_exports.array(QueueReplySchema2).optional(),
33356
33490
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
33357
33491
  repo: external_exports.string().nullable().optional(),
33358
33492
  createdAt: external_exports.string(),
@@ -33721,6 +33855,7 @@ async function ask(state, rail, input) {
33721
33855
  waiting: "hard",
33722
33856
  channel: input.call ? "call" : "notification"
33723
33857
  });
33858
+ if ("recorded" in sent) return null;
33724
33859
  const entryId = sent.requestEntryIds[0];
33725
33860
  if (!entryId) return null;
33726
33861
  (state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
@@ -33796,6 +33931,23 @@ async function finish(result, state, rail, opts) {
33796
33931
  opts?.log?.(`\u26A0 finish contact failed: ${error.message}`);
33797
33932
  }
33798
33933
  }
33934
+ async function failedToStart(reason, state, rail, opts) {
33935
+ try {
33936
+ const goalId = state.goal?.id ?? (await rail.claim()).goalId;
33937
+ state.goal = void 0;
33938
+ state.resting = true;
33939
+ if (!goalId) return;
33940
+ await rail.contact({
33941
+ asks: [{ ask: `The session could not start, so nothing is working on this yet.
33942
+
33943
+ ${reason}`, parentId: goalId }],
33944
+ waiting: "none",
33945
+ channel: "notification"
33946
+ });
33947
+ } catch (error) {
33948
+ opts?.log?.(`\u26A0 could not report the failed start: ${error.message}`);
33949
+ }
33950
+ }
33799
33951
  async function afterTurn(event, state, rail, opts) {
33800
33952
  if (!event.failed && !endsWithQuestion(event.result)) {
33801
33953
  await finish(event.result, state, rail, opts);
@@ -33820,10 +33972,16 @@ function runHarness(opts) {
33820
33972
  let tail = [];
33821
33973
  const state = { resting: true };
33822
33974
  let session = null;
33823
- const rail = railFor({ token: opts.token, ...opts.resume ? { reach: reachAs(opts.resume) } : {} });
33975
+ const speaksAs = opts.resume ?? opts.session;
33976
+ const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
33824
33977
  async function handle(event) {
33825
33978
  if (event.kind === "error") {
33826
33979
  opts.log(`\u26A0 ${event.message}`);
33980
+ if (event.fatal && running) {
33981
+ opts.log(`\u2716 could not start: ${event.message}`);
33982
+ await failedToStart(event.message, state, rail, { log: opts.log });
33983
+ stop2();
33984
+ }
33827
33985
  return;
33828
33986
  }
33829
33987
  if (event.kind === "permission") {
@@ -33869,6 +34027,7 @@ function runHarness(opts) {
33869
34027
  mode: opts.mode,
33870
34028
  ...opts.token ? { token: opts.token } : {},
33871
34029
  ...opts.resume ? { resume: opts.resume } : {},
34030
+ ...opts.session ? { session: opts.session } : {},
33872
34031
  ...opts.bin ? { bin: opts.bin } : {},
33873
34032
  ...opts.onExit ? { onExit: opts.onExit } : {},
33874
34033
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
@@ -33877,20 +34036,21 @@ function runHarness(opts) {
33877
34036
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
33878
34037
  );
33879
34038
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
34039
+ function stop2() {
34040
+ running = false;
34041
+ tail = [];
34042
+ cancelAsks(state);
34043
+ session?.stop();
34044
+ session = null;
34045
+ opts.log("\u25A0 stopped");
34046
+ }
33880
34047
  return {
33881
34048
  send(text) {
33882
34049
  session?.send(text);
33883
34050
  },
33884
34051
  working: () => running && state.resting !== true,
33885
34052
  tail: () => [...tail],
33886
- stop() {
33887
- running = false;
33888
- tail = [];
33889
- cancelAsks(state);
33890
- session?.stop();
33891
- session = null;
33892
- opts.log("\u25A0 stopped");
33893
- }
34053
+ stop: stop2
33894
34054
  };
33895
34055
  }
33896
34056
 
@@ -33899,14 +34059,14 @@ init_dist();
33899
34059
  init_catalog();
33900
34060
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
33901
34061
  import { homedir as homedir6 } from "os";
33902
- import { join as join6 } from "path";
34062
+ import { join as join7 } from "path";
33903
34063
 
33904
34064
  // src/workspaces.ts
33905
34065
  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";
34066
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
33907
34067
  import { homedir as homedir4 } from "os";
33908
34068
  function workspacesFile() {
33909
- return join3(homedir4(), ".paigy", "workspaces.json");
34069
+ return join4(homedir4(), ".paigy", "workspaces.json");
33910
34070
  }
33911
34071
  function listWorkspaces(deps) {
33912
34072
  try {
@@ -33941,7 +34101,7 @@ function resolveWakeDir(pinned, deps) {
33941
34101
  // src/owner.ts
33942
34102
  import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
33943
34103
  import { homedir as homedir5 } from "os";
33944
- import { join as join4 } from "path";
34104
+ import { join as join5 } from "path";
33945
34105
  var HEAD_BYTES = 64e3;
33946
34106
  function readHead(file) {
33947
34107
  const fd = openSync2(file, "r");
@@ -33957,7 +34117,7 @@ function ownerSession(slot, deps = {}) {
33957
34117
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
33958
34118
  if (!m) return null;
33959
34119
  const prefix = m[1];
33960
- const root = deps.projectsDir ?? join4(homedir5(), ".claude", "projects");
34120
+ const root = deps.projectsDir ?? join5(homedir5(), ".claude", "projects");
33961
34121
  const list = deps.list ?? ((dir) => readdirSync(dir));
33962
34122
  const head = deps.head ?? readHead;
33963
34123
  let folders;
@@ -33969,7 +34129,7 @@ function ownerSession(slot, deps = {}) {
33969
34129
  for (const folder of folders) {
33970
34130
  let files;
33971
34131
  try {
33972
- files = list(join4(root, folder));
34132
+ files = list(join5(root, folder));
33973
34133
  } catch {
33974
34134
  continue;
33975
34135
  }
@@ -33977,7 +34137,7 @@ function ownerSession(slot, deps = {}) {
33977
34137
  if (!file) continue;
33978
34138
  let text;
33979
34139
  try {
33980
- text = head(join4(root, folder, file));
34140
+ text = head(join5(root, folder, file));
33981
34141
  } catch {
33982
34142
  continue;
33983
34143
  }
@@ -33997,7 +34157,7 @@ function ownerSession(slot, deps = {}) {
33997
34157
  // src/update.ts
33998
34158
  import { execFile } from "child_process";
33999
34159
  import { createRequire } from "module";
34000
- import { dirname as dirname3, join as join5 } from "path";
34160
+ import { dirname as dirname3, join as join6 } from "path";
34001
34161
  import { promisify } from "util";
34002
34162
  var SERVICE_LABEL = "ai.paigy.harness";
34003
34163
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -34024,7 +34184,7 @@ async function latestVersion() {
34024
34184
  }
34025
34185
  async function install(version) {
34026
34186
  const bin = dirname3(process.execPath);
34027
- await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34187
+ await promisify(execFile)(join6(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34028
34188
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
34029
34189
  timeout: 5 * 6e4
34030
34190
  });
@@ -34075,12 +34235,13 @@ function selfUpdater(deps) {
34075
34235
  }
34076
34236
 
34077
34237
  // src/host.ts
34078
- var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
34238
+ var HOST_FILE = join7(homedir6(), ".paigy", "host.json");
34079
34239
  var runKey = (slot) => `slot:${slot}`;
34080
34240
  var claimedRun = (sessionId2) => {
34081
34241
  const slot = sessionSlot(sessionId2);
34082
34242
  return { slot, key: runKey(slot) };
34083
34243
  };
34244
+ var launchRun = (spec) => ({ session: spec.sessionId });
34084
34245
  async function slotHasWaitingWork(token, session) {
34085
34246
  return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
34086
34247
  }
@@ -34139,7 +34300,7 @@ function startHost(opts) {
34139
34300
  mode: "bypass",
34140
34301
  prompt: "",
34141
34302
  token: spec.token,
34142
- resume: spec.sessionId,
34303
+ ...launchRun(spec),
34143
34304
  onExit: () => {
34144
34305
  runs.delete(key);
34145
34306
  opts.log(`\u2716 ${label} ended`);
@@ -34441,7 +34602,7 @@ async function pairThisMachine() {
34441
34602
  }
34442
34603
  }
34443
34604
  function forwardCodexThreadId() {
34444
- const cfg = join7(process.env.CODEX_HOME || join7(homedir8(), ".codex"), "config.toml");
34605
+ const cfg = join8(process.env.CODEX_HOME || join8(homedir8(), ".codex"), "config.toml");
34445
34606
  if (!existsSync5(cfg)) return;
34446
34607
  const text = readFileSync5(cfg, "utf8");
34447
34608
  const head = "[mcp_servers.paigy]\n";
@@ -34543,7 +34704,7 @@ async function main() {
34543
34704
  const wsDeps = { file: workspacesFile() };
34544
34705
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
34545
34706
  if (!listWorkspaces(wsDeps).length) {
34546
- const guess = join7(homedir8(), "projects");
34707
+ const guess = join8(homedir8(), "projects");
34547
34708
  if (existsSync5(guess)) {
34548
34709
  addWorkspace(guess, wsDeps);
34549
34710
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -34584,7 +34745,7 @@ async function main() {
34584
34745
  }
34585
34746
  }
34586
34747
  try {
34587
- const settings = join7(homedir8(), ".claude", "settings.json");
34748
+ const settings = join8(homedir8(), ".claude", "settings.json");
34588
34749
  if (which("claude") && existsSync5(settings)) {
34589
34750
  const cfg = JSON.parse(readFileSync5(settings, "utf8"));
34590
34751
  if (!cfg["statusLine"]) {
@@ -34642,13 +34803,13 @@ async function main() {
34642
34803
  <key>SoftResourceLimits</key><dict>
34643
34804
  <key>NumberOfFiles</key><integer>65536</integer>
34644
34805
  </dict>
34645
- <key>StandardOutPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
34646
- <key>StandardErrorPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
34806
+ <key>StandardOutPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34807
+ <key>StandardErrorPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34647
34808
  </dict></plist>
34648
34809
  `;
34649
- const dir = join7(homedir8(), "Library", "LaunchAgents");
34810
+ const dir = join8(homedir8(), "Library", "LaunchAgents");
34650
34811
  mkdirSync3(dir, { recursive: true });
34651
- const path = join7(dir, `${SERVICE_LABEL}.plist`);
34812
+ const path = join8(dir, `${SERVICE_LABEL}.plist`);
34652
34813
  writeFileSync4(path, plist);
34653
34814
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
34654
34815
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
@@ -34695,7 +34856,11 @@ async function main() {
34695
34856
  if (parsed.hatch) {
34696
34857
  overrideToken(readToken("Desktop") || null);
34697
34858
  const minted = await hatch(parsed.hatch, parsed.voice ?? null);
34698
- saveToken({ access_token: minted.token, name: minted.name, device: null }, parsed.slot ?? minted.name);
34859
+ saveToken({ access_token: minted.token, name: minted.name ?? parsed.hatch, device: null }, parsed.slot ?? minted.name ?? parsed.hatch);
34860
+ if (!minted.name) {
34861
+ console.log(`\u2713 hatched, unnamed \u2014 you already have an agent called "${parsed.hatch}". Pick another name in the app's Agents screen.`);
34862
+ return;
34863
+ }
34699
34864
  console.log(`\u2713 hatched "${minted.name}" \u2014 it's on your phone's Agents screen now.`);
34700
34865
  console.log(` speak as it: PAIGY_AGENT="${minted.name}" <any paigy tool>`);
34701
34866
  console.log(` run with it: paigy-harness --identity "${minted.name}" --harness codex "\u2026"`);