very-happy-cli 0.2.155 → 0.2.157

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-DmFcRh54.cjs → AcpBackend-BymWX-LI.cjs} +2 -2
  2. package/dist/{AcpBackend-DVB0nzgR.mjs → AcpBackend-Cy18U7yw.mjs} +2 -2
  3. package/dist/{AcpSessionManager-CWaSYTnT.cjs → AcpSessionManager-BF7zBGAi.cjs} +1 -1
  4. package/dist/{AcpSessionManager-dGDgNb-p.mjs → AcpSessionManager-Ds8rcp95.mjs} +1 -1
  5. package/dist/{agentGuidance-BWxFC6UL.mjs → agentGuidance-CU6YD_Ms.mjs} +560 -29
  6. package/dist/{agentGuidance-un7YPryB.cjs → agentGuidance-Db76O1kw.cjs} +579 -28
  7. package/dist/{agentHome-aucT8BHw.cjs → agentHome-B0CLy0Qx.cjs} +2 -2
  8. package/dist/{agentHome-ByLb2HFg.mjs → agentHome-BaJ9xtxB.mjs} +2 -2
  9. package/dist/{auto-BQnD6gVx.mjs → auto-B1YAwaeU.mjs} +6 -6
  10. package/dist/{auto-P9RoJaY0.cjs → auto-DqKGFq8U.cjs} +6 -6
  11. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  12. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  13. package/dist/{config-BoePh9PX.cjs → config-CzkBjZnV.cjs} +4 -4
  14. package/dist/{config-BPZ2Y6o8.mjs → config-aAbhJllb.mjs} +4 -4
  15. package/dist/{index-BjzdXb9x.mjs → index-BVu8Cufk.mjs} +8 -8
  16. package/dist/{index-B4oXnbYw.mjs → index-BYU1e81k.mjs} +372 -65
  17. package/dist/{index-D_fYUxVg.cjs → index-C9QzZsUQ.cjs} +375 -68
  18. package/dist/{index-DRrLaljt.cjs → index-CTbGqVFY.cjs} +7 -7
  19. package/dist/{index--KCpVs1c.cjs → index-Cz-uqnJk.cjs} +4 -4
  20. package/dist/{index-DVVRqaVG.mjs → index-DQcRkI9r.mjs} +4 -4
  21. package/dist/index.cjs +4 -4
  22. package/dist/index.mjs +4 -4
  23. package/dist/{install-CGigiocs.cjs → install-BMlV0Tnr.cjs} +1 -1
  24. package/dist/{install-DP5aPNEX.mjs → install-BuYnqtVa.mjs} +1 -1
  25. package/dist/{installTerminalHooks-XrcPvfBs.cjs → installTerminalHooks-BVVOEGhG.cjs} +4 -4
  26. package/dist/{installTerminalHooks-tIg7mANc.mjs → installTerminalHooks-ELDoqh0A.mjs} +4 -4
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.d.cts +101 -9
  29. package/dist/lib.d.mts +101 -9
  30. package/dist/lib.mjs +2 -2
  31. package/dist/{limits-C_DW0M0q.mjs → limits-A2c-3M-_.mjs} +162 -11
  32. package/dist/{limits-DkfHJ3nS.cjs → limits-CyuIt-Vl.cjs} +169 -10
  33. package/dist/{mcp-o7QglAQM.mjs → mcp-Bo934Kpy.mjs} +4 -4
  34. package/dist/{mcp-BqlXl10F.cjs → mcp-CXkvnztT.cjs} +4 -4
  35. package/dist/{piRuntime-BxP4B_zY.cjs → piRuntime-CJjA26oD.cjs} +2 -2
  36. package/dist/{piRuntime-MjiJI-nV.mjs → piRuntime-DdEY2mE9.mjs} +2 -2
  37. package/dist/{piTerminal-DYzRcwgb.cjs → piTerminal-CpU3hzrX.cjs} +6 -6
  38. package/dist/{piTerminal-Dpq6yqKU.mjs → piTerminal-DwWmgumi.mjs} +6 -6
  39. package/dist/{runGemini-CDlcQ-OA.mjs → runGemini-Cj6oJQ-I.mjs} +6 -6
  40. package/dist/{runGemini-8Pxh94Fm.cjs → runGemini-DS-0qm0L.cjs} +6 -6
  41. package/dist/{runOpenClaw-B5FSZIbK.cjs → runOpenClaw-CvSm8oTc.cjs} +5 -5
  42. package/dist/{runOpenClaw-YkhsgPs0.mjs → runOpenClaw-DUCQxkI-.mjs} +5 -5
  43. package/dist/{send-CwrYXTPr.cjs → send-CHxobaoI.cjs} +43 -31
  44. package/dist/{send-DJStmLBv.mjs → send-D5CyRyZ_.mjs} +43 -31
  45. package/dist/{sessions-MX21NJXd.cjs → sessions-BFlIrKCY.cjs} +133 -49
  46. package/dist/{sessions-JaVXlSqj.mjs → sessions-DsE01xKc.mjs} +133 -49
  47. package/dist/{spawn-BaPnjClT.mjs → spawn-CdihhwHs.mjs} +4 -4
  48. package/dist/{spawn-cIoglKVf.cjs → spawn-HV345RN-.cjs} +4 -4
  49. package/dist/{teams-DXV5Pwwt.mjs → teams-BE8nMB6O.mjs} +5 -5
  50. package/dist/{teams-Dqk7F3D-.cjs → teams-BahinMnH.cjs} +5 -5
  51. package/dist/{todo-Buuqn9gm.cjs → todo-CMMQ6yhU.cjs} +2 -2
  52. package/dist/{todo-2Z1REhfx.mjs → todo-CuQAkSp2.mjs} +2 -2
  53. package/dist/{types-a2xsH2UI.cjs → types-B0sSi6eM.cjs} +125 -13
  54. package/dist/{types-CkbfdRzq.mjs → types-BrGJ7PJz.mjs} +125 -13
  55. package/package.json +7 -7
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var agentGuidance = require('./agentGuidance-un7YPryB.cjs');
5
- var persistence = require('./limits-DkfHJ3nS.cjs');
4
+ var agentGuidance = require('./agentGuidance-Db76O1kw.cjs');
5
+ var persistence = require('./limits-CyuIt-Vl.cjs');
6
6
  var path = require('node:path');
