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
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listAccountSessions, e as listSessions, f as cliPeerSelf, g as listPeerSessions, s as sendPeerMessage, w as waitForTurnEnd, T as TurnWaitTimeoutError, h as readSessionTranscript, i as stopSession, j as resolvePermissionRequest, k as archiveSession, m as PEER_SCOPES } from './agentGuidance-C604n0Zq.mjs';
3
- import { d as readPersistedSessions, i as isValidSessionId } from './limits-DmMdchTz.mjs';
2
+ import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listAccountSessions, f as fillForeignAccountRows, e as listRemoteSessions, g as listSessions, h as cliPeerSelf, i as listRemotePeers, j as listPeerSessions, s as sendPeerMessage, w as waitForTurnEnd, T as TurnWaitTimeoutError, k as readSessionTranscript, m as waitForRemoteTurnEnd, n as readRemoteTranscript, o as stopSession, p as resolvePermissionRequest, q as archiveSession, t as PEER_SCOPES } from './agentGuidance-CrR9l62U.mjs';
3
+ import { d as readPersistedSessions, i as isValidSessionId } from './limits-D2uN090G.mjs';
4
4
  import { resolve } from 'node:path';
5
5
  import 'zod';
6
6
  import 'node:crypto';
@@ -13,6 +13,7 @@ import '@paralleldrive/cuid2';
13
13
  import 'tweetnacl';
14
14
  import 'node:util';
15
15
 
16
+ const MACHINE_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
16
17
  const ACTIONS = ["list", "read", "stop", "archive", "approve", "deny", "peers", "message"];
17
18
  const ACTIONS_NEEDING_ID = /* @__PURE__ */ new Set(["read", "stop", "archive", "approve", "deny", "message"]);
18
19
  const ACTIONS_NEEDING_REQUEST_ID = /* @__PURE__ */ new Set(["approve", "deny"]);
@@ -65,6 +66,10 @@ function parseSessionsArgs(args) {
65
66
  const value = args[++i];
66
67
  if (value === void 0 || value.trim() === "") throw new Error("--reply-to requires a value");
67
68
  options.replyTo = value;
69
+ } else if (arg === "--machine") {
70
+ const value = args[++i];
71
+ if (value === void 0 || !MACHINE_ID_RE.test(value)) throw new Error("--machine requires a machine id");
72
+ options.machine = value;
68
73
  } else if (arg === "--tag") {
69
74
  const value = args[++i];
70
75
  if (value === void 0) throw new Error("--tag requires a value");
@@ -130,31 +135,45 @@ function parseSessionsArgs(args) {
130
135
  if (options.cwd !== void 0) throw new Error("--cwd only applies to `sessions peers`");
131
136
  }
132
137
  if (options.replyTo !== void 0 && options.action !== "message") throw new Error("--reply-to only applies to `sessions message`");
138
+ if (options.machine !== void 0) {
139
+ if (!MACHINE_ACTIONS.has(options.action)) throw new Error("--machine only applies to `sessions list|read|peers|message`");
140
+ if (options.all) throw new Error("--machine cannot be combined with --all (--all already asks every online machine)");
141
+ }
133
142
  return options;
134
143
  }
