very-happy-cli 0.2.156 → 0.2.158

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 (55) hide show
  1. package/dist/{AcpBackend-BHe1_F1h.cjs → AcpBackend-DIDRig5G.cjs} +2 -2
  2. package/dist/{AcpBackend-C3nc1u-s.mjs → AcpBackend-vDVLHuYJ.mjs} +2 -2
  3. package/dist/{AcpSessionManager-BVxkQnNf.cjs → AcpSessionManager-C44U99CC.cjs} +1 -1
  4. package/dist/{AcpSessionManager-dN3k1LbI.mjs → AcpSessionManager-CmRjfhn3.mjs} +1 -1
  5. package/dist/{agentGuidance-C604n0Zq.mjs → agentGuidance-CrR9l62U.mjs} +562 -30
  6. package/dist/{agentGuidance-C-CmV03f.cjs → agentGuidance-DwOWbX55.cjs} +581 -29
  7. package/dist/{agentHome-D2XGsoqs.mjs → agentHome-CyLmGH5L.mjs} +2 -2
  8. package/dist/{agentHome-CmIJNV7f.cjs → agentHome-Dw8Jx_CJ.cjs} +2 -2
  9. package/dist/{auto-B1I6G_Nf.mjs → auto-BlMZPwOk.mjs} +19 -10
  10. package/dist/{auto-CMDq4oIu.cjs → auto-DZbNcTXx.cjs} +19 -10
  11. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  12. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  13. package/dist/{config-BkTu25bH.mjs → config-BtPArqRH.mjs} +4 -4
  14. package/dist/{config-BYHSRY2E.cjs → config-DG84bjwF.cjs} +4 -4
  15. package/dist/{index-BbtR-Qto.cjs → index-BG932eCh.cjs} +292 -47
  16. package/dist/{index-DAAreiRF.mjs → index-CVCt3kSt.mjs} +289 -44
  17. package/dist/{index-CB8QnjvS.cjs → index-D5ScEErM.cjs} +4 -4
  18. package/dist/{index-B_-7NpGZ.cjs → index-DPCsWlWK.cjs} +7 -7
  19. package/dist/{index-PYO8Mdz4.mjs → index-DgO0FHOx.mjs} +4 -4
  20. package/dist/{index-B5QbzEga.mjs → index-ZChGsuB7.mjs} +8 -8
  21. package/dist/index.cjs +4 -4
  22. package/dist/index.mjs +4 -4
  23. package/dist/{install-CNtXzwT_.mjs → install-DarS_Wco.mjs} +1 -1
  24. package/dist/{install-CO1ps6Wt.cjs → install-wKQGgXvN.cjs} +1 -1
  25. package/dist/{installTerminalHooks-CMpOV3RU.mjs → installTerminalHooks-CpNbRFPz.mjs} +4 -4
  26. package/dist/{installTerminalHooks-BOAnQ1Qe.cjs → installTerminalHooks-DcQfee_O.cjs} +4 -4
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.d.cts +77 -1
  29. package/dist/lib.d.mts +77 -1
  30. package/dist/lib.mjs +2 -2
  31. package/dist/{limits-DmMdchTz.mjs → limits-D2uN090G.mjs} +163 -11
  32. package/dist/{limits-o29KXBgj.cjs → limits-kqFqhfsh.cjs} +170 -10
  33. package/dist/{mcp-C_uejotl.mjs → mcp-BN6bKIGb.mjs} +4 -4
  34. package/dist/{mcp-CwK1nI7Y.cjs → mcp-DGdeUUDJ.cjs} +4 -4
  35. package/dist/{piRuntime-Dj5WnqzJ.cjs → piRuntime-3qtbKujX.cjs} +2 -2
  36. package/dist/{piRuntime-Cu2-8nOT.mjs → piRuntime-bkw2NAZU.mjs} +2 -2
  37. package/dist/{piTerminal-D-BIbov9.cjs → piTerminal-BYxp3LMr.cjs} +6 -6
  38. package/dist/{piTerminal-DanZeBrO.mjs → piTerminal-DilKwM7B.mjs} +6 -6
  39. package/dist/{runGemini--QqK-Ap5.cjs → runGemini-28O2sqyI.cjs} +6 -6
  40. package/dist/{runGemini-CZ_lhMWY.mjs → runGemini-Cjii069p.mjs} +6 -6
  41. package/dist/{runOpenClaw-C0xLhTvk.cjs → runOpenClaw-CiMZad-H.cjs} +5 -5
  42. package/dist/{runOpenClaw-CxgLpm38.mjs → runOpenClaw-jaLr7KF1.mjs} +5 -5
  43. package/dist/{send-BaOkDgH0.mjs → send-C9mOtChi.mjs} +43 -31
  44. package/dist/{send-CTcGMVuG.cjs → send-D5ejHPXG.cjs} +43 -31
  45. package/dist/{sessions-OmvyJb0n.cjs → sessions-BNMPQoLE.cjs} +133 -49
  46. package/dist/{sessions-Bn304xF1.mjs → sessions-W6771g3X.mjs} +133 -49
  47. package/dist/{spawn-BKNabutV.mjs → spawn-D0QVhzRb.mjs} +4 -4
  48. package/dist/{spawn-CC6V2KiA.cjs → spawn-Ddtom1cb.cjs} +4 -4
  49. package/dist/{teams-XtrH3_h3.cjs → teams-BZ1tayg_.cjs} +5 -5
  50. package/dist/{teams-CLbS2_4Q.mjs → teams-CqSwtw2O.mjs} +5 -5
  51. package/dist/{todo-CEDvKNXK.cjs → todo-BYMg0fR0.cjs} +2 -2
  52. package/dist/{todo-BcPbNKt2.mjs → todo-BbusB1zM.mjs} +2 -2
  53. package/dist/{types-BEVzYSoV.mjs → types-DIBF4U0Y.mjs} +78 -5
  54. package/dist/{types-CphWjqHc.cjs → types-DnrPeN80.cjs} +78 -5
  55. package/package.json +7 -7
@@ -2,7 +2,7 @@
2
2
 
3
3
  var z = require('zod');
4
4
  var node_crypto = require('node:crypto');
5
- var persistence = require('./limits-o29KXBgj.cjs');
5
+ var persistence = require('./limits-kqFqhfsh.cjs');
6
6
  var promises = require('node:fs/promises');
7
7
  var node_fs = require('node:fs');
8
8
  var path = require('node:path');
@@ -305,7 +305,8 @@ function createAutomationsClient(options) {
305
305
  }).then((r) => r.runs),
306
306
  getRun: (id) => request("GET", `/v1/automations/runs/${enc(id)}`).then((r) => r.run),
307
307
  cancel: (id) => request("POST", `/v1/automations/runs/${enc(id)}/cancel`).then((r) => r.run),
308
- ack: (id) => request("POST", `/v1/automations/runs/${enc(id)}/ack`).then((r) => r.run),
308
+ /** B-508: `by` records what resolved the flag (server default `owner`; older servers ignore the body). */
309
+ ack: (id, by) => request("POST", `/v1/automations/runs/${enc(id)}/ack`, by === void 0 ? void 0 : { by }).then((r) => r.run),
309
310
  report: (id, input) => request("POST", `/v1/automations/runs/${enc(id)}/report`, input).then((r) => r.run),
310
311
  claim: (input) => request("POST", "/v1/automations/claim", input),
311
312
  stickies: (id, key) => request("GET", `/v1/automations/${enc(id)}/stickies`, void 0, { key }).then((r) => r.stickies),
@@ -884,7 +885,7 @@ async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}
884
885
  `No local key for session ${sessionId} \u2014 approve/deny needs the session key from ~/.happy/sessions.json (the RPC payload is encrypted with it), so only sessions this machine's daemon spawned within 14 days can be answered from here.`
885
886
  );
