@paigy/harness 0.3.5 → 0.3.7

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 +1148 -1570
  2. package/dist/main.js +957 -1352
  3. package/package.json +12 -13
package/dist/cli.js CHANGED
@@ -746,12 +746,12 @@ function isValidCidr(ip, version) {
746
746
  }
747
747
  return false;
748
748
  }
749
- function floatSafeRemainder(val, step) {
749
+ function floatSafeRemainder(val, step2) {
750
750
  const valDecCount = (val.toString().split(".")[1] || "").length;
751
- const stepDecCount = (step.toString().split(".")[1] || "").length;
751
+ const stepDecCount = (step2.toString().split(".")[1] || "").length;
752
752
  const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;
753
753
  const valInt = Number.parseInt(val.toFixed(decCount).replace(".", ""));
754
- const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", ""));
754
+ const stepInt = Number.parseInt(step2.toFixed(decCount).replace(".", ""));
755
755
  return valInt % stepInt / 10 ** decCount;
756
756
  }
757
757
  function deepPartialify(schema) {
@@ -6492,7 +6492,7 @@ var require_connect = __commonJS({
6492
6492
  const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions);
6493
6493
  timeout = timeout == null ? 1e4 : timeout;
6494
6494
  allowH2 = allowH2 != null ? allowH2 : false;
6495
- return function connect({ hostname: hostname2, host, protocol, port, servername, localAddress, httpSocket }, callback) {
6495
+ return function connect2({ hostname: hostname2, host, protocol, port, servername, localAddress, httpSocket }, callback) {
6496
6496
  let socket;
6497
6497
  if (protocol === "https:") {
6498
6498
  if (!tls) {
@@ -11578,7 +11578,7 @@ var require_client = __commonJS({
11578
11578
  strictContentLength,
11579
11579
  maxCachedSessions,
11580
11580
  maxRedirections,
11581
- connect: connect2,
11581
+ connect: connect3,
11582
11582
  maxRequestsPerClient,
11583
11583
  localAddress,
11584
11584
  maxResponseSize,
@@ -11629,7 +11629,7 @@ var require_client = __commonJS({
11629
11629
  if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
11630
11630
  throw new InvalidArgumentError("bodyTimeout must be a positive integer or zero");
11631
11631
  }
11632
- if (connect2 != null && typeof connect2 !== "function" && typeof connect2 !== "object") {
11632
+ if (connect3 != null && typeof connect3 !== "function" && typeof connect3 !== "object") {
11633
11633
  throw new InvalidArgumentError("connect must be a function or an object");
11634
11634
  }
11635
11635
  if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
@@ -11653,15 +11653,15 @@ var require_client = __commonJS({
11653
11653
  if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== "number" || maxConcurrentStreams < 1)) {
11654
11654
  throw new InvalidArgumentError("maxConcurrentStreams must be a positive integer, greater than 0");
11655
11655
  }
11656
- if (typeof connect2 !== "function") {
11657
- connect2 = buildConnector({
11656
+ if (typeof connect3 !== "function") {
11657
+ connect3 = buildConnector({
11658
11658
  ...tls,
11659
11659
  maxCachedSessions,
11660
11660
  allowH2,
11661
11661
  socketPath,
11662
11662
  timeout: connectTimeout,
11663
11663
  ...autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : void 0,
11664
- ...connect2
11664
+ ...connect3
11665
11665
  });
11666
11666
  }
11667
11667
  if (interceptors?.Client && Array.isArray(interceptors.Client)) {
@@ -11676,7 +11676,7 @@ var require_client = __commonJS({
11676
11676
  this[kInterceptors] = [createRedirectInterceptor({ maxRedirections })];
11677
11677
  }
11678
11678
  this[kUrl] = util2.parseOrigin(url);
11679
- this[kConnector] = connect2;
11679
+ this[kConnector] = connect3;
11680
11680
  this[kPipelining] = pipelining != null ? pipelining : 1;
11681
11681
  this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize;
11682
11682
  this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout;
@@ -11730,7 +11730,7 @@ var require_client = __commonJS({
11730
11730
  }
11731
11731
  /* istanbul ignore: only used for test */
11732
11732
  [kConnect](cb) {
11733
- connect(this);
11733
+ connect2(this);
11734
11734
  this.once("connect", cb);
11735
11735
  }
11736
11736
  [kDispatch](opts, handler) {
@@ -11794,7 +11794,7 @@ var require_client = __commonJS({
11794
11794
  assert(client[kSize] === 0);
11795
11795
  }
11796
11796
  }
11797
- async function connect(client) {
11797
+ async function connect2(client) {
11798
11798
  assert(!client[kConnecting]);
11799
11799
  assert(!client[kHTTPContext]);
11800
11800
  let { host, hostname: hostname2, protocol, port } = client[kUrl];
@@ -11965,7 +11965,7 @@ var require_client = __commonJS({
11965
11965
  return;
11966
11966
  }
11967
11967
  if (!client[kHTTPContext]) {
11968
- connect(client);
11968
+ connect2(client);
11969
11969
  return;
11970
11970
  }
11971
11971
  if (client[kHTTPContext].destroyed) {
@@ -12258,7 +12258,7 @@ var require_pool = __commonJS({
12258
12258
  constructor(origin, {
12259
12259
  connections,
12260
12260
  factory = defaultFactory,
12261
- connect,
12261
+ connect: connect2,
12262
12262
  connectTimeout,
12263
12263
  tls,
12264
12264
  maxCachedSessions,
@@ -12274,25 +12274,25 @@ var require_pool = __commonJS({
12274
12274
  if (typeof factory !== "function") {
12275
12275
  throw new InvalidArgumentError("factory must be a function.");
12276
12276
  }
12277
- if (connect != null && typeof connect !== "function" && typeof connect !== "object") {
12277
+ if (connect2 != null && typeof connect2 !== "function" && typeof connect2 !== "object") {
12278
12278
  throw new InvalidArgumentError("connect must be a function or an object");
12279
12279
  }
12280
- if (typeof connect !== "function") {
12281
- connect = buildConnector({
12280
+ if (typeof connect2 !== "function") {
12281
+ connect2 = buildConnector({
12282
12282
  ...tls,
12283
12283
  maxCachedSessions,
12284
12284
  allowH2,
12285
12285
  socketPath,
12286
12286
  timeout: connectTimeout,
12287
12287
  ...autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : void 0,
12288
- ...connect
12288
+ ...connect2
12289
12289
  });
12290
12290
  }
12291
12291
  super(options);
12292
12292
  this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool) ? options.interceptors.Pool : [];
12293
12293
  this[kConnections] = connections || null;
12294
12294
  this[kUrl] = util2.parseOrigin(origin);
12295
- this[kOptions] = { ...util2.deepClone(options), connect, allowH2 };
12295
+ this[kOptions] = { ...util2.deepClone(options), connect: connect2, allowH2 };
12296
12296
  this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : void 0;
12297
12297
  this[kFactory] = factory;
12298
12298
  this.on("connectionError", (origin2, targets, error) => {
@@ -12487,22 +12487,22 @@ var require_agent = __commonJS({
12487
12487
  return opts && opts.connections === 1 ? new Client(origin, opts) : new Pool(origin, opts);
12488
12488
  }
12489
12489
  var Agent = class extends DispatcherBase {
12490
- constructor({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) {
12490
+ constructor({ factory = defaultFactory, maxRedirections = 0, connect: connect2, ...options } = {}) {
12491
12491
  if (typeof factory !== "function") {
12492
12492
  throw new InvalidArgumentError("factory must be a function.");
12493
12493
  }
12494
- if (connect != null && typeof connect !== "function" && typeof connect !== "object") {
12494
+ if (connect2 != null && typeof connect2 !== "function" && typeof connect2 !== "object") {
12495
12495
  throw new InvalidArgumentError("connect must be a function or an object");
12496
12496
  }
12497
12497
  if (!Number.isInteger(maxRedirections) || maxRedirections < 0) {
12498
12498
  throw new InvalidArgumentError("maxRedirections must be a positive number");
12499
12499
  }
12500
12500
  super(options);
12501
- if (connect && typeof connect !== "function") {
12502
- connect = { ...connect };
12501
+ if (connect2 && typeof connect2 !== "function") {
12502
+ connect2 = { ...connect2 };
12503
12503
  }
12504
12504
  this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) ? options.interceptors.Agent : [createRedirectInterceptor({ maxRedirections })];
12505
- this[kOptions] = { ...util2.deepClone(options), connect };
12505
+ this[kOptions] = { ...util2.deepClone(options), connect: connect2 };
12506
12506
  this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : void 0;
12507
12507
  this[kMaxRedirections] = maxRedirections;
12508
12508
  this[kFactory] = factory;
@@ -12597,16 +12597,16 @@ var require_proxy_agent = __commonJS({
12597
12597
  }
12598
12598
  var Http1ProxyWrapper = class extends DispatcherBase {
12599
12599
  #client;
12600
- constructor(proxyUrl, { headers = {}, connect, factory }) {
12600
+ constructor(proxyUrl, { headers = {}, connect: connect2, factory }) {
12601
12601
  super();
12602
12602
  if (!proxyUrl) {
12603
12603
  throw new InvalidArgumentError("Proxy URL is mandatory");
12604
12604
  }
12605
12605
  this[kProxyHeaders] = headers;
12606
12606
  if (factory) {
12607
- this.#client = factory(proxyUrl, { connect });
12607
+ this.#client = factory(proxyUrl, { connect: connect2 });
12608
12608
  } else {
12609
- this.#client = new Client(proxyUrl, { connect });
12609
+ this.#client = new Client(proxyUrl, { connect: connect2 });
12610
12610
  }
12611
12611
  }
12612
12612
  [kDispatch](opts, handler) {
@@ -12668,7 +12668,7 @@ var require_proxy_agent = __commonJS({
12668
12668
  } else if (username && password) {
12669
12669
  this[kProxyHeaders]["proxy-authorization"] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString("base64")}`;
12670
12670
  }
12671
- const connect = buildConnector({ ...opts.proxyTls });
12671
+ const connect2 = buildConnector({ ...opts.proxyTls });
12672
12672
  this[kConnectEndpoint] = buildConnector({ ...opts.requestTls });
12673
12673
  const agentFactory = opts.factory || defaultAgentFactory;
12674
12674
  const factory = (origin2, options) => {
@@ -12676,13 +12676,13 @@ var require_proxy_agent = __commonJS({
12676
12676
  if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") {
12677
12677
  return new Http1ProxyWrapper(this[kProxy].uri, {
12678
12678
  headers: this[kProxyHeaders],
12679
- connect,
12679
+ connect: connect2,
12680
12680
  factory: agentFactory
12681
12681
  });
12682
12682
  }
12683
12683
  return agentFactory(origin2, options);
12684
12684
  };
12685
- this[kClient] = clientFactory(url, { connect });
12685
+ this[kClient] = clientFactory(url, { connect: connect2 });
12686
12686
  this[kAgent] = new Agent({
12687
12687
  ...opts,
12688
12688
  factory,
@@ -14379,10 +14379,10 @@ var require_api_connect = __commonJS({
14379
14379
  }
14380
14380
  }
14381
14381
  };
14382
- function connect(opts, callback) {
14382
+ function connect2(opts, callback) {
14383
14383
  if (callback === void 0) {
14384
14384
  return new Promise((resolve3, reject) => {
14385
- connect.call(this, opts, (err, data) => {
14385
+ connect2.call(this, opts, (err, data) => {
14386
14386
  return err ? reject(err) : resolve3(data);
14387
14387
  });
14388
14388
  });
@@ -14398,7 +14398,7 @@ var require_api_connect = __commonJS({
14398
14398
  queueMicrotask(() => callback(err, { opaque }));
14399
14399
  }
14400
14400
  }
14401
- module.exports = connect;
14401
+ module.exports = connect2;
14402
14402
  }
14403
14403
  });
14404
14404
 
@@ -21050,19 +21050,19 @@ var require_connection = __commonJS({
21050
21050
  ws[kReadyState] = states.CLOSING;
21051
21051
  } else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) {
21052
21052
  ws[kSentClose] = sentCloseFrameState.PROCESSING;
21053
- const frame2 = new WebsocketFrameSend();
21053
+ const frame3 = new WebsocketFrameSend();
21054
21054
  if (code !== void 0 && reason === void 0) {
21055
- frame2.frameData = Buffer.allocUnsafe(2);
21056
- frame2.frameData.writeUInt16BE(code, 0);
21055
+ frame3.frameData = Buffer.allocUnsafe(2);
21056
+ frame3.frameData.writeUInt16BE(code, 0);
21057
21057
  } else if (code !== void 0 && reason !== void 0) {
21058
- frame2.frameData = Buffer.allocUnsafe(2 + reasonByteLength);
21059
- frame2.frameData.writeUInt16BE(code, 0);
21060
- frame2.frameData.write(reason, 2, "utf-8");
21058
+ frame3.frameData = Buffer.allocUnsafe(2 + reasonByteLength);
21059
+ frame3.frameData.writeUInt16BE(code, 0);
21060
+ frame3.frameData.write(reason, 2, "utf-8");
21061
21061
  } else {
21062
- frame2.frameData = emptyBuffer;
21062
+ frame3.frameData = emptyBuffer;
21063
21063
  }
21064
21064
  const socket = ws[kResponse].socket;
21065
- socket.write(frame2.createFrame(opcodes.CLOSE));
21065
+ socket.write(frame3.createFrame(opcodes.CLOSE));
21066
21066
  ws[kSentClose] = sentCloseFrameState.SENT;
21067
21067
  ws[kReadyState] = states.CLOSING;
21068
21068
  } else {
@@ -21545,8 +21545,8 @@ var require_receiver = __commonJS({
21545
21545
  return false;
21546
21546
  } else if (opcode === opcodes.PING) {
21547
21547
  if (!this.ws[kReceivedClose]) {
21548
- const frame2 = new WebsocketFrameSend(body);
21549
- this.ws[kResponse].socket.write(frame2.createFrame(opcodes.PONG));
21548
+ const frame3 = new WebsocketFrameSend(body);
21549
+ this.ws[kResponse].socket.write(frame3.createFrame(opcodes.PONG));
21550
21550
  if (channels.ping.hasSubscribers) {
21551
21551
  channels.ping.publish({
21552
21552
  payload: body
@@ -21596,14 +21596,14 @@ var require_sender = __commonJS({
21596
21596
  }
21597
21597
  add(item, cb, hint) {
21598
21598
  if (hint !== sendHints.blob) {
21599
- const frame2 = createFrame(item, hint);
21599
+ const frame3 = createFrame(item, hint);
21600
21600
  if (!this.#running) {
21601
- this.#socket.write(frame2, cb);
21601
+ this.#socket.write(frame3, cb);
21602
21602
  } else {
21603
21603
  const node2 = {
21604
21604
  promise: null,
21605
21605
  callback: cb,
21606
- frame: frame2
21606
+ frame: frame3
21607
21607
  };
21608
21608
  this.#queue.push(node2);
21609
21609
  }
@@ -21685,7 +21685,7 @@ var require_websocket = __commonJS({
21685
21685
  var { types } = __require("util");
21686
21686
  var { ErrorEvent, CloseEvent } = require_events();
21687
21687
  var { SendQueue } = require_sender();
21688
- var WebSocket = class _WebSocket extends EventTarget {
21688
+ var WebSocket2 = class _WebSocket extends EventTarget {
21689
21689
  #events = {
21690
21690
  open: null,
21691
21691
  error: null,
@@ -21947,11 +21947,11 @@ var require_websocket = __commonJS({
21947
21947
  fireEvent("open", this);
21948
21948
  }
21949
21949
  };
21950
- WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING;
21951
- WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN;
21952
- WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING;
21953
- WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED;
21954
- Object.defineProperties(WebSocket.prototype, {
21950
+ WebSocket2.CONNECTING = WebSocket2.prototype.CONNECTING = states.CONNECTING;
21951
+ WebSocket2.OPEN = WebSocket2.prototype.OPEN = states.OPEN;
21952
+ WebSocket2.CLOSING = WebSocket2.prototype.CLOSING = states.CLOSING;
21953
+ WebSocket2.CLOSED = WebSocket2.prototype.CLOSED = states.CLOSED;
21954
+ Object.defineProperties(WebSocket2.prototype, {
21955
21955
  CONNECTING: staticPropertyDescriptors,
21956
21956
  OPEN: staticPropertyDescriptors,
21957
21957
  CLOSING: staticPropertyDescriptors,
@@ -21975,7 +21975,7 @@ var require_websocket = __commonJS({
21975
21975
  configurable: true
21976
21976
  }
21977
21977
  });
21978
- Object.defineProperties(WebSocket, {
21978
+ Object.defineProperties(WebSocket2, {
21979
21979
  CONNECTING: staticPropertyDescriptors,
21980
21980
  OPEN: staticPropertyDescriptors,
21981
21981
  CLOSING: staticPropertyDescriptors,
@@ -22039,7 +22039,7 @@ var require_websocket = __commonJS({
22039
22039
  closeWebSocketConnection(this, code);
22040
22040
  }
22041
22041
  module.exports = {
22042
- WebSocket
22042
+ WebSocket: WebSocket2
22043
22043
  };
22044
22044
  }
22045
22045
  });
@@ -22743,6 +22743,8 @@ var require_undici = __commonJS({
22743
22743
  // ../../packages/sdk/dist/index.js
22744
22744
  import { createRequire as __sdkCreateRequire } from "module";
22745
22745
  import { createHash, randomUUID } from "crypto";
22746
+ import { randomUUID as randomUUID3 } from "crypto";
22747
+ import { setTimeout as sleep2 } from "timers/promises";
22746
22748
  import { randomUUID as randomUUID2 } from "crypto";
22747
22749
  import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
22748
22750
  import { homedir as homedir2 } from "os";
@@ -23619,6 +23621,13 @@ function draft2020(node) {
23619
23621
  if (Array.isArray(node)) return node.map(draft2020);
23620
23622
  if (node && typeof node === "object") {
23621
23623
  const o = node;
23624
+ if (Array.isArray(o.items)) {
23625
+ o.prefixItems = o.items;
23626
+ if ("additionalItems" in o) {
23627
+ o.items = o.additionalItems;
23628
+ delete o.additionalItems;
23629
+ } else delete o.items;
23630
+ }
23622
23631
  for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
23623
23632
  if (typeof o[excl] === "boolean") {
23624
23633
  if (o[excl] === true && typeof o[lim] === "number") {
@@ -23637,41 +23646,6 @@ function mcpInputSchema(s) {
23637
23646
  delete schema.$schema;
23638
23647
  return draft2020(schema);
23639
23648
  }
23640
- function contactSchemaFrom(fields) {
23641
- const surface = external_exports.object({
23642
- ask: fields.ask.describe(
23643
- `What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). DONE IS SAID ONCE: "all set", "nothing open on my end", "that thread is complete" are the same report in new words, and each one reaches them separately (live 2026-08-12: three of them in three minutes). After the first, you are finished speaking; if they acknowledge it, stop rather than confirming the acknowledgement. Progress is never a contact: set_work_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it.`
23644
- ),
23645
- waiting: fields.waiting.describe(
23646
- "What happens to your work while you wait. 'none': you're just informing them. 'soft': you'd like an answer but can keep working. 'hard': you are STOPPED until they answer \u2014 reaches them urgently and escalates to a real phone call if unanswered."
23647
- ),
23648
- options: fields.options.describe(
23649
- `The choices the user picks from, when you have them \u2014 ${OPTIONS_MIN} to ${OPTIONS_MAX}, drawn from your own sentence.`
23650
- ),
23651
- channel: fields.channel.describe(
23652
- "Relay how the user explicitly said to reach them ('call me' \u2192 'call', 'just message/text me' \u2192 'message'), or use 'call' when promoting the same quiet ask after it becomes a substantial blocker. Omit otherwise; Paigy picks."
23653
- ),
23654
- parentId: fields.parentId.describe(
23655
- "To continue an earlier conversation, pass the parentId a previous contact or reply returned. Omit to start a new one."
23656
- ),
23657
- workId: fields.workId.describe(
23658
- "The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
23659
- ),
23660
- goalId: fields.goalId.describe(
23661
- "The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
23662
- ),
23663
- // THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
23664
- // A contact that rang holds its first window itself; the host caps one tool call at
23665
- // ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
23666
- wait: external_exports.string().uuid().optional().describe(
23667
- "KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
23668
- )
23669
- });
23670
- const out = mcpInputSchema(surface);
23671
- delete out.required;
23672
- out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
23673
- return out;
23674
- }
23675
23649
  function normalizeWaiting(req) {
23676
23650
  if (!req.waiting) return req;
23677
23651
  const { waiting, ...rest } = req;
@@ -23727,220 +23701,6 @@ function deriveAsk(req) {
23727
23701
  ...needs?.length ? { points: needs } : {}
23728
23702
  };
23729
23703
  }
23730
- function utf8ToBytes(s) {
23731
- const out = [];
23732
- for (let i = 0; i < s.length; i++) {
23733
- const c = s.charCodeAt(i);
23734
- if (c < 128) out.push(c);
23735
- else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
23736
- else if (c >= 55296 && c <= 56319) {
23737
- const c2 = s.charCodeAt(i + 1);
23738
- if (c2 >= 56320 && c2 <= 57343) {
23739
- i++;
23740
- const cp = 65536 + ((c & 1023) << 10) + (c2 & 1023);
23741
- out.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
23742
- } else out.push(239, 191, 189);
23743
- } else if (c >= 56320 && c <= 57343) out.push(239, 191, 189);
23744
- else out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
23745
- }
23746
- return Uint8Array.from(out);
23747
- }
23748
- function bytesToUtf8(b) {
23749
- let out = "";
23750
- for (let i = 0; i < b.length; ) {
23751
- const c = b[i++];
23752
- if (c < 128) out += String.fromCharCode(c);
23753
- else if (c < 224) out += String.fromCharCode((c & 31) << 6 | b[i++] & 63);
23754
- else if (c < 240) out += String.fromCharCode((c & 15) << 12 | (b[i++] & 63) << 6 | b[i++] & 63);
23755
- else {
23756
- const cp = (c & 7) << 18 | (b[i++] & 63) << 12 | (b[i++] & 63) << 6 | b[i++] & 63;
23757
- const u = cp - 65536;
23758
- out += String.fromCharCode(55296 + (u >> 10), 56320 + (u & 1023));
23759
- }
23760
- }
23761
- return out;
23762
- }
23763
- function toB64(bytes) {
23764
- let out = "";
23765
- for (let i = 0; i < bytes.length; i += 3) {
23766
- const b0 = bytes[i];
23767
- const has1 = i + 1 < bytes.length;
23768
- const has2 = i + 2 < bytes.length;
23769
- const b1 = has1 ? bytes[i + 1] : 0;
23770
- const b2 = has2 ? bytes[i + 2] : 0;
23771
- out += B64[b0 >> 2] + B64[(b0 & 3) << 4 | b1 >> 4];
23772
- out += has1 ? B64[(b1 & 15) << 2 | b2 >> 6] : "=";
23773
- out += has2 ? B64[b2 & 63] : "=";
23774
- }
23775
- return out;
23776
- }
23777
- function fromB64Lenient(s) {
23778
- const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
23779
- const out = [];
23780
- for (let i = 0; i < clean.length; i += 4) {
23781
- const c0 = B64.indexOf(clean[i]);
23782
- const c1 = B64.indexOf(clean[i + 1] ?? "A");
23783
- const c2 = clean[i + 2] !== void 0 ? B64.indexOf(clean[i + 2]) : -1;
23784
- const c3 = clean[i + 3] !== void 0 ? B64.indexOf(clean[i + 3]) : -1;
23785
- out.push(c0 << 2 | c1 >> 4);
23786
- if (c2 >= 0) out.push((c1 & 15) << 4 | c2 >> 2);
23787
- if (c3 >= 0) out.push((c2 & 3) << 6 | c3);
23788
- }
23789
- return Uint8Array.from(out);
23790
- }
23791
- function fromB64(s) {
23792
- const bytes = fromB64Lenient(s);
23793
- if (toB64(bytes) !== s) throw new Error("non-canonical base64");
23794
- return bytes;
23795
- }
23796
- function concat(parts) {
23797
- let n = 0;
23798
- for (const p of parts) n += p.length;
23799
- const out = new Uint8Array(n);
23800
- let o = 0;
23801
- for (const p of parts) {
23802
- out.set(p, o);
23803
- o += p.length;
23804
- }
23805
- return out;
23806
- }
23807
- function u16be(n) {
23808
- return Uint8Array.of(n >>> 8 & 255, n & 255);
23809
- }
23810
- function u32be(n) {
23811
- return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
23812
- }
23813
- function readU32be(b, o) {
23814
- return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
23815
- }
23816
- function lp(b) {
23817
- return concat([u32be(b.length), b]);
23818
- }
23819
- function eqCt(a, b) {
23820
- if (a.length !== b.length) return false;
23821
- let d = 0;
23822
- for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
23823
- return d === 0;
23824
- }
23825
- function ensureCsprng() {
23826
- if (ok) return;
23827
- let a;
23828
- let b;
23829
- try {
23830
- a = import_tweetnacl.default.randomBytes(32);
23831
- b = import_tweetnacl.default.randomBytes(32);
23832
- } catch {
23833
- throw new Error("paigy-crypto: no secure RNG available \u2014 refusing to generate keys (fail-closed)");
23834
- }
23835
- if (a.every((x) => x === 0) || eqCt(a, b)) {
23836
- throw new Error("paigy-crypto: RNG self-test failed (constant/low-entropy output)");
23837
- }
23838
- ok = true;
23839
- }
23840
- function randomBytes(n) {
23841
- ensureCsprng();
23842
- return import_tweetnacl.default.randomBytes(n);
23843
- }
23844
- function keyId(publicKeyB64) {
23845
- return toB64(import_tweetnacl2.default.hash(fromB64(publicKeyB64)).slice(0, 8));
23846
- }
23847
- function credentialBody(c) {
23848
- return concat([
23849
- lp(utf8ToBytes("paigy-device-credential-v1")),
23850
- lp(utf8ToBytes(c.deviceId)),
23851
- lp(utf8ToBytes(c.kind)),
23852
- lp(fromB64(c.x25519Pub)),
23853
- lp(fromB64(c.ed25519Pub))
23854
- ]);
23855
- }
23856
- function verifyDeviceCredential(cred, uikPublicB64) {
23857
- const { sig, ...fields } = cred;
23858
- try {
23859
- return import_tweetnacl2.default.sign.detached.verify(credentialBody(fields), fromB64(sig), fromB64(uikPublicB64));
23860
- } catch {
23861
- return false;
23862
- }
23863
- }
23864
- function padme(L) {
23865
- if (L < 2) return L;
23866
- const E = Math.floor(Math.log2(L));
23867
- const S = Math.floor(Math.log2(E)) + 1;
23868
- const lastBits = E - S;
23869
- if (lastBits <= 0) return L;
23870
- const mask = (1 << lastBits) - 1;
23871
- return L + mask & ~mask;
23872
- }
23873
- function canonicalHeader(msgId, hdr) {
23874
- const parts = [
23875
- u16be(ENVELOPE_VERSION),
23876
- lp(utf8ToBytes(ENVELOPE_ALG)),
23877
- lp(msgId),
23878
- lp(utf8ToBytes(hdr.field)),
23879
- lp(utf8ToBytes(hdr.kind)),
23880
- lp(utf8ToBytes(hdr.senderRole)),
23881
- u32be(hdr.seq),
23882
- u32be(hdr.recipientKeyIds.length),
23883
- ...hdr.recipientKeyIds.map((k) => lp(utf8ToBytes(k)))
23884
- ];
23885
- return concat(parts);
23886
- }
23887
- function seal(input) {
23888
- ensureCsprng();
23889
- if (input.recipients.length === 0) throw new Error("seal: no recipients");
23890
- const kind = input.kind ?? "";
23891
- const recs = input.recipients.map((pub) => ({ pub, id: keyId(pub) })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
23892
- const hdr = {
23893
- field: input.field,
23894
- kind,
23895
- senderRole: input.senderRole,
23896
- recipientKeyIds: recs.map((r) => r.id),
23897
- seq: input.seq
23898
- };
23899
- const msgIdBytes = input.msgId ? fromB64(input.msgId) : randomBytes(16);
23900
- const cek = randomBytes(32);
23901
- const canon = canonicalHeader(msgIdBytes, hdr);
23902
- const plaintext = concat([u32be(canon.length), canon, u32be(input.body.length), input.body]);
23903
- const padded = new Uint8Array(padme(plaintext.length));
23904
- padded.set(plaintext);
23905
- const nonce = randomBytes(24);
23906
- const ct = import_tweetnacl3.default.secretbox(padded, nonce, cek);
23907
- const recipients = recs.map((r) => {
23908
- const eph = import_tweetnacl3.default.box.keyPair();
23909
- const wnonce = randomBytes(24);
23910
- const wrap = import_tweetnacl3.default.box(cek, wnonce, fromB64(r.pub), eph.secretKey);
23911
- return { keyId: r.id, epk: toB64(eph.publicKey), wnonce: toB64(wnonce), wrap: toB64(wrap) };
23912
- });
23913
- return {
23914
- v: ENVELOPE_VERSION,
23915
- alg: ENVELOPE_ALG,
23916
- msgId: toB64(msgIdBytes),
23917
- hdr,
23918
- recipients,
23919
- nonce: toB64(nonce),
23920
- ct: toB64(ct)
23921
- };
23922
- }
23923
- function open(envelope, myKeyId, mySecretKeyB64) {
23924
- if (envelope.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version ${String(envelope.v)}`);
23925
- if (envelope.alg !== ENVELOPE_ALG) throw new Error(`unsupported alg ${String(envelope.alg)}`);
23926
- const rec = envelope.recipients.find((r) => r.keyId === myKeyId);
23927
- if (!rec) throw new Error("not a recipient of this envelope");
23928
- const cek = import_tweetnacl3.default.box.open(fromB64(rec.wrap), fromB64(rec.wnonce), fromB64(rec.epk), fromB64(mySecretKeyB64));
23929
- if (!cek) throw new Error("CEK unwrap failed (wrong key or tampered)");
23930
- const padded = import_tweetnacl3.default.secretbox.open(fromB64(envelope.ct), fromB64(envelope.nonce), cek);
23931
- if (!padded) throw new Error("body authentication failed (tampered ciphertext)");
23932
- if (padded.length < 8) throw new Error("malformed sealed plaintext");
23933
- const canonLen = readU32be(padded, 0);
23934
- if (canonLen < 0 || 8 + canonLen > padded.length) throw new Error("malformed sealed plaintext");
23935
- const sealedCanon = padded.slice(4, 4 + canonLen);
23936
- const bodyLen = readU32be(padded, 4 + canonLen);
23937
- const bodyStart = 8 + canonLen;
23938
- if (bodyStart + bodyLen > padded.length) throw new Error("malformed sealed plaintext");
23939
- const body = padded.slice(bodyStart, bodyStart + bodyLen);
23940
- const expected = canonicalHeader(fromB64(envelope.msgId), envelope.hdr);
23941
- if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
23942
- return { header: envelope.hdr, body };
23943
- }
23944
23704
  function sessionSlot(sessionId2) {
23945
23705
  const id = sessionId2 ?? sessionId();
23946
23706
  return `session:${id.slice(0, 8)}`;
@@ -24044,126 +23804,33 @@ function readKeyFile() {
24044
23804
  return null;
24045
23805
  }
24046
23806
  }
24047
- async function fetchRoster(token) {
24048
- const res = await reach(`${BACKEND_URL}/api/roster`, { headers: { authorization: `Bearer ${token}` } });
24049
- if (!res.ok) return null;
24050
- return await res.json().catch(() => null);
24051
- }
24052
- function verifiedRecipients(roster, pinnedUikPub) {
24053
- return roster.devices.filter((cred) => verifyDeviceCredential(cred, pinnedUikPub)).map((cred) => cred.x25519Pub);
24054
- }
24055
- function sealFields(content, recipients) {
24056
- if (recipients.length === 0) throw new Error("sealFields: no verified recipients");
24057
- const seq = Math.floor(Date.now() / 1e3);
24058
- const out = {};
24059
- for (const field of ["context", "options", "visuals"]) {
24060
- if (content[field] === void 0) continue;
24061
- out[field] = seal({
24062
- body: utf8ToBytes(JSON.stringify(content[field])),
24063
- field,
24064
- senderRole: "agent",
24065
- seq,
24066
- recipients
24067
- });
24068
- }
24069
- return out;
24070
- }
24071
- function openSealedAnswer(sealed, keypair) {
24072
- const myKeyId = keyId(keypair.x25519.publicKey);
24073
- const { header, body } = open(sealed.envelope, myKeyId, keypair.x25519.secretKey);
24074
- if (header.field !== "answer" || header.senderRole !== "user") {
24075
- throw new Error("sealed answer has the wrong envelope field/role \u2014 refusing (possible replay/misattribution)");
24076
- }
24077
- const parsed = UserAnswerSchema.safeParse(JSON.parse(bytesToUtf8(body)));
24078
- if (!parsed.success) throw new Error("sealed answer body is not a valid UserAnswer");
24079
- const answer = parsed.data;
24080
- const bodyIsIgnored = answer.kind === "ignored";
24081
- if (sealed.ignored !== bodyIsIgnored) {
24082
- throw new Error("sealed answer tamper: `ignored` hint disagrees with the decrypted body");
24083
- }
24084
- return answer;
24085
- }
24086
23807
  function ensureAuthed(res) {
24087
23808
  if (res.status === 401) throw new UnpairedError();
24088
23809
  return res;
24089
23810
  }
24090
- async function sealForE2ee(req, token, deps = {}) {
24091
- const keyFile = (deps.readKeyFile ?? readKeyFile)();
24092
- if (!keyFile?.e2ee) return req;
24093
- req = deriveAsk(req);
24094
- if (!keyFile.uikPub) {
24095
- throw new Error("E2EE pairing is missing its pinned identity key (key.json.uikPub) \u2014 re-pair; refusing to send plaintext.");
24096
- }
24097
- const roster = await (deps.fetchRoster ?? fetchRoster)(token);
24098
- if (!roster) {
24099
- throw new Error("E2EE pairing has no device roster to seal to \u2014 refusing to send plaintext. Approve a device on your phone, then retry.");
24100
- }
24101
- const recipients = verifiedRecipients(roster, keyFile.uikPub);
24102
- if (recipients.length === 0) {
24103
- throw new Error("No roster device verified against the pinned identity key \u2014 refusing to send plaintext (a server-injected device is dropped).");
24104
- }
24105
- const sealedContext = {
24106
- ...req.context,
24107
- ...req.repo !== void 0 ? { repo: req.repo } : {},
24108
- ...req.branch !== void 0 ? { branch: req.branch } : {}
24109
- };
24110
- const envelope = sealFields({ context: sealedContext, options: req.options, visuals: req.visuals }, recipients);
24111
- const { context: _c, options: _o, visuals: _v, repo: _r, branch: _b, ...meta } = req;
24112
- return { ...meta, envelope };
23811
+ async function fail(what, res) {
23812
+ throw new ApiError(what, res.status, await res.text());
24113
23813
  }
24114
- async function submitNotification(req, opts = {}) {
23814
+ async function claimGoal(goalId, opts = {}) {
24115
23815
  const token = authToken(opts.token) ?? "";
24116
- if (req.goalId) {
24117
- if (req.envelope) throw new Error("Goal-scoped contact does not support E2EE envelopes yet");
24118
- const shaped = deriveAsk(req);
24119
- if (!shaped.context || !shaped.select) throw new Error("Goal-scoped contact requires a shaped request or ask");
24120
- if (shaped.channel === "call" || shaped.urgency === "call") {
24121
- throw new Error("Goal-scoped call contact is not available yet; use a message contact");
24122
- }
24123
- if (shaped.urgency && shaped.urgency !== "inbox") throw new Error("Goal-scoped contact currently supports inbox delivery only");
24124
- if (shaped.points?.length) throw new Error("Goal-scoped multi-part contact is not available yet; send one decision at a time");
24125
- const content = [shaped.context.title, ...shaped.context.description].filter(Boolean).join("\n\n");
24126
- const res2 = ensureAuthed(await reach(`${BACKEND_URL}/api/goals/${encodeURIComponent(req.goalId)}/contact`, {
24127
- method: "POST",
24128
- headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
24129
- body: JSON.stringify({
24130
- operationId: randomUUID2(),
24131
- threadId: shaped.parentId ?? null,
24132
- content,
24133
- select: shaped.select,
24134
- options: shaped.options ?? [],
24135
- blocking: shaped.blocking,
24136
- channel: "message"
24137
- })
24138
- }));
24139
- if (!res2.ok) throw new Error(`goal contact failed: ${res2.status} ${await res2.text()}`);
24140
- return await res2.json();
24141
- }
24142
- const body = await sealForE2ee(req, token);
24143
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
24144
- method: "POST",
24145
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
24146
- body: JSON.stringify(body)
24147
- }));
24148
- if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
23816
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
23817
+ if (!res.ok) await fail("claim_goal", res);
24149
23818
  return await res.json();
24150
23819
  }
24151
- async function sweepLeased(opts = {}) {
24152
- const token = authToken(opts.token);
24153
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
24154
- headers: {
24155
- authorization: `Bearer ${token}`,
24156
- "x-paigy-await-ack": "1"
24157
- }
23820
+ async function getGoal(goalId, opts = {}) {
23821
+ const token = authToken(opts.token) ?? "";
23822
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
23823
+ method: "GET",
23824
+ headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
24158
23825
  }));
24159
- if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
23826
+ if (!res.ok) await fail("get_goal", res);
24160
23827
  return await res.json();
24161
23828
  }
24162
- async function pendingSummary(opts = {}) {
24163
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending/summary`, {
24164
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
24165
- }));
24166
- if (!res.ok) throw new Error(`pending_summary failed: ${res.status} ${await res.text()}`);
23829
+ async function updateGoal(goalId, input, opts = {}) {
23830
+ const token = authToken(opts.token) ?? "";
23831
+ const operationId = input.operationId ?? randomUUID2();
23832
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
23833
+ if (!res.ok) await fail("update_goal", res);
24167
23834
  return await res.json();
24168
23835
  }
24169
23836
  function overrideToken(secret) {
@@ -24208,6 +23875,15 @@ async function heartbeat(runtime, opts = {}) {
24208
23875
  }));
24209
23876
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
24210
23877
  }
23878
+ async function registerDelivery(mode, opts = {}) {
23879
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
23880
+ method: "POST",
23881
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23882
+ body: JSON.stringify({ mode })
23883
+ }));
23884
+ if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
23885
+ return await res.json();
23886
+ }
24211
23887
  async function listNotes(opts = {}) {
24212
23888
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
24213
23889
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -24240,138 +23916,153 @@ async function acceptTriage(proposalId, body, opts = {}) {
24240
23916
  if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
24241
23917
  return await res.json();
24242
23918
  }
24243
- function decryptAnswer(reply, deps = {}) {
24244
- if (!reply.sealed) return reply.answer;
24245
- const keyFile = (deps.readKeyFile ?? readKeyFile)();
24246
- if (!keyFile?.keypair) {
24247
- throw new Error(
24248
- "received a sealed answer but this agent has no device key to open it \u2014 re-pair."
24249
- );
23919
+ async function contact(input, opts = {}) {
23920
+ const parsed = ContactSchema.parse(input);
23921
+ opts.signal?.throwIfAborted();
23922
+ const send2 = opts.reach ?? reach;
23923
+ const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
23924
+ let deliveryId;
23925
+ if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
23926
+ else {
23927
+ if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
23928
+ const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
23929
+ const res = ensureAuthed(await send2(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
23930
+ method: "POST",
23931
+ headers,
23932
+ signal: opts.signal,
23933
+ body: JSON.stringify({ operationId: opts.operationId ?? randomUUID3(), threadId: parsed.threadId ?? null, content: parsed.ask, select: shaped.select, options: shaped.options ?? [], blocking: parsed.waiting === "hard", channel: parsed.channel === "call" ? "call" : "message" })
23934
+ }));
23935
+ if (!res.ok) await fail("goal contact", res);
23936
+ const receipt = await res.json();
23937
+ deliveryId = receipt.deliveryId ?? receipt.callId ?? receipt.notificationId ?? "";
23938
+ if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
23939
+ }
23940
+ const read = async (signal2) => {
23941
+ const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23942
+ if (!res.ok) await fail("read_delivery", res);
23943
+ const delivery = await res.json();
23944
+ return delivery.kind === "notification" ? { ...delivery, message: `${delivery.message}
23945
+ Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answers. Do not poll this Notification.` } : delivery;
23946
+ };
23947
+ const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
23948
+ if (opts.waits === false) return read(opts.signal);
23949
+ const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
23950
+ const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
23951
+ let latest;
23952
+ try {
23953
+ while (true) {
23954
+ signal.throwIfAborted();
23955
+ latest = await read(signal);
23956
+ if (settled(latest)) return latest;
23957
+ await sleep2(5e3, void 0, { signal });
23958
+ }
23959
+ } catch (error) {
23960
+ opts.signal?.throwIfAborted();
23961
+ if (window.aborted && latest) return latest;
23962
+ throw error;
24250
23963
  }
24251
- return openSealedAnswer(reply.sealed, keyFile.keypair);
24252
- }
24253
- function decodeReply(item, deps = {}) {
24254
- const { delivery: _delivery, ...withoutDelivery } = item;
24255
- const enriched = { ...withoutDelivery };
24256
- if (!enriched.sealed) return enriched;
24257
- const answer = decryptAnswer(enriched, deps);
24258
- const { sealed: _sealed, ...plaintext } = enriched;
24259
- return { ...plaintext, answer };
24260
23964
  }
24261
- function deliveryOf(item) {
24262
- if (item.type !== "reply" || !("delivery" in item) || item.delivery === void 0) {
24263
- return null;
24264
- }
24265
- const delivery = item.delivery;
24266
- if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
24267
- throw new Error(
24268
- "Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
24269
- );
24270
- }
24271
- return delivery;
23965
+ async function checkReplies(opts = {}) {
23966
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
23967
+ headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
23968
+ }));
23969
+ if (!res.ok) await fail("check_replies", res);
23970
+ return await res.json();
24272
23971
  }
24273
- async function responseDetail(res) {
23972
+ function frame(line, topic, event, payload) {
24274
23973
  try {
24275
- return (await res.text()).slice(0, 300);
23974
+ line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
24276
23975
  } catch {
24277
- return "";
24278
23976
  }
24279
23977
  }
24280
- async function acknowledge(token, leaseId, opts = {}) {
24281
- const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
24282
- const backoffMs = Math.max(0, opts.backoffMs ?? 100);
24283
- const doSleep = opts.doSleep ?? sleep3;
24284
- let lastError;
24285
- for (let attempt = 0; attempt < attempts; attempt += 1) {
23978
+ function stop(line) {
23979
+ line.open = false;
23980
+ clearInterval(line.heart);
23981
+ clearTimeout(line.retry);
23982
+ }
23983
+ function connect(line) {
23984
+ const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
23985
+ const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
23986
+ ws.addEventListener("open", () => {
23987
+ line.open = true;
23988
+ line.attempt = 0;
23989
+ for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
23990
+ line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
23991
+ });
23992
+ ws.addEventListener("message", (e) => {
23993
+ let m;
24286
23994
  try {
24287
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await/ack`, {
24288
- method: "POST",
24289
- headers: {
24290
- authorization: `Bearer ${token}`,
24291
- "content-type": "application/json"
24292
- },
24293
- body: JSON.stringify({ leaseId })
24294
- }));
24295
- if (res.ok) return;
24296
- const detail2 = await responseDetail(res);
24297
- if (res.status === 409) throw new ReplyLeaseExpiredError();
24298
- const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
24299
- throw new ReplyAckError(
24300
- `Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
24301
- retryable
24302
- );
24303
- } catch (error) {
24304
- if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
24305
- throw error;
24306
- }
24307
- lastError = error;
24308
- if (attempt + 1 < attempts) {
24309
- await doSleep(backoffMs * 2 ** attempt);
24310
- }
23995
+ m = JSON.parse(String(e.data));
23996
+ } catch {
23997
+ return;
23998
+ }
23999
+ if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
24000
+ for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
24001
+ if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
24311
24002
  }
24312
- }
24313
- const detail = lastError instanceof Error ? lastError.message : String(lastError);
24314
- throw new Error(
24315
- `Could not acknowledge the Paigy reply after ${attempts} attempts (${detail}). The answer was not returned to the agent and remains recoverable after the lease expires.`
24316
- );
24317
- }
24318
- async function receiveReply(item, token, opts = {}) {
24319
- const decoded = decodeReply(item, opts);
24320
- const delivery = deliveryOf(item);
24321
- if (!delivery) {
24322
- if (opts.requireDelivery) {
24323
- throw new Error("Paigy returned a catch-up reply without a delivery lease; the answer was not exposed.");
24324
- }
24325
- return decoded;
24326
- }
24327
- await acknowledge(token, delivery.leaseId, {
24328
- attempts: opts.ackAttempts,
24329
- backoffMs: opts.ackBackoffMs,
24330
- doSleep: opts.ackSleep
24331
24003
  });
24332
- return decoded;
24004
+ let gone = false;
24005
+ const again = () => {
24006
+ if (gone) return;
24007
+ gone = true;
24008
+ stop(line);
24009
+ if (!line.topics.size) return;
24010
+ line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
24011
+ };
24012
+ ws.addEventListener("close", again);
24013
+ ws.addEventListener("error", again);
24333
24014
  }
24334
- async function checkReplies(opts = {}) {
24335
- const pending = await sweepLeased(opts);
24336
- const token = authToken(opts.token) ?? "";
24337
- const replies = await Promise.all(pending.replies.map(async (reply) => {
24338
- const received = await receiveReply(
24339
- { type: "reply", ...reply },
24340
- token,
24341
- { requireDelivery: true }
24342
- );
24343
- const { type: _type, ...withoutType } = received;
24344
- return withoutType;
24345
- }));
24015
+ function subscribeRealtime(args) {
24016
+ const key = `${args.url} ${args.anonKey}`;
24017
+ let line = lines.get(key);
24018
+ if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
24019
+ let heard = line.topics.get(args.topic);
24020
+ if (!heard) {
24021
+ line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
24022
+ if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
24023
+ }
24024
+ const me = { event: args.event, onMessage: args.onMessage };
24025
+ heard.add(me);
24026
+ if (!line.ws) connect(line);
24346
24027
  return {
24347
- ...pending,
24348
- replies,
24349
- work: pending.work ?? []
24028
+ close: () => {
24029
+ if (!heard.delete(me) || heard.size) return;
24030
+ line.topics.delete(args.topic);
24031
+ if (line.topics.size) {
24032
+ if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
24033
+ return;
24034
+ }
24035
+ lines.delete(key);
24036
+ stop(line);
24037
+ try {
24038
+ line.ws?.close();
24039
+ } catch {
24040
+ }
24041
+ }
24350
24042
  };
24351
24043
  }
24352
- async function patchState(path, state, opts, operation) {
24353
- const token = authToken(opts.token) ?? "";
24354
- const res = ensureAuthed(await reach(
24355
- `${BACKEND_URL}${path}`,
24356
- {
24357
- method: "PATCH",
24358
- headers: {
24359
- "content-type": "application/json",
24360
- authorization: `Bearer ${token}`,
24361
- "x-paigy-await-ack": "1"
24362
- },
24363
- body: JSON.stringify({ state })
24044
+ async function subscribeWake(onNudge, opts = {}) {
24045
+ const cfg = await registerDelivery("self_hosted", opts);
24046
+ const { url, anonKey } = cfg.realtime;
24047
+ const channel = wakeChannel(cfg.tokenId);
24048
+ const ch = subscribeRealtime({
24049
+ url,
24050
+ anonKey,
24051
+ topic: channel,
24052
+ event: WAKE_EVENT,
24053
+ onMessage: (payload) => onNudge(payload ?? {})
24054
+ });
24055
+ return {
24056
+ tokenId: cfg.tokenId,
24057
+ channel,
24058
+ close: async () => {
24059
+ ch.close();
24060
+ await registerDelivery("poll", opts).catch(() => {
24061
+ });
24364
24062
  }
24365
- ));
24366
- if (!res.ok) {
24367
- throw new Error(`${operation} failed: ${res.status} ${await res.text()}`);
24368
- }
24369
- return await res.json();
24370
- }
24371
- function setTaskState(notificationId, state, opts = {}) {
24372
- return patchState(`/api/notify/${encodeURIComponent(notificationId)}/state`, state, opts, "set_task_state");
24063
+ };
24373
24064
  }
24374
- var require2, __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __require2, __commonJS2, __copyProps2, __toESM2, require_nacl_fast, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, SESSION_ID, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, MISSED_CALL_PLAN, CreateGoalSchema, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, fmtMin, STANDARD_MEANS, CONTACT_DESCRIPTION, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, SetTaskStateSchema, SetWorkStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, CallbackTriggerSchema, ScheduleCallbackSchema, PendingRepliesSchema, NotifyResponseSchema, NotifyPlanUnitSchema, NotifyPlanSchema, UserResponseSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, SnoozeRequestSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, CreateRequestSchema, HandoffSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, CreateNoteSchema, RecordDecisionSchema, AssignNoteSchema, TriageVerdictSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, StatusSchema, EnvelopeRecipientSchema, EnvelopeHeaderSchema, EnvelopeSchema, SealedAnswerSchema, DeviceCredentialSchema, DeviceRosterSchema, WakeNudgeSchema, PairingStatusSchema, PairingRevealSchema, DeviceCodeRequestSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenRequestSchema, DeviceTokenSchema, DeviceCommitRequestSchema, DevicePeerCommitSchema, SupportRequestSchema, NotificationFeedbackKindSchema, NotificationFeedbackSchema, FeedbackResolutionSchema, FeedbackOutcomeSchema, CONTACT_SCHEMA, import_tweetnacl, import_tweetnacl2, import_tweetnacl3, import_tweetnacl4, import_tweetnacl5, B64, ok, ENVELOPE_VERSION, ENVELOPE_ALG, AGENT_NAME, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, tokenOverride, authToken, sleep3, ReplyLeaseExpiredError, ReplyAckError;
24065
+ var import_undici, require2, __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __require2, __commonJS2, __copyProps2, __toESM2, require_nacl_fast, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, SESSION_ID, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, 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, GetThreadSchema, GET_THREAD_DESCRIPTION, SearchThreadsSchema, SEARCH_THREADS_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, CallbackTriggerSchema, NotifyResponseSchema, NotifyPlanUnitSchema, NotifyPlanSchema, UserResponseSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, SnoozeRequestSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, CreateRequestSchema, QueueQuestionSchema, QueueItemSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, CreateNoteSchema, RecordDecisionSchema, AssignNoteSchema, TriageVerdictSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, StatusSchema, EnvelopeRecipientSchema, EnvelopeHeaderSchema, EnvelopeSchema, SealedAnswerSchema, DeviceCredentialSchema, DeviceRosterSchema, WakeNudgeSchema, PairingStatusSchema, PairingRevealSchema, DeviceCodeRequestSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenRequestSchema, DeviceTokenSchema, DeviceCommitRequestSchema, DevicePeerCommitSchema, SupportRequestSchema, NotificationFeedbackKindSchema, NotificationFeedbackSchema, FeedbackResolutionSchema, FeedbackOutcomeSchema, import_tweetnacl, import_tweetnacl2, import_tweetnacl3, import_tweetnacl4, import_tweetnacl5, AGENT_NAME, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, AWAIT_WINDOW_MS, tokenOverride, authToken, HEARTBEAT_MS, RETRY_MS, JOIN, lines;
24375
24066
  var init_dist = __esm({
24376
24067
  "../../packages/sdk/dist/index.js"() {
24377
24068
  "use strict";
@@ -24380,6 +24071,8 @@ var init_dist = __esm({
24380
24071
  init_v3();
24381
24072
  init_v3();
24382
24073
  init_v3();
24074
+ init_zod();
24075
+ import_undici = __toESM(require_undici(), 1);
24383
24076
  require2 = __sdkCreateRequire(import.meta.url);
24384
24077
  __create2 = Object.create;
24385
24078
  __defProp2 = Object.defineProperty;
@@ -24710,17 +24403,17 @@ var init_dist = __esm({
24710
24403
  }
24711
24404
  var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]);
24712
24405
  function crypto_stream_salsa20_xor(c, cpos, m, mpos, b, n, k) {
24713
- var z3 = new Uint8Array(16), x = new Uint8Array(64);
24406
+ var z4 = new Uint8Array(16), x = new Uint8Array(64);
24714
24407
  var u, i;
24715
- for (i = 0; i < 16; i++) z3[i] = 0;
24716
- for (i = 0; i < 8; i++) z3[i] = n[i];
24408
+ for (i = 0; i < 16; i++) z4[i] = 0;
24409
+ for (i = 0; i < 8; i++) z4[i] = n[i];
24717
24410
  while (b >= 64) {
24718
- crypto_core_salsa20(x, z3, k, sigma);
24411
+ crypto_core_salsa20(x, z4, k, sigma);
24719
24412
  for (i = 0; i < 64; i++) c[cpos + i] = m[mpos + i] ^ x[i];
24720
24413
  u = 1;
24721
24414
  for (i = 8; i < 16; i++) {
24722
- u = u + (z3[i] & 255) | 0;
24723
- z3[i] = u & 255;
24415
+ u = u + (z4[i] & 255) | 0;
24416
+ z4[i] = u & 255;
24724
24417
  u >>>= 8;
24725
24418
  }
24726
24419
  b -= 64;
@@ -24728,30 +24421,30 @@ var init_dist = __esm({
24728
24421
  mpos += 64;
24729
24422
  }
24730
24423
  if (b > 0) {
24731
- crypto_core_salsa20(x, z3, k, sigma);
24424
+ crypto_core_salsa20(x, z4, k, sigma);
24732
24425
  for (i = 0; i < b; i++) c[cpos + i] = m[mpos + i] ^ x[i];
24733
24426
  }
24734
24427
  return 0;
24735
24428
  }
24736
24429
  function crypto_stream_salsa20(c, cpos, b, n, k) {
24737
- var z3 = new Uint8Array(16), x = new Uint8Array(64);
24430
+ var z4 = new Uint8Array(16), x = new Uint8Array(64);
24738
24431
  var u, i;
24739
- for (i = 0; i < 16; i++) z3[i] = 0;
24740
- for (i = 0; i < 8; i++) z3[i] = n[i];
24432
+ for (i = 0; i < 16; i++) z4[i] = 0;
24433
+ for (i = 0; i < 8; i++) z4[i] = n[i];
24741
24434
  while (b >= 64) {
24742
- crypto_core_salsa20(x, z3, k, sigma);
24435
+ crypto_core_salsa20(x, z4, k, sigma);
24743
24436
  for (i = 0; i < 64; i++) c[cpos + i] = x[i];
24744
24437
  u = 1;
24745
24438
  for (i = 8; i < 16; i++) {
24746
- u = u + (z3[i] & 255) | 0;
24747
- z3[i] = u & 255;
24439
+ u = u + (z4[i] & 255) | 0;
24440
+ z4[i] = u & 255;
24748
24441
  u >>>= 8;
24749
24442
  }
24750
24443
  b -= 64;
24751
24444
  cpos += 64;
24752
24445
  }
24753
24446
  if (b > 0) {
24754
- crypto_core_salsa20(x, z3, k, sigma);
24447
+ crypto_core_salsa20(x, z4, k, sigma);
24755
24448
  for (i = 0; i < b; i++) c[cpos + i] = x[i];
24756
24449
  }
24757
24450
  return 0;
@@ -25631,12 +25324,12 @@ var init_dist = __esm({
25631
25324
  for (a = 0; a < 16; a++) o[a] = c[a];
25632
25325
  }
25633
25326
  function crypto_scalarmult(q, n, p) {
25634
- var z3 = new Uint8Array(32);
25327
+ var z4 = new Uint8Array(32);
25635
25328
  var x = new Float64Array(80), r, i;
25636
25329
  var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf();
25637
- for (i = 0; i < 31; i++) z3[i] = n[i];
25638
- z3[31] = n[31] & 127 | 64;
25639
- z3[0] &= 248;
25330
+ for (i = 0; i < 31; i++) z4[i] = n[i];
25331
+ z4[31] = n[31] & 127 | 64;
25332
+ z4[0] &= 248;
25640
25333
  unpack25519(x, p);
25641
25334
  for (i = 0; i < 16; i++) {
25642
25335
  b[i] = x[i];
@@ -25644,7 +25337,7 @@ var init_dist = __esm({
25644
25337
  }
25645
25338
  a[0] = d[0] = 1;
25646
25339
  for (i = 254; i >= 0; --i) {
25647
- r = z3[i >>> 3] >>> (i & 7) & 1;
25340
+ r = z4[i >>> 3] >>> (i & 7) & 1;
25648
25341
  sel25519(a, b, r);
25649
25342
  sel25519(c, d, r);
25650
25343
  A(e, a, c);
@@ -27014,21 +26707,54 @@ var init_dist = __esm({
27014
26707
  };
27015
26708
  OPTIONS_MIN = 2;
27016
26709
  OPTIONS_MAX = 6;
27017
- MISSED_CALL_PLAN = {
27018
- retry_10m: { kind: "every", minutes: 10 },
27019
- retry_30m: { kind: "every", minutes: 30 },
27020
- retry_60m: { kind: "every", minutes: 60 },
27021
- backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
27022
- backoff_standard: { kind: "at", minutes: [10, 30, 120] },
27023
- backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
27024
- inbox: { kind: "once" },
27025
- dismiss: { kind: "grace", minutes: 2 }
27026
- };
26710
+ StartContactSchema = external_exports.object({
26711
+ goalIds: external_exports.tuple([external_exports.string().uuid()]),
26712
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
26713
+ //
26714
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
26715
+ // findings and a decision into one, and the person answers whichever part engaged them —
26716
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
26717
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
26718
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
26719
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
26720
+ // asks carrying a decision, 27 are over 600 characters.
26721
+ //
26722
+ // Sending them separately costs nothing, because a CALL contact joins the call already
26723
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
26724
+ // one call, planned as its turns, and each keeps its own settleable need.
26725
+ ask: external_exports.string().trim().min(1).max(1e4).describe(
26726
+ "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."
26727
+ ),
26728
+ waiting: external_exports.enum(["none", "hard"]).default("none"),
26729
+ channel: external_exports.enum(["notification", "call"]).default("notification"),
26730
+ options: external_exports.array(external_exports.object({ label: external_exports.string().trim().min(1).max(1e3), image: external_exports.string().url().optional(), html: external_exports.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
26731
+ threadId: external_exports.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
26732
+ }).strict();
26733
+ ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
26734
+ CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
26735
+ CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
27027
26736
  CreateGoalSchema = external_exports.object({
27028
26737
  outcome: external_exports.string().trim().min(1).max(1e4),
27029
26738
  ownerParticipant: external_exports.string().trim().min(1).optional(),
27030
- idempotencyKey: external_exports.string().trim().min(1).max(200)
26739
+ idempotencyKey: external_exports.string().trim().min(1).max(200),
26740
+ /** A past conversation this Goal should be read against — History's "new session from this"
26741
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
26742
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
26743
+ * the writer refuses a thread belonging to another account. */
26744
+ contextThreadId: external_exports.string().uuid().optional(),
26745
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
26746
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
26747
+ * worked"). It could not have been: this door took no parent, so the only route was
26748
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
26749
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
26750
+ * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
26751
+ * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
26752
+ parentGoalId: external_exports.string().uuid().optional()
27031
26753
  });
26754
+ CreateGoalToolSchema = CreateGoalSchema.extend({
26755
+ idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
26756
+ }).strict();
26757
+ CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
27032
26758
  UpdateGoalSchema = external_exports.object({
27033
26759
  revision: external_exports.number().int().positive(),
27034
26760
  changes: external_exports.object({
@@ -27044,17 +26770,33 @@ var init_dist = __esm({
27044
26770
  reason: external_exports.string().trim().min(1).max(2e3),
27045
26771
  operationId: external_exports.string().uuid().optional()
27046
26772
  }).strict();
27047
- UpdateGoalToolSchema = UpdateGoalSchema.extend({ goalId: external_exports.string().uuid() }).strict();
26773
+ UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
27048
26774
  ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
27049
- fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
27050
- STANDARD_MEANS = (() => {
27051
- const plan = MISSED_CALL_PLAN.backoff_standard;
27052
- const mins = plan.kind === "at" ? plan.minutes : [];
27053
- const parts = mins.map(fmtMin);
27054
- const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
27055
- return `Rings again ${list} after the missed call, then leaves it in your inbox`;
27056
- })();
27057
- CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
26775
+ GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
26776
+ GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
26777
+ UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
26778
+ CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
26779
+ 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 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. 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 with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
26780
+ CheckRepliesSchema = external_exports.object({}).strict();
26781
+ GetThreadSchema = external_exports.object({
26782
+ parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
26783
+ }).strict();
26784
+ GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
26785
+ SearchThreadsSchema = external_exports.object({
26786
+ q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
26787
+ }).strict();
26788
+ SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
26789
+ AGENT_TOOLS = [
26790
+ { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
26791
+ { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
26792
+ { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
26793
+ { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
26794
+ { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
26795
+ { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
26796
+ { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
26797
+ { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
26798
+ ];
26799
+ AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
27058
26800
  ContextSchema = external_exports.object({
27059
26801
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
27060
26802
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -27285,16 +27027,6 @@ var init_dist = __esm({
27285
27027
  });
27286
27028
  NotifyStatusSchema = external_exports.enum(["pending", "answered", "ignored"]);
27287
27029
  AgentStateSchema = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
27288
- SetTaskStateSchema = external_exports.object({
27289
- state: external_exports.enum(["in_progress", "completed", "needs_input"])
27290
- });
27291
- SetWorkStateSchema = external_exports.object({
27292
- workId: external_exports.string().uuid().optional(),
27293
- notificationId: external_exports.string().optional(),
27294
- state: SetTaskStateSchema.shape.state
27295
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
27296
- message: "exactly one of workId or notificationId is required"
27297
- });
27298
27030
  TurnSchema = external_exports.object({
27299
27031
  prompt: external_exports.string(),
27300
27032
  reply: external_exports.string()
@@ -27424,108 +27156,6 @@ var init_dist = __esm({
27424
27156
  })
27425
27157
  ]);
27426
27158
  CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
27427
- ScheduleCallbackSchema = external_exports.object({
27428
- parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
27429
- goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
27430
- trigger: CallbackTriggerSchema,
27431
- dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
27432
- note: external_exports.string().optional().describe("What to tell the user when you follow up.")
27433
- });
27434
- PendingRepliesSchema = external_exports.object({
27435
- replies: external_exports.array(
27436
- external_exports.object({
27437
- parentId: external_exports.string(),
27438
- notificationId: external_exports.string(),
27439
- workId: external_exports.string().uuid().optional(),
27440
- decisionId: external_exports.string().uuid().optional(),
27441
- answer: UserAnswerSchema,
27442
- /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
27443
- * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
27444
- * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
27445
- sealed: external_exports.lazy(() => SealedAnswerSchema).optional(),
27446
- /** The call record rendered for THIS agent: the raw words the shaped answer was
27447
- * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
27448
- transcript: external_exports.string().optional(),
27449
- /** Coverage report (#396): which declared `points` this answer addressed. */
27450
- covered: external_exports.array(external_exports.string()).optional()
27451
- })
27452
- ),
27453
- pending: external_exports.array(
27454
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
27455
- ),
27456
- /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
27457
- * sees for this session's identity. From inside a session there was no way to find out —
27458
- * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
27459
- * agent that cannot tell which agent it is cannot tell whether work addressed to
27460
- * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
27461
- you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
27462
- /** User-initiated requests addressed to this agent; act on them and reply via
27463
- * contact on the same parentId. Keeps reappearing until you call
27464
- * set_work_state on its workId (or notificationId once while bootstrapping). */
27465
- requests: external_exports.array(
27466
- external_exports.object({
27467
- parentId: external_exports.string(),
27468
- notificationId: external_exports.string(),
27469
- workId: external_exports.string().uuid().optional(),
27470
- text: external_exports.string(),
27471
- createdAt: external_exports.string(),
27472
- /** The user seeded this request with a past conversation — call get_thread on it
27473
- * FIRST and treat the transcript as prior context (#57/#251). */
27474
- contextParentId: external_exports.string().optional(),
27475
- /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
27476
- * the account — the name here — which has not been seen since it landed, so nobody
27477
- * came for it. Handed to you because you are the session that is here. Take it like
27478
- * any request (set_work_state claims it, reply with contact on its parentId), and say
27479
- * whose it was, because the user chose that agent on purpose. */
27480
- stranded: external_exports.string().optional()
27481
- })
27482
- ),
27483
- /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
27484
- * notification lists remain during migration so old clients keep their existing view. */
27485
- work: external_exports.array(external_exports.object({
27486
- workId: external_exports.string().uuid(),
27487
- parentId: external_exports.string().optional(),
27488
- objective: external_exports.string().nullable(),
27489
- state: external_exports.enum(["active", "blocked", "waiting_external"]),
27490
- blockedOn: external_exports.array(external_exports.string().uuid()),
27491
- updatedAt: external_exports.string()
27492
- })).optional(),
27493
- /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
27494
- * if blocked, or at a time that has passed). Re-surfaced every sweep until you
27495
- * fulfill one by calling contact on its parentId. */
27496
- /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
27497
- * reach. Same array the contact/await replies carry — one queue, every carrier. */
27498
- also: external_exports.array(RideAlongSchema).optional(),
27499
- owedCallbacks: external_exports.array(
27500
- external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema, note: external_exports.string() })
27501
- ),
27502
- /** Work (either direction) you reported in_progress a while ago and never reported
27503
- * completed — likely left half-done by this session or a prior one that crashed or
27504
- * went idle. Report a real state (set_work_state) or continue the work. */
27505
- stalled: external_exports.array(
27506
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), title: external_exports.string().nullable(), startedAt: external_exports.string() })
27507
- ),
27508
- /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
27509
- * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
27510
- * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
27511
- * thread already has a turn in progress (younger than the stall cutoff); let it finish and
27512
- * ride the next turn. `items` are that thread's replies/requests in arrival order; the
27513
- * full payload for each is in the flat `replies`/`requests` arrays (matched by
27514
- * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
27515
- threads: external_exports.array(
27516
- external_exports.object({
27517
- parentId: external_exports.string(),
27518
- busy: external_exports.boolean(),
27519
- items: external_exports.array(
27520
- external_exports.object({
27521
- kind: external_exports.enum(["reply", "request"]),
27522
- notificationId: external_exports.string(),
27523
- at: external_exports.string()
27524
- })
27525
- )
27526
- })
27527
- )
27528
- });
27529
27159
  NotifyResponseSchema = external_exports.object({
27530
27160
  notificationId: external_exports.string(),
27531
27161
  workId: external_exports.string().uuid().optional(),
@@ -27588,6 +27218,13 @@ var init_dist = __esm({
27588
27218
  });
27589
27219
  VoiceKeySchema = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
27590
27220
  AgendaTurnSchema = external_exports.object({
27221
+ /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
27222
+ * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
27223
+ * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
27224
+ * completion of a streamed re-plan carries the published sentence again, and the walk
27225
+ * drops what it already said by identity, never by the API's guess of what was polled.
27226
+ * Absent on plans nothing streams (a ring plan, a floor). */
27227
+ id: external_exports.string().optional(),
27591
27228
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
27592
27229
  * the planner declares duplicates instead of asking them twice. */
27593
27230
  coveredIds: external_exports.array(external_exports.string()).optional(),
@@ -27746,7 +27383,21 @@ var init_dist = __esm({
27746
27383
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
27747
27384
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
27748
27385
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
27749
- agentX25519: external_exports.string().optional()
27386
+ agentX25519: external_exports.string().optional(),
27387
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
27388
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
27389
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
27390
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
27391
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
27392
+ communication: external_exports.object({
27393
+ deliveryId: external_exports.string(),
27394
+ kind: external_exports.enum(["notification", "call"]),
27395
+ entryId: external_exports.string(),
27396
+ goalIds: external_exports.array(external_exports.string()),
27397
+ decisionNeedId: external_exports.string().optional(),
27398
+ sealed: external_exports.boolean(),
27399
+ goalState: external_exports.string().optional()
27400
+ }).optional()
27750
27401
  });
27751
27402
  SnoozeRequestSchema = external_exports.object({
27752
27403
  requestId: external_exports.string(),
@@ -27982,26 +27633,54 @@ var init_dist = __esm({
27982
27633
  * the agent reads it via get_thread. Must belong to the requesting user. */
27983
27634
  contextParentId: external_exports.string().optional()
27984
27635
  });
27985
- HandoffSchema = external_exports.object({
27986
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
27987
- workId: external_exports.string().uuid().optional(),
27988
- /** Land the note on an existing thread; omitted mints a fresh one. */
27989
- parentId: external_exports.string().uuid().optional(),
27990
- /** One-line headline of the working context handed off. */
27991
- title: external_exports.string().min(1),
27992
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
27993
- notes: external_exports.array(external_exports.string().min(1)).min(1),
27994
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
27995
- * only; omit to leave the thread for the user to hand off in the app. */
27996
- target: external_exports.string().optional(),
27997
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
27998
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
27999
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
28000
- * same primitive; a recap is one whose audience includes you. */
28001
- recap: external_exports.boolean().optional()
28002
- }).refine((value) => !value.workId || Boolean(value.target), {
28003
- message: "target is required when handing off Work",
28004
- path: ["target"]
27636
+ QueueQuestionSchema = external_exports.object({
27637
+ /** The decision need's id what an answer is accepted against. */
27638
+ id: external_exports.string(),
27639
+ /** The words that were asked, from the request Entry that asked them. */
27640
+ question: external_exports.string(),
27641
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
27642
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
27643
+ * can answer. */
27644
+ deliveryId: external_exports.string().nullable().default(null),
27645
+ /** The Entry the ruling is about. */
27646
+ aboutId: external_exports.string().nullable().default(null),
27647
+ /** Empty for a free-text question. */
27648
+ options: external_exports.array(OptionSchema).default([]),
27649
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
27650
+ askedAt: external_exports.string(),
27651
+ /** Null while the question is open which is how the page tells the two apart. */
27652
+ answeredAt: external_exports.string().nullable().default(null),
27653
+ /** The ruling in the person's own words, from the contribution that replied — not the
27654
+ * option id, which is not something anyone reads back. Null while it is open, and null
27655
+ * for a settled question whose reply carried nothing readable. */
27656
+ answer: external_exports.string().nullable().default(null)
27657
+ });
27658
+ QueueItemSchema = external_exports.object({
27659
+ id: external_exports.string(),
27660
+ /** One-line headline — the first sentence of the outcome. */
27661
+ title: external_exports.string(),
27662
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
27663
+ intent: external_exports.string(),
27664
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
27665
+ state: external_exports.string(),
27666
+ /** Who holds it (a participant ref); null when nobody does yet. */
27667
+ assignee: external_exports.string().nullable().default(null),
27668
+ /** What the agent last said it was doing; null if it has said nothing. */
27669
+ progress: external_exports.string().nullable().default(null),
27670
+ reviewPending: external_exports.boolean().default(false),
27671
+ dueAt: external_exports.string().nullable().default(null),
27672
+ /** The Goal this one was opened under; null at the root. */
27673
+ parentGoalId: external_exports.string().nullable().default(null),
27674
+ /** Goals opened under this one — only those the same list holds. */
27675
+ childGoalIds: external_exports.array(external_exports.string()).default([]),
27676
+ /** Goals this one waits on (start or finish gates). */
27677
+ dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
27678
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
27679
+ blocked: external_exports.boolean().default(false),
27680
+ /** Every decision need on it, open or settled — the page decides which to show. */
27681
+ questions: external_exports.array(QueueQuestionSchema).default([]),
27682
+ createdAt: external_exports.string(),
27683
+ updatedAt: external_exports.string().nullable().default(null)
28005
27684
  });
28006
27685
  NoteSourceSchema = external_exports.enum(["app", "call"]);
28007
27686
  NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
@@ -28130,6 +27809,8 @@ var init_dist = __esm({
28130
27809
  failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
28131
27810
  });
28132
27811
  DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
27812
+ WAKE_EVENT = "wake";
27813
+ wakeChannel = (tokenId) => `wake:${tokenId}`;
28133
27814
  RegisterDeliverySchema = external_exports.object({ mode: DeliveryModeSchema });
28134
27815
  OAuthStartSchema = external_exports.object({
28135
27816
  provider: external_exports.enum(["cma"]),
@@ -28138,15 +27819,29 @@ var init_dist = __esm({
28138
27819
  DeliveryConfigSchema = external_exports.object({
28139
27820
  tokenId: external_exports.string(),
28140
27821
  mode: DeliveryModeSchema,
28141
- /** null when the server has no SUPABASE_ANON_KEY set the listener then falls
28142
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
27822
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
27823
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
27824
+ * carries credentials; only a `poll` registration can come back with null here. */
28143
27825
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
28144
27826
  });
28145
27827
  StatusSchema = external_exports.object({
28146
27828
  name: external_exports.string(),
28147
27829
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
28148
27830
  /** A phone is registered for push/ring (any push token on the account). */
28149
- phone: external_exports.boolean()
27831
+ phone: external_exports.boolean(),
27832
+ /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
27833
+ * it never picked up. THE SAME NUMBER the harness's wake gate reads
27834
+ * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
27835
+ * zero while the sweep sees one is two ideas of "waiting".
27836
+ *
27837
+ * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
27838
+ * that nobody spawned, so a terminal session only learns of work by asking. The harness
27839
+ * used to paper over that by spawning a SECOND process on the identity; now it stands
27840
+ * back, correctly, and the person sitting at the terminal is the one who can act. A
27841
+ * coffee-beans request sat unread for three days.
27842
+ *
27843
+ * Optional: an older API sends no field, and the statusline then renders exactly as before. */
27844
+ waiting: external_exports.number().int().nonnegative().optional()
28150
27845
  });
28151
27846
  EnvelopeRecipientSchema = external_exports.object({
28152
27847
  keyId: external_exports.string(),
@@ -28312,24 +28007,11 @@ var init_dist = __esm({
28312
28007
  message: external_exports.string(),
28313
28008
  childIds: external_exports.array(external_exports.string()).optional()
28314
28009
  });
28315
- CONTACT_SCHEMA = contactSchemaFrom({
28316
- ask: NotifyRequestFields.shape.ask,
28317
- waiting: NotifyRequestFields.shape.waiting,
28318
- options: NotifyRequestFields.shape.options,
28319
- channel: NotifyRequestFields.shape.channel,
28320
- parentId: NotifyRequestFields.shape.parentId,
28321
- workId: NotifyRequestFields.shape.workId,
28322
- goalId: NotifyRequestFields.shape.goalId
28323
- });
28324
28010
  import_tweetnacl = __toESM2(require_nacl_fast(), 1);
28325
28011
  import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
28326
28012
  import_tweetnacl3 = __toESM2(require_nacl_fast(), 1);
28327
28013
  import_tweetnacl4 = __toESM2(require_nacl_fast(), 1);
28328
28014
  import_tweetnacl5 = __toESM2(require_nacl_fast(), 1);
28329
- B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
28330
- ok = false;
28331
- ENVELOPE_VERSION = 1;
28332
- ENVELOPE_ALG = "x25519-xsalsa20poly1305";
28333
28015
  AGENT_NAME = process.env.PAIGY_AGENT || sessionSlot();
28334
28016
  TOKEN_PATH = join2(homedir2(), ".paigy", "token.json");
28335
28017
  KEY_PATH = join2(homedir2(), ".paigy", "key.json");
@@ -28340,25 +28022,28 @@ var init_dist = __esm({
28340
28022
  this.name = "UnpairedError";
28341
28023
  }
28342
28024
  };
28343
- tokenOverride = null;
28344
- authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
28345
- sleep3 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
28346
- ReplyLeaseExpiredError = class extends Error {
28347
- constructor() {
28348
- super(
28349
- "The Paigy reply lease expired before acknowledgement. The answer remains durable; wait on it again (contact with only { wait: notificationId }) to reclaim it before acting."
28350
- );
28351
- this.name = "ReplyLeaseExpiredError";
28352
- }
28353
- };
28354
- ReplyAckError = class extends Error {
28355
- constructor(message, retryable) {
28356
- super(message);
28357
- this.retryable = retryable;
28358
- this.name = "ReplyAckError";
28025
+ ApiError = class extends Error {
28026
+ status;
28027
+ body;
28028
+ constructor(what, status, text) {
28029
+ super(`${what} failed: ${status} ${text}`);
28030
+ this.name = "ApiError";
28031
+ this.status = status;
28032
+ let body = void 0;
28033
+ try {
28034
+ body = JSON.parse(text);
28035
+ } catch {
28036
+ }
28037
+ this.body = body;
28359
28038
  }
28360
- retryable;
28361
28039
  };
28040
+ AWAIT_WINDOW_MS = 45e3;
28041
+ tokenOverride = null;
28042
+ authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
28043
+ HEARTBEAT_MS = 25e3;
28044
+ RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
28045
+ JOIN = { config: { broadcast: { self: false } } };
28046
+ lines = /* @__PURE__ */ new Map();
28362
28047
  }
28363
28048
  });
28364
28049
 
@@ -29661,11 +29346,11 @@ var require_dijkstra = __commonJS({
29661
29346
  var predecessors = {};
29662
29347
  var costs = {};
29663
29348
  costs[s] = 0;
29664
- var open2 = dijkstra.PriorityQueue.make();
29665
- open2.push(s, 0);
29349
+ var open = dijkstra.PriorityQueue.make();
29350
+ open.push(s, 0);
29666
29351
  var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit;
29667
- while (!open2.empty()) {
29668
- closest = open2.pop();
29352
+ while (!open.empty()) {
29353
+ closest = open.pop();
29669
29354
  u = closest.value;
29670
29355
  cost_of_s_to_u = closest.cost;
29671
29356
  adjacent_nodes = graph[u] || {};
@@ -29677,7 +29362,7 @@ var require_dijkstra = __commonJS({
29677
29362
  first_visit = typeof costs[v] === "undefined";
29678
29363
  if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
29679
29364
  costs[v] = cost_of_s_to_u_plus_cost_of_e;
29680
- open2.push(v, cost_of_s_to_u_plus_cost_of_e);
29365
+ open.push(v, cost_of_s_to_u_plus_cost_of_e);
29681
29366
  predecessors[v] = u;
29682
29367
  }
29683
29368
  }
@@ -32969,17 +32654,17 @@ async function available(deps = {}) {
32969
32654
  return [ollama, ...harnesses];
32970
32655
  }
32971
32656
  function refusal(options, asked) {
32972
- const lines = [
32657
+ const lines2 = [
32973
32658
  asked ? `Can't judge with --via ${asked}: ${options.find((o) => o.provider === asked)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
32974
32659
  "",
32975
32660
  "Triage runs the model where your queue already lives (#1106). Your options:"
32976
32661
  ];
32977
32662
  for (const o of options) {
32978
- lines.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
32979
- if (!o.ready && o.hint) lines.push(` ${o.hint}`);
32663
+ lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
32664
+ if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
32980
32665
  }
32981
- lines.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
32982
- return lines.join("\n");
32666
+ lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
32667
+ return lines2.join("\n");
32983
32668
  }
32984
32669
  async function judgeFor(asked, deps = {}) {
32985
32670
  const options = await available(deps);
@@ -33128,8 +32813,8 @@ function agentSignals(notes, connections, now) {
33128
32813
  }
33129
32814
  function noteSignals(notes, connections, now) {
33130
32815
  const nameOf = new Map(connections.map((c) => [c.id, c.name]));
33131
- const open2 = notes.filter((n) => OPEN_STATUSES.includes(n.status));
33132
- return open2.map((n) => {
32816
+ const open = notes.filter((n) => OPEN_STATUSES.includes(n.status));
32817
+ return open.map((n) => {
33133
32818
  const superseded = supersededBy(n, notes);
33134
32819
  const echo = doneEcho(n, notes);
33135
32820
  const token = assigneeToken(n);
@@ -33377,27 +33062,27 @@ function suggested(run) {
33377
33062
  return run.close.length + run.stale.length + run.assign.reduce((n, a) => n + a.notes.length, 0);
33378
33063
  }
33379
33064
  function renderTable(run) {
33380
- const lines = [];
33065
+ const lines2 = [];
33381
33066
  const width = Math.max(
33382
33067
  24,
33383
33068
  ...[...run.close, ...run.stale, ...run.assign.flatMap((a) => a.notes)].map((n) => Math.min(n.title.length, 48))
33384
33069
  );
33385
33070
  const row = (n) => ` ${n.title.slice(0, width).padEnd(width)} ${n.why}`;
33386
33071
  if (run.close.length) {
33387
- lines.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
33388
- lines.push(...run.close.map(row), "");
33072
+ lines2.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
33073
+ lines2.push(...run.close.map(row), "");
33389
33074
  }
33390
33075
  if (run.stale.length) {
33391
- lines.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
33392
- lines.push(...run.stale.map(row), "");
33076
+ lines2.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
33077
+ lines2.push(...run.stale.map(row), "");
33393
33078
  }
33394
33079
  for (const group of run.assign) {
33395
- lines.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
33396
- lines.push(...group.notes.map(row), "");
33080
+ lines2.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
33081
+ lines2.push(...group.notes.map(row), "");
33397
33082
  }
33398
33083
  const acted = suggested(run);
33399
- lines.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
33400
- return lines.join("\n");
33084
+ lines2.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
33085
+ return lines2.join("\n");
33401
33086
  }
33402
33087
  async function confirm(question) {
33403
33088
  if (!process.stdin.isTTY) return false;
@@ -33595,8 +33280,301 @@ import { execSync } from "child_process";
33595
33280
  import { homedir as homedir7 } from "os";
33596
33281
  import { join as join5 } from "path";
33597
33282
 
33598
- // src/paigy/bridge.ts
33599
- init_dist();
33283
+ // src/harness/session.ts
33284
+ import { spawn } from "child_process";
33285
+ import { existsSync as existsSync3 } from "fs";
33286
+ import { homedir as homedir3 } from "os";
33287
+ import { resolve, delimiter as delimiter2 } from "path";
33288
+
33289
+ // src/harness/acp.ts
33290
+ var none = { events: [], writes: [] };
33291
+ function optionFor(options, decision) {
33292
+ const want = decision.allow ? "allow_once" : "reject_once";
33293
+ return options.find((o) => o.kind === want)?.optionId ?? null;
33294
+ }
33295
+ function createAcpDriver(opts) {
33296
+ return new AcpDriver(opts.cwd, opts.mode);
33297
+ }
33298
+ var AcpDriver = class {
33299
+ constructor(cwd, mode) {
33300
+ this.cwd = cwd;
33301
+ this.mode = mode;
33302
+ }
33303
+ cwd;
33304
+ mode;
33305
+ nextId = 1;
33306
+ initId;
33307
+ sessionNewId;
33308
+ promptId;
33309
+ sessionId;
33310
+ queued = [];
33311
+ /** Options of each unanswered permission request, keyed by its JSON-RPC id. */
33312
+ pending = /* @__PURE__ */ new Map();
33313
+ /** Where the REPORT starts in `text` — everything before the LAST tool call is working
33314
+ * narration ("Now the API endpoints." → runs a tool), and it used to ship: the chunks
33315
+ * concatenate with no separator, so the owner's phone got "…find the repo.Now I have
33316
+ * the full picture. Writing the migration.Now…" as the opening paragraph of a finished
33317
+ * task (live, 2026-08-11 — "looks like a working log"). The narration's audience is the
33318
+ * terminal and host.log; what the agent composed AFTER its last tool call is the part
33319
+ * addressed to a human, and that is what leaves the machine. */
33320
+ reportFrom = 0;
33321
+ /** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
33322
+ text = "";
33323
+ tools = [];
33324
+ /** The opening frame. Everything after is driven by responses in `handleLine`. */
33325
+ open() {
33326
+ this.initId = this.nextId++;
33327
+ return [
33328
+ frame2({
33329
+ id: this.initId,
33330
+ method: "initialize",
33331
+ params: {
33332
+ protocolVersion: 2,
33333
+ // We are not an editor: no file services offered, the agent uses its own.
33334
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
33335
+ clientInfo: { name: "paigy-desktop", version: "0.0.0" }
33336
+ }
33337
+ })
33338
+ ];
33339
+ }
33340
+ /** Queue a prompt; it goes out when the session exists and no turn is running. */
33341
+ send(text) {
33342
+ this.queued.push(text);
33343
+ return this.flush();
33344
+ }
33345
+ /** Answer a PermissionEvent (ask mode). The id is the request's JSON-RPC id as a string. */
33346
+ respond(id, decision) {
33347
+ const request = this.pending.get(id);
33348
+ if (!request) return [];
33349
+ this.pending.delete(id);
33350
+ const optionId = optionFor(request.options, decision);
33351
+ return [
33352
+ optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
33353
+ ];
33354
+ }
33355
+ /** Cancel anything still blocked — called on stop so the process can exit cleanly. */
33356
+ close() {
33357
+ const writes = [...this.pending.values()].map(
33358
+ (r) => frame2({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
33359
+ );
33360
+ this.pending.clear();
33361
+ return writes;
33362
+ }
33363
+ handleLine(line) {
33364
+ const trimmed = line.trim();
33365
+ if (!trimmed) return none;
33366
+ let msg;
33367
+ try {
33368
+ msg = JSON.parse(trimmed);
33369
+ } catch {
33370
+ return none;
33371
+ }
33372
+ if (msg.method !== void 0) {
33373
+ return msg.id !== void 0 ? this.handleRequest(msg) : this.handleNotification(msg);
33374
+ }
33375
+ if (msg.id !== void 0) return this.handleResponse(msg);
33376
+ return none;
33377
+ }
33378
+ // ── responses to our requests ──
33379
+ handleResponse(msg) {
33380
+ if (msg.id === this.initId) {
33381
+ this.initId = void 0;
33382
+ if (msg.error) {
33383
+ return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
33384
+ }
33385
+ this.sessionNewId = this.nextId++;
33386
+ return {
33387
+ events: [],
33388
+ writes: [frame2({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
33389
+ };
33390
+ }
33391
+ if (msg.id === this.sessionNewId) {
33392
+ this.sessionNewId = void 0;
33393
+ const sessionId2 = msg.result?.sessionId;
33394
+ if (!sessionId2) {
33395
+ return {
33396
+ events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
33397
+ writes: []
33398
+ };
33399
+ }
33400
+ this.sessionId = sessionId2;
33401
+ const value = this.mode === "bypass" ? "bypassPermissions" : "default";
33402
+ const writes = [
33403
+ frame2({
33404
+ id: this.nextId++,
33405
+ method: "session/set_config_option",
33406
+ params: { sessionId: sessionId2, configId: "mode", value }
33407
+ }),
33408
+ frame2({ id: this.nextId++, method: "session/set_mode", params: { sessionId: sessionId2, modeId: value } })
33409
+ ];
33410
+ writes.push(...this.flush());
33411
+ return { events: [], writes };
33412
+ }
33413
+ if (msg.id === this.promptId) {
33414
+ this.promptId = void 0;
33415
+ const events = [];
33416
+ const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
33417
+ if (report || this.tools.length) {
33418
+ events.push({
33419
+ kind: "turn",
33420
+ role: "agent",
33421
+ text: report,
33422
+ ...this.tools.length ? { tools: [...this.tools] } : {}
33423
+ });
33424
+ }
33425
+ const result = report;
33426
+ const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
33427
+ this.text = "";
33428
+ this.tools = [];
33429
+ this.reportFrom = 0;
33430
+ const writes = this.flush();
33431
+ if (!writes.length) {
33432
+ events.push({
33433
+ kind: "idle",
33434
+ ...result ? { result } : {},
33435
+ ...failed ? { failed: true } : {}
33436
+ });
33437
+ }
33438
+ return { events, writes };
33439
+ }
33440
+ return none;
33441
+ }
33442
+ // ── the agent talking to us ──
33443
+ handleNotification(msg) {
33444
+ if (msg.method !== "session/update") return none;
33445
+ const update = msg.params?.update;
33446
+ switch (update?.sessionUpdate) {
33447
+ case "agent_message_chunk":
33448
+ this.text += update.content?.text ?? "";
33449
+ return none;
33450
+ case "tool_call": {
33451
+ const title = update.title?.trim() || update.kind || "tool";
33452
+ this.tools.push(title);
33453
+ const note = this.text.slice(this.reportFrom).trim();
33454
+ this.reportFrom = this.text.length;
33455
+ return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
33456
+ }
33457
+ default:
33458
+ return none;
33459
+ }
33460
+ }
33461
+ handleRequest(msg) {
33462
+ if (msg.method !== "session/request_permission") {
33463
+ return {
33464
+ events: [],
33465
+ writes: [frame2({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
33466
+ };
33467
+ }
33468
+ const id = msg.id;
33469
+ const options = msg.params?.options ?? [];
33470
+ const title = msg.params?.toolCall?.title?.trim() || "a tool call";
33471
+ const tool = msg.params?.toolCall?.kind ?? "tool";
33472
+ if (this.mode === "bypass") {
33473
+ const optionId = optionFor(options, { allow: true }) ?? optionFor(options, { allow: false });
33474
+ return {
33475
+ // The decision still goes through Paigy — as history, not a question. Bypass
33476
+ // means "don't stall the agent", never "don't tell the user".
33477
+ events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
33478
+ writes: [
33479
+ optionId ? frame2({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id, result: { outcome: { outcome: "cancelled" } } })
33480
+ ]
33481
+ };
33482
+ }
33483
+ this.pending.set(String(id), { id, options });
33484
+ return {
33485
+ events: [{ kind: "permission", id: String(id), tool, summary: title }],
33486
+ writes: []
33487
+ };
33488
+ }
33489
+ /** Send the queued prompts as one turn, if the agent can take one right now. */
33490
+ flush() {
33491
+ if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
33492
+ const blocks = this.queued.map((text) => ({ type: "text", text }));
33493
+ this.queued = [];
33494
+ this.promptId = this.nextId++;
33495
+ return [
33496
+ frame2({
33497
+ id: this.promptId,
33498
+ method: "session/prompt",
33499
+ params: { sessionId: this.sessionId, prompt: blocks }
33500
+ })
33501
+ ];
33502
+ }
33503
+ };
33504
+ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
33505
+
33506
+ // src/harness/session.ts
33507
+ init_catalog();
33508
+ var ADAPTER_BIN = {
33509
+ claude: "claude-agent-acp",
33510
+ codex: "codex-acp",
33511
+ agy: "agy"
33512
+ };
33513
+ function splitLines(buffer, chunk) {
33514
+ const combined = buffer + chunk;
33515
+ const parts = combined.split("\n");
33516
+ const rest = parts.pop() ?? "";
33517
+ return { lines: parts.filter((l) => l.trim()), rest };
33518
+ }
33519
+ function startSession(opts) {
33520
+ const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir3()));
33521
+ if (!existsSync3(cwd)) {
33522
+ queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
33523
+ }
33524
+ const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
33525
+ cwd,
33526
+ // The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
33527
+ // launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
33528
+ // node's own bin dir (nvm installs the adapters next to node; launchd's bare
33529
+ // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
33530
+ env: {
33531
+ ...process.env,
33532
+ PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
33533
+ ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
33534
+ },
33535
+ stdio: ["pipe", "pipe", "pipe"]
33536
+ });
33537
+ const write = (frames) => {
33538
+ for (const f of frames) child.stdin?.write(`${f}
33539
+ `);
33540
+ };
33541
+ child.stderr?.on("data", (chunk) => {
33542
+ const text = String(chunk).trim();
33543
+ if (text) opts.onEvent({ kind: "error", message: text });
33544
+ });
33545
+ child.on("exit", (code) => {
33546
+ opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
33547
+ opts.onExit?.();
33548
+ });
33549
+ child.on("error", (e) => {
33550
+ opts.onEvent({ kind: "error", message: e.message });
33551
+ opts.onExit?.();
33552
+ });
33553
+ const driver = createAcpDriver({ cwd, mode: opts.mode });
33554
+ let buffer = "";
33555
+ child.stdout?.on("data", (chunk) => {
33556
+ const { lines: lines2, rest } = splitLines(buffer, String(chunk));
33557
+ buffer = rest;
33558
+ for (const line of lines2) {
33559
+ const { events, writes } = driver.handleLine(line);
33560
+ write(writes);
33561
+ for (const event of events) opts.onEvent(event);
33562
+ }
33563
+ });
33564
+ write(driver.open());
33565
+ return {
33566
+ send(text) {
33567
+ write(driver.send(text));
33568
+ },
33569
+ respond(id, decision) {
33570
+ write(driver.respond(id, decision));
33571
+ },
33572
+ stop() {
33573
+ write(driver.close());
33574
+ child.kill();
33575
+ }
33576
+ };
33577
+ }
33600
33578
 
33601
33579
  // ../../packages/schema/dist/index.js
33602
33580
  init_zod();
@@ -34894,22 +34872,20 @@ var zodToJsonSchema2 = (schema, options) => {
34894
34872
  };
34895
34873
 
34896
34874
  // ../../packages/schema/dist/index.js
34875
+ init_zod();
34897
34876
  var OPTIONS_MIN2 = 2;
34898
34877
  var OPTIONS_MAX2 = 6;
34899
- var MISSED_CALL_PLAN2 = {
34900
- retry_10m: { kind: "every", minutes: 10 },
34901
- retry_30m: { kind: "every", minutes: 30 },
34902
- retry_60m: { kind: "every", minutes: 60 },
34903
- backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
34904
- backoff_standard: { kind: "at", minutes: [10, 30, 120] },
34905
- backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
34906
- inbox: { kind: "once" },
34907
- dismiss: { kind: "grace", minutes: 2 }
34908
- };
34909
34878
  function draft20202(node) {
34910
34879
  if (Array.isArray(node)) return node.map(draft20202);
34911
34880
  if (node && typeof node === "object") {
34912
34881
  const o = node;
34882
+ if (Array.isArray(o.items)) {
34883
+ o.prefixItems = o.items;
34884
+ if ("additionalItems" in o) {
34885
+ o.items = o.additionalItems;
34886
+ delete o.additionalItems;
34887
+ } else delete o.items;
34888
+ }
34913
34889
  for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
34914
34890
  if (typeof o[excl] === "boolean") {
34915
34891
  if (o[excl] === true && typeof o[lim] === "number") {
@@ -34928,11 +34904,54 @@ function mcpInputSchema2(s) {
34928
34904
  delete schema.$schema;
34929
34905
  return draft20202(schema);
34930
34906
  }
34907
+ var StartContactSchema2 = external_exports.object({
34908
+ goalIds: external_exports.tuple([external_exports.string().uuid()]),
34909
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
34910
+ //
34911
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
34912
+ // findings and a decision into one, and the person answers whichever part engaged them —
34913
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
34914
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
34915
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
34916
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
34917
+ // asks carrying a decision, 27 are over 600 characters.
34918
+ //
34919
+ // Sending them separately costs nothing, because a CALL contact joins the call already
34920
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
34921
+ // one call, planned as its turns, and each keeps its own settleable need.
34922
+ ask: external_exports.string().trim().min(1).max(1e4).describe(
34923
+ "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."
34924
+ ),
34925
+ waiting: external_exports.enum(["none", "hard"]).default("none"),
34926
+ channel: external_exports.enum(["notification", "call"]).default("notification"),
34927
+ options: external_exports.array(external_exports.object({ label: external_exports.string().trim().min(1).max(1e3), image: external_exports.string().url().optional(), html: external_exports.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
34928
+ threadId: external_exports.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
34929
+ }).strict();
34930
+ var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
34931
+ var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
34932
+ var CONTACT_DESCRIPTION2 = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
34931
34933
  var CreateGoalSchema2 = external_exports.object({
34932
34934
  outcome: external_exports.string().trim().min(1).max(1e4),
34933
34935
  ownerParticipant: external_exports.string().trim().min(1).optional(),
34934
- idempotencyKey: external_exports.string().trim().min(1).max(200)
34936
+ idempotencyKey: external_exports.string().trim().min(1).max(200),
34937
+ /** A past conversation this Goal should be read against — History's "new session from this"
34938
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
34939
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
34940
+ * the writer refuses a thread belonging to another account. */
34941
+ contextThreadId: external_exports.string().uuid().optional(),
34942
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
34943
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
34944
+ * worked"). It could not have been: this door took no parent, so the only route was
34945
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
34946
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
34947
+ * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
34948
+ * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
34949
+ parentGoalId: external_exports.string().uuid().optional()
34935
34950
  });
34951
+ var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
34952
+ idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
34953
+ }).strict();
34954
+ var CREATE_GOAL_DESCRIPTION2 = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
34936
34955
  var UpdateGoalSchema2 = external_exports.object({
34937
34956
  revision: external_exports.number().int().positive(),
34938
34957
  changes: external_exports.object({
@@ -34948,52 +34967,48 @@ var UpdateGoalSchema2 = external_exports.object({
34948
34967
  reason: external_exports.string().trim().min(1).max(2e3),
34949
34968
  operationId: external_exports.string().uuid().optional()
34950
34969
  }).strict();
34951
- var UpdateGoalToolSchema2 = UpdateGoalSchema2.extend({ goalId: external_exports.string().uuid() }).strict();
34970
+ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
34952
34971
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
34953
- var fmtMin2 = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
34954
- var STANDARD_MEANS2 = (() => {
34955
- const plan = MISSED_CALL_PLAN2.backoff_standard;
34956
- const mins = plan.kind === "at" ? plan.minutes : [];
34957
- const parts = mins.map(fmtMin2);
34958
- const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
34959
- return `Rings again ${list} after the missed call, then leaves it in your inbox`;
34960
- })();
34961
- function contactSchemaFrom2(fields) {
34962
- const surface = external_exports.object({
34963
- ask: fields.ask.describe(
34964
- `What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). DONE IS SAID ONCE: "all set", "nothing open on my end", "that thread is complete" are the same report in new words, and each one reaches them separately (live 2026-08-12: three of them in three minutes). After the first, you are finished speaking; if they acknowledge it, stop rather than confirming the acknowledgement. Progress is never a contact: set_work_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it.`
34965
- ),
34966
- waiting: fields.waiting.describe(
34967
- "What happens to your work while you wait. 'none': you're just informing them. 'soft': you'd like an answer but can keep working. 'hard': you are STOPPED until they answer \u2014 reaches them urgently and escalates to a real phone call if unanswered."
34968
- ),
34969
- options: fields.options.describe(
34970
- `The choices the user picks from, when you have them \u2014 ${OPTIONS_MIN2} to ${OPTIONS_MAX2}, drawn from your own sentence.`
34971
- ),
34972
- channel: fields.channel.describe(
34973
- "Relay how the user explicitly said to reach them ('call me' \u2192 'call', 'just message/text me' \u2192 'message'), or use 'call' when promoting the same quiet ask after it becomes a substantial blocker. Omit otherwise; Paigy picks."
34974
- ),
34975
- parentId: fields.parentId.describe(
34976
- "To continue an earlier conversation, pass the parentId a previous contact or reply returned. Omit to start a new one."
34977
- ),
34978
- workId: fields.workId.describe(
34979
- "The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
34980
- ),
34981
- goalId: fields.goalId.describe(
34982
- "The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
34983
- ),
34984
- // THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
34985
- // A contact that rang holds its first window itself; the host caps one tool call at
34986
- // ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
34987
- wait: external_exports.string().uuid().optional().describe(
34988
- "KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
34989
- )
34990
- });
34991
- const out = mcpInputSchema2(surface);
34992
- delete out.required;
34993
- out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
34994
- return out;
34972
+ var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
34973
+ var GET_GOAL_DESCRIPTION2 = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
34974
+ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
34975
+ var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
34976
+ 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 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. 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 with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
34977
+ var CheckRepliesSchema2 = external_exports.object({}).strict();
34978
+ var GetThreadSchema2 = external_exports.object({
34979
+ parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
34980
+ }).strict();
34981
+ var GET_THREAD_DESCRIPTION2 = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
34982
+ var SearchThreadsSchema2 = external_exports.object({
34983
+ q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
34984
+ }).strict();
34985
+ var SEARCH_THREADS_DESCRIPTION2 = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
34986
+ var AGENT_TOOLS2 = [
34987
+ { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
34988
+ { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
34989
+ { name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
34990
+ { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
34991
+ { name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
34992
+ { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
34993
+ { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
34994
+ { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
34995
+ ];
34996
+ var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
34997
+ function entryWords(entry) {
34998
+ const content = entry.content;
34999
+ if (content && "sealed" in content) return "";
35000
+ const plain = content?.plain;
35001
+ if (typeof plain === "string") return plain;
35002
+ if (plain && typeof plain === "object") {
35003
+ const text = plain.text;
35004
+ if (typeof text === "string") return text;
35005
+ const title = plain.title;
35006
+ const description = plain.description;
35007
+ const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
35008
+ if (parts.length) return parts.join("\n\n");
35009
+ }
35010
+ return entry.sources.map((source) => source.text).join("\n");
34995
35011
  }
34996
- var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS2}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN2}-${OPTIONS_MAX2} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
34997
35012
  var ContextSchema2 = external_exports.object({
34998
35013
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
34999
35014
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -35222,35 +35237,8 @@ var NotifyRequestSchema2 = NotifyRequestFields2.superRefine((r, ctx) => {
35222
35237
  if (!needsOptions && r.options?.length)
35223
35238
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
35224
35239
  });
35225
- function unitsOf2(text) {
35226
- const spans = [];
35227
- const re = /\n\s*\n+/g;
35228
- let cursor = 0;
35229
- const push = (from, to) => {
35230
- const slice = text.slice(from, to);
35231
- const lead = slice.length - slice.trimStart().length;
35232
- const tail = slice.length - slice.trimEnd().length;
35233
- if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
35234
- };
35235
- for (let m = re.exec(text); m; m = re.exec(text)) {
35236
- push(cursor, m.index);
35237
- cursor = m.index + m[0].length;
35238
- }
35239
- push(cursor, text.length);
35240
- return spans;
35241
- }
35242
35240
  var NotifyStatusSchema2 = external_exports.enum(["pending", "answered", "ignored"]);
35243
35241
  var AgentStateSchema2 = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
35244
- var SetTaskStateSchema2 = external_exports.object({
35245
- state: external_exports.enum(["in_progress", "completed", "needs_input"])
35246
- });
35247
- var SetWorkStateSchema2 = external_exports.object({
35248
- workId: external_exports.string().uuid().optional(),
35249
- notificationId: external_exports.string().optional(),
35250
- state: SetTaskStateSchema2.shape.state
35251
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
35252
- message: "exactly one of workId or notificationId is required"
35253
- });
35254
35242
  var TurnSchema2 = external_exports.object({
35255
35243
  prompt: external_exports.string(),
35256
35244
  reply: external_exports.string()
@@ -35380,108 +35368,6 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
35380
35368
  })
35381
35369
  ]);
35382
35370
  var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
35383
- var ScheduleCallbackSchema2 = external_exports.object({
35384
- parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
35385
- goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
35386
- trigger: CallbackTriggerSchema2,
35387
- dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
35388
- note: external_exports.string().optional().describe("What to tell the user when you follow up.")
35389
- });
35390
- var PendingRepliesSchema2 = external_exports.object({
35391
- replies: external_exports.array(
35392
- external_exports.object({
35393
- parentId: external_exports.string(),
35394
- notificationId: external_exports.string(),
35395
- workId: external_exports.string().uuid().optional(),
35396
- decisionId: external_exports.string().uuid().optional(),
35397
- answer: UserAnswerSchema2,
35398
- /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
35399
- * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
35400
- * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
35401
- sealed: external_exports.lazy(() => SealedAnswerSchema2).optional(),
35402
- /** The call record rendered for THIS agent: the raw words the shaped answer was
35403
- * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
35404
- transcript: external_exports.string().optional(),
35405
- /** Coverage report (#396): which declared `points` this answer addressed. */
35406
- covered: external_exports.array(external_exports.string()).optional()
35407
- })
35408
- ),
35409
- pending: external_exports.array(
35410
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
35411
- ),
35412
- /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
35413
- * sees for this session's identity. From inside a session there was no way to find out —
35414
- * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
35415
- * agent that cannot tell which agent it is cannot tell whether work addressed to
35416
- * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
35417
- you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
35418
- /** User-initiated requests addressed to this agent; act on them and reply via
35419
- * contact on the same parentId. Keeps reappearing until you call
35420
- * set_work_state on its workId (or notificationId once while bootstrapping). */
35421
- requests: external_exports.array(
35422
- external_exports.object({
35423
- parentId: external_exports.string(),
35424
- notificationId: external_exports.string(),
35425
- workId: external_exports.string().uuid().optional(),
35426
- text: external_exports.string(),
35427
- createdAt: external_exports.string(),
35428
- /** The user seeded this request with a past conversation — call get_thread on it
35429
- * FIRST and treat the transcript as prior context (#57/#251). */
35430
- contextParentId: external_exports.string().optional(),
35431
- /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
35432
- * the account — the name here — which has not been seen since it landed, so nobody
35433
- * came for it. Handed to you because you are the session that is here. Take it like
35434
- * any request (set_work_state claims it, reply with contact on its parentId), and say
35435
- * whose it was, because the user chose that agent on purpose. */
35436
- stranded: external_exports.string().optional()
35437
- })
35438
- ),
35439
- /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
35440
- * notification lists remain during migration so old clients keep their existing view. */
35441
- work: external_exports.array(external_exports.object({
35442
- workId: external_exports.string().uuid(),
35443
- parentId: external_exports.string().optional(),
35444
- objective: external_exports.string().nullable(),
35445
- state: external_exports.enum(["active", "blocked", "waiting_external"]),
35446
- blockedOn: external_exports.array(external_exports.string().uuid()),
35447
- updatedAt: external_exports.string()
35448
- })).optional(),
35449
- /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
35450
- * if blocked, or at a time that has passed). Re-surfaced every sweep until you
35451
- * fulfill one by calling contact on its parentId. */
35452
- /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
35453
- * reach. Same array the contact/await replies carry — one queue, every carrier. */
35454
- also: external_exports.array(RideAlongSchema2).optional(),
35455
- owedCallbacks: external_exports.array(
35456
- external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema2, note: external_exports.string() })
35457
- ),
35458
- /** Work (either direction) you reported in_progress a while ago and never reported
35459
- * completed — likely left half-done by this session or a prior one that crashed or
35460
- * went idle. Report a real state (set_work_state) or continue the work. */
35461
- stalled: external_exports.array(
35462
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), title: external_exports.string().nullable(), startedAt: external_exports.string() })
35463
- ),
35464
- /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
35465
- * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
35466
- * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
35467
- * thread already has a turn in progress (younger than the stall cutoff); let it finish and
35468
- * ride the next turn. `items` are that thread's replies/requests in arrival order; the
35469
- * full payload for each is in the flat `replies`/`requests` arrays (matched by
35470
- * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
35471
- threads: external_exports.array(
35472
- external_exports.object({
35473
- parentId: external_exports.string(),
35474
- busy: external_exports.boolean(),
35475
- items: external_exports.array(
35476
- external_exports.object({
35477
- kind: external_exports.enum(["reply", "request"]),
35478
- notificationId: external_exports.string(),
35479
- at: external_exports.string()
35480
- })
35481
- )
35482
- })
35483
- )
35484
- });
35485
35371
  var NotifyResponseSchema2 = external_exports.object({
35486
35372
  notificationId: external_exports.string(),
35487
35373
  workId: external_exports.string().uuid().optional(),
@@ -35544,6 +35430,13 @@ var UserResponseSchema2 = external_exports.object({
35544
35430
  });
35545
35431
  var VoiceKeySchema2 = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
35546
35432
  var AgendaTurnSchema2 = external_exports.object({
35433
+ /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
35434
+ * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
35435
+ * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
35436
+ * completion of a streamed re-plan carries the published sentence again, and the walk
35437
+ * drops what it already said by identity, never by the API's guess of what was polled.
35438
+ * Absent on plans nothing streams (a ring plan, a floor). */
35439
+ id: external_exports.string().optional(),
35547
35440
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
35548
35441
  * the planner declares duplicates instead of asking them twice. */
35549
35442
  coveredIds: external_exports.array(external_exports.string()).optional(),
@@ -35702,7 +35595,21 @@ var InboxItemSchema2 = external_exports.object({
35702
35595
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
35703
35596
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
35704
35597
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
35705
- agentX25519: external_exports.string().optional()
35598
+ agentX25519: external_exports.string().optional(),
35599
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
35600
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
35601
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
35602
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
35603
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
35604
+ communication: external_exports.object({
35605
+ deliveryId: external_exports.string(),
35606
+ kind: external_exports.enum(["notification", "call"]),
35607
+ entryId: external_exports.string(),
35608
+ goalIds: external_exports.array(external_exports.string()),
35609
+ decisionNeedId: external_exports.string().optional(),
35610
+ sealed: external_exports.boolean(),
35611
+ goalState: external_exports.string().optional()
35612
+ }).optional()
35706
35613
  });
35707
35614
  var SnoozeRequestSchema2 = external_exports.object({
35708
35615
  requestId: external_exports.string(),
@@ -35938,26 +35845,54 @@ var CreateRequestSchema2 = external_exports.object({
35938
35845
  * the agent reads it via get_thread. Must belong to the requesting user. */
35939
35846
  contextParentId: external_exports.string().optional()
35940
35847
  });
35941
- var HandoffSchema2 = external_exports.object({
35942
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
35943
- workId: external_exports.string().uuid().optional(),
35944
- /** Land the note on an existing thread; omitted mints a fresh one. */
35945
- parentId: external_exports.string().uuid().optional(),
35946
- /** One-line headline of the working context handed off. */
35947
- title: external_exports.string().min(1),
35948
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
35949
- notes: external_exports.array(external_exports.string().min(1)).min(1),
35950
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
35951
- * only; omit to leave the thread for the user to hand off in the app. */
35952
- target: external_exports.string().optional(),
35953
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
35954
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
35955
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
35956
- * same primitive; a recap is one whose audience includes you. */
35957
- recap: external_exports.boolean().optional()
35958
- }).refine((value) => !value.workId || Boolean(value.target), {
35959
- message: "target is required when handing off Work",
35960
- path: ["target"]
35848
+ var QueueQuestionSchema2 = external_exports.object({
35849
+ /** The decision need's id what an answer is accepted against. */
35850
+ id: external_exports.string(),
35851
+ /** The words that were asked, from the request Entry that asked them. */
35852
+ question: external_exports.string(),
35853
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
35854
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
35855
+ * can answer. */
35856
+ deliveryId: external_exports.string().nullable().default(null),
35857
+ /** The Entry the ruling is about. */
35858
+ aboutId: external_exports.string().nullable().default(null),
35859
+ /** Empty for a free-text question. */
35860
+ options: external_exports.array(OptionSchema2).default([]),
35861
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
35862
+ askedAt: external_exports.string(),
35863
+ /** Null while the question is open which is how the page tells the two apart. */
35864
+ answeredAt: external_exports.string().nullable().default(null),
35865
+ /** The ruling in the person's own words, from the contribution that replied — not the
35866
+ * option id, which is not something anyone reads back. Null while it is open, and null
35867
+ * for a settled question whose reply carried nothing readable. */
35868
+ answer: external_exports.string().nullable().default(null)
35869
+ });
35870
+ var QueueItemSchema2 = external_exports.object({
35871
+ id: external_exports.string(),
35872
+ /** One-line headline — the first sentence of the outcome. */
35873
+ title: external_exports.string(),
35874
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
35875
+ intent: external_exports.string(),
35876
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
35877
+ state: external_exports.string(),
35878
+ /** Who holds it (a participant ref); null when nobody does yet. */
35879
+ assignee: external_exports.string().nullable().default(null),
35880
+ /** What the agent last said it was doing; null if it has said nothing. */
35881
+ progress: external_exports.string().nullable().default(null),
35882
+ reviewPending: external_exports.boolean().default(false),
35883
+ dueAt: external_exports.string().nullable().default(null),
35884
+ /** The Goal this one was opened under; null at the root. */
35885
+ parentGoalId: external_exports.string().nullable().default(null),
35886
+ /** Goals opened under this one — only those the same list holds. */
35887
+ childGoalIds: external_exports.array(external_exports.string()).default([]),
35888
+ /** Goals this one waits on (start or finish gates). */
35889
+ dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
35890
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
35891
+ blocked: external_exports.boolean().default(false),
35892
+ /** Every decision need on it, open or settled — the page decides which to show. */
35893
+ questions: external_exports.array(QueueQuestionSchema2).default([]),
35894
+ createdAt: external_exports.string(),
35895
+ updatedAt: external_exports.string().nullable().default(null)
35961
35896
  });
35962
35897
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
35963
35898
  var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
@@ -36094,15 +36029,29 @@ var OAuthStartSchema2 = external_exports.object({
36094
36029
  var DeliveryConfigSchema2 = external_exports.object({
36095
36030
  tokenId: external_exports.string(),
36096
36031
  mode: DeliveryModeSchema2,
36097
- /** null when the server has no SUPABASE_ANON_KEY set the listener then falls
36098
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
36032
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
36033
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
36034
+ * carries credentials; only a `poll` registration can come back with null here. */
36099
36035
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
36100
36036
  });
36101
36037
  var StatusSchema2 = external_exports.object({
36102
36038
  name: external_exports.string(),
36103
36039
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
36104
36040
  /** A phone is registered for push/ring (any push token on the account). */
36105
- phone: external_exports.boolean()
36041
+ phone: external_exports.boolean(),
36042
+ /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
36043
+ * it never picked up. THE SAME NUMBER the harness's wake gate reads
36044
+ * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
36045
+ * zero while the sweep sees one is two ideas of "waiting".
36046
+ *
36047
+ * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
36048
+ * that nobody spawned, so a terminal session only learns of work by asking. The harness
36049
+ * used to paper over that by spawning a SECOND process on the identity; now it stands
36050
+ * back, correctly, and the person sitting at the terminal is the one who can act. A
36051
+ * coffee-beans request sat unread for three days.
36052
+ *
36053
+ * Optional: an older API sends no field, and the statusline then renders exactly as before. */
36054
+ waiting: external_exports.number().int().nonnegative().optional()
36106
36055
  });
36107
36056
  var EnvelopeRecipientSchema2 = external_exports.object({
36108
36057
  keyId: external_exports.string(),
@@ -36268,18 +36217,31 @@ var FeedbackOutcomeSchema2 = external_exports.object({
36268
36217
  message: external_exports.string(),
36269
36218
  childIds: external_exports.array(external_exports.string()).optional()
36270
36219
  });
36271
- var CONTACT_SCHEMA2 = contactSchemaFrom2({
36272
- ask: NotifyRequestFields2.shape.ask,
36273
- waiting: NotifyRequestFields2.shape.waiting,
36274
- options: NotifyRequestFields2.shape.options,
36275
- channel: NotifyRequestFields2.shape.channel,
36276
- parentId: NotifyRequestFields2.shape.parentId,
36277
- workId: NotifyRequestFields2.shape.workId,
36278
- goalId: NotifyRequestFields2.shape.goalId
36279
- });
36280
36220
 
36281
- // src/paigy/conversation.ts
36221
+ // src/paigy/activity.ts
36222
+ function shortenPaths(s) {
36223
+ return s.replace(/(?<![\w.@+-])(?:\/[\w.@+-]+){3,}/g, (p) => {
36224
+ const parts = p.split("/").filter(Boolean);
36225
+ return `\u2026/${parts.slice(-2).join("/")}`;
36226
+ });
36227
+ }
36228
+ function workLine(event) {
36229
+ const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
36230
+ const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
36231
+ return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
36232
+ }
36233
+ function pushWork(lines2, line) {
36234
+ if (!line || lines2[lines2.length - 1] === line) return [...lines2];
36235
+ return [...lines2, line].slice(-ACTIVITY_LINES2);
36236
+ }
36237
+ function sameTail(a, b) {
36238
+ return a.length === b.length && a.every((l, i) => l === b[i]);
36239
+ }
36240
+
36241
+ // src/paigy/bridge.ts
36282
36242
  init_dist();
36243
+
36244
+ // src/paigy/conversation.ts
36283
36245
  function endsWithQuestion(text) {
36284
36246
  if (!text) return false;
36285
36247
  const tail = text.trim().split("\n").filter(Boolean).slice(-3).join(" ").toLowerCase();
@@ -36292,57 +36254,60 @@ function endsWithQuestion(text) {
36292
36254
  }
36293
36255
  var POLL_MS = 5e3;
36294
36256
  var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
36295
- async function drainInput(state, deps = {}) {
36296
- if (!state.parentId && !state.exclusive) return [];
36297
- let work;
36298
- try {
36299
- work = await (deps.check ?? checkReplies)();
36300
- } catch {
36301
- return [];
36257
+ var ended = (state) => state === "done" || state === "cancelled";
36258
+ function unheard(brief, state) {
36259
+ const seen = state.seen ??= /* @__PURE__ */ new Set();
36260
+ const words2 = [];
36261
+ for (const e of brief.entries ?? []) {
36262
+ if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
36263
+ seen.add(e.entryId);
36264
+ if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
36265
+ const text = entryWords(e).trim();
36266
+ if (text) words2.push(text);
36267
+ }
36268
+ return words2;
36269
+ }
36270
+ async function step(session, state, rail) {
36271
+ if (!state.goal) {
36272
+ if (!state.resting) return;
36273
+ const brief2 = await rail.claim();
36274
+ if (!brief2.goalId || brief2.revision === void 0) return;
36275
+ unheard(brief2, state);
36276
+ if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
36277
+ else state.goal = { id: brief2.goalId, revision: brief2.revision };
36278
+ send(session, state, brief2.message);
36279
+ return;
36302
36280
  }
36303
- const ours = (parentId) => state.exclusive || parentId === state.parentId;
36304
- const opening = !!state.exclusive && !state.parentId;
36305
- const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
36306
- const mine = [
36307
- ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
36308
- ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
36309
- ];
36310
- const claimed = [];
36311
- for (const item of mine) {
36312
- try {
36313
- const ack = await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
36314
- if (ack?.won === false) continue;
36315
- claimed.push(item);
36316
- } catch {
36317
- }
36281
+ const brief = await rail.read(state.goal.id);
36282
+ state.goal.revision = brief.revision ?? state.goal.revision;
36283
+ for (const answer of brief.answers ?? []) {
36284
+ const resolve3 = state.asks?.get(answer.requestEntryId);
36285
+ if (!resolve3) continue;
36286
+ state.asks.delete(answer.requestEntryId);
36287
+ resolve3(answer.result);
36288
+ }
36289
+ if (ended(brief.state)) {
36290
+ state.goal = void 0;
36291
+ for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
36292
+ state.asks?.clear();
36293
+ return;
36318
36294
  }
36319
- return claimed;
36295
+ if (state.asks?.size || !state.resting) return;
36296
+ const words2 = unheard(brief, state);
36297
+ if (words2.length) send(session, state, words2.join("\n\n"));
36320
36298
  }
36321
- async function nextTurnFrom(event, state, deps = {}) {
36322
- if (!endsWithQuestion(event.result)) return null;
36323
- const answer = await askQuestion(event.result ?? "", state, deps);
36324
- return answer?.trim() ? answer : null;
36299
+ function send(session, state, text) {
36300
+ state.resting = false;
36301
+ session.send(text);
36325
36302
  }
36326
- async function pump(session, state, deps = {}) {
36303
+ async function pump(session, state, rail, opts) {
36327
36304
  for (; ; ) {
36328
- for (const item of await drainInput(state, deps)) {
36329
- state.parentId ??= item.parentId;
36330
- const key = state.asks?.has(item.notificationId) ? item.notificationId : state.asks?.keys().next().value;
36331
- const resolve3 = key !== void 0 ? state.asks?.get(key) : void 0;
36332
- if (key !== void 0 && resolve3) {
36333
- state.asks?.delete(key);
36334
- resolve3(item.answer);
36335
- continue;
36336
- }
36337
- const words2 = spokenText(item.answer);
36338
- if (words2) {
36339
- state.resting = false;
36340
- session.send(words2);
36341
- }
36305
+ try {
36306
+ await step(session, state, rail);
36307
+ } catch {
36342
36308
  }
36343
- const keepGoing = await deps.onIdle?.() ?? false;
36344
- if (!keepGoing) return;
36345
- await (deps.pause ?? wait)();
36309
+ if (!await opts.onIdle()) return;
36310
+ await (opts.pause ?? wait)();
36346
36311
  }
36347
36312
  }
36348
36313
 
@@ -36364,530 +36329,107 @@ var DESTRUCTIVE = [
36364
36329
  function isDestructive(summary) {
36365
36330
  return DESTRUCTIVE.some((re) => re.test(summary));
36366
36331
  }
36367
- function levelFor(event) {
36368
- switch (event.kind) {
36369
- case "turn":
36370
- return "inbox";
36371
- case "permission":
36372
- return isDestructive(event.summary) ? "call" : "banner";
36373
- case "idle":
36374
- if (endsWithQuestion(event.result)) return "banner";
36375
- return event.failed ? "push" : "inbox";
36376
- case "work":
36377
- return "inbox";
36378
- // moot — entryFor drops work before a level ever matters
36379
- case "error":
36380
- return "inbox";
36381
- }
36382
- }
36383
36332
 
36384
36333
  // src/paigy/bridge.ts
36334
+ function railFor(opts) {
36335
+ return {
36336
+ claim: () => claimGoal(void 0, opts),
36337
+ read: (goalId) => getGoal(goalId, opts),
36338
+ update: (goalId, input) => updateGoal(goalId, input, opts),
36339
+ // One read and back: the pump owns the waiting, so a Call must not hold a window here.
36340
+ contact: (input) => contact(input, { ...opts, waits: false })
36341
+ };
36342
+ }
36385
36343
  function cancelAsks(state) {
36386
- for (const resolve3 of state.asks?.values() ?? []) resolve3({ kind: "ignored" });
36344
+ for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
36387
36345
  state.asks?.clear();
36388
36346
  }
36389
- function entryFor(event, state = {}) {
36390
- const common = {
36391
- ...state.parentId ? { parentId: state.parentId } : {},
36392
- ...state.repo ? { repo: state.repo } : {},
36393
- ...state.branch ? { branch: state.branch } : {},
36394
- urgency: levelFor(event)
36395
- };
36396
- switch (event.kind) {
36397
- case "turn": {
36398
- const who = event.role === "agent" ? "Agent" : "You";
36399
- const body = event.text ? event.role === "agent" ? event.text : `${who}: ${event.text}` : `${who} ran ${(event.tools ?? []).join(", ")}.`;
36400
- return {
36401
- ...common,
36402
- ask: body
36403
- // No `select`: the contract refuses it on the `ask` form ("the broker derives it"),
36404
- // and with no options it derives "text" anyway — the shape this wants, since any
36405
- // inbox row can be replied to and a reply to history is just the user initiating
36406
- // (the pump feeds it back in).
36407
- };
36408
- }
36409
- case "permission":
36410
- return {
36411
- ...common,
36412
- context: {
36413
- title: clip(event.summary),
36414
- description: [
36415
- `The agent is blocked waiting to run: ${event.summary}`,
36416
- ...event.reason ? [event.reason] : []
36417
- ]
36418
- },
36419
- select: "confirm",
36420
- confirmStyle: "approve",
36421
- // Literally true, and the inbox uses it: a blocking item gets the badge and the
36422
- // extra confirm-before-dismiss, which is exactly right for something holding a
36423
- // real process still.
36424
- blocking: true
36425
- };
36426
- case "idle": {
36427
- const asking = endsWithQuestion(event.result);
36428
- const result = event.result?.trim();
36429
- const norm = (t) => (t ?? "").replace(/\s+/g, " ").trim();
36430
- if (result && !event.failed && norm(result) === norm(state.lastAgentText)) {
36431
- if (!asking) return null;
36432
- const spans = unitsOf2(result);
36433
- const tail = spans.map((sp) => result.slice(sp.start, sp.end)).reverse().find((t) => t.includes("?"));
36434
- return { ...common, ask: tail ?? result, blocking: true };
36435
- }
36436
- const lead = asking ? "" : event.failed ? "The agent stopped without finishing. " : "Turn complete. ";
36437
- return {
36438
- ...common,
36439
- ask: `${lead}${result || (event.failed ? "No result reported." : "Done.")}`,
36440
- ...asking ? { blocking: true } : {}
36441
- };
36347
+ var ALLOW = "Allow";
36348
+ function decisionFrom(result) {
36349
+ if (result && "kind" in result) {
36350
+ if (result.kind === "one") return result.option.label === ALLOW ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
36351
+ if (result.kind === "confirm") return result.approved ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
36352
+ if (result.kind === "text") {
36353
+ if (/^(yes|y|ok|okay|approve[d]?|allow|go ahead|do it)\b/.test(result.text.trim().toLowerCase())) return { allow: true };
36354
+ return { allow: false, reason: result.text };
36442
36355
  }
36443
- case "work":
36444
- return null;
36445
- // log-only by contract — the live texture of the working log
36446
- case "error":
36447
- return null;
36448
- }
36449
- }
36450
- function decisionFrom(answer) {
36451
- if (answer.kind === "confirm") {
36452
- return answer.approved ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
36453
- }
36454
- if (answer.kind === "text") {
36455
- const said = answer.text.trim().toLowerCase();
36456
- if (/^(yes|y|ok|okay|approve[d]?|allow|go ahead|do it)\b/.test(said)) return { allow: true };
36457
- return { allow: false, reason: answer.text };
36458
- }
36459
- if (answer.kind === "turns") {
36460
- const first = answer.turns[0]?.reply ?? "";
36461
- return decisionFrom({ kind: "text", text: first });
36462
36356
  }
36463
36357
  return { allow: false, reason: "No approval given" };
36464
36358
  }
36465
- async function mirror(event, state, deps = {}) {
36466
- const entry = entryFor(event, state);
36467
- if (event.kind === "turn" && event.role === "agent" && event.text) state.lastAgentText = event.text;
36468
- if (!entry) return {};
36469
- let req;
36470
- try {
36471
- req = NotifyRequestSchema2.parse(entry);
36472
- } catch (e) {
36473
- console.error(`paigy: mirror entry failed the contract \u2014 a bridge bug, not the network: ${e instanceof Error ? e.message.slice(0, 300) : String(e)}`);
36474
- return {};
36475
- }
36476
- try {
36477
- const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
36478
- (state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
36479
- return { parentId, notificationId };
36480
- } catch {
36481
- return {};
36482
- }
36483
- }
36484
- function awaitAnswer(state, notificationId) {
36485
- state.asks ??= /* @__PURE__ */ new Map();
36486
- return new Promise((resolve3) => state.asks.set(notificationId, resolve3));
36487
- }
36488
- async function askPermission(event, state, deps = {}) {
36489
- const entry = entryFor(event, state);
36490
- if (!entry) return { decision: { allow: false, reason: "Could not ask" } };
36491
- try {
36492
- const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
36493
- (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
36494
- const answer = await awaitAnswer(state, sent.notificationId);
36495
- return { decision: decisionFrom(answer), parentId: sent.parentId };
36496
- } catch (e) {
36497
- return { decision: { allow: false, reason: `Could not reach Paigy: ${e.message}` } };
36359
+ function wordsOf(result) {
36360
+ if (!result || !("kind" in result)) return null;
36361
+ switch (result.kind) {
36362
+ case "text":
36363
+ return result.text.trim() || null;
36364
+ case "confirm":
36365
+ return result.approved ? "yes" : "no";
36366
+ case "one":
36367
+ return result.option.label;
36368
+ case "many":
36369
+ case "rank":
36370
+ return result.options.map((o) => o.label).join(", ");
36498
36371
  }
36499
36372
  }
36500
- async function askQuestion(question, state, deps = {}) {
36501
- const entry = {
36502
- ...state.parentId ? { parentId: state.parentId } : {},
36503
- ...state.repo ? { repo: state.repo } : {},
36504
- ...state.branch ? { branch: state.branch } : {},
36505
- // Prose in — see `entryFor`'s `turn` case. An agent's question is the case most likely
36506
- // to run long, and its title was a clipped prefix of itself.
36507
- ask: question,
36508
- blocking: true,
36509
- urgency: "banner"
36510
- };
36373
+ async function ask(state, rail, input) {
36374
+ if (!state.goal) return null;
36511
36375
  try {
36512
- const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
36513
- (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
36514
- return spokenText(await awaitAnswer(state, sent.notificationId));
36376
+ const sent = await rail.contact({
36377
+ goalIds: [state.goal.id],
36378
+ ask: input.ask,
36379
+ waiting: "hard",
36380
+ channel: input.call ? "call" : "notification",
36381
+ ...input.options ? { options: input.options } : {}
36382
+ });
36383
+ const entryId = sent.requestEntryIds[0];
36384
+ if (!entryId) return null;
36385
+ (state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
36386
+ return await new Promise((resolve3) => (state.asks ??= /* @__PURE__ */ new Map()).set(entryId, resolve3));
36515
36387
  } catch {
36516
36388
  return null;
36517
36389
  }
36518
36390
  }
36519
- async function nudgeSetup(label, hint, deps = {}) {
36520
- const entry = {
36521
- // Keeps `context`: the title here SUMMARISES rather than repeating "<label> needs
36522
- // setup" is not a prefix of the hint — which is exactly the case a hand-written context
36523
- // is for. `ask` would derive a title from the hint's first sentence and lose the label.
36524
- context: { title: clip(`${label} needs setup`), description: [hint] },
36525
- select: "text",
36526
- urgency: "push"
36527
- };
36528
- try {
36529
- await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
36530
- } catch {
36531
- }
36532
- }
36533
- function spokenText(answer) {
36534
- switch (answer.kind) {
36535
- case "text":
36536
- return answer.text.trim() || null;
36537
- case "option":
36538
- return answer.label ?? answer.optionId;
36539
- case "multi":
36540
- case "ranked":
36541
- return (answer.labels ?? answer.optionIds).join(", ");
36542
- case "confirm":
36543
- return answer.approved ? "yes" : "no";
36544
- case "clarify":
36545
- return answer.chunks.join(" ");
36546
- // A call log: the user's replies are the instruction, the bot's prompts are not.
36547
- case "turns":
36548
- return answer.turns.map((t) => t.reply).join(" ").trim() || null;
36549
- // An auto-answer derived from the user's past decisions — reads like a fast human.
36550
- case "precedent":
36551
- return answer.answer;
36552
- case "ignored":
36553
- return null;
36554
- }
36555
- }
36556
- var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text)";
36557
-
36558
- // src/run.ts
36559
- init_dist();
36560
-
36561
- // src/harness/session.ts
36562
- import { spawn } from "child_process";
36563
- import { existsSync as existsSync3 } from "fs";
36564
- import { homedir as homedir3 } from "os";
36565
- import { resolve, delimiter as delimiter2 } from "path";
36566
-
36567
- // src/harness/acp.ts
36568
- var none = { events: [], writes: [] };
36569
- function optionFor(options, decision) {
36570
- const want = decision.allow ? "allow_once" : "reject_once";
36571
- return options.find((o) => o.kind === want)?.optionId ?? null;
36391
+ async function askPermission(event, state, rail) {
36392
+ return decisionFrom(await ask(state, rail, {
36393
+ ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
36394
+ options: [{ label: ALLOW }, { label: "Deny" }],
36395
+ call: isDestructive(event.summary)
36396
+ }));
36572
36397
  }
36573
- function createAcpDriver(opts) {
36574
- return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
36398
+ async function askQuestion(question, state, rail) {
36399
+ return wordsOf(await ask(state, rail, { ask: question }));
36575
36400
  }
36576
- var AcpDriver = class {
36577
- constructor(cwd, mode, mcp = []) {
36578
- this.cwd = cwd;
36579
- this.mode = mode;
36580
- this.mcp = mcp;
36581
- }
36582
- cwd;
36583
- mode;
36584
- mcp;
36585
- nextId = 1;
36586
- initId;
36587
- sessionNewId;
36588
- promptId;
36589
- sessionId;
36590
- queued = [];
36591
- /** Options of each unanswered permission request, keyed by its JSON-RPC id. */
36592
- pending = /* @__PURE__ */ new Map();
36593
- /** Where the REPORT starts in `text` — everything before the LAST tool call is working
36594
- * narration ("Now the API endpoints." → runs a tool), and it used to ship: the chunks
36595
- * concatenate with no separator, so the owner's phone got "…find the repo.Now I have
36596
- * the full picture. Writing the migration.Now…" as the opening paragraph of a finished
36597
- * task (live, 2026-08-11 — "looks like a working log"). The narration's audience is the
36598
- * terminal and host.log; what the agent composed AFTER its last tool call is the part
36599
- * addressed to a human, and that is what leaves the machine. */
36600
- reportFrom = 0;
36601
- /** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
36602
- text = "";
36603
- tools = [];
36604
- /** The opening frame. Everything after is driven by responses in `handleLine`. */
36605
- open() {
36606
- this.initId = this.nextId++;
36607
- return [
36608
- frame({
36609
- id: this.initId,
36610
- method: "initialize",
36611
- params: {
36612
- protocolVersion: 2,
36613
- // We are not an editor: no file services offered, the agent uses its own.
36614
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
36615
- clientInfo: { name: "paigy-desktop", version: "0.0.0" }
36616
- }
36617
- })
36618
- ];
36619
- }
36620
- /** Queue a prompt; it goes out when the session exists and no turn is running. */
36621
- send(text) {
36622
- this.queued.push(text);
36623
- return this.flush();
36624
- }
36625
- /** Answer a PermissionEvent (ask mode). The id is the request's JSON-RPC id as a string. */
36626
- respond(id, decision) {
36627
- const request = this.pending.get(id);
36628
- if (!request) return [];
36629
- this.pending.delete(id);
36630
- const optionId = optionFor(request.options, decision);
36631
- return [
36632
- optionId ? frame({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
36633
- ];
36634
- }
36635
- /** Cancel anything still blocked — called on stop so the process can exit cleanly. */
36636
- close() {
36637
- const writes = [...this.pending.values()].map(
36638
- (r) => frame({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
36639
- );
36640
- this.pending.clear();
36641
- return writes;
36642
- }
36643
- handleLine(line) {
36644
- const trimmed = line.trim();
36645
- if (!trimmed) return none;
36646
- let msg;
36647
- try {
36648
- msg = JSON.parse(trimmed);
36649
- } catch {
36650
- return none;
36651
- }
36652
- if (msg.method !== void 0) {
36653
- return msg.id !== void 0 ? this.handleRequest(msg) : this.handleNotification(msg);
36654
- }
36655
- if (msg.id !== void 0) return this.handleResponse(msg);
36656
- return none;
36657
- }
36658
- // ── responses to our requests ──
36659
- handleResponse(msg) {
36660
- if (msg.id === this.initId) {
36661
- this.initId = void 0;
36662
- if (msg.error) {
36663
- return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
36664
- }
36665
- this.sessionNewId = this.nextId++;
36666
- return {
36667
- events: [],
36668
- writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
36669
- };
36670
- }
36671
- if (msg.id === this.sessionNewId) {
36672
- this.sessionNewId = void 0;
36673
- const sessionId2 = msg.result?.sessionId;
36674
- if (!sessionId2) {
36675
- return {
36676
- events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
36677
- writes: []
36678
- };
36679
- }
36680
- this.sessionId = sessionId2;
36681
- const value = this.mode === "bypass" ? "bypassPermissions" : "default";
36682
- const writes = [
36683
- frame({
36684
- id: this.nextId++,
36685
- method: "session/set_config_option",
36686
- params: { sessionId: sessionId2, configId: "mode", value }
36687
- }),
36688
- frame({ id: this.nextId++, method: "session/set_mode", params: { sessionId: sessionId2, modeId: value } })
36689
- ];
36690
- writes.push(...this.flush());
36691
- return { events: [], writes };
36692
- }
36693
- if (msg.id === this.promptId) {
36694
- this.promptId = void 0;
36695
- const events = [];
36696
- const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
36697
- if (report || this.tools.length) {
36698
- events.push({
36699
- kind: "turn",
36700
- role: "agent",
36701
- text: report,
36702
- ...this.tools.length ? { tools: [...this.tools] } : {}
36703
- });
36704
- }
36705
- const result = report;
36706
- const failed = msg.error !== void 0 || msg.result?.stopReason === "refusal";
36707
- this.text = "";
36708
- this.tools = [];
36709
- this.reportFrom = 0;
36710
- const writes = this.flush();
36711
- if (!writes.length) {
36712
- events.push({
36713
- kind: "idle",
36714
- ...result ? { result } : {},
36715
- ...failed ? { failed: true } : {}
36716
- });
36717
- }
36718
- return { events, writes };
36719
- }
36720
- return none;
36721
- }
36722
- // ── the agent talking to us ──
36723
- handleNotification(msg) {
36724
- if (msg.method !== "session/update") return none;
36725
- const update = msg.params?.update;
36726
- switch (update?.sessionUpdate) {
36727
- case "agent_message_chunk":
36728
- this.text += update.content?.text ?? "";
36729
- return none;
36730
- case "tool_call": {
36731
- const title = update.title?.trim() || update.kind || "tool";
36732
- this.tools.push(title);
36733
- const note = this.text.slice(this.reportFrom).trim();
36734
- this.reportFrom = this.text.length;
36735
- return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
36736
- }
36737
- default:
36738
- return none;
36739
- }
36740
- }
36741
- handleRequest(msg) {
36742
- if (msg.method !== "session/request_permission") {
36743
- return {
36744
- events: [],
36745
- writes: [frame({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
36746
- };
36747
- }
36748
- const id = msg.id;
36749
- const options = msg.params?.options ?? [];
36750
- const title = msg.params?.toolCall?.title?.trim() || "a tool call";
36751
- const tool = msg.params?.toolCall?.kind ?? "tool";
36752
- if (this.mode === "bypass") {
36753
- const optionId = optionFor(options, { allow: true }) ?? optionFor(options, { allow: false });
36754
- return {
36755
- // The decision still goes through Paigy — as history, not a question. Bypass
36756
- // means "don't stall the agent", never "don't tell the user".
36757
- events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
36758
- writes: [
36759
- optionId ? frame({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id, result: { outcome: { outcome: "cancelled" } } })
36760
- ]
36761
- };
36762
- }
36763
- this.pending.set(String(id), { id, options });
36764
- return {
36765
- events: [{ kind: "permission", id: String(id), tool, summary: title }],
36766
- writes: []
36767
- };
36768
- }
36769
- /** Send the queued prompts as one turn, if the agent can take one right now. */
36770
- flush() {
36771
- if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
36772
- const blocks = this.queued.map((text) => ({ type: "text", text }));
36773
- this.queued = [];
36774
- this.promptId = this.nextId++;
36775
- return [
36776
- frame({
36777
- id: this.promptId,
36778
- method: "session/prompt",
36779
- params: { sessionId: this.sessionId, prompt: blocks }
36780
- })
36781
- ];
36401
+ async function finish(result, state, rail) {
36402
+ const goal = state.goal;
36403
+ if (!goal) return;
36404
+ state.goal = void 0;
36405
+ const report = result?.trim() || "Done.";
36406
+ try {
36407
+ await rail.contact({ goalIds: [goal.id], ask: report, waiting: "none", channel: "notification" });
36408
+ const { revision } = await rail.read(goal.id);
36409
+ await rail.update(goal.id, { revision: revision ?? goal.revision, changes: { state: "done", progress: report.slice(0, 1e4) }, reason: "The agent finished its turn without asking anything." });
36410
+ } catch {
36782
36411
  }
36783
- };
36784
- var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
36785
-
36786
- // src/harness/session.ts
36787
- init_catalog();
36788
- var ADAPTER_BIN = {
36789
- claude: "claude-agent-acp",
36790
- codex: "codex-acp",
36791
- agy: "agy"
36792
- };
36793
- function splitLines(buffer, chunk) {
36794
- const combined = buffer + chunk;
36795
- const parts = combined.split("\n");
36796
- const rest = parts.pop() ?? "";
36797
- return { lines: parts.filter((l) => l.trim()), rest };
36798
36412
  }
36799
- function startSession(opts) {
36800
- const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir3()));
36801
- if (!existsSync3(cwd)) {
36802
- queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
36413
+ async function afterTurn(event, state, rail) {
36414
+ if (!event.failed && !endsWithQuestion(event.result)) {
36415
+ await finish(event.result, state, rail);
36416
+ state.resting = true;
36417
+ return null;
36803
36418
  }
36804
- const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
36805
- cwd,
36806
- // The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
36807
- // launchd-launched process's PATH won't have it — probe dirs plus the RUNNING
36808
- // node's own bin dir (nvm installs the adapters next to node; launchd's bare
36809
- // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
36810
- env: {
36811
- ...process.env,
36812
- PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
36813
- ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
36814
- },
36815
- stdio: ["pipe", "pipe", "pipe"]
36816
- });
36817
- const write = (frames) => {
36818
- for (const f of frames) child.stdin?.write(`${f}
36819
- `);
36820
- };
36821
- child.stderr?.on("data", (chunk) => {
36822
- const text = String(chunk).trim();
36823
- if (text) opts.onEvent({ kind: "error", message: text });
36824
- });
36825
- child.on("exit", (code) => {
36826
- opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
36827
- opts.onExit?.();
36828
- });
36829
- child.on("error", (e) => {
36830
- opts.onEvent({ kind: "error", message: e.message });
36831
- opts.onExit?.();
36832
- });
36833
- const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
36834
- let buffer = "";
36835
- child.stdout?.on("data", (chunk) => {
36836
- const { lines, rest } = splitLines(buffer, String(chunk));
36837
- buffer = rest;
36838
- for (const line of lines) {
36839
- const { events, writes } = driver.handleLine(line);
36840
- write(writes);
36841
- for (const event of events) opts.onEvent(event);
36842
- }
36843
- });
36844
- write(driver.open());
36845
- return {
36846
- send(text) {
36847
- write(driver.send(text));
36848
- },
36849
- respond(id, decision) {
36850
- write(driver.respond(id, decision));
36851
- },
36852
- stop() {
36853
- write(driver.close());
36854
- child.kill();
36855
- }
36856
- };
36857
- }
36419
+ const reply = await askQuestion(event.failed ? `The agent stopped without finishing.
36858
36420
 
36859
- // src/paigy/activity.ts
36860
- function shortenPaths(s) {
36861
- return s.replace(/(?<![\w.@+-])(?:\/[\w.@+-]+){3,}/g, (p) => {
36862
- const parts = p.split("/").filter(Boolean);
36863
- return `\u2026/${parts.slice(-2).join("/")}`;
36864
- });
36865
- }
36866
- function workLine(event) {
36867
- const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
36868
- const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
36869
- return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
36870
- }
36871
- function pushWork(lines, line) {
36872
- if (!line || lines[lines.length - 1] === line) return [...lines];
36873
- return [...lines, line].slice(-ACTIVITY_LINES2);
36874
- }
36875
- function sameTail(a, b) {
36876
- return a.length === b.length && a.every((l, i) => l === b[i]);
36421
+ ${event.result?.trim() || "No result reported."}` : event.result ?? "", state, rail);
36422
+ if (!reply) state.resting = true;
36423
+ return reply;
36877
36424
  }
36878
36425
 
36879
36426
  // src/run.ts
36880
36427
  function runHarness(opts) {
36881
36428
  let running = true;
36882
36429
  let tail = [];
36883
- const state = { ...opts.exclusive ? { exclusive: true } : {} };
36430
+ const state = { resting: true };
36884
36431
  let session = null;
36885
- const asMe = { token: opts.token };
36886
- const deps = {
36887
- submit: (req) => submitNotification(req, asMe),
36888
- check: () => checkReplies(asMe),
36889
- ack: (id, st) => setTaskState(id, st, asMe)
36890
- };
36432
+ const rail = railFor({ token: opts.token });
36891
36433
  async function handle(event) {
36892
36434
  if (event.kind === "error") {
36893
36435
  opts.log(`\u26A0 ${event.message}`);
@@ -36901,8 +36443,7 @@ function runHarness(opts) {
36901
36443
  return;
36902
36444
  }
36903
36445
  opts.log(`\u23F8 blocked: ${event.summary} \u2014 asking your phone`);
36904
- const { decision, parentId: parentId2 } = await askPermission(event, state, deps);
36905
- state.parentId ??= parentId2;
36446
+ const decision = await askPermission(event, state, rail);
36906
36447
  session?.respond(event.id, decision);
36907
36448
  opts.log(decision.allow ? `\u2713 approved: ${event.summary}` : `\u2717 denied: ${event.summary}`);
36908
36449
  return;
@@ -36912,41 +36453,39 @@ function runHarness(opts) {
36912
36453
  tail = pushWork(tail, workLine(event));
36913
36454
  return;
36914
36455
  }
36915
- const { parentId } = await mirror(event, state, deps);
36916
- state.parentId ??= parentId;
36917
36456
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
36918
36457
  if (event.kind === "idle") {
36919
- state.resting = true;
36920
36458
  tail = [];
36921
36459
  if (endsWithQuestion(event.result)) {
36922
36460
  const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
36923
36461
  if (local?.trim() && session && running) {
36924
36462
  opts.log(`you (here): ${local.split("\n")[0] ?? ""}`);
36463
+ state.resting = false;
36925
36464
  session.send(local);
36926
36465
  return;
36927
36466
  }
36928
36467
  }
36929
- const reply = await nextTurnFrom(event, state, deps);
36468
+ const reply = await afterTurn(event, state, rail);
36930
36469
  if (reply && session && running) {
36931
36470
  opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
36932
36471
  session.send(reply);
36933
36472
  }
36934
36473
  }
36935
36474
  }
36936
- const paigyMcp = opts.exclusive && opts.token ? [{ name: "paigy", command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_TOKEN: opts.token } }] : void 0;
36937
- if (paigyMcp) state.sharedWithAgent = true;
36938
36475
  session = startSession({
36939
36476
  harness: opts.harness,
36940
36477
  cwd: opts.cwd,
36941
36478
  mode: opts.mode,
36942
- ...paigyMcp ? { mcp: paigyMcp } : {},
36943
36479
  ...opts.token ? { token: opts.token } : {},
36944
36480
  ...opts.bin ? { bin: opts.bin } : {},
36945
36481
  ...opts.onExit ? { onExit: opts.onExit } : {},
36946
36482
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
36947
36483
  });
36948
- if (opts.prompt) session.send(opts.prompt);
36949
- void pump(session, state, { ...deps, onIdle: () => running }).catch(
36484
+ if (opts.prompt) {
36485
+ state.resting = false;
36486
+ session.send(opts.prompt);
36487
+ }
36488
+ void pump(session, state, rail, { onIdle: () => running }).catch(
36950
36489
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
36951
36490
  );
36952
36491
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
@@ -36976,7 +36515,7 @@ import { join as join4 } from "path";
36976
36515
 
36977
36516
  // src/workspaces.ts
36978
36517
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
36979
- import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
36518
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
36980
36519
  import { homedir as homedir4 } from "os";
36981
36520
  function workspacesFile() {
36982
36521
  return join3(homedir4(), ".paigy", "workspaces.json");
@@ -36994,7 +36533,7 @@ function addWorkspace(dir, deps) {
36994
36533
  const all = listWorkspaces(deps);
36995
36534
  if (!all.includes(next)) {
36996
36535
  all.push(next);
36997
- mkdirSync2(dirname3(deps.file), { recursive: true });
36536
+ mkdirSync2(dirname2(deps.file), { recursive: true });
36998
36537
  writeFileSync2(deps.file, JSON.stringify(all, null, 2));
36999
36538
  }
37000
36539
  return all;
@@ -37013,21 +36552,17 @@ function resolveWakeDir(pinned, deps) {
37013
36552
 
37014
36553
  // src/host.ts
37015
36554
  var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
37016
- var IDLE_WAKE_MS = 10 * 6e4;
37017
36555
  var runKey = (slot) => `slot:${slot}`;
37018
36556
  var claimedRun = (sessionId2) => {
37019
36557
  const slot = sessionSlot(sessionId2);
37020
36558
  return { slot, key: runKey(slot) };
37021
36559
  };
37022
36560
  async function slotHasWaitingWork(token) {
37023
- return (await pendingSummary({ token })).unacknowledged > 0;
36561
+ return (await checkReplies({ token })).deliveries.length > 0;
37024
36562
  }
37025
36563
  var PRESENCE_FRESH_MS = 3 * 6e4;
37026
- function wakeAction(live, seenAt, now, serverSeenAt) {
37027
- if (live) {
37028
- if (seenAt === void 0) return "skip";
37029
- return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
37030
- }
36564
+ function wakeAction(live, now, serverSeenAt) {
36565
+ if (live) return "skip";
37031
36566
  if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
37032
36567
  return "spawn";
37033
36568
  }
@@ -37055,6 +36590,8 @@ function startHost(opts) {
37055
36590
  workspaces: listWorkspaces(opts.wsDeps)
37056
36591
  }, asHost).catch(() => {
37057
36592
  });
36593
+ void listenSlots().then(sweepSlots).catch(() => {
36594
+ });
37058
36595
  };
37059
36596
  async function claimAndSpawn() {
37060
36597
  const specs = await claimSessions(asHost).catch(() => []);
@@ -37066,8 +36603,7 @@ function startHost(opts) {
37066
36603
  harness: spec.harness,
37067
36604
  cwd: spec.workspace,
37068
36605
  mode: "bypass",
37069
- prompt: spec.prompt ?? "",
37070
- exclusive: true,
36606
+ prompt: "",
37071
36607
  token: spec.token,
37072
36608
  onExit: () => {
37073
36609
  runs.delete(key);
@@ -37087,45 +36623,39 @@ function startHost(opts) {
37087
36623
  }
37088
36624
  }
37089
36625
  const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
37090
- const wakePrompt = (fresh) => "You were woken because Paigy work is waiting for you. " + (fresh ? "This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. " : "You have been running, so some of this may already be yours \u2014 but not what arrived while you were idle. ") + "Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
37091
- async function sweepSlots() {
36626
+ async function presenceMap() {
37092
36627
  const presence = /* @__PURE__ */ new Map();
37093
36628
  try {
37094
36629
  for (const c of (await listConnections(asHost)).items) {
37095
36630
  if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
37096
36631
  }
36632
+ lastPresence = presence;
37097
36633
  } catch {
37098
36634
  }
37099
- for (const slot of listSlots()) {
36635
+ return presence;
36636
+ }
36637
+ let lastPresence = /* @__PURE__ */ new Map();
36638
+ async function wakeSlot(slot, presence) {
36639
+ {
37100
36640
  const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
37101
36641
  const key = runKey(slot);
37102
- if (!harness) continue;
36642
+ if (!harness) return;
37103
36643
  const live = runs.get(key);
37104
36644
  const tokenId = slotIdentity(slot).tokenId;
37105
36645
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
37106
- const action = wakeAction(!!live, published.get(key)?.movedAt, Date.now(), seenByServer);
37107
- if (action === "skip") continue;
36646
+ if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
37108
36647
  const token = readToken(slot);
37109
- if (!token) continue;
36648
+ if (!token) return;
37110
36649
  const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
37111
- if (!workspace) continue;
37112
- if (!await slotHasWaitingWork(token).catch(() => false)) continue;
36650
+ if (!workspace) return;
36651
+ if (!await slotHasWaitingWork(token).catch(() => false)) return;
37113
36652
  const label = slotName(slot) ?? slot;
37114
- if (live) {
37115
- const seen = published.get(key);
37116
- const quiet = seen ? Math.round((Date.now() - seen.movedAt) / 6e4) : 0;
37117
- live.run.send(wakePrompt(false));
37118
- if (seen) seen.movedAt = Date.now();
37119
- opts.log(`\u25B6 re-woke ${label} \u2014 quiet ${quiet}m with work waiting`);
37120
- continue;
37121
- }
37122
36653
  const log = (line) => opts.log(`[${label}] ${line}`);
37123
36654
  const run = runHarness({
37124
36655
  harness,
37125
36656
  cwd: workspace,
37126
36657
  mode: "bypass",
37127
- prompt: wakePrompt(true),
37128
- exclusive: true,
36658
+ prompt: "",
37129
36659
  token,
37130
36660
  // A dead run must not squat the slot — evict so the next wake can respawn.
37131
36661
  onExit: () => {
@@ -37140,20 +36670,54 @@ function startHost(opts) {
37140
36670
  opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
37141
36671
  }
37142
36672
  }
36673
+ const listening = /* @__PURE__ */ new Map();
36674
+ const joining = /* @__PURE__ */ new Set();
36675
+ async function listenSlots() {
36676
+ const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot)));
36677
+ for (const [slot, sub] of [...listening]) {
36678
+ if (want.has(slot)) continue;
36679
+ listening.delete(slot);
36680
+ void sub.close().catch(() => {
36681
+ });
36682
+ }
36683
+ for (const slot of want) {
36684
+ if (listening.has(slot) || joining.has(slot)) continue;
36685
+ const token = readToken(slot);
36686
+ if (!token) continue;
36687
+ joining.add(slot);
36688
+ try {
36689
+ const sub = await subscribeWake(
36690
+ () => void presenceMap().then((presence) => wakeSlot(slot, presence)).catch(() => {
36691
+ }),
36692
+ { token }
36693
+ );
36694
+ listening.set(slot, sub);
36695
+ opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
36696
+ } catch (e) {
36697
+ opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
36698
+ } finally {
36699
+ joining.delete(slot);
36700
+ }
36701
+ }
36702
+ }
36703
+ async function sweepSlots() {
36704
+ const presence = await presenceMap();
36705
+ for (const slot of listSlots()) await wakeSlot(slot, presence);
36706
+ }
37143
36707
  const ACTIVITY_MS = 2e3;
37144
36708
  const published = /* @__PURE__ */ new Map();
37145
36709
  const streamActivity = () => {
37146
- const publishTail = (token, lines) => {
37147
- void heartbeat(void 0, { token, activity: { lines, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
36710
+ const publishTail = (token, lines2) => {
36711
+ void heartbeat(void 0, { token, activity: { lines: lines2, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
37148
36712
  });
37149
36713
  };
37150
36714
  for (const [key, r] of runs) {
37151
36715
  if (!r.token) continue;
37152
- const lines = r.run.tail();
36716
+ const lines2 = r.run.tail();
37153
36717
  const was = published.get(key);
37154
- if (was && sameTail(was.lines, lines)) continue;
37155
- published.set(key, { token: r.token, lines, movedAt: Date.now() });
37156
- publishTail(r.token, lines);
36718
+ if (was && sameTail(was.lines, lines2)) continue;
36719
+ published.set(key, { token: r.token, lines: lines2 });
36720
+ publishTail(r.token, lines2);
37157
36721
  }
37158
36722
  for (const [key, was] of published) {
37159
36723
  if (runs.has(key)) continue;
@@ -37171,7 +36735,6 @@ function startHost(opts) {
37171
36735
  const pulse = setInterval(beat, 6e4);
37172
36736
  const spawnPoll = setInterval(() => {
37173
36737
  void claimAndSpawn();
37174
- void sweepSlots();
37175
36738
  publish();
37176
36739
  }, 5e3);
37177
36740
  const activityTick = setInterval(streamActivity, ACTIVITY_MS);
@@ -37182,6 +36745,12 @@ function startHost(opts) {
37182
36745
  }
37183
36746
  runs.clear();
37184
36747
  };
36748
+ const stopListening = async () => {
36749
+ const subs = [...listening.values()];
36750
+ listening.clear();
36751
+ await Promise.all(subs.map((sub) => sub.close().catch(() => {
36752
+ })));
36753
+ };
37185
36754
  const api = {
37186
36755
  sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
37187
36756
  // Every identity this machine holds, plus what it's doing right now. Slots are the
@@ -37202,7 +36771,16 @@ function startHost(opts) {
37202
36771
  slot,
37203
36772
  tokenId: id.tokenId,
37204
36773
  voice: id.voice,
37205
- running: live.has(name),
36774
+ // RUNNING IS ABOUT THE IDENTITY, NOT ABOUT US (owner, 2026-09-14). This was
36775
+ // `live.has(name)` — "this host spawned it and it has not exited" — which is a fact
36776
+ // about our own process table, not about the agent. An ordinary terminal session
36777
+ // heartbeating every 60 s read as NOT running, and after a host restart so did the
36778
+ // sessions we started ourselves. The server already knows: `last_seen_at` on the
36779
+ // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
36780
+ // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
36781
+ running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
36782
+ // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
36783
+ // thing a tail can tell us. A process we did not spawn has no tail here to read.
37206
36784
  working: live.get(name)?.working() ?? false
37207
36785
  };
37208
36786
  });
@@ -37212,6 +36790,7 @@ function startHost(opts) {
37212
36790
  clearInterval(pulse);
37213
36791
  clearInterval(spawnPoll);
37214
36792
  stopSessions();
36793
+ void stopListening();
37215
36794
  streamActivity();
37216
36795
  clearInterval(activityTick);
37217
36796
  try {
@@ -37221,6 +36800,8 @@ function startHost(opts) {
37221
36800
  }
37222
36801
  };
37223
36802
  publish();
36803
+ void listenSlots().then(sweepSlots).catch(() => {
36804
+ });
37224
36805
  return api;
37225
36806
  }
37226
36807
 
@@ -37243,7 +36824,7 @@ function bridgesToInstall(adapters) {
37243
36824
  function startBridgeInstalls(bridges, deps = {}) {
37244
36825
  const spawn2 = deps.spawn ?? shSpawner;
37245
36826
  const boundMs = deps.boundMs ?? BRIDGE_BOUND_MS;
37246
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
36827
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
37247
36828
  const running = bridges.map((b) => {
37248
36829
  let proc = null;
37249
36830
  let outcome;
@@ -37262,7 +36843,7 @@ function startBridgeInstalls(bridges, deps = {}) {
37262
36843
  started: running.length,
37263
36844
  wait: async () => {
37264
36845
  let timedOut = false;
37265
- const bound = sleep2(boundMs).then(() => {
36846
+ const bound = sleep3(boundMs).then(() => {
37266
36847
  timedOut = true;
37267
36848
  });
37268
36849
  await Promise.race([Promise.all(running.map((r) => r.outcome)), bound]);
@@ -37601,7 +37182,6 @@ async function main() {
37601
37182
  if (state && state.status !== "ready") {
37602
37183
  const hint = state.hint ?? "setup incomplete";
37603
37184
  console.error(`\u270B ${state.label}: ${hint}`);
37604
- await nudgeSetup(state.label, hint);
37605
37185
  process.exit(1);
37606
37186
  }
37607
37187
  const askTerminal = async (promptText) => {
@@ -37626,9 +37206,7 @@ async function main() {
37626
37206
  mode: parsed.mode,
37627
37207
  prompt: parsed.prompt,
37628
37208
  ...state?.bin ? { bin: state.bin } : {},
37629
- // A hatched identity's token is its own — exclusive draining, same as
37630
- // phone-launched sessions (the opening-move rule).
37631
- ...identityToken ? { exclusive: true, token: identityToken } : {},
37209
+ ...identityToken ? { token: identityToken } : {},
37632
37210
  localAsk: {
37633
37211
  permission: async (summary) => {
37634
37212
  const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);