7
7
  require('zod');
8
8
  require('node:crypto');
@@ -15,6 +15,7 @@ require('@paralleldrive/cuid2');
15
15
  require('tweetnacl');
16
16
  require('node:util');
17
17
 
18
+ const MACHINE_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
18
19
  const ACTIONS = ["list", "read", "stop", "archive", "approve", "deny", "peers", "message"];
19
20
  const ACTIONS_NEEDING_ID = /* @__PURE__ */ new Set(["read", "stop", "archive", "approve", "deny", "message"]);
20
21
  const ACTIONS_NEEDING_REQUEST_ID = /* @__PURE__ */ new Set(["approve", "deny"]);
@@ -67,6 +68,10 @@ function parseSessionsArgs(args) {
67
68
  const value = args[++i];
68
69
  if (value === void 0 || value.trim() === "") throw new Error("--reply-to requires a value");
69
70
  options.replyTo = value;
71
+ } else if (arg === "--machine") {
72
+ const value = args[++i];
73
+ if (value === void 0 || !MACHINE_ID_RE.test(value)) throw new Error("--machine requires a machine id");
74
+ options.machine = value;
70
75
  } else if (arg === "--tag") {
71
76
  const value = args[++i];
72
77
  if (value === void 0) throw new Error("--tag requires a value");
@@ -132,31 +137,45 @@ function parseSessionsArgs(args) {
132
137
  if (options.cwd !== void 0) throw new Error("--cwd only applies to `sessions peers`");
133
138
  }
134
139
  if (options.replyTo !== void 0 && options.action !== "message") throw new Error("--reply-to only applies to `sessions message`");
140
+ if (options.machine !== void 0) {
141
+ if (!MACHINE_ACTIONS.has(options.action)) throw new Error("--machine only applies to `sessions list|read|peers|message`");
142
+ if (options.all) throw new Error("--machine cannot be combined with --all (--all already asks every online machine)");
143
+ }
135
144
  return options;
136
145
  }
