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,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var axios = require('axios');
4
- var persistence = require('./limits-DkfHJ3nS.cjs');
4
+ var persistence = require('./limits-CyuIt-Vl.cjs');
5
5
  var node_events = require('node:events');
6
6
  var socket_ioClient = require('socket.io-client');
7
7
  var z = require('zod');
@@ -438,6 +438,15 @@ async function deriveKey$1(master, usage, path) {
438
438
 
439
439
  class RpcHandlerManager {
440
440
  handlers = /* @__PURE__ */ new Map();
441
+ /**
442
+ * B-506: methods whose params/result are PLAINTEXT JSON strings instead of
443
+ * machine-key ciphertext. The caller is a CLI on another machine of the
444
+ * same account, which holds neither this machine's key nor the account
445
+ * content key; the trusted relay sees the payload either way (this fork
446
+ * is server-trusted, not e2e). Only `registerPlainHandler` puts a method
447
+ * here; everything else keeps decrypting.
448
+ */
449
+ plainMethods = /* @__PURE__ */ new Set();
441
450
  scopePrefix;
442
451
  encryptionKey;
443
452
  encryptionVariant;
@@ -462,14 +471,31 @@ class RpcHandlerManager {
462
471
  unregisterHandler(method) {
463
472
  const prefixedMethod = this.getPrefixedMethod(method);
464
473
  this.handlers.delete(prefixedMethod);
474
+ this.plainMethods.delete(prefixedMethod);
465
475
  for (const socket of this.sockets) socket.emit("rpc-unregister", { method: prefixedMethod });
466
476
  }
477
+ /**
478
+ * B-506: register a method whose request is a plaintext JSON string and
479
+ * whose response is returned as a plaintext JSON string (see
480
+ * `plainMethods`). The handler receives the parsed params; a throw is
481
+ * answered with `{ error }` like the encrypted path.
482
+ */
483
+ registerPlainHandler(method, handler) {
484
+ this.plainMethods.add(this.getPrefixedMethod(method));
485
+ this.registerHandler(method, handler);
486
+ }
487
+ isPlainMethod(prefixedMethod) {
488
+ return this.plainMethods.has(prefixedMethod);
489
+ }
467
490
  /**
468
491
  * Handle an incoming RPC request
469
492
  * @param request - The RPC request data
470
493
  * @param callback - The response callback
471
494
  */
472
495
  async handleRequest(request) {
496
+ if (this.plainMethods.has(request.method)) {
497
+ return this.handlePlainRequest(request);
498
+ }
473
499
  try {
474
500
  const handler = this.handlers.get(request.method);
475
501
  if (!handler) {
@@ -493,6 +519,25 @@ class RpcHandlerManager {
493
519
  return persistence.encodeBase64(persistence.encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
494
520
  }
495
521
  }
522
+ /** Plaintext variant of handleRequest (B-506). Never throws; never encrypts. */
523
+ async handlePlainRequest(request) {
524
+ const handler = this.handlers.get(request.method);
525
+ if (!handler) return JSON.stringify({ error: "Method not found" });
526
+ let params;
527
+ try {
528
+ params = typeof request.params === "string" && request.params.length > 0 ? JSON.parse(request.params) : {};
529
+ } catch {
530
+ return JSON.stringify({ error: "Invalid JSON params" });
531
+ }
532
+ try {
533
+ this.logger("[RPC] Calling plain handler", { method: request.method });
534
+ const result = await handler(params);
535
+ return JSON.stringify(result === void 0 ? null : result);
536
+ } catch (error) {
537
+ this.logger("[RPC] [ERROR] Error handling plain request", { error });
538
+ return JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" });
539
+ }
540
+ }
496
541
  onSocketConnect(socket) {
497
542
  this.sockets.add(socket);
498
543
  for (const [prefixedMethod] of this.handlers) {
@@ -3000,16 +3045,29 @@ class ApiSessionClient extends node_events.EventEmitter {
3000
3045
  /** B-466: turn edges (and a periodic lease) reported to the local daemon so
3001
3046
  * auto-update waits for "no turn in flight" instead of "no sessions". */
3002
3047
  turnReporter = new TurnReporter((event) => void persistence.notifyDaemonTurnEvent(this.sessionId, event).catch((error) => persistence.logger.debug("[API] Failed to report turn event to daemon:", error)));
3003
- keepAlive(thinking, mode) {
3048
+ /** B-507: background tasks in flight, published on change and renewed every
3049
+ * 60s to the daemon (`/session-event background_tasks`) and into agentState
3050
+ * (`backgroundTasks: { updatedAt, tasks }`) — the two readers that outlive a
3051
+ * single heartbeat. The heartbeat itself carries only the count. */
3052
+ backgroundTaskReporter = new persistence.BackgroundTaskReporter((tasks, now) => {
3053
+ void persistence.notifyDaemonBackgroundTasks(this.sessionId, tasks).catch((error) => persistence.logger.debug("[API] Failed to report background tasks to daemon:", error));
3054
+ this.updateAgentState((state) => ({
3055
+ ...state,
3056
+ backgroundTasks: tasks.length > 0 ? { updatedAt: now, tasks } : void 0
3057
+ }));
3058
+ });
3059
+ keepAlive(thinking, mode, backgroundTasks) {
3004
3060
  if (process.env.DEBUG) {
3005
3061
  persistence.logger.debug(`[API] Sending keep alive message: ${thinking}`);
3006
3062
  }
3007
3063
  this.turnReporter.observe(thinking);
3064
+ if (backgroundTasks) this.backgroundTaskReporter.observe(backgroundTasks);
3008
3065
  this.socket.volatile.emit("session-alive", {
3009
3066
  sid: this.sessionId,
3010
3067
  time: Date.now(),
3011
3068
  thinking,
3012
- mode
3069
+ mode,
3070
+ ...backgroundTasks ? { backgroundTasks: backgroundTasks.length } : {}
3013
3071
  });
3014
3072
  }
3015
3073
  /**
@@ -14356,6 +14414,17 @@ class ApiMachineClient {
14356
14414
  return { claudeAuth };
14357
14415
  });
14358
14416
  }
14417
+ /**
14418
+ * B-506: the five `sessions.*` methods a CLI on ANOTHER machine of this
14419
+ * account calls to read / message sessions this daemon spawned. Plaintext
14420
+ * on purpose (no shared key with the caller); see
14421
+ * `api/rpc/RpcHandlerManager.ts` `registerPlainHandler`.
14422
+ */
14423
+ setRemoteSessionOpsHandlers(handlers) {
14424
+ for (const [method, handler] of Object.entries(handlers)) {
14425
+ this.rpcHandlerManager.registerPlainHandler(method, handler);
14426
+ }
14427
+ }
14359
14428
  setRPCHandlers({
14360
14429
  spawnSession,
14361
14430
  resumeSession,
@@ -15184,7 +15253,11 @@ class ApiMachineClient {
15184
15253
  }
15185
15254
  bindRpcRequestHandler(socket, transport) {
15186
15255
  socket.on("rpc-request", async (data, callback) => {
15187
- persistence.logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
15256
+ if (typeof data?.method === "string" && this.rpcHandlerManager.isPlainMethod(data.method)) {
15257
+ persistence.logger.debug(`[API MACHINE] Received plain RPC request via ${transport}: ${data.method}`);
15258
+ } else {
15259
+ persistence.logger.debugLargeJson(`[API MACHINE] Received RPC request via ${transport}:`, data);
15260
+ }
15188
15261
  await answerSocketRequest(
15189
15262
  callback,
15190
15263
  () => this.rpcHandlerManager.handleRequest(data),
@@ -16222,17 +16295,56 @@ class ApiClient {
16222
16295
  }
16223
16296
  }
16224
16297
  /**
16225
- * Mark a session as inactive on the server (active=false). Does NOT
16226
- * change `lifecycleState`, so the session remains visible in the app
16227
- * and resumable — same effect as the in-app "Archive" button hitting
16228
- * the /archive endpoint, but without the extra metadata.
16298
+ * B-505: mark a session OFFLINE on the server (active=false, `archivedAt`
16299
+ * untouched) — the same row state a missed keepalive produces, so the web
16300
+ * shows it offline and resumable instead of archived.
16301
+ *
16302
+ * Used when the wrapper exits for an infrastructure reason (SIGTERM /
16303
+ * SIGINT: daemon stop, supervisor restart, machine shutdown) as a
16304
+ * synchronous fallback for the socket `session-end` signal: even if the
16305
+ * socket emit doesn't drain before the process exits, the HTTP response
16306
+ * confirms the row went inactive.
16229
16307
  *
16230
- * Used during graceful shutdown (Ctrl-C / SIGTERM) as a synchronous
16231
- * fallback for the socket-based session-end signal: even if the
16232
- * socket emit doesn't drain before the process exits, the HTTP
16233
- * response confirms the deactivate landed.
16308
+ * This MUST NOT hit `/archive`: since B-265 that route writes the
16309
+ * server-owned `archivedAt` tombstone (the only thing that distinguishes
16310
+ * "archived" from "offline"), and the server answers it with a
16311
+ * `session-archive` command that makes the wrapper archive itself. That is
16312
+ * exactly how every dev-sg upgrade archived the sessions mid-turn on
16313
+ * 2026-09-25. A server without `/deactivate` returns 404, which is a
16314
+ * compatible no-op: `session-end` already sets active=false there.
16234
16315
  */
16235
16316
  async deactivateSession(sessionId) {
16317
+ try {
16318
+ const response = await axios.post(
16319
+ `${persistence.configuration.serverUrl}/v1/sessions/${sessionId}/deactivate`,
16320
+ {},
16321
+ {
16322
+ headers: {
16323
+ "Authorization": `Bearer ${this.credential.token}`,
16324
+ "X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
16325
+ },
16326
+ timeout: 3e3,
16327
+ validateStatus: (status) => status >= 200 && status < 300 || status === 404
16328
+ }
16329
+ );
16330
+ if (response.status === 404) {
16331
+ persistence.logger.debug("[API] deactivateSession: server has no /deactivate route; relying on session-end");
16332
+ return false;
16333
+ }
16334
+ return true;
16335
+ } catch (error) {
16336
+ persistence.logger.debug("[API] deactivateSession failed:", error);
16337
+ return false;
16338
+ }
16339
+ }
16340
+ /**
16341
+ * Archive a session on the server: writes the `archivedAt` tombstone and
16342
+ * makes the server tell every wrapper of this session to exit. Only for an
16343
+ * intentional end of life — the user's Archive / kill, a crash the wrapper
16344
+ * cannot recover from, a terminal mirror whose pane went back to a shell.
16345
+ * Never call this from a signal handler; see `deactivateSession`.
16346
+ */
16347
+ async archiveSession(sessionId) {
16236
16348
  try {
16237
16349
  const response = await axios.post(
16238
16350
  `${persistence.configuration.serverUrl}/v1/sessions/${sessionId}/archive`,
@@ -16247,7 +16359,7 @@ class ApiClient {
16247
16359
  );
16248
16360
  return response.status >= 200 && response.status < 300;
16249
16361
  } catch (error) {
16250
- persistence.logger.debug("[API] deactivateSession failed:", error);
16362
+ persistence.logger.debug("[API] archiveSession failed:", error);
16251
16363
  return false;
16252
16364
  }
16253
16365
  }
@@ -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-C_DW0M0q.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-A2c-3M-_.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) {
@@ -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),
@@ -16201,17 +16274,56 @@ class ApiClient {
16201
16274
  }
16202
16275
  }
16203
16276
  /**
16204
- * Mark a session as inactive on the server (active=false). Does NOT
16205
- * change `lifecycleState`, so the session remains visible in the app
16206
- * and resumable — same effect as the in-app "Archive" button hitting
16207
- * the /archive endpoint, but without the extra metadata.
16277
+ * B-505: mark a session OFFLINE on the server (active=false, `archivedAt`
16278
+ * untouched) — the same row state a missed keepalive produces, so the web
16279
+ * shows it offline and resumable instead of archived.
16280
+ *
16281
+ * Used when the wrapper exits for an infrastructure reason (SIGTERM /
16282
+ * SIGINT: daemon stop, supervisor restart, machine shutdown) as a
16283
+ * synchronous fallback for the socket `session-end` signal: even if the
16284
+ * socket emit doesn't drain before the process exits, the HTTP response
16285
+ * confirms the row went inactive.
16208
16286
  *
16209
- * Used during graceful shutdown (Ctrl-C / SIGTERM) as a synchronous
16210
- * fallback for the socket-based session-end signal: even if the
16211
- * socket emit doesn't drain before the process exits, the HTTP
16212
- * response confirms the deactivate landed.
16287
+ * This MUST NOT hit `/archive`: since B-265 that route writes the
16288
+ * server-owned `archivedAt` tombstone (the only thing that distinguishes
16289
+ * "archived" from "offline"), and the server answers it with a
16290
+ * `session-archive` command that makes the wrapper archive itself. That is
16291
+ * exactly how every dev-sg upgrade archived the sessions mid-turn on
16292
+ * 2026-09-25. A server without `/deactivate` returns 404, which is a
16293
+ * compatible no-op: `session-end` already sets active=false there.
16213
16294
  */
16214
16295
  async deactivateSession(sessionId) {
16296
+ try {
16297
+ const response = await axios.post(
16298
+ `${configuration.serverUrl}/v1/sessions/${sessionId}/deactivate`,
16299
+ {},
16300
+ {
16301
+ headers: {
16302
+ "Authorization": `Bearer ${this.credential.token}`,
16303
+ "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
16304
+ },
16305
+ timeout: 3e3,
16306
+ validateStatus: (status) => status >= 200 && status < 300 || status === 404
16307
+ }
16308
+ );
16309
+ if (response.status === 404) {
16310
+ logger.debug("[API] deactivateSession: server has no /deactivate route; relying on session-end");
16311
+ return false;
16312
+ }
16313
+ return true;
16314
+ } catch (error) {
16315
+ logger.debug("[API] deactivateSession failed:", error);
16316
+ return false;
16317
+ }
16318
+ }
16319
+ /**
16320
+ * Archive a session on the server: writes the `archivedAt` tombstone and
16321
+ * makes the server tell every wrapper of this session to exit. Only for an
16322
+ * intentional end of life — the user's Archive / kill, a crash the wrapper
16323
+ * cannot recover from, a terminal mirror whose pane went back to a shell.
16324
+ * Never call this from a signal handler; see `deactivateSession`.
16325
+ */
16326
+ async archiveSession(sessionId) {
16215
16327
  try {
16216
16328
  const response = await axios.post(
16217
16329
  `${configuration.serverUrl}/v1/sessions/${sessionId}/archive`,
@@ -16226,7 +16338,7 @@ class ApiClient {
16226
16338
  );
16227
16339
  return response.status >= 200 && response.status < 300;
16228
16340
  } catch (error) {
16229
- logger.debug("[API] deactivateSession failed:", error);
16341
+ logger.debug("[API] archiveSession failed:", error);
16230
16342
  return false;
16231
16343
  }
16232
16344
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.155",
3
+ "version": "0.2.157",
4
4
  "description": "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "contributors": [
@@ -117,12 +117,12 @@
117
117
  "zod": "^4.0.0"
118
118
  },
119
119
  "optionalDependencies": {
120
- "very-happy-tools-arm64-darwin": "0.2.155",
121
- "very-happy-tools-x64-darwin": "0.2.155",
122
- "very-happy-tools-arm64-linux": "0.2.155",
123
- "very-happy-tools-x64-linux": "0.2.155",
124
- "very-happy-tools-arm64-win32": "0.2.155",
125
- "very-happy-tools-x64-win32": "0.2.155"
120
+ "very-happy-tools-arm64-darwin": "0.2.157",
121
+ "very-happy-tools-x64-darwin": "0.2.157",
122
+ "very-happy-tools-arm64-linux": "0.2.157",
123
+ "very-happy-tools-x64-linux": "0.2.157",
124
+ "very-happy-tools-arm64-win32": "0.2.157",
125
+ "very-happy-tools-x64-win32": "0.2.157"
126
126
  },
127
127
  "devDependencies": {
128
128
  "@slopus/happy-wire": "workspace:*",