144
+ const MACHINE_ACTIONS = /* @__PURE__ */ new Set(["list", "read", "peers", "message"]);
135
145
  const SESSIONS_HELP = `
136
- ${chalk.bold("very-happy sessions")} - Inspect and control sessions on this machine (for automation)
146
+ ${chalk.bold("very-happy sessions")} - Inspect and control sessions on this machine and, through their daemons, on the account's other machines (for automation)
137
147
 
138
148
  ${chalk.bold("Usage:")}
139
- very-happy sessions list [--all [--include-archived]] [--tag <name>] [--limit <n>] [--json]
140
- very-happy sessions read <id> [--limit <n>] [--full] [--answer]
149
+ very-happy sessions list [--all [--include-archived]] [--machine <id>] [--tag <name>] [--limit <n>] [--json]
150
+ very-happy sessions read <id> [--machine <id>] [--limit <n>] [--full] [--answer]
141
151
  [--wait [--timeout <s>]] [--json]
142
152
  very-happy sessions stop <id> [--json]
143
153
  very-happy sessions archive <id> [--json]
144
154
  very-happy sessions approve <id> <requestId> [--for-session] [--json]
145
155
  very-happy sessions deny <id> <requestId> [--reason <text>] [--json]
146
- very-happy sessions peers [--scope repo|cwd|machine] [--cwd <dir>] [--json]
147
- very-happy sessions message <id> <text> [--reply-to <msgId>] [--json]
156
+ very-happy sessions peers [--scope repo|cwd|machine] [--cwd <dir>] [--machine <id>] [--json]
157
+ very-happy sessions message <id> <text> [--reply-to <msgId>] [--machine <id>] [--json]
148
158
 
149
159
  ${chalk.bold("Actions:")}
150
160
  list Running sessions plus recently seen ones, newest first.
151
161
  With --all: every session on the account (newest 150, server
152
- REST), attention first. Each row says whether this machine could
153
- decrypt it (\`decryptable\`); rows from other machines show only
162
+ REST), attention first. Rows this machine cannot decrypt are
163
+ filled in by the online machine that spawned them (\`readable\`,
164
+ \`via\`, \`machine\` in --json; \`decryptable\` stays "this
165
+ machine's own key"); what no online machine holds shows only
154
166
  id / active / archived / timestamps / url.
167
+ With --machine <id>: that machine's own listing (its daemon's
168
+ children plus what it saw lately).
155
169
  read The tail of a session as a role-tagged transcript, plus where
156
170
  the latest turn stands (\`turn\` in --json: ended, status and
157
- \`answer\` = the agent's reply to the latest prompt).
171
+ \`answer\` = the agent's reply to the latest prompt). A session
172
+ another machine spawned is read through that machine's daemon.
173
+ Both list and read carry \`backgroundTasks\` ({count, tasks[]}):
174
+ async sub-agents / backgrounded commands still running after
175
+ the turn ended \u2014 such a session is NOT idle (absent = the
176
+ daemon or CLI is too old to report it).
158
177
  stop SIGTERM the session's process via the local daemon.
159
178
  archive Mark the session inactive server-side (it stays resumable).
160
179
  approve Answer a pending permission request \`requestId\` with approve \u2014
@@ -165,13 +184,17 @@ ${chalk.bold("Actions:")}
165
184
  the last 30 minutes. Default scope \`repo\`: same git repository,
166
185
  including its other worktrees (\`sameWorktree\` says which share
167
186
  your checkout); \`cwd\`: same directory; \`machine\`: everything.
168
- message Send <text> into a running session here. It arrives tagged with
169
- the sender (the session named by HAPPY_SESSION_ID when run from a
187
+ With --machine <id>: the live sessions on that machine (scope
188
+ \`machine\` unless --cwd names a directory there).
189
+ message Send <text> into a running session. It arrives tagged with the
190
+ sender (the session named by VH_PEER_SESSION_ID when run from a
170
191
  managed session's shell, else this CLI) and tells the peer how to
171
- reply. Refused for sessions that are not running here, not
172
- spawned by this machine, or terminal mirrors. \`delivered\`
173
- follows \`very-happy send\`: true only when a wrapper was attached
174
- before and after the POST (\`stored\` = it is on the server anyway).
192
+ reply. A session another machine spawned is delivered through
193
+ that machine's daemon, with this host named in the header.
194
+ Refused for sessions that are not running, or terminal mirrors.
195
+ \`delivered\` follows \`very-happy send\`: true only when a
196
+ wrapper was attached before and after the POST (\`stored\` = it
197
+ is on the server anyway).
175
198
 
176
199
  ${chalk.bold("Options:")}
177
200
  --all list only: account-wide over REST instead of this
@@ -197,26 +220,31 @@ ${chalk.bold("Options:")}
197
220
  --cwd <dir> peers only: directory the scope is computed from
198
221
  (default: the process cwd, or HAPPY_SESSION_ID's cwd).
199
222
  --reply-to <id> message only: the peer message id being answered.
223
+ --machine <id> list / read / peers / message: go through this account
224
+ machine's daemon (its id as the web's machine list shows
225
+ it). read / message: skip the lookup across machines.
200
226
  --json Machine-readable output.
201
227
  -h, --help Show this help
202
228
 
203
229
  ${chalk.bold("Scope:")}
204
- read / approve / deny need the session key from ~/.happy/sessions.json,
205
- which exists for sessions this machine's daemon spawned and is pruned after
206
- 14 days (the message payloads and the permission RPC are encrypted with it).
207
- \`list --all\` sees every session on the account but can only decrypt those
208
- same ones; the rest come back with decryptable=false. Reading, listing in
209
- full and answering another machine's session from here needs the CLI to hold
210
- the account content key \u2014 a credentials change, not a flag.
230
+ Sessions this machine's daemon spawned are handled with the local key in
231
+ ~/.happy/sessions.json (pruned after 14 days). Any other session of the
232
+ account is handled by the daemon of the machine that spawned it: that
233
+ machine must be online and on CLI \u2265 0.2.157, the operation runs there with
234
+ its keys, and only the result travels back \u2014 this CLI never holds the
235
+ account content key or another machine's session key. Without --machine
236
+ the online machines are asked in turn which one holds the session.
237
+ approve / deny / stop / archive remain local-only.
211
238
 
212
239
  ${chalk.bold("Exit codes:")}
213
240
  2 read --wait: the turn had not ended when --timeout ran out (the
214
241
  partial state is still printed)
215
242
  0 success (approve/deny: the wrapper acknowledged the verdict; check
216
243
  \`settled\` in --json to see whether the request has left the pending set)
217
- 1 bad arguments, unknown session, no local key, request not pending,
218
- session not online, or the operation failed (including \`stop\` on a
219
- session the daemon is not running)
244
+ 1 bad arguments, unknown session, no machine of the account holds it (or
245
+ its machine is offline / too old), request not pending, session not
246
+ online, or the operation failed (including \`stop\` on a session the
247
+ daemon is not running)
220
248
  `;