146
+ const MACHINE_ACTIONS = /* @__PURE__ */ new Set(["list", "read", "peers", "message"]);
137
147
  const SESSIONS_HELP = `
138
- ${chalk.bold("very-happy sessions")} - Inspect and control sessions on this machine (for automation)
148
+ ${chalk.bold("very-happy sessions")} - Inspect and control sessions on this machine and, through their daemons, on the account's other machines (for automation)
139
149
 
140
150
  ${chalk.bold("Usage:")}
141
- very-happy sessions list [--all [--include-archived]] [--tag <name>] [--limit <n>] [--json]
142
- very-happy sessions read <id> [--limit <n>] [--full] [--answer]
151
+ very-happy sessions list [--all [--include-archived]] [--machine <id>] [--tag <name>] [--limit <n>] [--json]
152
+ very-happy sessions read <id> [--machine <id>] [--limit <n>] [--full] [--answer]
143
153
  [--wait [--timeout <s>]] [--json]
144
154
  very-happy sessions stop <id> [--json]
145
155
  very-happy sessions archive <id> [--json]
146
156
  very-happy sessions approve <id> <requestId> [--for-session] [--json]
147
157
  very-happy sessions deny <id> <requestId> [--reason <text>] [--json]
148
- very-happy sessions peers [--scope repo|cwd|machine] [--cwd <dir>] [--json]
149
- very-happy sessions message <id> <text> [--reply-to <msgId>] [--json]
158
+ very-happy sessions peers [--scope repo|cwd|machine] [--cwd <dir>] [--machine <id>] [--json]
159
+ very-happy sessions message <id> <text> [--reply-to <msgId>] [--machine <id>] [--json]
150
160
 
151
161
  ${chalk.bold("Actions:")}
152
162
  list Running sessions plus recently seen ones, newest first.
153
163
  With --all: every session on the account (newest 150, server
154
- REST), attention first. Each row says whether this machine could
155
- decrypt it (\`decryptable\`); rows from other machines show only
164
+ REST), attention first. Rows this machine cannot decrypt are
165
+ filled in by the online machine that spawned them (\`readable\`,
166
+ \`via\`, \`machine\` in --json; \`decryptable\` stays "this
167
+ machine's own key"); what no online machine holds shows only
156
168
  id / active / archived / timestamps / url.
169
+ With --machine <id>: that machine's own listing (its daemon's
170
+ children plus what it saw lately).
157
171
  read The tail of a session as a role-tagged transcript, plus where
158
172
  the latest turn stands (\`turn\` in --json: ended, status and
159
- \`answer\` = the agent's reply to the latest prompt).
173
+ \`answer\` = the agent's reply to the latest prompt). A session
174
+ another machine spawned is read through that machine's daemon.
175
+ Both list and read carry \`backgroundTasks\` ({count, tasks[]}):
176
+ async sub-agents / backgrounded commands still running after
177
+ the turn ended \u2014 such a session is NOT idle (absent = the
178
+ daemon or CLI is too old to report it).
160
179
  stop SIGTERM the session's process via the local daemon.
161
180
  archive Mark the session inactive server-side (it stays resumable).
162
181
  approve Answer a pending permission request \`requestId\` with approve \u2014
@@ -167,13 +186,17 @@ ${chalk.bold("Actions:")}
167
186
  the last 30 minutes. Default scope \`repo\`: same git repository,
168
187
  including its other worktrees (\`sameWorktree\` says which share
169
188
  your checkout); \`cwd\`: same directory; \`machine\`: everything.
170
- message Send <text> into a running session here. It arrives tagged with
171
- the sender (the session named by HAPPY_SESSION_ID when run from a
189
+ With --machine <id>: the live sessions on that machine (scope
190
+ \`machine\` unless --cwd names a directory there).
191
+ message Send <text> into a running session. It arrives tagged with the
192
+ sender (the session named by VH_PEER_SESSION_ID when run from a
172
193
  managed session's shell, else this CLI) and tells the peer how to
173
- reply. Refused for sessions that are not running here, not
174
- spawned by this machine, or terminal mirrors. \`delivered\`
175
- follows \`very-happy send\`: true only when a wrapper was attached
176
- before and after the POST (\`stored\` = it is on the server anyway).
194
+ reply. A session another machine spawned is delivered through
195
+ that machine's daemon, with this host named in the header.
196
+ Refused for sessions that are not running, or terminal mirrors.
197
+ \`delivered\` follows \`very-happy send\`: true only when a
198
+ wrapper was attached before and after the POST (\`stored\` = it
199
+ is on the server anyway).
177
200
 
178
201
  ${chalk.bold("Options:")}
179
202
  --all list only: account-wide over REST instead of this
@@ -199,26 +222,31 @@ ${chalk.bold("Options:")}
199
222
  --cwd <dir> peers only: directory the scope is computed from
200
223
  (default: the process cwd, or HAPPY_SESSION_ID's cwd).
201
224
  --reply-to <id> message only: the peer message id being answered.
225
+ --machine <id> list / read / peers / message: go through this account
226
+ machine's daemon (its id as the web's machine list shows
227
+ it). read / message: skip the lookup across machines.
202
228
  --json Machine-readable output.
203
229
  -h, --help Show this help
204
230
 
205
231
  ${chalk.bold("Scope:")}
206
- read / approve / deny need the session key from ~/.happy/sessions.json,
207
- which exists for sessions this machine's daemon spawned and is pruned after
208
- 14 days (the message payloads and the permission RPC are encrypted with it).
209
- \`list --all\` sees every session on the account but can only decrypt those
210
- same ones; the rest come back with decryptable=false. Reading, listing in
211
- full and answering another machine's session from here needs the CLI to hold
212
- the account content key \u2014 a credentials change, not a flag.
232
+ Sessions this machine's daemon spawned are handled with the local key in
233
+ ~/.happy/sessions.json (pruned after 14 days). Any other session of the
234
+ account is handled by the daemon of the machine that spawned it: that
235
+ machine must be online and on CLI \u2265 0.2.157, the operation runs there with
236
+ its keys, and only the result travels back \u2014 this CLI never holds the
237
+ account content key or another machine's session key. Without --machine
238
+ the online machines are asked in turn which one holds the session.
239
+ approve / deny / stop / archive remain local-only.
213
240
 
214
241
  ${chalk.bold("Exit codes:")}
215
242
  2 read --wait: the turn had not ended when --timeout ran out (the
216
243
  partial state is still printed)
217
244
  0 success (approve/deny: the wrapper acknowledged the verdict; check
218
245
  \`settled\` in --json to see whether the request has left the pending set)
219
- 1 bad arguments, unknown session, no local key, request not pending,
220
- session not online, or the operation failed (including \`stop\` on a
221
- session the daemon is not running)
246
+ 1 bad arguments, unknown session, no machine of the account holds it (or
247
+ its machine is offline / too old), request not pending, session not
248
+ online, or the operation failed (including \`stop\` on a session the
249
+ daemon is not running)
222
250
  `;