886
887
  }
887
- const token = await (deps.bearerToken ?? bearerToken$1)();
888
+ const token = await (deps.bearerToken ?? bearerToken$2)();
888
889
  const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
889
890
  const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
890
891
  if (!before.some((request) => request.id === requestId)) {
@@ -934,7 +935,7 @@ async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}
934
935
  }
935
936
  return { sessionId, requestId, payload, outcome, settled };
936
937
  }
937
- async function bearerToken$1() {
938
+ async function bearerToken$2() {
938
939
  const credentials = await persistence.readCredentialsForConfiguredRelay();
939
940
  if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
940
941
  return credentials.token;
@@ -963,6 +964,18 @@ async function fetchDecryptedAgentState(sessionId, persisted, token) {
963
964
  const SESSION_OPS_CLIENT = "session-ops";
964
965
  const DEFAULT_RECENT_LIMIT = 15;
965
966
  const MAX_READ_LIMIT = 100;
967
+ function backgroundTasksOf(child) {
968
+ const raw = child.backgroundTasks;
969
+ if (!raw || typeof raw !== "object") return void 0;
970
+ const r = raw;
971
+ if (typeof r.count !== "number" || !Array.isArray(r.tasks)) return void 0;
972
+ return {
973
+ count: r.count,
974
+ tasks: r.tasks.filter((task) => !!task && typeof task === "object" && typeof task.id === "string"),
975
+ ...typeof r.reportedAt === "number" ? { reportedAt: r.reportedAt } : {},
976
+ ...r.stale === true ? { stale: true } : {}
977
+ };
978
+ }
966
979
  function mergeSessionSummaries(live, persisted, options = {}) {
967
980
  const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
968
981
  const seen = /* @__PURE__ */ new Set();
@@ -982,7 +995,8 @@ function mergeSessionSummaries(live, persisted, options = {}) {
982
995
  return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
983
996
  }
984
997
  function livenessOf(child) {
985
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
998
+ const backgroundTasks = backgroundTasksOf(child);
999
+ return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0, ...backgroundTasks ? { backgroundTasks } : {} };
986
1000
  }
987
1001
  function sessionLiveness(live, sessionId) {
988
1002
  const child = live.find((entry) => entry.happySessionId === sessionId);
@@ -994,6 +1008,7 @@ function toSummary(id, persisted, extra) {
994
1008
  id,
995
1009
  live: extra.live,
996
1010
  ...extra.pid !== void 0 ? { pid: extra.pid } : {},
1011
+ ...extra.backgroundTasks ? { backgroundTasks: extra.backgroundTasks } : {},
997
1012
  ...meta?.summary?.text ? { title: meta.summary.text } : {},
998
1013
  ...meta?.path ? { cwd: meta.path } : {},
999
1014
  ...meta?.flavor ? { flavor: meta.flavor } : {},
@@ -1013,6 +1028,7 @@ function summarizeAccountSession(row, persisted, liveIds, now) {
1013
1028
  live: liveIds.has(row.id),
1014
1029
  url: sessionWebUrl(row.id),
1015
1030
  decryptable: false,
1031
+ readable: false,
1016
1032
  active: row.active === true,
1017
1033
  archived: typeof row.archivedAt === "number",
1018
1034
  ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
@@ -1032,9 +1048,12 @@ function summarizeAccountSession(row, persisted, liveIds, now) {
1032
1048
  if (!metadata) return base;
1033
1049
  const meta = metadata;
1034
1050
  const pending = pendingRequestsOf(agentState, now);
1051
+ const backgroundTasks = persistence.backgroundTasksReport(agentState?.backgroundTasks, now, { active: row.active === true });
1035
1052
  return {
1036
1053
  ...base,
1037
1054
  decryptable: true,
1055
+ readable: true,
1056
+ backgroundTasks,
1038
1057
  ...meta.summary?.text ? { title: meta.summary.text } : {},
1039
1058
  ...meta.path ? { cwd: meta.path } : {},
1040
1059
  ...meta.flavor ? { flavor: meta.flavor } : {},
@@ -1064,7 +1083,7 @@ function orderAccountSessions(summaries, options = {}) {
1064
1083
  return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
1065
1084
  }
1066
1085
  async function listAccountSessions(options = {}) {
1067
- const token = await bearerToken();
1086
+ const token = await bearerToken$1();
1068
1087
  const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions`, {
1069
1088
  headers: {
1070
1089
  "Authorization": `Bearer ${token}`,
@@ -1080,7 +1099,7 @@ async function listAccountSessions(options = {}) {
1080
1099
  const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
1081
1100
  return orderAccountSessions(summaries, options);
1082
1101
  }
1083
- async function bearerToken() {
1102
+ async function bearerToken$1() {
1084
1103
  const credentials = await persistence.readCredentialsForConfiguredRelay();
1085
1104
  if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
1086
1105
  return credentials.token;
@@ -1095,7 +1114,7 @@ function requirePersisted(sessionId) {
1095
1114
  return persisted;
1096
1115
  }
1097
1116
  async function readSessionLog(sessionId, persisted, limit) {
1098
- const token = await bearerToken();
1117
+ const token = await bearerToken$1();
1099
1118
  const response = await axios.get(
1100
1119
  `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
1101
1120
  {
@@ -1134,7 +1153,7 @@ async function readSessionTranscript(sessionId, limit, options = {}) {
1134
1153
  };
1135
1154
  }
1136
1155
  async function readSessionMetadata(sessionId, persisted) {
1137
- const token = await bearerToken();
1156
+ const token = await bearerToken$1();
1138
1157
  const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
1139
1158
  headers: {
1140
1159
  "Authorization": `Bearer ${token}`,
@@ -1147,7 +1166,7 @@ async function readSessionMetadata(sessionId, persisted) {
1147
1166
  return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(ciphertext));
1148
1167
  }
1149
1168
  async function readSessionState(sessionId, persisted) {
1150
- const token = await bearerToken();
1169
+ const token = await bearerToken$1();
1151
1170
  const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
1152
1171
  headers: {
1153
1172
  "Authorization": `Bearer ${token}`,
@@ -1199,7 +1218,7 @@ async function stopSession(sessionId) {
1199
1218
  return await persistence.stopDaemonSession(sessionId);
1200
1219
  }
1201
1220
  async function archiveSession(sessionId) {
1202
- const token = await bearerToken();
1221
+ const token = await bearerToken$1();
1203
1222
  await axios.post(
1204
1223
  `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
1205
1224
  {},
@@ -1213,7 +1232,7 @@ async function archiveSession(sessionId) {
1213
1232
  );
1214
1233
  }
1215
1234
  async function unarchiveSession(sessionId) {
1216
- const token = await bearerToken();
1235
+ const token = await bearerToken$1();
1217
1236
  await axios.post(
1218
1237
  `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
1219
1238
  {},
@@ -1249,7 +1268,7 @@ function explainNotLive(sessionId, status, resumeHint) {
1249
1268
  }
1250
1269
  }
1251
1270
  const CLI_RESUME_HINT = "Use --resume to bring it back on this machine, or spawn a new session.";
1252
- const defaultDeps = {
1271
+ const defaultDeps$1 = {
1253
1272
  listLive: () => persistence.listDaemonSessions(),
1254
1273
  readState: (sessionId, persisted) => readSessionState(sessionId, persisted),
1255
1274
  send: (sessionId, persisted, text, client, options) => sendUserMessage(sessionId, persisted, text, client, options),
@@ -1260,7 +1279,7 @@ const defaultDeps = {
1260
1279
  now: () => Date.now()
1261
1280
  };
1262
1281
  async function deliverToSession(sessionId, persisted, text, client, options = {}, deps = {}) {
1263
- const d = { ...defaultDeps, ...deps };
1282
+ const d = { ...defaultDeps$1, ...deps };
1264
1283
  const resumeHint = options.resumeHint ?? CLI_RESUME_HINT;
1265
1284
  const classify = async () => {
1266
1285
  const [live, server] = await Promise.all([d.listLive(), d.readState(sessionId, persisted)]);
@@ -1351,6 +1370,10 @@ async function waitUntilLive(classify, d, options) {
1351
1370
  const SESSION_PEER_SENT_FROM = "session-peer";
1352
1371
  const CLI_PEER_SENDER_ID = "cli";
1353
1372
  const MESSAGE_FOOTER = "This message comes from another agent session on this machine, not from the user.";
1373
+ const MESSAGE_FOOTER_PREFIX = "This message comes from another agent session";
1374
+ function messageFooterFor(sender) {
1375
+ return sender.machine ? `${MESSAGE_FOOTER_PREFIX} on machine ${sanitizeHeaderValue(sender.machine, 128)}, not from the user.` : MESSAGE_FOOTER;
1376
+ }
1354
1377
  function sanitizeHeaderValue(value, max = 200) {
1355
1378
  if (!value) return "";
1356
1379
  const collapsed = value.replace(/\s+/g, " ").trim().slice(0, max);
@@ -1360,6 +1383,7 @@ function senderFields(sender) {
1360
1383
  const parts = [];
1361
1384
  if (sender.flavor) parts.push(`agent ${sanitizeHeaderValue(sender.flavor, 32)}`);
1362
1385
  if (sender.cwd) parts.push(`cwd ${sanitizeHeaderValue(sender.cwd, 400)}`);
1386
+ if (sender.machine) parts.push(`machine ${sanitizeHeaderValue(sender.machine, 128)}`);
1363
1387
  return parts.map((part) => `; ${part}`).join("");
1364
1388
  }
1365
1389
  function quotedTitle(sender) {
@@ -1367,7 +1391,9 @@ function quotedTitle(sender) {
1367
1391
  }
1368
1392
  function formatSessionPeerMessage(input) {
1369
1393
  const header = `[Very Happy session message ${input.id} from ${quotedTitle(input.from)} ${input.from.sessionId}${senderFields(input.from)}${input.replyTo ? `; re ${sanitizeHeaderValue(input.replyTo, 64)}` : ""}]`;
1370
- const footer = input.from.sessionId === CLI_PEER_SENDER_ID ? `${MESSAGE_FOOTER} It was sent from the command line (\`very-happy sessions message\`), so there is no session to reply to; act on it here.` : `${MESSAGE_FOOTER} Reply with session_message(to: "${input.from.sessionId}") \u2014 or \`very-happy sessions message ${input.from.sessionId} "<text>"\` \u2014 only when you have something to add (a question, a decision, a heads-up). Do not reply just to acknowledge or thank; the exchange ends when nothing is left to coordinate.`;
1394
+ const base = messageFooterFor(input.from);
1395
+ const routed = input.from.machine ? " (it is routed to that machine for you)" : "";
1396
+ const footer = input.from.sessionId === CLI_PEER_SENDER_ID ? `${base} It was sent from the command line (\`very-happy sessions message\`), so there is no session to reply to; act on it here.` : `${base} Reply with session_message(to: "${input.from.sessionId}")${routed} \u2014 or \`very-happy sessions message ${input.from.sessionId} "<text>"\` \u2014 only when you have something to add (a question, a decision, a heads-up). Do not reply just to acknowledge or thank; the exchange ends when nothing is left to coordinate.`;
1371
1397
  return `${header}
1372
1398
  ${input.body.trim()}
1373
1399
 
@@ -1429,7 +1455,7 @@ function resolveRepoIdentity(cwd, fs = realFs) {
1429
1455
  }
1430
1456
  return { root: null, common: null };
1431
1457
  }
1432
- const PEER_SCOPES = ["repo", "cwd", "machine"];
1458
+ const PEER_SCOPES$1 = ["repo", "cwd", "machine"];
1433
1459
  function isPeerInScope(scope, self, other) {
1434
1460
  if (scope === "machine") return true;
1435
1461
  if (scope === "cwd") return path.resolve(self.cwd) === path.resolve(other.cwd);
@@ -1437,24 +1463,508 @@ function isPeerInScope(scope, self, other) {
1437
1463
  return path.resolve(self.cwd) === path.resolve(other.cwd);
1438
1464
  }
1439
1465
 
1466
+ const REMOTE_SESSION_OPS_MIN_CLI_VERSION = "0.2.157";
1467
+ const REMOTE_OPS_PROTOCOL_VERSION = 1;
1468
+ const REMOTE_TEXT_MAX_BYTES = 64 * 1024;
1469
+ const REMOTE_TRANSCRIPT_MAX_BYTES = 200 * 1024;
1470
+ const REMOTE_CALLS_PER_MINUTE = 60;
1471
+ const REMOTE_WRITE_CALLS_PER_MINUTE = 30;
1472
+ const REMOTE_LOCATE_MAX_IDS = 200;
1473
+ const REMOTE_OP_DEADLINE_MS = 25e3;
1474
+ const REMOTE_SEND_COMMIT_DEADLINE_MS = 18e3;
1475
+ const REMOTE_RESUME_WAIT_MS = 1e4;
1476
+ const REMOTE_WAIT_POLL_MS = 5e3;
1477
+ const REMOTE_ANSWER_MAX_BYTES = 64 * 1024;
1478
+ const REMOTE_TITLE_MAX_BYTES = 1024;
1479
+ const REMOTE_SENT_FROM_ALLOWED = ["cli", "assistant", "automation"];
1480
+ const LOCAL_ID_RE = /^[a-zA-Z0-9_.:-]{1,128}$/;
1481
+ function remoteError(code, message) {
1482
+ return { ok: false, error: { code, message } };
1483
+ }
1484
+ const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
1485
+ const PEER_SCOPES = ["repo", "cwd", "machine"];
1486
+ function isSessionId(value) {
1487
+ return typeof value === "string" && SESSION_ID_RE.test(value);
1488
+ }
1489
+ function utf8Bytes(text) {
1490
+ return Buffer.byteLength(text, "utf8");
1491
+ }
1492
+ function sanitizeSender(raw) {
1493
+ if (!raw || typeof raw !== "object") return null;
1494
+ const r = raw;
1495
+ if (typeof r.sessionId !== "string" || r.sessionId.length === 0 || r.sessionId.length > 128) return null;
1496
+ const optional = (key, max) => typeof r[key] === "string" && r[key].length > 0 ? { [key]: r[key].slice(0, max) } : {};
1497
+ return {
1498
+ sessionId: r.sessionId,
1499
+ ...optional("title", 200),
1500
+ ...optional("flavor", 32),
1501
+ ...optional("cwd", 1024),
1502
+ ...optional("machine", 128)
1503
+ };
1504
+ }
1505
+ function guardRemoteSessionOpsRequest(method, raw, options) {
1506
+ if (!options.enabled) {
1507
+ return remoteError("disabled", 'Remote session operations are turned off on this machine (settings.json remoteSessionOps = "off")');
1508
+ }
1509
+ if (!raw || typeof raw !== "object") return remoteError("bad_request", "Request must be an object");
1510
+ const r = raw;
1511
+ if (r.v !== REMOTE_OPS_PROTOCOL_VERSION) return remoteError("bad_request", `Unsupported protocol version ${String(r.v)} (this daemon speaks v${REMOTE_OPS_PROTOCOL_VERSION})`);
1512
+ const from = r.from;
1513
+ if (!from || typeof from !== "object" || typeof from.host !== "string" || from.host.length === 0 || typeof from.cli !== "string") {
1514
+ return remoteError("bad_request", "from.host and from.cli are required");
1515
+ }
1516
+ const caller = {
1517
+ host: from.host.slice(0, 128),
1518
+ cli: from.cli.slice(0, 32),
1519
+ ...typeof from.machineId === "string" && from.machineId.length > 0 ? { machineId: from.machineId.slice(0, 128) } : {},
1520
+ ...isSessionId(from.sessionId) ? { sessionId: from.sessionId } : {}
1521
+ };
1522
+ const args = r.args && typeof r.args === "object" ? r.args : {};
1523
+ const limit = typeof args.limit === "number" && Number.isFinite(args.limit) && args.limit >= 1 ? Math.floor(args.limit) : void 0;
1524
+ let clean;
1525
+ switch (method) {
1526
+ case "sessions.list": {
1527
+ if (args.ids !== void 0) {
1528
+ if (!Array.isArray(args.ids) || !args.ids.every(isSessionId)) return remoteError("bad_request", "ids must be an array of session ids");
1529
+ if (args.ids.length > REMOTE_LOCATE_MAX_IDS) return remoteError("too_large", `ids: at most ${REMOTE_LOCATE_MAX_IDS} per call`);
1530
+ }
1531
+ const out = {
1532
+ ...args.ids !== void 0 ? { ids: args.ids } : {},
1533
+ ...args.all === true ? { all: true } : {},
1534
+ ...typeof args.tag === "string" && args.tag.length > 0 ? { tag: args.tag.slice(0, 128) } : {},
1535
+ ...limit !== void 0 ? { limit } : {}
1536
+ };
1537
+ clean = out;
1538
+ break;
1539
+ }
1540
+ case "sessions.read": {
1541
+ if (!isSessionId(args.sessionId)) return remoteError("bad_request", "sessionId is required");
1542
+ const out = { sessionId: args.sessionId, ...limit !== void 0 ? { limit } : {}, ...args.full === true ? { full: true } : {} };
1543
+ clean = out;
1544
+ break;
1545
+ }
1546
+ case "sessions.send": {
1547
+ if (!isSessionId(args.sessionId)) return remoteError("bad_request", "sessionId is required");
1548
+ if (typeof args.text !== "string" || args.text.trim().length === 0) return remoteError("bad_request", "text must be non-empty");
1549
+ if (utf8Bytes(args.text) > REMOTE_TEXT_MAX_BYTES) return remoteError("too_large", `text exceeds ${REMOTE_TEXT_MAX_BYTES} bytes`);
1550
+ if (args.localId !== void 0 && (typeof args.localId !== "string" || !LOCAL_ID_RE.test(args.localId))) return remoteError("bad_request", "localId must be a short id");
1551
+ const out = {
1552
+ sessionId: args.sessionId,
1553
+ text: args.text,
1554
+ ...args.resume === true ? { resume: true } : {},
1555
+ ...args.model === null || typeof args.model === "string" && args.model.length <= 128 ? { model: args.model } : {},
1556
+ ...REMOTE_SENT_FROM_ALLOWED.includes(args.sentFrom) ? { sentFrom: args.sentFrom } : {},
1557
+ ...typeof args.localId === "string" ? { localId: args.localId } : {}
1558
+ };
1559
+ clean = out;
1560
+ break;
1561
+ }
1562
+ case "sessions.peers": {
1563
+ if (args.scope !== void 0 && !PEER_SCOPES.includes(args.scope)) return remoteError("bad_request", `scope must be one of ${PEER_SCOPES.join(", ")}`);
1564
+ const out = {
1565
+ ...args.scope !== void 0 ? { scope: args.scope } : {},
1566
+ ...typeof args.cwd === "string" && args.cwd.length > 0 ? { cwd: args.cwd.slice(0, 4096) } : {}
1567
+ };
1568
+ clean = out;
1569
+ break;
1570
+ }
1571
+ case "sessions.message": {
1572
+ if (!isSessionId(args.to)) return remoteError("bad_request", "to is required");
1573
+ if (typeof args.body !== "string" || args.body.trim().length === 0) return remoteError("bad_request", "body must be non-empty");
1574
+ if (utf8Bytes(args.body) > REMOTE_TEXT_MAX_BYTES) return remoteError("too_large", `body exceeds ${REMOTE_TEXT_MAX_BYTES} bytes`);
1575
+ const sender = sanitizeSender(args.from);
1576
+ if (!sender) return remoteError("bad_request", "from.sessionId (the sender) is required");
1577
+ const out = {
1578
+ to: args.to,
1579
+ body: args.body,
1580
+ ...typeof args.replyTo === "string" && args.replyTo.length > 0 ? { replyTo: args.replyTo.slice(0, 64) } : {},
1581
+ // The recipient's header names the machine the call came
1582
+ // from, as the caller identified itself — never a value the
1583
+ // sender picked for someone else.
1584
+ from: { ...sender, machine: caller.host }
1585
+ };
1586
+ clean = out;
1587
+ break;
1588
+ }
1589
+ }
1590
+ return { ok: true, request: { v: REMOTE_OPS_PROTOCOL_VERSION, from: caller, args: clean } };
1591
+ }
1592
+ class RemoteCallBudget {
1593
+ constructor(perMinute = REMOTE_CALLS_PER_MINUTE, now = Date.now) {
1594
+ this.perMinute = perMinute;
1595
+ this.now = now;
1596
+ this.tokens = perMinute;
1597
+ this.lastRefill = now();
1598
+ }
1599
+ tokens;
1600
+ lastRefill;
1601
+ /** Take one token; false = over budget (with ms until the next token). */
1602
+ take() {
1603
+ if (this.perMinute <= 0) return { ok: true };
1604
+ const t = this.now();
1605
+ const refill = (t - this.lastRefill) / 6e4 * this.perMinute;
1606
+ if (refill > 0) {
1607
+ this.tokens = Math.min(this.perMinute, this.tokens + refill);
1608
+ this.lastRefill = t;
1609
+ }
1610
+ if (this.tokens >= 1) {
1611
+ this.tokens -= 1;
1612
+ return { ok: true };
1613
+ }
1614
+ return { ok: false, retryAfterMs: Math.ceil((1 - this.tokens) / this.perMinute * 6e4) };
1615
+ }
1616
+ }
1617
+ function sanitizeAuditValue(value, max = 64) {
1618
+ if (typeof value !== "string" || value.length === 0) return "?";
1619
+ return value.slice(0, max).replace(/[^\x20-\x7e]/g, "?");
1620
+ }
1621
+ function formatRemoteAuditLine(input) {
1622
+ const from = input.from;
1623
+ const opt = (value) => value === void 0 ? "-" : sanitizeAuditValue(value);
1624
+ return `[REMOTE SESSION OPS] ${sanitizeAuditValue(input.method)} from machine=${sanitizeAuditValue(from?.machineId)} host=${sanitizeAuditValue(from?.host)} cli=${sanitizeAuditValue(from?.cli, 32)} session=${opt(from?.sessionId)} target=${opt(input.sessionId)} \u2192 ${input.outcome} (${Math.max(0, Math.round(input.durationMs))}ms)`;
1625
+ }
1626
+ function capRemoteText(text, maxBytes, keep = "head") {
1627
+ if (utf8Bytes(text) <= maxBytes) return text;
1628
+ const buffer = Buffer.from(text, "utf8");
1629
+ const marker = "\u2026";
1630
+ const room = Math.max(0, maxBytes - Buffer.byteLength(marker));
1631
+ const slice = keep === "head" ? buffer.subarray(0, room) : buffer.subarray(buffer.length - room);
1632
+ const clean = slice.toString("utf8").replace(/^�+|�+$/g, "");
1633
+ return keep === "head" ? clean + marker : marker + clean;
1634
+ }
1635
+ function capRemoteTranscript(transcript, maxBytes = REMOTE_TRANSCRIPT_MAX_BYTES) {
1636
+ if (utf8Bytes(transcript) <= maxBytes) return { transcript, truncated: false };
1637
+ const buffer = Buffer.from(transcript, "utf8");
1638
+ const marker = "\u2026 [truncated by the remote machine: transcript exceeded the RPC budget; use --limit] \u2026\n";
1639
+ const tail = buffer.subarray(buffer.length - (maxBytes - Buffer.byteLength(marker))).toString("utf8");
1640
+ return { transcript: marker + tail.replace(/^�+/, ""), truncated: true };
1641
+ }
1642
+ function parseHappyClientVersion(value) {
1643
+ if (typeof value !== "string") return null;
1644
+ const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
1645
+ return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
1646
+ }
1647
+ function compareVersions(a, b) {
1648
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
1649
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
1650
+ for (let i = 0; i < 3; i++) {
1651
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1652
+ if (d !== 0) return d;
1653
+ }
1654
+ return 0;
1655
+ }
1656
+ function supportsRemoteSessionOps(cliVersion) {
1657
+ if (!cliVersion) return null;
1658
+ return compareVersions(cliVersion, REMOTE_SESSION_OPS_MIN_CLI_VERSION) >= 0;
1659
+ }
1660
+
1661
+ const REMOTE_CLIENT_TAG = "session-ops";
1662
+ const REMOTE_RPC_TIMEOUT_MS = 3e4;
1663
+ const REMOTE_LOCATE_MAX_MACHINES = 8;
1664
+ class RemoteSessionOpsError extends Error {
1665
+ constructor(code, message, machineId, ambiguous = false) {
1666
+ super(message);
1667
+ this.code = code;
1668
+ this.machineId = machineId;
1669
+ this.ambiguous = ambiguous;
1670
+ }
1671
+ }
1672
+ async function bearerToken() {
1673
+ const credentials = await persistence.readCredentialsForConfiguredRelay();
1674
+ if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
1675
+ return credentials.token;
1676
+ }
1677
+ async function listAccountMachines() {
1678
+ const token = await bearerToken();
1679
+ const response = await axios.get(`${persistence.configuration.serverUrl}/v1/machines`, {
1680
+ headers: { "Authorization": `Bearer ${token}`, "X-Happy-Client": `${REMOTE_CLIENT_TAG}/${persistence.configuration.currentCliVersion}` },
1681
+ timeout: 15e3
1682
+ });
1683
+ const rows = Array.isArray(response.data) ? response.data : [];
1684
+ return rows.filter((row) => !!row && typeof row === "object" && typeof row.id === "string").map((row) => ({
1685
+ id: row.id,
1686
+ active: row.active === true,
1687
+ ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
1688
+ cliVersion: parseHappyClientVersion(row.lastHappyClient)
1689
+ }));
1690
+ }
1691
+ async function callerIdentity(env = process.env) {
1692
+ const settings = await persistence.readSettings().catch(() => null);
1693
+ const sessionId = env.VH_PEER_SESSION_ID;
1694
+ return {
1695
+ ...settings?.machineId ? { machineId: settings.machineId } : {},
1696
+ host: os.hostname(),
1697
+ cli: persistence.configuration.currentCliVersion,
1698
+ ...sessionId && /^[a-zA-Z0-9_-]{1,128}$/.test(sessionId) ? { sessionId } : {}
1699
+ };
1700
+ }
1701
+ const defaultDeps = {
1702
+ listMachines: listAccountMachines,
1703
+ selfMachineId: async () => (await persistence.readSettings().catch(() => null))?.machineId,
1704
+ openTransport: async () => {
1705
+ const socket = await openUserScopedSocket(await bearerToken());
1706
+ return {
1707
+ call: (method, paramsJson) => socket.rpcCall({ method, params: paramsJson }, REMOTE_RPC_TIMEOUT_MS),
1708
+ close: () => socket.close()
1709
+ };
1710
+ },
1711
+ caller: () => callerIdentity(),
1712
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1713
+ now: () => Date.now()
1714
+ };
1715
+ function withDefaults(deps) {
1716
+ return { ...defaultDeps, ...deps };
1717
+ }
1718
+ function pickRemoteCandidates(machines, selfId) {
1719
+ const candidates = [];
1720
+ const skipped = [];
1721
+ for (const machine of machines) {
1722
+ if (selfId && machine.id === selfId) {
1723
+ skipped.push({ machine, reason: "self" });
1724
+ continue;
1725
+ }
1726
+ if (!machine.active) {
1727
+ skipped.push({ machine, reason: "offline" });
1728
+ continue;
1729
+ }
1730
+ if (supportsRemoteSessionOps(machine.cliVersion) === false) {
1731
+ skipped.push({ machine, reason: "too_old" });
1732
+ continue;
1733
+ }
1734
+ candidates.push(machine);
1735
+ }
1736
+ candidates.sort((a, b) => (b.activeAt ?? 0) - (a.activeAt ?? 0));
1737
+ return { candidates: candidates.slice(0, REMOTE_LOCATE_MAX_MACHINES), skipped };
1738
+ }
1739
+ function describeAge(activeAt, now) {
1740
+ if (activeAt === void 0) return "never seen";
1741
+ const minutes = Math.round((now - activeAt) / 6e4);
1742
+ if (minutes < 2) return "seen just now";
1743
+ if (minutes < 120) return `last seen ${minutes}m ago`;
1744
+ if (minutes < 48 * 60) return `last seen ${Math.round(minutes / 60)}h ago`;
1745
+ return `last seen ${Math.round(minutes / 1440)}d ago`;
1746
+ }
1747
+ async function resolveExplicitMachine(machineId, deps) {
1748
+ const machines = await deps.listMachines();
1749
+ const machine = machines.find((row) => row.id === machineId);
1750
+ if (!machine) throw new RemoteSessionOpsError("unknown_machine", `Machine ${machineId} is not on this account`, machineId);
1751
+ if (!machine.active) throw new RemoteSessionOpsError("offline", `Machine ${machineId} is offline (${describeAge(machine.activeAt, deps.now())}); its sessions cannot be reached until its daemon reconnects`, machineId);
1752
+ if (supportsRemoteSessionOps(machine.cliVersion) === false) {
1753
+ throw new RemoteSessionOpsError("too_old", `Machine ${machineId} runs CLI ${machine.cliVersion}; cross-machine session operations need \u2265 ${REMOTE_SESSION_OPS_MIN_CLI_VERSION} on the target`, machineId);
1754
+ }
1755
+ return machine;
1756
+ }
1757
+ async function callRemoteSessionOp(transport, machineId, method, args, from) {
1758
+ const params = JSON.stringify({ v: REMOTE_OPS_PROTOCOL_VERSION, from, args });
1759
+ let ack;
1760
+ try {
1761
+ ack = await transport.call(`${machineId}:${method}`, params);
1762
+ } catch (error) {
1763
+ const reason = error instanceof Error ? error.message : String(error);
1764
+ throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} did not answer ${method} (${reason})`, machineId, true);
1765
+ }
1766
+ if (!ack?.ok) {
1767
+ const reason = ack?.error ?? "no ack";
1768
+ if (/not available/i.test(reason)) {
1769
+ throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} did not answer ${method}: its daemon is offline, restarting, or runs a CLI older than ${REMOTE_SESSION_OPS_MIN_CLI_VERSION}`, machineId);
1770
+ }
1771
+ throw new RemoteSessionOpsError("unreachable", `Machine ${machineId}: ${reason}`, machineId, true);
1772
+ }
1773
+ let envelope;
1774
+ try {
1775
+ envelope = typeof ack.result === "string" ? JSON.parse(ack.result) : ack.result;
1776
+ } catch {
1777
+ throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} returned an unreadable ${method} response`, machineId);
1778
+ }
1779
+ if (!envelope || typeof envelope !== "object") throw new RemoteSessionOpsError("unreachable", `Machine ${machineId} returned an empty ${method} response`, machineId);
1780
+ if ("ok" in envelope) {
1781
+ if (envelope.ok) return envelope.result;
1782
+ throw new RemoteSessionOpsError(envelope.error.code, `Machine ${machineId}: ${envelope.error.message}`, machineId);
1783
+ }
1784
+ throw new RemoteSessionOpsError("internal", `Machine ${machineId}: ${envelope.error ?? "remote handler failed"}`, machineId);
1785
+ }
1786
+ async function locateRemoteSession(transport, sessionId, options, deps) {
1787
+ const from = await deps.caller();
1788
+ if (options.machineId) {
1789
+ const machine = await resolveExplicitMachine(options.machineId, deps);
1790
+ const listing = await callRemoteSessionOp(transport, machine.id, "sessions.list", { ids: [sessionId] }, from);
1791
+ if (!listing.sessions.some((row) => row.id === sessionId)) {
1792
+ throw new RemoteSessionOpsError("no_local_key", `Machine ${machine.id} (${listing.host}) has no key for session ${sessionId}: it was not spawned there (or is older than 14 days)`, machine.id);
1793
+ }
1794
+ return { machine, host: listing.host };
1795
+ }
1796
+ const { candidates, skipped } = pickRemoteCandidates(await deps.listMachines(), await deps.selfMachineId());
1797
+ const failures = [];
1798
+ for (const machine of candidates) {
1799
+ try {
1800
+ const listing = await callRemoteSessionOp(transport, machine.id, "sessions.list", { ids: [sessionId] }, from);
1801
+ if (listing.sessions.some((row) => row.id === sessionId)) return { machine, host: listing.host };
1802
+ failures.push(`${listing.host || machine.id}: not there`);
1803
+ } catch (error) {
1804
+ failures.push(`${machine.id}: ${error instanceof RemoteSessionOpsError ? error.code : error instanceof Error ? error.message : String(error)}`);
1805
+ }
1806
+ }
1807
+ const skippedText = skipped.filter((s) => s.reason !== "self").map((s) => `${s.machine.id}: ${s.reason}`);
1808
+ const detail = [...failures, ...skippedText];
1809
+ throw new RemoteSessionOpsError(
1810
+ "not_located",
1811
+ `Session ${sessionId} is not on this machine and no online machine of this account reports it${detail.length ? ` (${detail.join("; ")})` : " (no other machines are online)"}. Pass --machine <id> to name one, or bring its machine online.`
1812
+ );
1813
+ }
1814
+ async function withRemoteTransport(deps, fn) {
1815
+ const transport = await deps.openTransport();
1816
+ try {
1817
+ return await fn(transport);
1818
+ } finally {
1819
+ try {
1820
+ transport.close();
1821
+ } catch {
1822
+ }
1823
+ }
1824
+ }
1825
+ async function readRemoteTranscript(sessionId, options = {}, overrides = {}) {
1826
+ const deps = withDefaults(overrides);
1827
+ return withRemoteTransport(deps, async (transport) => {
1828
+ const located = await locateRemoteSession(transport, sessionId, { machineId: options.machineId }, deps);
1829
+ return callRemoteSessionOp(transport, located.machine.id, "sessions.read", {
1830
+ sessionId,
1831
+ ...options.limit !== void 0 ? { limit: options.limit } : {},
1832
+ ...options.full ? { full: true } : {}
1833
+ }, await deps.caller());
1834
+ });
1835
+ }
1836
+ async function waitForRemoteTurnEnd(sessionId, options, overrides = {}) {
1837
+ const deps = withDefaults(overrides);
1838
+ const pollMs = options.pollMs ?? REMOTE_WAIT_POLL_MS;
1839
+ return withRemoteTransport(deps, async (transport) => {
1840
+ const located = await locateRemoteSession(transport, sessionId, { machineId: options.machineId }, deps);
1841
+ const from = await deps.caller();
1842
+ const deadline = deps.now() + options.timeoutMs;
1843
+ const args = { sessionId, ...options.limit !== void 0 ? { limit: options.limit } : {}, ...options.full ? { full: true } : {} };
1844
+ while (true) {
1845
+ const read = await callRemoteSessionOp(transport, located.machine.id, "sessions.read", args, from);
1846
+ if (read.turn.ended) return { read, timedOut: false };
1847
+ if (deps.now() + pollMs > deadline) return { read, timedOut: true };
1848
+ await deps.sleep(pollMs);
1849
+ }
1850
+ });
1851
+ }
1852
+ async function sendRemoteMessage(sessionId, text, options = {}, overrides = {}) {
1853
+ const deps = withDefaults(overrides);
1854
+ return withRemoteTransport(deps, async (transport) => {
1855
+ const located = await locateRemoteSession(transport, sessionId, { machineId: options.machineId }, deps);
1856
+ const from = await deps.caller();
1857
+ const localId = options.localId ?? `remote-send-${node_crypto.randomUUID()}`;
1858
+ const args = {
1859
+ sessionId,
1860
+ text,
1861
+ localId,
1862
+ ...options.resume ? { resume: true } : {},
1863
+ ...options.model !== void 0 ? { model: options.model } : {},
1864
+ ...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
1865
+ };
1866
+ const attempt = () => callRemoteSessionOp(transport, located.machine.id, "sessions.send", args, from);
1867
+ let result;
1868
+ try {
1869
+ result = await attempt();
1870
+ } catch (error) {
1871
+ const retryable = error instanceof RemoteSessionOpsError && (error.ambiguous || error.code === "timeout");
1872
+ if (!retryable) throw error;
1873
+ result = await attempt();
1874
+ }
1875
+ return { ...result, machine: { id: located.machine.id, host: located.host } };
1876
+ });
1877
+ }
1878
+ async function sendRemotePeerMessage(args, options = {}, overrides = {}) {
1879
+ const deps = withDefaults(overrides);
1880
+ return withRemoteTransport(deps, async (transport) => {
1881
+ const located = await locateRemoteSession(transport, args.to, { machineId: options.machineId }, deps);
1882
+ const caller = await deps.caller();
1883
+ const result = await callRemoteSessionOp(transport, located.machine.id, "sessions.message", {
1884
+ to: args.to,
1885
+ body: args.body,
1886
+ ...args.replyTo ? { replyTo: args.replyTo } : {},
1887
+ from: { ...args.from, machine: args.from.machine ?? caller.host }
1888
+ }, caller);
1889
+ return { ...result, machine: { id: located.machine.id, host: located.host } };
1890
+ });
1891
+ }
1892
+ async function listRemotePeers(machineId, options = {}, overrides = {}) {
1893
+ const deps = withDefaults(overrides);
1894
+ return withRemoteTransport(deps, async (transport) => {
1895
+ const machine = await resolveExplicitMachine(machineId, deps);
1896
+ return callRemoteSessionOp(transport, machine.id, "sessions.peers", {
1897
+ ...options.scope ? { scope: options.scope } : {},
1898
+ ...options.cwd ? { cwd: options.cwd } : {}
1899
+ }, await deps.caller());
1900
+ });
1901
+ }
1902
+ async function listRemoteSessions(machineId, options = {}, overrides = {}) {
1903
+ const deps = withDefaults(overrides);
1904
+ return withRemoteTransport(deps, async (transport) => {
1905
+ const machine = await resolveExplicitMachine(machineId, deps);
1906
+ return callRemoteSessionOp(transport, machine.id, "sessions.list", {
1907
+ ...options.tag ? { tag: options.tag } : {},
1908
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
1909
+ }, await deps.caller());
1910
+ });
1911
+ }
1912
+ async function fillForeignAccountRows(rows, overrides = {}) {
1913
+ const deps = withDefaults(overrides);
1914
+ const pending = new Set(rows.filter((row) => !row.decryptable).map((row) => row.id));
1915
+ const asked = [];
1916
+ if (pending.size === 0) return { rows, asked, skipped: [] };
1917
+ const { candidates, skipped } = pickRemoteCandidates(await deps.listMachines(), await deps.selfMachineId());
1918
+ const filled = /* @__PURE__ */ new Map();
1919
+ if (candidates.length > 0) {
1920
+ await withRemoteTransport(deps, async (transport) => {
1921
+ const from = await deps.caller();
1922
+ for (const machine of candidates) {
1923
+ if (pending.size === 0) break;
1924
+ try {
1925
+ const listing = await callRemoteSessionOp(transport, machine.id, "sessions.list", { ids: [...pending], all: true }, from);
1926
+ let count = 0;
1927
+ for (const row of listing.sessions) {
1928
+ if (!pending.has(row.id)) continue;
1929
+ pending.delete(row.id);
1930
+ count++;
1931
+ filled.set(row.id, { ...row, decryptable: false, readable: true, via: machine.id, machine: { id: machine.id, host: listing.host } });
1932
+ }
1933
+ asked.push({ machineId: machine.id, host: listing.host, filled: count });
1934
+ } catch (error) {
1935
+ asked.push({ machineId: machine.id, filled: 0, error: error instanceof Error ? error.message : String(error) });
1936
+ }
1937
+ }
1938
+ });
1939
+ }
1940
+ return {
1941
+ rows: rows.map((row) => filled.get(row.id) ?? row),
1942
+ asked,
1943
+ skipped: skipped.filter((s) => s.reason !== "self").map((s) => ({ machineId: s.machine.id, reason: s.reason }))
1944
+ };
1945
+ }
1946
+
1440
1947
  const SESSION_PEER_TOOL_NAMES = ["session_message", "session_peers"];
1441
1948
  const SESSION_PEER_TOOL_SCHEMAS = {
1442
1949
  session_message: {
1443
1950
  title: "Message Another Session",
1444
1951
  readOnly: false,
1445
- description: "Send a message to another agent session running on this machine (find ids with session_peers). It lands in that session's chat with your title, session id and cwd, and tells the peer how to reply to you. Use it to coordinate when you both edit the same files: say what you are changing and ask what they are changing. Returns as soon as the message is queued; the reply arrives later as a message from that session. Only sessions this machine's daemon runs can be reached; terminal-mirror sessions cannot receive messages.",
1952
+ description: "Send a message to another agent session (find ids with session_peers). It lands in that session's chat with your title, session id and cwd, and tells the peer how to reply to you. Use it to coordinate when you both edit the same files: say what you are changing and ask what they are changing. Returns as soon as the message is queued; the reply arrives later as a message from that session. Sessions on this machine are reached directly; a session spawned by another machine of this account is routed to that machine's daemon (it must be online). Terminal-mirror sessions cannot receive messages.",
1446
1953
  inputSchema: {
1447
1954
  to: z.z.string().min(1).max(128).describe("Target session id (from session_peers, or a peer's message header)"),
1448
1955
  body: z.z.string().min(1).max(16e3).describe("The message text"),
1449
- replyTo: z.z.string().min(1).max(64).optional().describe("Id of the peer message you are answering (from its header), if any")
1956
+ replyTo: z.z.string().min(1).max(64).optional().describe("Id of the peer message you are answering (from its header), if any"),
1957
+ machineId: z.z.string().min(1).max(128).optional().describe("Machine id the target session runs on, when known (skips the lookup across the account's machines)")
1450
1958
  }
1451
1959
  },
1452
1960
  session_peers: {
1453
1961
  title: "List Peer Sessions",
1454
1962
  readOnly: true,
1455
- description: 'List the other agent sessions running on this machine and the files each edited in the last 30 minutes. Default scope "repo": sessions in the same git repository, including its other worktrees (sameWorktree says whether they share your checkout). "cwd": the same directory only. "machine": every live session here.',
1963
+ description: 'List the other agent sessions running on this machine and the files each edited in the last 30 minutes. Default scope "repo": sessions in the same git repository, including its other worktrees (sameWorktree says whether they share your checkout). "cwd": the same directory only. "machine": every live session here. With machineId, list the live sessions on that other machine of this account instead (scope "machine" unless you pass a cwd that exists there).',
1456
1964
  inputSchema: {
1457
- scope: z.z.enum(["repo", "cwd", "machine"]).optional().describe("repo (default) | cwd | machine")
1965
+ scope: z.z.enum(["repo", "cwd", "machine"]).optional().describe("repo (default) | cwd | machine"),
1966
+ machineId: z.z.string().min(1).max(128).optional().describe("Another machine of this account (from the web's machine list) to list instead of this one"),
1967
+ cwd: z.z.string().min(1).max(4096).optional().describe("With machineId: a directory on that machine to compute repo/cwd scope from")
1458
1968
  }
1459
1969
  }
1460
1970
  };
@@ -1495,19 +2005,27 @@ async function sendPeerMessage(context, args) {
1495
2005
  if (args.to === self.sessionId) throw new Error("That is this session; message a peer instead");
1496
2006
  const body = typeof args.body === "string" ? args.body.trim() : "";
1497
2007
  if (!body) throw new Error("body must be non-empty");
2008
+ const now = (context.now ?? Date.now)();
1498
2009
  const persisted = (context.readPersisted ?? persistence.readPersistedSessions)()[args.to];
1499
- if (!persisted) {
1500
- throw new Error(`Session ${args.to} is not on this machine (no local key); messaging sessions on other machines is not supported yet`);
2010
+ if (!persisted || args.machineId) {
2011
+ const remote = context.remoteMessage === void 0 ? sendRemotePeerMessage : context.remoteMessage;
2012
+ if (remote === null) {
2013
+ throw new Error(`Session ${args.to} is not on this machine (no local key)`);
2014
+ }
2015
+ if (context.sentTo) {
2016
+ const budget = checkPeerSendBudget(context.sentTo, args.to, now);
2017
+ if (!budget.ok) throw new Error(replyLoopRefusal(budget.count, args.to));
2018
+ }
2019
+ const result2 = await remote({ to: args.to, body, replyTo: args.replyTo, from: self, machineId: args.machineId });
2020
+ context.sentTo?.get(args.to)?.push(now);
2021
+ return result2;
1501
2022
  }
1502
2023
  const live = (await (context.listPeers ?? persistence.listDaemonPeers)()).find((session) => session.sessionId === args.to);
1503
2024
  if (!live) throw new Error(`Session ${args.to} is not running on this machine; nothing would read the message`);
1504
2025
  if (live.kind === "mirror") throw new Error(`Session ${args.to} is a terminal mirror (a person typing claude in a terminal); it cannot receive messages`);
1505
- const now = (context.now ?? Date.now)();
1506
2026
  if (context.sentTo) {
1507
2027
  const budget = checkPeerSendBudget(context.sentTo, args.to, now);
1508
- if (!budget.ok) {
1509
- throw new Error(`Refused: this session already sent ${budget.count} messages to ${args.to} in the last ${Math.round(PEER_MESSAGE_RATE_WINDOW_MS / 6e4)} minutes. That is a reply loop, not coordination \u2014 STOP replying to that session now; do not send acknowledgements or thanks. Continue your own work.`);
1510
- }
2028
+ if (!budget.ok) throw new Error(replyLoopRefusal(budget.count, args.to));
1511
2029
  }
1512
2030
  const messageId = node_crypto.randomUUID().slice(0, 8);
1513
2031
  const text = formatSessionPeerMessage({ id: messageId, from: self, body, replyTo: args.replyTo });
@@ -1523,13 +2041,27 @@ async function sendPeerMessage(context, args) {
1523
2041
  ...result.error ? { error: result.error } : {}
1524
2042
  };
1525
2043
  }
2044
+ function replyLoopRefusal(count, to) {
2045
+ return `Refused: this session already sent ${count} messages to ${to} in the last ${Math.round(PEER_MESSAGE_RATE_WINDOW_MS / 6e4)} minutes. That is a reply loop, not coordination \u2014 STOP replying to that session now; do not send acknowledgements or thanks. Continue your own work.`;
2046
+ }
1526
2047
  async function executeSessionPeerTool(name, args, context) {
1527
2048
  switch (name) {
1528
2049
  case "session_message":
1529
- return sendPeerMessage(context, { to: String(args?.to ?? ""), body: String(args?.body ?? ""), ...args?.replyTo ? { replyTo: String(args.replyTo) } : {} });
2050
+ return sendPeerMessage(context, {
2051
+ to: String(args?.to ?? ""),
2052
+ body: String(args?.body ?? ""),
2053
+ ...args?.replyTo ? { replyTo: String(args.replyTo) } : {},
2054
+ ...args?.machineId ? { machineId: String(args.machineId) } : {}
2055
+ });
1530
2056
  case "session_peers": {
1531
2057
  const scope = args?.scope;
1532
- if (scope !== void 0 && !PEER_SCOPES.includes(scope)) throw new Error(`scope must be one of ${PEER_SCOPES.join(", ")}`);
2058
+ if (scope !== void 0 && !PEER_SCOPES$1.includes(scope)) throw new Error(`scope must be one of ${PEER_SCOPES$1.join(", ")}`);
2059
+ if (args?.machineId) {
2060
+ return (context.remotePeers ?? listRemotePeers)(String(args.machineId), {
2061
+ ...scope !== void 0 ? { scope } : {},
2062
+ ...typeof args.cwd === "string" && args.cwd ? { cwd: args.cwd } : {}
2063
+ });
2064
+ }
1533
2065
  return listPeerSessions(context, scope ?? "repo");
1534
2066
  }
1535
2067
  }
@@ -1616,15 +2148,24 @@ exports.AGENT_GUIDANCE = AGENT_GUIDANCE;
1616
2148
  exports.AUTOMATIONS_UNAVAILABLE_MESSAGE = AUTOMATIONS_UNAVAILABLE_MESSAGE;
1617
2149
  exports.AUTOMATION_TOOL_NAMES = AUTOMATION_TOOL_NAMES;
1618
2150
  exports.AutomationsRequestError = AutomationsRequestError;
2151
+ exports.CLI_PEER_SENDER_ID = CLI_PEER_SENDER_ID;
1619
2152
  exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT;
1620
2153
  exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
1621
- exports.PEER_SCOPES = PEER_SCOPES;
2154
+ exports.PEER_SCOPES = PEER_SCOPES$1;
1622
2155
  exports.PREVIEW_TOOL_DESCRIPTION = PREVIEW_TOOL_DESCRIPTION;
1623
2156
  exports.PREVIEW_TOOL_NAME = PREVIEW_TOOL_NAME;
1624
2157
  exports.PREVIEW_TOOL_TITLE = PREVIEW_TOOL_TITLE;
2158
+ exports.REMOTE_ANSWER_MAX_BYTES = REMOTE_ANSWER_MAX_BYTES;
2159
+ exports.REMOTE_OP_DEADLINE_MS = REMOTE_OP_DEADLINE_MS;
2160
+ exports.REMOTE_RESUME_WAIT_MS = REMOTE_RESUME_WAIT_MS;
2161
+ exports.REMOTE_SEND_COMMIT_DEADLINE_MS = REMOTE_SEND_COMMIT_DEADLINE_MS;
2162
+ exports.REMOTE_TITLE_MAX_BYTES = REMOTE_TITLE_MAX_BYTES;
2163
+ exports.REMOTE_WRITE_CALLS_PER_MINUTE = REMOTE_WRITE_CALLS_PER_MINUTE;
1625
2164
  exports.REPORT_PROGRESS_TOOL_DESCRIPTION = REPORT_PROGRESS_TOOL_DESCRIPTION;
1626
2165
  exports.REPORT_PROGRESS_TOOL_NAME = REPORT_PROGRESS_TOOL_NAME;
1627
2166
  exports.REPORT_PROGRESS_TOOL_TITLE = REPORT_PROGRESS_TOOL_TITLE;
2167
+ exports.RemoteCallBudget = RemoteCallBudget;
2168
+ exports.RemoteSessionOpsError = RemoteSessionOpsError;
1628
2169
  exports.SESSION_PEER_SENT_FROM = SESSION_PEER_SENT_FROM;
1629
2170
  exports.SESSION_PEER_TOOL_NAMES = SESSION_PEER_TOOL_NAMES;
1630
2171
  exports.TEAM_TOOL_NAMES = TEAM_TOOL_NAMES;
@@ -1635,16 +2176,24 @@ exports.analyzeLatestTurn = analyzeLatestTurn;
1635
2176
  exports.archiveSession = archiveSession;
1636
2177
  exports.authenticatedAutomationsClient = authenticatedAutomationsClient;
1637
2178
  exports.automationRunIdFromEnv = automationRunIdFromEnv;
2179
+ exports.capRemoteText = capRemoteText;
2180
+ exports.capRemoteTranscript = capRemoteTranscript;
1638
2181
  exports.cliPeerSelf = cliPeerSelf;
1639
2182
  exports.createAutomationsClient = createAutomationsClient;
1640
2183
  exports.createSessionPeerToolExecutor = createSessionPeerToolExecutor;
1641
2184
  exports.createTeamsClient = createTeamsClient;
1642
2185
  exports.deliverToSession = deliverToSession;
2186
+ exports.fillForeignAccountRows = fillForeignAccountRows;
1643
2187
  exports.formatEditConflictNotice = formatEditConflictNotice;
2188
+ exports.formatRemoteAuditLine = formatRemoteAuditLine;
2189
+ exports.guardRemoteSessionOpsRequest = guardRemoteSessionOpsRequest;
1644
2190
  exports.isAutomationsUnavailable = isAutomationsUnavailable;
1645
2191
  exports.listAccountSessions = listAccountSessions;
1646
2192
  exports.listPeerSessions = listPeerSessions;
2193
+ exports.listRemotePeers = listRemotePeers;
2194
+ exports.listRemoteSessions = listRemoteSessions;
1647
2195
  exports.listSessions = listSessions;
2196
+ exports.readRemoteTranscript = readRemoteTranscript;
1648
2197
  exports.readSessionLog = readSessionLog;
1649
2198
  exports.readSessionMetadata = readSessionMetadata;
1650
2199
  exports.readSessionTranscript = readSessionTranscript;
@@ -1652,8 +2201,10 @@ exports.readTeamScope = readTeamScope;
1652
2201
  exports.registerAutomationTools = registerAutomationTools;
1653
2202
  exports.registerSessionPeerTools = registerSessionPeerTools;
1654
2203
  exports.registerTeamsTools = registerTeamsTools;
2204
+ exports.remoteError = remoteError;
1655
2205
  exports.resolvePermissionRequest = resolvePermissionRequest;
1656
2206
  exports.sendPeerMessage = sendPeerMessage;
2207
+ exports.sendRemoteMessage = sendRemoteMessage;
1657
2208
  exports.sendUserMessage = sendUserMessage;
1658
2209
  exports.sessionWebUrl = sessionWebUrl;
1659
2210
  exports.stopSession = stopSession;
@@ -1661,5 +2212,6 @@ exports.teamRequest = teamRequest;
1661
2212
  exports.teamScopePath = teamScopePath;
1662
2213
  exports.trimIdent = trimIdent;
1663
2214
  exports.userPromptText = userPromptText;
2215
+ exports.waitForRemoteTurnEnd = waitForRemoteTurnEnd;
1664
2216
  exports.waitForSessionKey = waitForSessionKey;
1665
2217
  exports.waitForTurnEnd = waitForTurnEnd;