221
249
  function formatWait(ms) {
222
250
  const minutes = Math.floor(ms / 6e4);
@@ -225,17 +253,19 @@ function formatWait(ms) {
225
253
  return `${Math.floor(minutes / 60)}h${minutes % 60}m`;
226
254
  }
227
255
  function formatAccountSummaryLine(summary) {
228
- const state = summary.attention ? "[attention]" : summary.live ? "[running here]" : summary.active ? "[active elsewhere]" : summary.archived ? "[archived]" : "[idle]";
256
+ const state = summary.attention ? "[attention]" : summary.live ? summary.machine ? `[running on ${summary.machine.host}]` : "[running here]" : summary.active ? "[active elsewhere]" : summary.archived ? "[archived]" : "[idle]";
229
257
  const parts = [summary.id, state];
230
- if (!summary.decryptable) {
231
- parts.push("(not decryptable from this machine)");
258
+ if (!summary.readable) {
259
+ parts.push("(not readable: no online machine holds its key)");
232
260
  } else {
261
+ if (summary.machine) parts.push(`via=${summary.machine.host}`);
233
262
  if (summary.title) parts.push(`title="${summary.title}"`);
234
263
  if (summary.flavor) parts.push(`agent=${summary.flavor}`);
235
264
  if (summary.tags?.length) parts.push(`tags=${summary.tags.join(",")}`);
236
265
  if (summary.variant) parts.push(`variant=${summary.variant}`);
237
266
  if (summary.machineId) parts.push(`machine=${summary.machineId}`);
238
267
  if (summary.cwd) parts.push(`cwd=${summary.cwd}`);
268
+ if (summary.backgroundTasks && summary.backgroundTasks.count > 0) parts.push(formatBackgroundTasks(summary.backgroundTasks));
239
269
  for (const request of summary.pending ?? []) {
240
270
  parts.push(`pending=${request.id}:${request.tool}${request.waitingMs !== void 0 ? `(${formatWait(request.waitingMs)})` : ""}`);
241
271
  }
@@ -246,6 +276,10 @@ function formatAccountSummaryLine(summary) {
246
276
  function permissionFailureRecord(sessionId, requestId, error) {
247
277
  return { sessionId, requestId, error: error instanceof Error ? error.message : String(error) };
248
278
  }
279
+ function formatBackgroundTasks(report) {
280
+ const types = report.tasks.map((task) => task.type).filter(Boolean);
281
+ return `background=${report.count}${types.length ? `(${types.join(",")})` : ""}`;
282
+ }
249
283
  function formatSummaryLine(summary) {
250
284
  const parts = [summary.id, summary.live ? "[running]" : "[not running]"];
251
285
  if (summary.title) parts.push(`title="${summary.title}"`);
@@ -254,6 +288,7 @@ function formatSummaryLine(summary) {
254
288
  if (summary.variant) parts.push(`variant=${summary.variant}`);
255
289
  if (summary.cwd) parts.push(`cwd=${summary.cwd}`);
256
290
  if (summary.pid !== void 0) parts.push(`pid=${summary.pid}`);
291
+ if (summary.backgroundTasks && summary.backgroundTasks.count > 0) parts.push(formatBackgroundTasks(summary.backgroundTasks));
257
292
  parts.push(summary.url);
258
293
  return parts.join(" ");
259
294
  }
@@ -272,20 +307,48 @@ async function handleSessionsCommand(args) {
272
307
  }
273
308
  try {
274
309
  if (options.action === "list" && options.all) {
275
- const sessions = await listAccountSessions({ tag: options.tag, recentLimit: options.limit, includeArchived: options.includeArchived });
310
+ const listed = await listAccountSessions({ recentLimit: options.tag ? 150 : options.limit, includeArchived: options.includeArchived });
311
+ const fill = await fillForeignAccountRows(listed);
312
+ let sessions = fill.rows;
313
+ if (options.tag) {
314
+ const wanted = options.tag;
315
+ sessions = sessions.filter((summary) => summary.tags?.includes(wanted) === true);
316
+ const recentLimit = Math.max(0, options.limit ?? DEFAULT_RECENT_LIMIT);
317
+ let idleKept = 0;
318
+ sessions = sessions.filter((summary) => summary.attention || summary.live || idleKept++ < recentLimit);
319
+ }
276
320
  if (options.json) {
277
- console.log(JSON.stringify({ sessions, scope: "account" }));
321
+ console.log(JSON.stringify({ sessions, scope: "account", machines: { asked: fill.asked, skipped: fill.skipped } }));
278
322
  } else if (sessions.length === 0) {
279
- console.log(options.tag ? `No decryptable sessions tagged "${options.tag}" on this account.` : "No sessions found on this account.");
323
+ console.log(options.tag ? `No readable sessions tagged "${options.tag}" on this account.` : "No sessions found on this account.");
280
324
  } else {
281
325
  for (const summary of sessions) console.log(formatAccountSummaryLine(summary));
282
- const foreign = sessions.filter((summary) => !summary.decryptable).length;
326
+ const foreign = sessions.filter((summary) => !summary.readable).length;
327
+ const filled = fill.asked.reduce((n, a) => n + a.filled, 0);
328
+ if (filled > 0) {
329
+ console.log(chalk.dim(`${filled} session(s) read through their own machine's daemon (${fill.asked.filter((a) => a.filled > 0).map((a) => a.host ?? a.machineId).join(", ")}).`));
330
+ }
331
+ for (const asked of fill.asked.filter((a) => a.error)) console.log(chalk.dim(`machine ${asked.machineId}: ${asked.error}`));
283
332
  if (foreign > 0) {
284
- console.log(chalk.dim(`${foreign} session(s) belong to another machine and cannot be decrypted here (no local key).`));
333
+ const why = fill.skipped.length > 0 ? ` (${fill.skipped.map((s) => `${s.machineId}: ${s.reason}`).join("; ")})` : "";
334
+ console.log(chalk.dim(`${foreign} session(s) belong to a machine that is not reachable right now${why}.`));
285
335
  }
286
336
  }
287
337
  process.exit(0);
288
338
  }
339
+ if (options.action === "list" && options.machine) {
340
+ const listing = await listRemoteSessions(options.machine, { tag: options.tag, limit: options.limit });
341
+ const sessions = listing.sessions;
342
+ if (options.json) {
343
+ console.log(JSON.stringify({ sessions, machine: { id: listing.machineId, host: listing.host } }));
344
+ } else if (sessions.length === 0) {
345
+ console.log(options.tag ? `No sessions tagged "${options.tag}" on ${listing.host}.` : `No sessions found on ${listing.host}.`);
346
+ } else {
347
+ console.log(chalk.dim(`machine ${listing.host} (${listing.machineId})`));
348
+ for (const summary of sessions) console.log(formatSummaryLine(summary));
349
+ }
350
+ process.exit(0);
351
+ }
289
352
  if (options.action === "list") {
290
353
  const sessions = await listSessions({ tag: options.tag, recentLimit: options.limit });
291
354
  if (options.json) {
@@ -302,12 +365,13 @@ async function handleSessionsCommand(args) {
302
365
  const persisted = readPersistedSessions();
303
366
  const context = { self: () => cliPeerSelf(process.env, cwd, persisted), readPersisted: () => persisted };
304
367
  if (options.action === "peers") {
305
- const listing = await listPeerSessions(context, options.scope ?? "repo");
368
+ const listing = options.machine ? await listRemotePeers(options.machine, { ...options.scope ? { scope: options.scope } : {}, ...options.cwd ? { cwd: options.cwd } : {} }) : await listPeerSessions(context, options.scope ?? "repo");
306
369
  if (options.json) {
307
370
  console.log(JSON.stringify(listing));
308
371
  } else if (listing.peers.length === 0) {
309
- console.log(`No other live sessions in scope "${listing.scope}"${listing.self.repoRoot ? ` (repo ${listing.self.repoRoot})` : ""}.`);
372
+ console.log(`No other live sessions in scope "${listing.scope}"${listing.self.repoRoot ? ` (repo ${listing.self.repoRoot})` : ""}${"host" in listing ? ` on ${listing.host}` : ""}.`);
310
373
  } else {
374
+ if ("host" in listing) console.log(chalk.dim(`machine ${String(listing.host)} (${String(listing.machineId)})`));
311
375
  for (const peer of listing.peers) {
312
376
  const parts = [peer.sessionId, `[${peer.kind === "mirror" ? "terminal" : "running"}]`];
313
377
  if (peer.title) parts.push(`title="${peer.title}"`);
@@ -322,9 +386,9 @@ async function handleSessionsCommand(args) {
322
386
  }
323
387
  const target = options.sessionId;
324
388
  try {
325
- const result = await sendPeerMessage(context, { to: target, body: options.text, replyTo: options.replyTo });
389
+ const result = await sendPeerMessage(context, { to: target, body: options.text, replyTo: options.replyTo, ...options.machine ? { machineId: options.machine } : {} });
326
390
  if (options.json) console.log(JSON.stringify(result));
327
- else if (result.delivered) console.log(`Message ${result.messageId} delivered to ${target}
391
+ else if (result.delivered) console.log(`Message ${result.messageId} delivered to ${target}${result.machine ? ` on ${result.machine.host}` : ""}
328
392
  ${result.url}`);
329
393
  else console.error(chalk.yellow("Not delivered:"), result.error ?? `session is ${result.status}`, result.stored ? "(stored server-side, unread)" : "");
330
394
  process.exit(result.delivered ? 0 : 1);
@@ -336,24 +400,44 @@ ${result.url}`);
336
400
  const sessionId = options.sessionId;
337
401
  if (options.action === "read") {
338
402
  let timedOut = false;
339
- if (options.wait) {
340
- try {
341
- await waitForTurnEnd(sessionId, { timeoutMs: (options.timeoutSec ?? 600) * 1e3 });
342
- } catch (error) {
343
- if (!(error instanceof TurnWaitTimeoutError)) throw error;
344
- timedOut = true;
345
- console.error(chalk.yellow("Timeout:"), error.message);
403
+ const local = !options.machine && readPersistedSessions()[sessionId] !== void 0;
404
+ let result;
405
+ if (local) {
406
+ if (options.wait) {
407
+ try {
408
+ await waitForTurnEnd(sessionId, { timeoutMs: (options.timeoutSec ?? 600) * 1e3 });
409
+ } catch (error) {
410
+ if (!(error instanceof TurnWaitTimeoutError)) throw error;
411
+ timedOut = true;
412
+ console.error(chalk.yellow("Timeout:"), error.message);
413
+ }
414
+ }
415
+ result = await readSessionTranscript(sessionId, options.limit ?? 20, { full: options.full });
416
+ } else {
417
+ const readOptions = { limit: options.limit ?? 20, full: options.full, ...options.machine ? { machineId: options.machine } : {} };
418
+ let remote;
419
+ if (options.wait) {
420
+ const timeoutMs = (options.timeoutSec ?? 600) * 1e3;
421
+ const waited = await waitForRemoteTurnEnd(sessionId, { ...readOptions, timeoutMs });
422
+ remote = waited.read;
423
+ if (waited.timedOut) {
424
+ timedOut = true;
425
+ console.error(chalk.yellow("Timeout:"), `Session ${sessionId} on ${remote.host}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
426
+ }
427
+ } else {
428
+ remote = await readRemoteTranscript(sessionId, readOptions);
346
429
  }
430
+ const { machineId, host, ...rest } = remote;
431
+ result = { ...rest, machine: { id: machineId, host } };
347
432
  }
348
- const result = await readSessionTranscript(sessionId, options.limit ?? 20, { full: options.full });
349
433
  if (options.json) {
350
- console.log(JSON.stringify(options.answer ? { sessionId, turn: result.turn } : result));
434
+ console.log(JSON.stringify(options.answer ? { sessionId, turn: result.turn, ...result.machine ? { machine: result.machine } : {} } : result));
351
435
  } else if (options.answer) {
352
436
  if (result.turn.answer.length > 0) console.log(result.turn.answer);
353
437
  else console.error(chalk.dim("(the agent has not replied to the latest prompt yet)"));
354
438
  } else {
355
- console.log(formatSummaryLine(result.summary));
356
- console.log(`--- last ${result.messageCount} message(s) ---`);
439
+ console.log(formatSummaryLine(result.summary) + (result.machine ? chalk.dim(` machine=${result.machine.host}`) : ""));
440
+ console.log(`--- last ${result.messageCount} message(s)${result.truncated ? " (truncated to fit the RPC budget)" : ""} ---`);
357
441
  console.log(result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)");
358
442
  const turn = result.turn;
359
443
  console.log(chalk.dim(`--- latest turn: ${turn.ended ? `ended (${turn.status ?? "unknown"})` : "running"}${turn.error ? ` \u2014 ${turn.error}` : ""} ---`));
@@ -1,10 +1,10 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync, statSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { d as readPersistedSessions, k as checkIfDaemonRunningAndCleanupStaleState, l as logger, m as spawnDaemonSession, n as stopDaemonSession } from './limits-DmMdchTz.mjs';
5
- import { r as resolveSpawnPermissionMode, c as resolveFirstMessageMeta, i as isValidSpawnOrigin, s as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS } from './index-DAAreiRF.mjs';
6
- import { f as forkSession, g as getProjectPath, F as ForkSourceMissingError, C as CodexAppServerClient, c as forkCodexThread } from './types-BEVzYSoV.mjs';
7
- import { x as readSessionMetadata, o as sessionWebUrl, y as waitForSessionKey, z as sendUserMessage } from './agentGuidance-C604n0Zq.mjs';
4
+ import { d as readPersistedSessions, k as checkIfDaemonRunningAndCleanupStaleState, l as logger, m as spawnDaemonSession, n as stopDaemonSession } from './limits-D2uN090G.mjs';
5
+ import { r as resolveSpawnPermissionMode, c as resolveFirstMessageMeta, i as isValidSpawnOrigin, s as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS } from './index-CVCt3kSt.mjs';
6
+ import { f as forkSession, g as getProjectPath, F as ForkSourceMissingError, C as CodexAppServerClient, c as forkCodexThread } from './types-DIBF4U0Y.mjs';
7
+ import { F as readSessionMetadata, x as sessionWebUrl, G as waitForSessionKey, H as sendUserMessage } from './agentGuidance-CrR9l62U.mjs';
8
8
  import 'zod';
9
9
  import '@paralleldrive/cuid2';
10
10
  import 'node:fs/promises';
@@ -3,10 +3,10 @@
3
3
  var chalk = require('chalk');
4
4
  var node_fs = require('node:fs');
5
5
  var path = require('node:path');
6
- var persistence = require('./limits-o29KXBgj.cjs');
7
- var index = require('./index-BbtR-Qto.cjs');
8
- var index$1 = require('./types-CphWjqHc.cjs');
9
- var agentGuidance = require('./agentGuidance-C-CmV03f.cjs');
6
+ var persistence = require('./limits-kqFqhfsh.cjs');
7
+ var index = require('./index-BG932eCh.cjs');
8
+ var index$1 = require('./types-DnrPeN80.cjs');
9
+ var agentGuidance = require('./agentGuidance-DwOWbX55.cjs');
10
10
  require('zod');
11
11
  require('@paralleldrive/cuid2');
12
12
  require('node:fs/promises');
@@ -4,9 +4,9 @@ var os = require('node:os');
4
4
  var promises = require('node:fs/promises');
5
5
  var z = require('zod');
6
6
  var node_crypto = require('node:crypto');
7
- var agentGuidance = require('./agentGuidance-C-CmV03f.cjs');
8
- var persistence = require('./limits-o29KXBgj.cjs');
9
- var install = require('./install-CO1ps6Wt.cjs');
7
+ var agentGuidance = require('./agentGuidance-DwOWbX55.cjs');
8
+ var persistence = require('./limits-kqFqhfsh.cjs');
9
+ var install = require('./install-wKQGgXvN.cjs');
10
10
  require('node:fs');
11
11
  require('node:path');
12
12
  require('axios');
@@ -15,8 +15,8 @@ require('@paralleldrive/cuid2');
15
15
  require('tweetnacl');
16
16
  require('chalk');
17
17
  require('node:util');
18
- require('./index-BbtR-Qto.cjs');
19
- require('./types-CphWjqHc.cjs');
18
+ require('./index-BG932eCh.cjs');
19
+ require('./types-DnrPeN80.cjs');
20
20
  require('node:events');
21
21
  require('child_process');
22
22
  require('util');
@@ -2,9 +2,9 @@ import { homedir } from 'node:os';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { z } from 'zod';
4
4
  import { randomUUID } from 'node:crypto';
5
- import { B as teamRequest, C as createTeamsClient } from './agentGuidance-C604n0Zq.mjs';
6
- import { h as readCredentialsForConfiguredRelay } from './limits-DmMdchTz.mjs';
7
- import { a as installTeamSkill, i as installAutomationSkill } from './install-CNtXzwT_.mjs';
5
+ import { I as teamRequest, J as createTeamsClient } from './agentGuidance-CrR9l62U.mjs';
6
+ import { h as readCredentialsForConfiguredRelay } from './limits-D2uN090G.mjs';
7
+ import { a as installTeamSkill, i as installAutomationSkill } from './install-DarS_Wco.mjs';
8
8
  import 'node:fs';
9
9
  import 'node:path';
10
10
  import 'axios';
@@ -13,8 +13,8 @@ import '@paralleldrive/cuid2';
13
13
  import 'tweetnacl';
14
14
  import 'chalk';
15
15
  import 'node:util';
16
- import './index-DAAreiRF.mjs';
17
- import './types-BEVzYSoV.mjs';
16
+ import './index-CVCt3kSt.mjs';
17
+ import './types-DIBF4U0Y.mjs';
18
18
  import 'node:events';
19
19
  import 'child_process';
20
20
  import 'util';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var persistence = require('./limits-o29KXBgj.cjs');
4
- var agentGuidance = require('./agentGuidance-C-CmV03f.cjs');
3
+ var persistence = require('./limits-kqFqhfsh.cjs');
4
+ var agentGuidance = require('./agentGuidance-DwOWbX55.cjs');
5
5
  require('zod');
6
6
  require('@paralleldrive/cuid2');
7
7
  require('node:fs/promises');
@@ -1,5 +1,5 @@
1
- import { h as readCredentialsForConfiguredRelay, j as createBuiltinTodoClient, c as configuration, B as BUILTIN_TODO_SKILL } from './limits-DmMdchTz.mjs';
2
- import { u as readTeamScope, v as teamScopePath } from './agentGuidance-C604n0Zq.mjs';
1
+ import { h as readCredentialsForConfiguredRelay, j as createBuiltinTodoClient, c as configuration, B as BUILTIN_TODO_SKILL } from './limits-D2uN090G.mjs';
2
+ import { C as readTeamScope, E as teamScopePath } from './agentGuidance-CrR9l62U.mjs';
3
3
  import 'zod';
4
4
  import '@paralleldrive/cuid2';
5
5
  import 'node:fs/promises';
@@ -1,5 +1,5 @@
1
1
  import axios from 'axios';
2
- import { R as AgentVersionsSchema, l as logger, w as encodeBase64, L as encrypt, N as decrypt, M as decodeBase64, c as configuration, e as createEnvelope, s as streamKeyOf, S as backoff, T as ReleaseDrainNoticeSchema, U as decryptBlob, V as RelayAssignmentResponseSchema, W as notifyDaemonTurnEvent, X as prepareClipboardText, Y as prepareClipboardHistoryText, g as delay, r as readSettings, d as readPersistedSessions, Z as credentialRelayProblem, F as packageJson, _ as RelayCandidatesResponseSchema, $ as readTrackedClaudeSessionIds, a0 as readTrackedCodexThreadIds, a1 as isValidTerminalId, a2 as exponentialBackoffDelay, a3 as libsodiumEncryptForPublicKey, a4 as libsodiumPublicKeyFromSecretKey, a5 as getRandomBytes, a6 as redactLogValue } from './limits-DmMdchTz.mjs';
2
+ import { S as AgentVersionsSchema, l as logger, w as encodeBase64, L as encrypt, N as decrypt, M as decodeBase64, c as configuration, e as createEnvelope, s as streamKeyOf, T as backoff, U as ReleaseDrainNoticeSchema, V as decryptBlob, W as RelayAssignmentResponseSchema, X as notifyDaemonTurnEvent, Y as BackgroundTaskReporter, Z as notifyDaemonBackgroundTasks, _ as prepareClipboardText, $ as prepareClipboardHistoryText, g as delay, r as readSettings, d as readPersistedSessions, a0 as credentialRelayProblem, F as packageJson, a1 as RelayCandidatesResponseSchema, a2 as readTrackedClaudeSessionIds, a3 as readTrackedCodexThreadIds, a4 as isValidTerminalId, a5 as exponentialBackoffDelay, a6 as libsodiumEncryptForPublicKey, a7 as libsodiumPublicKeyFromSecretKey, a8 as getRandomBytes, a9 as redactLogValue } from './limits-D2uN090G.mjs';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { io } from 'socket.io-client';
5
5
  import { z } from 'zod';
@@ -417,6 +417,15 @@ async function deriveKey$1(master, usage, path) {
417
417
 
418
418
  class RpcHandlerManager {
419
419
  handlers = /* @__PURE__ */ new Map();
420
+ /**
421
+ * B-506: methods whose params/result are PLAINTEXT JSON strings instead of
422
+ * machine-key ciphertext. The caller is a CLI on another machine of the
423
+ * same account, which holds neither this machine's key nor the account
424
+ * content key; the trusted relay sees the payload either way (this fork
425
+ * is server-trusted, not e2e). Only `registerPlainHandler` puts a method
426
+ * here; everything else keeps decrypting.
427
+ */
428
+ plainMethods = /* @__PURE__ */ new Set();
420
429
  scopePrefix;
421
430
  encryptionKey;
422
431
  encryptionVariant;
@@ -441,14 +450,31 @@ class RpcHandlerManager {
441
450
  unregisterHandler(method) {
442
451
  const prefixedMethod = this.getPrefixedMethod(method);
443
452
  this.handlers.delete(prefixedMethod);
453
+ this.plainMethods.delete(prefixedMethod);
444
454
  for (const socket of this.sockets) socket.emit("rpc-unregister", { method: prefixedMethod });
445
455
  }
456
+ /**
457
+ * B-506: register a method whose request is a plaintext JSON string and
458
+ * whose response is returned as a plaintext JSON string (see
459
+ * `plainMethods`). The handler receives the parsed params; a throw is
460
+ * answered with `{ error }` like the encrypted path.
461
+ */
462
+ registerPlainHandler(method, handler) {
463
+ this.plainMethods.add(this.getPrefixedMethod(method));
464
+ this.registerHandler(method, handler);
465
+ }
466
+ isPlainMethod(prefixedMethod) {
467
+ return this.plainMethods.has(prefixedMethod);
468
+ }
446
469
  /**
447
470
  * Handle an incoming RPC request
448
471
  * @param request - The RPC request data
449
472
  * @param callback - The response callback
450
473
  */
451
474
  async handleRequest(request) {
475
+ if (this.plainMethods.has(request.method)) {
476
+ return this.handlePlainRequest(request);
477
+ }
452
478
  try {
453
479
  const handler = this.handlers.get(request.method);
454
480
  if (!handler) {
@@ -472,6 +498,25 @@ class RpcHandlerManager {
472
498
  return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
473
499
  }
474
500
  }
501
+ /** Plaintext variant of handleRequest (B-506). Never throws; never encrypts. */
502
+ async handlePlainRequest(request) {
503
+ const handler = this.handlers.get(request.method);
504
+ if (!handler) return JSON.stringify({ error: "Method not found" });
505
+ let params;
506
+ try {
507
+ params = typeof request.params === "string" && request.params.length > 0 ? JSON.parse(request.params) : {};
508
+ } catch {
509
+ return JSON.stringify({ error: "Invalid JSON params" });
510
+ }
511
+ try {
512
+ this.logger("[RPC] Calling plain handler", { method: request.method });
513
+ const result = await handler(params);
514
+ return JSON.stringify(result === void 0 ? null : result);
515
+ } catch (error) {
516
+ this.logger("[RPC] [ERROR] Error handling plain request", { error });
517
+ return JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" });
518
+ }
519
+ }
475
520
  onSocketConnect(socket) {
476
521
  this.sockets.add(socket);
477
522
  for (const [prefixedMethod] of this.handlers) {
@@ -2751,7 +2796,7 @@ class ApiSessionClient extends EventEmitter {
2751
2796
  const persisted = await axios.post(
2752
2797
  `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
2753
2798
  { messages: batch },
2754
- { headers: this.authHeaders(), timeout: 6e4 }
2799
+ { headers: { ...this.authHeaders(), "X-Happy-Message-Origin": "relay-client" }, timeout: 6e4 }
2755
2800
  );
2756
2801
  const stored = Array.isArray(persisted.data.messages) ? persisted.data.messages : [];
2757
2802
  const storedLocalIds = new Set(stored.map((message) => message.localId));
@@ -2979,16 +3024,29 @@ class ApiSessionClient extends EventEmitter {
2979
3024
  /** B-466: turn edges (and a periodic lease) reported to the local daemon so
2980
3025
  * auto-update waits for "no turn in flight" instead of "no sessions". */
2981
3026
  turnReporter = new TurnReporter((event) => void notifyDaemonTurnEvent(this.sessionId, event).catch((error) => logger.debug("[API] Failed to report turn event to daemon:", error)));
2982
- keepAlive(thinking, mode) {
3027
+ /** B-507: background tasks in flight, published on change and renewed every
3028
+ * 60s to the daemon (`/session-event background_tasks`) and into agentState
3029
+ * (`backgroundTasks: { updatedAt, tasks }`) — the two readers that outlive a
3030
+ * single heartbeat. The heartbeat itself carries only the count. */
3031
+ backgroundTaskReporter = new BackgroundTaskReporter((tasks, now) => {
3032
+ void notifyDaemonBackgroundTasks(this.sessionId, tasks).catch((error) => logger.debug("[API] Failed to report background tasks to daemon:", error));
3033
+ this.updateAgentState((state) => ({
3034
+ ...state,
3035
+ backgroundTasks: tasks.length > 0 ? { updatedAt: now, tasks } : void 0
3036
+ }));
3037
+ });
3038
+ keepAlive(thinking, mode, backgroundTasks) {
2983
3039
  if (process.env.DEBUG) {
2984
3040
  logger.debug(`[API] Sending keep alive message: ${thinking}`);
2985
3041
  }
2986
3042
  this.turnReporter.observe(thinking);
3043
+ if (backgroundTasks) this.backgroundTaskReporter.observe(backgroundTasks);
2987
3044
  this.socket.volatile.emit("session-alive", {
2988
3045
  sid: this.sessionId,
2989
3046
  time: Date.now(),
2990
3047
  thinking,
2991
- mode
3048
+ mode,
3049
+ ...backgroundTasks ? { backgroundTasks: backgroundTasks.length } : {}
2992
3050
  });
2993
3051
  }
2994
3052
  /**
@@ -14335,6 +14393,17 @@ class ApiMachineClient {
14335
14393
  return { claudeAuth };
14336
14394
  });
14337
14395
  }
14396
+ /**
14397
+ * B-506: the five `sessions.*` methods a CLI on ANOTHER machine of this
14398
+ * account calls to read / message sessions this daemon spawned. Plaintext
14399
+ * on purpose (no shared key with the caller); see
14400
+ * `api/rpc/RpcHandlerManager.ts` `registerPlainHandler`.
14401
+ */
14402
+ setRemoteSessionOpsHandlers(handlers) {
14403
+ for (const [method, handler] of Object.entries(handlers)) {
14404
+ this.rpcHandlerManager.registerPlainHandler(method, handler);
14405
+ }
14406
+ }
14338
14407
  setRPCHandlers({
14339
14408
  spawnSession,
14340
14409
  resumeSession,
@@ -15163,7 +15232,11 @@ class ApiMachineClient {
15163
15232
  }
15164
15233
  bindRpcRequestHandler(socket, transport) {
15165
15234
  socket.on("rpc-request", async (data, callback) => {
15166
- logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
15235
+ if (typeof data?.method === "string" && this.rpcHandlerManager.isPlainMethod(data.method)) {
15236
+ logger.debug(`[API MACHINE] Received plain RPC request via ${transport}: ${data.method}`);
15237
+ } else {
15238
+ logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
15239
+ }
15167
15240
  await answerSocketRequest(
15168
15241
  callback,
15169
15242
  () => this.rpcHandlerManager.handleRequest(data),