223
251
  function formatWait(ms) {
224
252
  const minutes = Math.floor(ms / 6e4);
@@ -227,17 +255,19 @@ function formatWait(ms) {
227
255
  return `${Math.floor(minutes / 60)}h${minutes % 60}m`;
228
256
  }
229
257
  function formatAccountSummaryLine(summary) {
230
- const state = summary.attention ? "[attention]" : summary.live ? "[running here]" : summary.active ? "[active elsewhere]" : summary.archived ? "[archived]" : "[idle]";
258
+ const state = summary.attention ? "[attention]" : summary.live ? summary.machine ? `[running on ${summary.machine.host}]` : "[running here]" : summary.active ? "[active elsewhere]" : summary.archived ? "[archived]" : "[idle]";
231
259
  const parts = [summary.id, state];
232
- if (!summary.decryptable) {
233
- parts.push("(not decryptable from this machine)");
260
+ if (!summary.readable) {
261
+ parts.push("(not readable: no online machine holds its key)");
234
262
  } else {
263
+ if (summary.machine) parts.push(`via=${summary.machine.host}`);
235
264
  if (summary.title) parts.push(`title="${summary.title}"`);
236
265
  if (summary.flavor) parts.push(`agent=${summary.flavor}`);
237
266
  if (summary.tags?.length) parts.push(`tags=${summary.tags.join(",")}`);
238
267
  if (summary.variant) parts.push(`variant=${summary.variant}`);
239
268
  if (summary.machineId) parts.push(`machine=${summary.machineId}`);
240
269
  if (summary.cwd) parts.push(`cwd=${summary.cwd}`);
270
+ if (summary.backgroundTasks && summary.backgroundTasks.count > 0) parts.push(formatBackgroundTasks(summary.backgroundTasks));
241
271
  for (const request of summary.pending ?? []) {
242
272
  parts.push(`pending=${request.id}:${request.tool}${request.waitingMs !== void 0 ? `(${formatWait(request.waitingMs)})` : ""}`);
243
273
  }
@@ -248,6 +278,10 @@ function formatAccountSummaryLine(summary) {
248
278
  function permissionFailureRecord(sessionId, requestId, error) {
249
279
  return { sessionId, requestId, error: error instanceof Error ? error.message : String(error) };
250
280
  }
281
+ function formatBackgroundTasks(report) {
282
+ const types = report.tasks.map((task) => task.type).filter(Boolean);
283
+ return `background=${report.count}${types.length ? `(${types.join(",")})` : ""}`;
284
+ }
251
285
  function formatSummaryLine(summary) {
252
286
  const parts = [summary.id, summary.live ? "[running]" : "[not running]"];
253
287
  if (summary.title) parts.push(`title="${summary.title}"`);
@@ -256,6 +290,7 @@ function formatSummaryLine(summary) {
256
290
  if (summary.variant) parts.push(`variant=${summary.variant}`);
257
291
  if (summary.cwd) parts.push(`cwd=${summary.cwd}`);
258
292
  if (summary.pid !== void 0) parts.push(`pid=${summary.pid}`);
293
+ if (summary.backgroundTasks && summary.backgroundTasks.count > 0) parts.push(formatBackgroundTasks(summary.backgroundTasks));
259
294
  parts.push(summary.url);
260
295
  return parts.join(" ");
261
296
  }
@@ -274,20 +309,48 @@ async function handleSessionsCommand(args) {
274
309
  }
275
310
  try {
276
311
  if (options.action === "list" && options.all) {
277
- const sessions = await agentGuidance.listAccountSessions({ tag: options.tag, recentLimit: options.limit, includeArchived: options.includeArchived });
312
+ const listed = await agentGuidance.listAccountSessions({ recentLimit: options.tag ? 150 : options.limit, includeArchived: options.includeArchived });
313
+ const fill = await agentGuidance.fillForeignAccountRows(listed);
314
+ let sessions = fill.rows;
315
+ if (options.tag) {
316
+ const wanted = options.tag;
317
+ sessions = sessions.filter((summary) => summary.tags?.includes(wanted) === true);
318
+ const recentLimit = Math.max(0, options.limit ?? agentGuidance.DEFAULT_RECENT_LIMIT);
319
+ let idleKept = 0;
320
+ sessions = sessions.filter((summary) => summary.attention || summary.live || idleKept++ < recentLimit);
321
+ }
278
322
  if (options.json) {
279
- console.log(JSON.stringify({ sessions, scope: "account" }));
323
+ console.log(JSON.stringify({ sessions, scope: "account", machines: { asked: fill.asked, skipped: fill.skipped } }));
280
324
  } else if (sessions.length === 0) {
281
- console.log(options.tag ? `No decryptable sessions tagged "${options.tag}" on this account.` : "No sessions found on this account.");
325
+ console.log(options.tag ? `No readable sessions tagged "${options.tag}" on this account.` : "No sessions found on this account.");
282
326
  } else {
283
327
  for (const summary of sessions) console.log(formatAccountSummaryLine(summary));
284
- const foreign = sessions.filter((summary) => !summary.decryptable).length;
328
+ const foreign = sessions.filter((summary) => !summary.readable).length;
329
+ const filled = fill.asked.reduce((n, a) => n + a.filled, 0);
330
+ if (filled > 0) {
331
+ 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(", ")}).`));
332
+ }
333
+ for (const asked of fill.asked.filter((a) => a.error)) console.log(chalk.dim(`machine ${asked.machineId}: ${asked.error}`));
285
334
  if (foreign > 0) {
286
- console.log(chalk.dim(`${foreign} session(s) belong to another machine and cannot be decrypted here (no local key).`));
335
+ const why = fill.skipped.length > 0 ? ` (${fill.skipped.map((s) => `${s.machineId}: ${s.reason}`).join("; ")})` : "";
336
+ console.log(chalk.dim(`${foreign} session(s) belong to a machine that is not reachable right now${why}.`));
287
337
  }
288
338
  }
289
339
  process.exit(0);
290
340
  }
341
+ if (options.action === "list" && options.machine) {
342
+ const listing = await agentGuidance.listRemoteSessions(options.machine, { tag: options.tag, limit: options.limit });
343
+ const sessions = listing.sessions;
344
+ if (options.json) {
345
+ console.log(JSON.stringify({ sessions, machine: { id: listing.machineId, host: listing.host } }));
346
+ } else if (sessions.length === 0) {
347
+ console.log(options.tag ? `No sessions tagged "${options.tag}" on ${listing.host}.` : `No sessions found on ${listing.host}.`);
348
+ } else {
349
+ console.log(chalk.dim(`machine ${listing.host} (${listing.machineId})`));
350
+ for (const summary of sessions) console.log(formatSummaryLine(summary));
351
+ }
352
+ process.exit(0);
353
+ }
291
354
  if (options.action === "list") {
292
355
  const sessions = await agentGuidance.listSessions({ tag: options.tag, recentLimit: options.limit });
293
356
  if (options.json) {
@@ -304,12 +367,13 @@ async function handleSessionsCommand(args) {
304
367
  const persisted = persistence.readPersistedSessions();
305
368
  const context = { self: () => agentGuidance.cliPeerSelf(process.env, cwd, persisted), readPersisted: () => persisted };
306
369
  if (options.action === "peers") {
307
- const listing = await agentGuidance.listPeerSessions(context, options.scope ?? "repo");
370
+ const listing = options.machine ? await agentGuidance.listRemotePeers(options.machine, { ...options.scope ? { scope: options.scope } : {}, ...options.cwd ? { cwd: options.cwd } : {} }) : await agentGuidance.listPeerSessions(context, options.scope ?? "repo");
308
371
  if (options.json) {
309
372
  console.log(JSON.stringify(listing));
310
373
  } else if (listing.peers.length === 0) {
311
- console.log(`No other live sessions in scope "${listing.scope}"${listing.self.repoRoot ? ` (repo ${listing.self.repoRoot})` : ""}.`);
374
+ console.log(`No other live sessions in scope "${listing.scope}"${listing.self.repoRoot ? ` (repo ${listing.self.repoRoot})` : ""}${"host" in listing ? ` on ${listing.host}` : ""}.`);
312
375
  } else {
376
+ if ("host" in listing) console.log(chalk.dim(`machine ${String(listing.host)} (${String(listing.machineId)})`));
313
377
  for (const peer of listing.peers) {
314
378
  const parts = [peer.sessionId, `[${peer.kind === "mirror" ? "terminal" : "running"}]`];
315
379
  if (peer.title) parts.push(`title="${peer.title}"`);
@@ -324,9 +388,9 @@ async function handleSessionsCommand(args) {
324
388
  }
325
389
  const target = options.sessionId;
326
390
  try {
327
- const result = await agentGuidance.sendPeerMessage(context, { to: target, body: options.text, replyTo: options.replyTo });
391
+ const result = await agentGuidance.sendPeerMessage(context, { to: target, body: options.text, replyTo: options.replyTo, ...options.machine ? { machineId: options.machine } : {} });
328
392
  if (options.json) console.log(JSON.stringify(result));
329
- else if (result.delivered) console.log(`Message ${result.messageId} delivered to ${target}
393
+ else if (result.delivered) console.log(`Message ${result.messageId} delivered to ${target}${result.machine ? ` on ${result.machine.host}` : ""}
330
394
  ${result.url}`);
331
395
  else console.error(chalk.yellow("Not delivered:"), result.error ?? `session is ${result.status}`, result.stored ? "(stored server-side, unread)" : "");
332
396
  process.exit(result.delivered ? 0 : 1);
@@ -338,24 +402,44 @@ ${result.url}`);
338
402
  const sessionId = options.sessionId;
339
403
  if (options.action === "read") {
340
404
  let timedOut = false;
341
- if (options.wait) {
342
- try {
343
- await agentGuidance.waitForTurnEnd(sessionId, { timeoutMs: (options.timeoutSec ?? 600) * 1e3 });
344
- } catch (error) {
345
- if (!(error instanceof agentGuidance.TurnWaitTimeoutError)) throw error;
346
- timedOut = true;
347
- console.error(chalk.yellow("Timeout:"), error.message);
405
+ const local = !options.machine && persistence.readPersistedSessions()[sessionId] !== void 0;
406
+ let result;
407
+ if (local) {
408
+ if (options.wait) {
409
+ try {
410
+ await agentGuidance.waitForTurnEnd(sessionId, { timeoutMs: (options.timeoutSec ?? 600) * 1e3 });
411
+ } catch (error) {
412
+ if (!(error instanceof agentGuidance.TurnWaitTimeoutError)) throw error;
413
+ timedOut = true;
414
+ console.error(chalk.yellow("Timeout:"), error.message);
415
+ }
416
+ }
417
+ result = await agentGuidance.readSessionTranscript(sessionId, options.limit ?? 20, { full: options.full });
418
+ } else {
419
+ const readOptions = { limit: options.limit ?? 20, full: options.full, ...options.machine ? { machineId: options.machine } : {} };
420
+ let remote;
421
+ if (options.wait) {
422
+ const timeoutMs = (options.timeoutSec ?? 600) * 1e3;
423
+ const waited = await agentGuidance.waitForRemoteTurnEnd(sessionId, { ...readOptions, timeoutMs });
424
+ remote = waited.read;
425
+ if (waited.timedOut) {
426
+ timedOut = true;
427
+ console.error(chalk.yellow("Timeout:"), `Session ${sessionId} on ${remote.host}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
428
+ }
429
+ } else {
430
+ remote = await agentGuidance.readRemoteTranscript(sessionId, readOptions);
348
431
  }
432
+ const { machineId, host, ...rest } = remote;
433
+ result = { ...rest, machine: { id: machineId, host } };
349
434
  }
350
- const result = await agentGuidance.readSessionTranscript(sessionId, options.limit ?? 20, { full: options.full });
351
435
  if (options.json) {
352
- console.log(JSON.stringify(options.answer ? { sessionId, turn: result.turn } : result));
436
+ console.log(JSON.stringify(options.answer ? { sessionId, turn: result.turn, ...result.machine ? { machine: result.machine } : {} } : result));
353
437
  } else if (options.answer) {
354
438
  if (result.turn.answer.length > 0) console.log(result.turn.answer);
355
439
  else console.error(chalk.dim("(the agent has not replied to the latest prompt yet)"));
356
440
  } else {
357
- console.log(formatSummaryLine(result.summary));
358
- console.log(`--- last ${result.messageCount} message(s) ---`);
441
+ console.log(formatSummaryLine(result.summary) + (result.machine ? chalk.dim(` machine=${result.machine.host}`) : ""));
442
+ console.log(`--- last ${result.messageCount} message(s)${result.truncated ? " (truncated to fit the RPC budget)" : ""} ---`);
359
443
  console.log(result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)");
360
444
  const turn = result.turn;
361
445
  console.log(chalk.dim(`--- latest turn: ${turn.ended ? `ended (${turn.status ?? "unknown"})` : "running"}${turn.error ? ` \u2014 ${turn.error}` : ""} ---`));