very-happy-cli 0.2.156 → 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-BHe1_F1h.cjs → AcpBackend-BymWX-LI.cjs} +2 -2
  2. package/dist/{AcpBackend-C3nc1u-s.mjs → AcpBackend-Cy18U7yw.mjs} +2 -2
  3. package/dist/{AcpSessionManager-BVxkQnNf.cjs → AcpSessionManager-BF7zBGAi.cjs} +1 -1
  4. package/dist/{AcpSessionManager-dN3k1LbI.mjs → AcpSessionManager-Ds8rcp95.mjs} +1 -1
  5. package/dist/{agentGuidance-C604n0Zq.mjs → agentGuidance-CU6YD_Ms.mjs} +560 -29
  6. package/dist/{agentGuidance-C-CmV03f.cjs → agentGuidance-Db76O1kw.cjs} +579 -28
  7. package/dist/{agentHome-CmIJNV7f.cjs → agentHome-B0CLy0Qx.cjs} +2 -2
  8. package/dist/{agentHome-D2XGsoqs.mjs → agentHome-BaJ9xtxB.mjs} +2 -2
  9. package/dist/{auto-B1I6G_Nf.mjs → auto-B1YAwaeU.mjs} +6 -6
  10. package/dist/{auto-CMDq4oIu.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-BYHSRY2E.cjs → config-CzkBjZnV.cjs} +4 -4
  14. package/dist/{config-BkTu25bH.mjs → config-aAbhJllb.mjs} +4 -4
  15. package/dist/{index-B5QbzEga.mjs → index-BVu8Cufk.mjs} +8 -8
  16. package/dist/{index-DAAreiRF.mjs → index-BYU1e81k.mjs} +288 -44
  17. package/dist/{index-BbtR-Qto.cjs → index-C9QzZsUQ.cjs} +291 -47
  18. package/dist/{index-B_-7NpGZ.cjs → index-CTbGqVFY.cjs} +7 -7
  19. package/dist/{index-CB8QnjvS.cjs → index-Cz-uqnJk.cjs} +4 -4
  20. package/dist/{index-PYO8Mdz4.mjs → index-DQcRkI9r.mjs} +4 -4
  21. package/dist/index.cjs +4 -4
  22. package/dist/index.mjs +4 -4
  23. package/dist/{install-CO1ps6Wt.cjs → install-BMlV0Tnr.cjs} +1 -1
  24. package/dist/{install-CNtXzwT_.mjs → install-BuYnqtVa.mjs} +1 -1
  25. package/dist/{installTerminalHooks-BOAnQ1Qe.cjs → installTerminalHooks-BVVOEGhG.cjs} +4 -4
  26. package/dist/{installTerminalHooks-CMpOV3RU.mjs → installTerminalHooks-ELDoqh0A.mjs} +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-A2c-3M-_.mjs} +162 -11
  32. package/dist/{limits-o29KXBgj.cjs → limits-CyuIt-Vl.cjs} +169 -10
  33. package/dist/{mcp-C_uejotl.mjs → mcp-Bo934Kpy.mjs} +4 -4
  34. package/dist/{mcp-CwK1nI7Y.cjs → mcp-CXkvnztT.cjs} +4 -4
  35. package/dist/{piRuntime-Dj5WnqzJ.cjs → piRuntime-CJjA26oD.cjs} +2 -2
  36. package/dist/{piRuntime-Cu2-8nOT.mjs → piRuntime-DdEY2mE9.mjs} +2 -2
  37. package/dist/{piTerminal-D-BIbov9.cjs → piTerminal-CpU3hzrX.cjs} +6 -6
  38. package/dist/{piTerminal-DanZeBrO.mjs → piTerminal-DwWmgumi.mjs} +6 -6
  39. package/dist/{runGemini-CZ_lhMWY.mjs → runGemini-Cj6oJQ-I.mjs} +6 -6
  40. package/dist/{runGemini--QqK-Ap5.cjs → runGemini-DS-0qm0L.cjs} +6 -6
  41. package/dist/{runOpenClaw-C0xLhTvk.cjs → runOpenClaw-CvSm8oTc.cjs} +5 -5
  42. package/dist/{runOpenClaw-CxgLpm38.mjs → runOpenClaw-DUCQxkI-.mjs} +5 -5
  43. package/dist/{send-CTcGMVuG.cjs → send-CHxobaoI.cjs} +43 -31
  44. package/dist/{send-BaOkDgH0.mjs → send-D5CyRyZ_.mjs} +43 -31
  45. package/dist/{sessions-OmvyJb0n.cjs → sessions-BFlIrKCY.cjs} +133 -49
  46. package/dist/{sessions-Bn304xF1.mjs → sessions-DsE01xKc.mjs} +133 -49
  47. package/dist/{spawn-BKNabutV.mjs → spawn-CdihhwHs.mjs} +4 -4
  48. package/dist/{spawn-CC6V2KiA.cjs → spawn-HV345RN-.cjs} +4 -4
  49. package/dist/{teams-CLbS2_4Q.mjs → teams-BE8nMB6O.mjs} +5 -5
  50. package/dist/{teams-XtrH3_h3.cjs → teams-BahinMnH.cjs} +5 -5
  51. package/dist/{todo-CEDvKNXK.cjs → todo-CMMQ6yhU.cjs} +2 -2
  52. package/dist/{todo-BcPbNKt2.mjs → todo-CuQAkSp2.mjs} +2 -2
  53. package/dist/{types-CphWjqHc.cjs → types-B0sSi6eM.cjs} +77 -4
  54. package/dist/{types-BEVzYSoV.mjs → types-BrGJ7PJz.mjs} +77 -4
  55. package/package.json +7 -7
@@ -1,14 +1,14 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
- import os, { homedir, userInfo, tmpdir } from 'node:os';
2
+ import os, { homedir, hostname, userInfo, tmpdir } from 'node:os';
3
3
  import { randomUUID, randomInt, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
- import { g as getProjectPath, k as initializeSandbox, w as wrapCommand, j as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, l as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, m as commandOnPath, n as deriveLocalCliUpdateSummary, o as isClaudeSessionId, q as getLocalHappyAgentCredentialPath, s as readLocalHappyAgentCredentials, t as parseExactVersion, u as compareExactVersions, v as extractCompleteLines, x as decideBackfill, y as parseTerminalHookPayload, z as tmuxArgs, B as scrubTmuxClientEnv, D as decideMirrorBinding, E as MIRROR_BACKFILL_LINES_DEFAULT, G as mirrorLineKey, H as mirrorLocalId, J as detectCLIAvailability, K as detectResumeSupport, L as refreshAgentHomes, d as describeAgentHome, T as TurnActivityTracker, N as summarizeSpawnSessionForLog, O as locateClaudeConversation, P as agentHomeSpawnEnv, Q as shellescape, A as ApiClient, S as fetchCliUpdateState, U as resolveCliUpdateCheckInterval, V as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, W as listVhTerminals, X as readVhTerminal, Y as sendToVhTerminal, Z as TITLE_PROMPT_PREFIX, _ as BOARD_ANALYZER_PROMPT_PREFIX, h as connectionState, $ as startOfflineReconnection, C as CodexAppServerClient } from './types-BEVzYSoV.mjs';
5
- import { l as logger, a7 as notifyDaemonSessionEvent, x as BUILTIN_TODO_DISCOVERY, g as delay, a8 as STREAM_DELTA_MAX_CHARS, a9 as ensurePrivateDirectory, aa as hardenPrivateFile, c as configuration, ab as notifyDaemonClaudeAuthFailed, F as packageJson, r as readSettings, ac as readCredentials, Z as credentialRelayProblem, k as checkIfDaemonRunningAndCleanupStaleState, ad as readDaemonState, ae as AGENT_VERSION_SOURCES, w as encodeBase64, af as readSettingsOutcome, u as updateSettings, ag as encodeBase64Url, M as decodeBase64, ah as writeCredentialsLegacy, ai as writeCredentialsDataKey, d as readPersistedSessions, E as writePrivateFileSync, aj as decryptWithDataKey, ak as decryptLegacy, al as persistSession, z as ensurePrivateDirectorySync, am as getAgentCodeDefaults, an as normalizeAgentKey, ao as isClaudeOpus55Model, ap as supportsClaudeOpus55, aq as AUTOMATION_SUMMARY_MAX_CHARS, ar as AUTOMATION_CLAIM_MAX_RUNS, as as AUTOMATION_DEFAULT_LEASE_MS, f as isAutomationRunTerminal, at as isDaemonRunningCurrentlyInstalledHappyVersion, au as stopDaemon, av as acquireDaemonLock, aw as deletePersistedSessions, N as decrypt, ax as writeDaemonState, ay as writeSettings, az as cleanupDaemonState, aA as releaseDaemonLock, i as isValidSessionId, m as spawnDaemonSession, a1 as isValidTerminalId, aB as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, o as CLIPBOARD_MAX_BYTES, v as notifyDaemonSessionStarted, y as reportSessionEditToDaemon, aC as clearCredentials, aD as clearMachineId, h as readCredentialsForConfiguredRelay, aE as SandboxConfigSchema, e as createEnvelope, O as listDaemonSessions, n as stopDaemonSession, aF as getLatestDaemonLog } from './limits-DmMdchTz.mjs';
4
+ import { g as getProjectPath, k as initializeSandbox, w as wrapCommand, j as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, l as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, m as commandOnPath, n as deriveLocalCliUpdateSummary, o as isClaudeSessionId, q as getLocalHappyAgentCredentialPath, s as readLocalHappyAgentCredentials, t as parseExactVersion, u as compareExactVersions, v as extractCompleteLines, x as decideBackfill, y as parseTerminalHookPayload, z as tmuxArgs, B as scrubTmuxClientEnv, D as decideMirrorBinding, E as MIRROR_BACKFILL_LINES_DEFAULT, G as mirrorLineKey, H as mirrorLocalId, J as detectCLIAvailability, K as detectResumeSupport, L as refreshAgentHomes, d as describeAgentHome, T as TurnActivityTracker, N as summarizeSpawnSessionForLog, O as locateClaudeConversation, P as agentHomeSpawnEnv, Q as shellescape, A as ApiClient, S as fetchCliUpdateState, U as resolveCliUpdateCheckInterval, V as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, W as listVhTerminals, X as readVhTerminal, Y as sendToVhTerminal, Z as TITLE_PROMPT_PREFIX, _ as BOARD_ANALYZER_PROMPT_PREFIX, h as connectionState, $ as startOfflineReconnection, C as CodexAppServerClient } from './types-BrGJ7PJz.mjs';
5
+ import { l as logger, aa as sameBackgroundTasks, ab as notifyDaemonSessionEvent, x as BUILTIN_TODO_DISCOVERY, g as delay, ac as STREAM_DELTA_MAX_CHARS, ad as ensurePrivateDirectory, ae as hardenPrivateFile, c as configuration, af as notifyDaemonClaudeAuthFailed, ag as createBackgroundTaskTracker, F as packageJson, r as readSettings, ah as readCredentials, a0 as credentialRelayProblem, k as checkIfDaemonRunningAndCleanupStaleState, ai as readDaemonState, aj as AGENT_VERSION_SOURCES, w as encodeBase64, ak as readSettingsOutcome, u as updateSettings, al as encodeBase64Url, M as decodeBase64, am as writeCredentialsLegacy, an as writeCredentialsDataKey, ao as BACKGROUND_TASK_DESCRIPTION_MAX, ap as BACKGROUND_TASKS_MAX, d as readPersistedSessions, E as writePrivateFileSync, aq as decryptWithDataKey, ar as decryptLegacy, as as persistSession, z as ensurePrivateDirectorySync, at as getAgentCodeDefaults, au as normalizeAgentKey, av as isClaudeOpus55Model, aw as supportsClaudeOpus55, ax as AUTOMATION_SUMMARY_MAX_CHARS, ay as AUTOMATION_CLAIM_MAX_RUNS, az as AUTOMATION_DEFAULT_LEASE_MS, f as isAutomationRunTerminal, aA as isDaemonRunningCurrentlyInstalledHappyVersion, aB as stopDaemon, aC as acquireDaemonLock, aD as BackgroundTaskTracker, aE as deletePersistedSessions, N as decrypt, aF as writeDaemonState, aG as writeSettings, aH as cleanupDaemonState, aI as releaseDaemonLock, i as isValidSessionId, m as spawnDaemonSession, a4 as isValidTerminalId, aJ as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, o as CLIPBOARD_MAX_BYTES, v as notifyDaemonSessionStarted, y as reportSessionEditToDaemon, aK as clearCredentials, aL as clearMachineId, h as readCredentialsForConfiguredRelay, aM as SandboxConfigSchema, e as createEnvelope, O as listDaemonSessions, n as stopDaemonSession, aN as getLatestDaemonLog } from './limits-A2c-3M-_.mjs';
6
6
  import spawn$1, { spawn } from 'cross-spawn';
7
7
  import path, { join, resolve, basename, delimiter, isAbsolute, dirname, sep, normalize } from 'node:path';
8
8
  import { createInterface } from 'node:readline';
9
9
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, realpathSync, unlinkSync, renameSync, writeFileSync, accessSync, constants as constants$1, openSync, fsyncSync, closeSync, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
10
10
  import { p as projectPath } from './terminalTitleSuggest-DKgX3osW.mjs';
11
- import { E as trimIdent, F as AGENT_GUIDANCE, G as formatEditConflictNotice, z as sendUserMessage, S as SESSION_PEER_SENT_FROM, x as readSessionMetadata, y as waitForSessionKey, k as archiveSession, H as createAutomationsClient, t as isAutomationsUnavailable, I as AutomationsRequestError, J as analyzeLatestTurn, K as readSessionLog$1, L as TURN_WINDOW, N as userPromptText, v as teamScopePath, O as TeamConnectionPreflightError, C as createTeamsClient, e as listSessions, h as readSessionTranscript, n as deliverToSession, o as sessionWebUrl, i as stopSession, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, Q as TEAM_TOOL_NAMES, U as AUTOMATION_TOOL_NAMES, V as SESSION_PEER_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, d as registerSessionPeerTools, W as createSessionPeerToolExecutor, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, X as REPORT_PROGRESS_TOOL_TITLE, Y as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-C604n0Zq.mjs';
11
+ import { K as trimIdent, L as AGENT_GUIDANCE, N as formatEditConflictNotice, H as sendUserMessage, S as SESSION_PEER_SENT_FROM, F as readSessionMetadata, R as RemoteCallBudget, O as REMOTE_WRITE_CALLS_PER_MINUTE, s as sendPeerMessage, j as listPeerSessions, u as deliverToSession, k as readSessionTranscript, l as listAccountSessions, g as listSessions, Q as guardRemoteSessionOpsRequest, U as remoteError, V as REMOTE_OP_DEADLINE_MS, W as formatRemoteAuditLine, X as REMOTE_SEND_COMMIT_DEADLINE_MS, Y as REMOTE_RESUME_WAIT_MS, Z as CLI_PEER_SENDER_ID, M as MAX_READ_LIMIT, _ as capRemoteTranscript, $ as capRemoteText, x as sessionWebUrl, a0 as REMOTE_ANSWER_MAX_BYTES, a1 as REMOTE_TITLE_MAX_BYTES, G as waitForSessionKey, q as archiveSession, a2 as createAutomationsClient, A as isAutomationsUnavailable, a3 as AutomationsRequestError, a4 as analyzeLatestTurn, a5 as readSessionLog$1, a6 as TURN_WINDOW, a7 as userPromptText, E as teamScopePath, a8 as TeamConnectionPreflightError, J as createTeamsClient, n as readRemoteTranscript, v as sendRemoteMessage, a9 as RemoteSessionOpsError, o as stopSession, P as PREVIEW_TOOL_NAME, aa as REPORT_PROGRESS_TOOL_NAME, ab as TEAM_TOOL_NAMES, ac as AUTOMATION_TOOL_NAMES, ad as SESSION_PEER_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, d as registerSessionPeerTools, ae as createSessionPeerToolExecutor, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, af as REPORT_PROGRESS_TOOL_TITLE, ag as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-CU6YD_Ms.mjs';
12
12
  import { readFile, writeFile, rename, unlink, mkdir, access as access$1, symlink, rm, readlink, lstat, constants, stat, open as open$1, appendFile } from 'node:fs/promises';
13
13
  import fs, { watch, access } from 'fs/promises';
14
14
  import { useStdout, useInput, Box, Text, render } from 'ink';
@@ -115,6 +115,11 @@ class Session {
115
115
  sessionId;
116
116
  mode = "local";
117
117
  thinking = false;
118
+ /** B-507: background tasks still in flight (async sub-agents, backgrounded
119
+ * commands, monitors) — published with every keepAlive. Always an array
120
+ * for the Claude runner: an empty one says "nothing running", which is a
121
+ * different statement from a runner that has no such notion (undefined). */
122
+ backgroundTasks = [];
118
123
  /**
119
124
  * Window after a user-initiated abort during which stray promise rejections
120
125
  * from tearing down the SDK query (MCP transports, in-flight tool calls,
@@ -176,11 +181,21 @@ class Session {
176
181
  logger.debug("[Session] Failed to create notification producer:", err);
177
182
  this.notificationProducer = null;
178
183
  }
179
- this.client.keepAlive(this.thinking, this.mode);
184
+ this.client.keepAlive(this.thinking, this.mode, this.backgroundTasks);
180
185
  this.keepAliveInterval = setInterval(() => {
181
- this.client.keepAlive(this.thinking, this.mode);
186
+ this.client.keepAlive(this.thinking, this.mode, this.backgroundTasks);
182
187
  }, 2e3);
183
188
  }
189
+ /**
190
+ * B-507: the tracker's current set (`claude/backgroundTasks.ts`). A change
191
+ * beats at once so the daemon, agentState and the web see the edge without
192
+ * waiting for the 2s tick; an unchanged set is left to the interval.
193
+ */
194
+ setBackgroundTasks = (tasks) => {
195
+ if (sameBackgroundTasks(this.backgroundTasks, tasks)) return;
196
+ this.backgroundTasks = tasks;
197
+ this.client.keepAlive(this.thinking, this.mode, this.backgroundTasks);
198
+ };
184
199
  /**
185
200
  * Cleanup resources (call when session is no longer needed)
186
201
  */
@@ -215,7 +230,7 @@ class Session {
215
230
  this.lastAssistantSnippet = void 0;
216
231
  }
217
232
  this.thinking = thinking;
218
- this.client.keepAlive(thinking, this.mode);
233
+ this.client.keepAlive(thinking, this.mode, this.backgroundTasks);
219
234
  };
220
235
  /**
221
236
  * Record that Claude emitted an assistant text reply during this turn.
@@ -282,7 +297,7 @@ class Session {
282
297
  };
283
298
  onModeChange = (mode) => {
284
299
  this.mode = mode;
285
- this.client.keepAlive(this.thinking, mode);
300
+ this.client.keepAlive(this.thinking, mode, this.backgroundTasks);
286
301
  this._onModeChange(mode);
287
302
  };
288
303
  onAbort = () => {
@@ -3926,6 +3941,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
3926
3941
  const turnSteering = createTurnSteeringController();
3927
3942
  const runtimeControls = createRuntimeControls(() => session.thinking);
3928
3943
  session.client.rpcHandlerManager.registerHandler("claude-runtime-control", async (input) => runtimeControls.request(input));
3944
+ const backgroundTasks = createBackgroundTaskTracker();
3929
3945
  async function abort() {
3930
3946
  if (abortController && !abortController.signal.aborted) {
3931
3947
  abortController.abort();
@@ -4011,6 +4027,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4011
4027
  let notifiedQuestionToolCalls = /* @__PURE__ */ new Set();
4012
4028
  function onMessage(message) {
4013
4029
  runtimeControls.observe(message);
4030
+ backgroundTasks.observe(message);
4031
+ session.setBackgroundTasks(backgroundTasks.list());
4014
4032
  if (isRuntimeCheckpointReplay(message)) return;
4015
4033
  if (message.type === "user" && isClaudeInterruptSentinelContent(message.message.content)) {
4016
4034
  sdkToLogConverter.convert(message);
@@ -4235,6 +4253,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4235
4253
  },
4236
4254
  onQueryReady: (q) => {
4237
4255
  runtimeControls.setQuery(q);
4256
+ backgroundTasks.reset();
4257
+ session.setBackgroundTasks([]);
4238
4258
  turnSteering.setInterrupt(q.interrupt);
4239
4259
  session.setSideQuestionLive({ ask: q.sideQuestion, canControl: q.canControl });
4240
4260
  session.setSteerHandler(async (input) => {
@@ -4331,6 +4351,8 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4331
4351
  }
4332
4352
  } finally {
4333
4353
  runtimeControls.setQuery(null);
4354
+ backgroundTasks.reset();
4355
+ session.setBackgroundTasks([]);
4334
4356
  turnSteering.reset();
4335
4357
  session.setSteerHandler(null);
4336
4358
  session.setSideQuestionLive(null);
@@ -5380,8 +5402,8 @@ ${typeLabels[type] || type}:`));
5380
5402
  }
5381
5403
  console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
5382
5404
  try {
5383
- const { resolveAgentHomesForCli } = await import('./agentHome-D2XGsoqs.mjs');
5384
- const { describeAgentHome } = await import('./types-BEVzYSoV.mjs').then(function (n) { return n.a0; });
5405
+ const { resolveAgentHomesForCli } = await import('./agentHome-BaJ9xtxB.mjs');
5406
+ const { describeAgentHome } = await import('./types-BrGJ7PJz.mjs').then(function (n) { return n.a0; });
5385
5407
  const homes = await resolveAgentHomesForCli();
5386
5408
  console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
5387
5409
  console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
@@ -6204,6 +6226,18 @@ function checkPreviewPath(input, opts) {
6204
6226
  return { resolved, deniedReason: null };
6205
6227
  }
6206
6228
 
6229
+ const BackgroundTaskInfoSchema = z.object({
6230
+ id: z.string().min(1).max(200),
6231
+ type: z.string().max(64),
6232
+ description: z.string().max(BACKGROUND_TASK_DESCRIPTION_MAX + 1),
6233
+ startedAt: z.number()
6234
+ });
6235
+ const BackgroundTasksReportSchema = z.object({
6236
+ count: z.number().int().nonnegative(),
6237
+ tasks: z.array(BackgroundTaskInfoSchema),
6238
+ reportedAt: z.number().optional(),
6239
+ stale: z.boolean().optional()
6240
+ });
6207
6241
  function startDaemonControlServer({
6208
6242
  controlToken,
6209
6243
  getChildren,
@@ -6215,6 +6249,8 @@ function startDaemonControlServer({
6215
6249
  onSessionStateEvent,
6216
6250
  onClaudeAuthFailed,
6217
6251
  onSessionTurnEvent,
6252
+ onSessionBackgroundTasks,
6253
+ getBackgroundTasks,
6218
6254
  pushClipboard,
6219
6255
  pushFilePreview,
6220
6256
  onTerminalHook,
@@ -6275,8 +6311,10 @@ function startDaemonControlServer({
6275
6311
  schema: {
6276
6312
  body: z.object({
6277
6313
  sessionId: z.string(),
6278
- event: z.enum(["completed", "needs_input", "auth_failed", "turn_started", "turn_ended"]),
6279
- spawnedBy: z.string().optional()
6314
+ event: z.enum(["completed", "needs_input", "auth_failed", "turn_started", "turn_ended", "background_tasks"]),
6315
+ spawnedBy: z.string().optional(),
6316
+ // B-507: only with event 'background_tasks' — the wrapper's full set.
6317
+ backgroundTasks: z.array(BackgroundTaskInfoSchema).max(BACKGROUND_TASKS_MAX).optional()
6280
6318
  }),
6281
6319
  response: {
6282
6320
  200: z.object({
@@ -6287,6 +6325,10 @@ function startDaemonControlServer({
6287
6325
  }, async (request) => {
6288
6326
  const { sessionId, event, spawnedBy } = request.body;
6289
6327
  logger.debug(`[CONTROL SERVER] Session event: ${sessionId} ${event} (spawnedBy=${spawnedBy ?? "unset"})`);
6328
+ if (event === "background_tasks") {
6329
+ onSessionBackgroundTasks?.(sessionId, request.body.backgroundTasks ?? []);
6330
+ return { status: "ok" };
6331
+ }
6290
6332
  if (event === "auth_failed") {
6291
6333
  onClaudeAuthFailed?.(sessionId);
6292
6334
  return { status: "ok" };
@@ -6306,7 +6348,9 @@ function startDaemonControlServer({
6306
6348
  startedBy: z.string(),
6307
6349
  happySessionId: z.string(),
6308
6350
  pid: z.number(),
6309
- turnActive: z.boolean()
6351
+ turnActive: z.boolean(),
6352
+ // B-507: in-flight background tasks per the wrapper's last report (lease-expired = count 0).
6353
+ backgroundTasks: BackgroundTasksReportSchema.optional()
6310
6354
  }))
6311
6355
  })
6312
6356
  }
@@ -6319,7 +6363,8 @@ function startDaemonControlServer({
6319
6363
  startedBy: child.startedBy,
6320
6364
  happySessionId: child.happySessionId,
6321
6365
  pid: child.pid,
6322
- turnActive: isTurnActive ? isTurnActive(child.happySessionId) : false
6366
+ turnActive: isTurnActive ? isTurnActive(child.happySessionId) : false,
6367
+ ...getBackgroundTasks ? { backgroundTasks: getBackgroundTasks(child.happySessionId) } : {}
6323
6368
  }))
6324
6369
  };
6325
6370
  });
@@ -6887,6 +6932,170 @@ function createPeerCoordinator(deps) {
6887
6932
  };
6888
6933
  }
6889
6934
 
6935
+ const WRITE_METHODS = /* @__PURE__ */ new Set(["sessions.send", "sessions.message"]);
6936
+ const REMOTE_CLIENT_TAG = "cli-send-remote";
6937
+ function defaultDeps(machineId) {
6938
+ return {
6939
+ machineId,
6940
+ host: hostname(),
6941
+ enabled: async () => (await readSettings()).remoteSessionOps !== "off",
6942
+ readPersisted: readPersistedSessions,
6943
+ listLocal: (options) => listSessions(options),
6944
+ listAccount: (options) => listAccountSessions(options),
6945
+ readTranscript: (sessionId, limit, options) => readSessionTranscript(sessionId, limit, options),
6946
+ deliver: (sessionId, persisted, text, client, options, deps) => deliverToSession(sessionId, persisted, text, client, options, deps),
6947
+ listPeers: (context, scope) => listPeerSessions(context, scope),
6948
+ sendPeer: (context, args) => sendPeerMessage(context, args),
6949
+ log: (line) => logger.info(line),
6950
+ now: () => Date.now()
6951
+ };
6952
+ }
6953
+ function createRemoteSessionOpsHandlers(machineId, overrides = {}) {
6954
+ const deps = { ...defaultDeps(machineId), ...overrides };
6955
+ const readBudget = deps.budget ?? new RemoteCallBudget(void 0, deps.now);
6956
+ const writeBudget = deps.writeBudget ?? new RemoteCallBudget(REMOTE_WRITE_CALLS_PER_MINUTE, deps.now);
6957
+ const deadlineMs = deps.deadlineMs ?? REMOTE_OP_DEADLINE_MS;
6958
+ const sendCommitDeadlineMs = deps.sendCommitDeadlineMs ?? REMOTE_SEND_COMMIT_DEADLINE_MS;
6959
+ const resumeWaitMs = deps.resumeWaitMs ?? REMOTE_RESUME_WAIT_MS;
6960
+ const run = (method, op, targetOf) => async (params) => {
6961
+ const started = deps.now();
6962
+ let from = null;
6963
+ let sessionId;
6964
+ const finish = (response) => {
6965
+ const outcome = response.ok ? "ok" : response.error.code;
6966
+ deps.log(formatRemoteAuditLine({ method, from, sessionId, outcome, durationMs: deps.now() - started }));
6967
+ return response;
6968
+ };
6969
+ let enabled = false;
6970
+ try {
6971
+ enabled = await deps.enabled();
6972
+ } catch {
6973
+ enabled = false;
6974
+ }
6975
+ const guard = guardRemoteSessionOpsRequest(method, params, { enabled });
6976
+ if (!guard.ok) return finish(guard);
6977
+ from = guard.request.from;
6978
+ sessionId = targetOf(guard.request.args);
6979
+ const budget = WRITE_METHODS.has(method) ? writeBudget : readBudget;
6980
+ const token = budget.take();
6981
+ if (!token.ok) {
6982
+ return finish(remoteError("rate_limited", `This machine accepts at most ${budget.perMinute} remote ${WRITE_METHODS.has(method) ? "send/message" : "list/read/peers"} calls per minute; retry in ${Math.ceil(token.retryAfterMs / 1e3)}s`));
6983
+ }
6984
+ let timer;
6985
+ const deadline = new Promise((_, reject) => {
6986
+ timer = setTimeout(() => reject(new RemoteOpFailure("timeout", `${method} did not finish within ${Math.round(deadlineMs / 1e3)}s on ${deps.host}`)), deadlineMs);
6987
+ timer.unref?.();
6988
+ });
6989
+ try {
6990
+ return finish({ ok: true, result: await Promise.race([op(guard.request.args, from, started), deadline]) });
6991
+ } catch (error) {
6992
+ const message = error instanceof Error ? error.message : "Remote session operation failed";
6993
+ if (error instanceof RemoteOpFailure) return finish(remoteError(error.code, message));
6994
+ return finish(remoteError("internal", message));
6995
+ } finally {
6996
+ if (timer) clearTimeout(timer);
6997
+ }
6998
+ };
6999
+ const requirePersisted = (id) => {
7000
+ const persisted = deps.readPersisted()[id];
7001
+ if (!persisted) throw new RemoteOpFailure("no_local_key", `Session ${id} is not on ${deps.host} (no local key there either)`);
7002
+ return persisted;
7003
+ };
7004
+ return {
7005
+ "sessions.list": run("sessions.list", async (args) => {
7006
+ const wanted = args.ids ? new Set(args.ids) : null;
7007
+ let sessions;
7008
+ if (args.all) {
7009
+ sessions = (await deps.listAccount({ tag: args.tag, recentLimit: wanted ? 1e4 : args.limit, includeArchived: true })).filter((row) => row.decryptable);
7010
+ } else if (wanted) {
7011
+ const persisted = deps.readPersisted();
7012
+ const live = await deps.listLocal({ recentLimit: 1e4 });
7013
+ const liveById = new Map(live.map((row) => [row.id, row]));
7014
+ sessions = [...wanted].filter((id) => persisted[id] !== void 0 || liveById.has(id)).map((id) => liveById.get(id) ?? summaryFromPersisted(id, persisted[id]));
7015
+ } else {
7016
+ sessions = await deps.listLocal({ tag: args.tag, recentLimit: args.limit });
7017
+ }
7018
+ if (wanted) sessions = sessions.filter((row) => wanted.has(row.id));
7019
+ const result = { machineId: deps.machineId, host: deps.host, sessions };
7020
+ return result;
7021
+ }, () => void 0),
7022
+ "sessions.read": run("sessions.read", async (args) => {
7023
+ requirePersisted(args.sessionId);
7024
+ const limit = Math.max(1, Math.min(MAX_READ_LIMIT, args.limit ?? 20));
7025
+ const transcript = await deps.readTranscript(args.sessionId, limit, { full: args.full === true });
7026
+ const capped = capRemoteTranscript(transcript.transcript);
7027
+ const result = {
7028
+ ...transcript,
7029
+ summary: {
7030
+ ...transcript.summary,
7031
+ ...transcript.summary.title !== void 0 ? { title: capRemoteText(transcript.summary.title, REMOTE_TITLE_MAX_BYTES) } : {}
7032
+ },
7033
+ turn: { ...transcript.turn, answer: capRemoteText(transcript.turn.answer, REMOTE_ANSWER_MAX_BYTES, "tail") },
7034
+ transcript: capped.transcript,
7035
+ ...capped.truncated ? { truncated: true } : {},
7036
+ machineId: deps.machineId,
7037
+ host: deps.host
7038
+ };
7039
+ return result;
7040
+ }, (args) => args.sessionId),
7041
+ "sessions.send": run("sessions.send", async (args, _from, started) => {
7042
+ const persisted = requirePersisted(args.sessionId);
7043
+ const send = (sessionId, key, text, client, options) => {
7044
+ if (deps.now() - started > sendCommitDeadlineMs) {
7045
+ throw new RemoteOpFailure("timeout", `Session ${sessionId} was not live within ${Math.round(sendCommitDeadlineMs / 1e3)}s on ${deps.host}; nothing was sent \u2014 retry (the same localId cannot double-deliver)`);
7046
+ }
7047
+ return sendUserMessage(sessionId, key, text, client, { ...options, ...args.localId ? { localId: args.localId } : {} });
7048
+ };
7049
+ return deps.deliver(args.sessionId, persisted, args.text, REMOTE_CLIENT_TAG, {
7050
+ resume: args.resume === true,
7051
+ waitMs: resumeWaitMs,
7052
+ pollMs: 1e3,
7053
+ ...args.model !== void 0 ? { model: args.model } : {},
7054
+ ...args.sentFrom !== void 0 ? { sentFrom: args.sentFrom } : {}
7055
+ }, { send });
7056
+ }, (args) => args.sessionId),
7057
+ "sessions.peers": run("sessions.peers", async (args, from) => {
7058
+ const scope = args.cwd ? args.scope ?? "machine" : "machine";
7059
+ const context = {
7060
+ self: () => ({ sessionId: from.sessionId ?? CLI_PEER_SENDER_ID, cwd: args.cwd ?? "/", machine: from.host }),
7061
+ readPersisted: deps.readPersisted
7062
+ };
7063
+ const listing = await deps.listPeers(context, scope);
7064
+ return { ...listing, machineId: deps.machineId, host: deps.host };
7065
+ }, () => void 0),
7066
+ "sessions.message": run("sessions.message", async (args) => {
7067
+ requirePersisted(args.to);
7068
+ const context = {
7069
+ self: () => ({ ...args.from, cwd: args.from.cwd ?? "/" }),
7070
+ readPersisted: deps.readPersisted,
7071
+ // The target must be HERE; never bounce a remote request onward.
7072
+ remoteMessage: null
7073
+ };
7074
+ return deps.sendPeer(context, { to: args.to, body: args.body, ...args.replyTo ? { replyTo: args.replyTo } : {} });
7075
+ }, (args) => args.to)
7076
+ };
7077
+ }
7078
+ class RemoteOpFailure extends Error {
7079
+ constructor(code, message) {
7080
+ super(message);
7081
+ this.code = code;
7082
+ }
7083
+ }
7084
+ function summaryFromPersisted(id, persisted) {
7085
+ const meta = persisted?.metadata;
7086
+ return {
7087
+ id,
7088
+ live: false,
7089
+ ...meta?.summary?.text ? { title: meta.summary.text } : {},
7090
+ ...meta?.path ? { cwd: meta.path } : {},
7091
+ ...meta?.flavor ? { flavor: meta.flavor } : {},
7092
+ ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
7093
+ ...meta?.variant ? { variant: meta.variant } : {},
7094
+ ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
7095
+ url: sessionWebUrl(id)
7096
+ };
7097
+ }
7098
+
6890
7099
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6891
7100
 
6892
7101
  \u4F60\u662F very-happy \u7684\u8C03\u5EA6\u4E2D\u5FC3\uFF0C\u8DD1\u5728\u7528\u6237\u81EA\u5DF1\u7684\u673A\u5668\u4E0A\u3002\u7528\u6237\u4E3B\u8981\u901A\u8FC7\u8BED\u97F3\u8DDF\u4F60\u8BF4\u8BDD\uFF0C
@@ -10595,6 +10804,13 @@ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
10595
10804
  if (edgeFell) item.sawTurn = false;
10596
10805
  const turn = await readTurn(sessionId, item.runId).catch(() => null);
10597
10806
  if (!turn?.ended) continue;
10807
+ const background = deps.backgroundTasks?.(sessionId) ?? 0;
10808
+ if (background > 0 && turn.status !== "failed" && turn.status !== "cancelled") {
10809
+ if (!item.waitingOnBackground) deps.log(`Automation run ${item.runId}: turn ended but ${background} background task(s) still running; not done yet`);
10810
+ item.waitingOnBackground = true;
10811
+ continue;
10812
+ }
10813
+ item.waitingOnBackground = false;
10598
10814
  if (turn.status === "failed" || turn.status === "cancelled") await finish(item, { claimId: item.claimId, status: "failed", sessionId, error: turn.error ?? `Turn ${turn.status}`, needsAttention: true, attentionReason: `turn_${turn.status}` });
10599
10815
  else await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn.answer, AUTOMATION_SUMMARY_MAX_CHARS) });
10600
10816
  }
@@ -10889,6 +11105,7 @@ async function startDaemon() {
10889
11105
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
10890
11106
  const pidToTrackedSession = /* @__PURE__ */ new Map();
10891
11107
  const turnActivity = new TurnActivityTracker();
11108
+ const backgroundTasks = new BackgroundTaskTracker();
10892
11109
  const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
10893
11110
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
10894
11111
  let teamWorker;
@@ -11749,6 +11966,7 @@ async function startDaemon() {
11749
11966
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
11750
11967
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
11751
11968
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11969
+ if (session?.happySessionId) backgroundTasks.forget(session.happySessionId);
11752
11970
  if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
11753
11971
  if (session?.happySessionId && session.encryption) {
11754
11972
  sessionIdToFinishedSession.set(session.happySessionId, session);
@@ -11835,6 +12053,8 @@ async function startDaemon() {
11835
12053
  onSessionStateEvent,
11836
12054
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
11837
12055
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
12056
+ onSessionBackgroundTasks: (sessionId, tasks) => backgroundTasks.apply(sessionId, tasks),
12057
+ getBackgroundTasks: (sessionId) => backgroundTasks.report(sessionId),
11838
12058
  onSessionEdit: (edit) => {
11839
12059
  peerCoordinator.onEdit(edit);
11840
12060
  },
@@ -11934,6 +12154,7 @@ async function startDaemon() {
11934
12154
  listTrackedSessionIds: () => [...pidToTrackedSession.values()].map((session) => session.happySessionId).filter((sessionId) => typeof sessionId === "string"),
11935
12155
  requestShutdown: () => requestShutdown("happy-app")
11936
12156
  });
12157
+ apiMachine.setRemoteSessionOpsHandlers(createRemoteSessionOpsHandlers(machineId));
11937
12158
  const claudeAuthService = new ClaudeAuthService({
11938
12159
  happyHomeDir: configuration.happyHomeDir,
11939
12160
  happyLibDir: projectPath(),
@@ -11966,8 +12187,9 @@ async function startDaemon() {
11966
12187
  enabled: async () => ((await readSettings()).cliAutoUpdate ?? "idle") !== "off",
11967
12188
  // B-466: "idle" = no agent turn in flight. Idle wrappers survive a
11968
12189
  // handover (they talk to the server themselves) and web terminals live
11969
- // in tmux, so neither is a reason to hold an update.
11970
- idle: () => !turnActivity.hasActiveTurn(),
12190
+ // in tmux, so neither is a reason to hold an update. B-507: a session
12191
+ // whose turn ended but still has background tasks is busy too.
12192
+ idle: () => !turnActivity.hasActiveTurn() && !backgroundTasks.hasAny(),
11971
12193
  install: installCliSafely,
11972
12194
  publish: (state) => {
11973
12195
  cliUpdateStateRef = state;
@@ -12022,7 +12244,7 @@ async function startDaemon() {
12022
12244
  } catch {
12023
12245
  }
12024
12246
  }
12025
- if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
12247
+ if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn() && !backgroundTasks.hasAny()) {
12026
12248
  const preflight = await preflightNewBundle(bundlePath);
12027
12249
  if (preflight.action === "hold") {
12028
12250
  if (lastHandoverHold !== preflight.reason) {
@@ -12032,7 +12254,7 @@ async function startDaemon() {
12032
12254
  updateController.holdHandover(preflight.reason);
12033
12255
  return;
12034
12256
  }
12035
- if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
12257
+ if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn() || backgroundTasks.hasAny()) return;
12036
12258
  lastHandoverHold = null;
12037
12259
  logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
12038
12260
  teamWorker?.stop();
@@ -12098,6 +12320,7 @@ async function startDaemon() {
12098
12320
  spawn: spawnSession,
12099
12321
  live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
12100
12322
  turnActive: (id) => turnActivity.activeSessions().includes(id),
12323
+ backgroundTasks: (id) => backgroundTasks.count(id),
12101
12324
  log: (message) => logger.debug(`[DAEMON RUN] ${message}`)
12102
12325
  });
12103
12326
  void apiMachine.updateMachineMetadata((current) => ({
@@ -12391,36 +12614,54 @@ ${sessions.map(describeSummary).join("\n")}`);
12391
12614
  }
12392
12615
  });
12393
12616
  mcp.registerTool("session_read", {
12394
- description: "Read the latest messages of a session as a compact role-tagged transcript (user / assistant / tool summaries; large payloads truncated). Only works for sessions spawned by this machine (their keys live in ~/.happy/sessions.json).",
12617
+ description: "Read the latest messages of a session as a compact role-tagged transcript (user / assistant / tool summaries; large payloads truncated). Sessions spawned by this machine are read directly; a session spawned by another machine of this account is read through that machine's daemon (it must be online).",
12395
12618
  title: "Read Session Transcript",
12396
12619
  inputSchema: {
12397
12620
  sessionId: z.string().describe("The Happy session id to read"),
12398
- limit: z.number().optional().describe("How many recent messages to fetch (default 20, max 100)")
12621
+ limit: z.number().optional().describe("How many recent messages to fetch (default 20, max 100)"),
12622
+ machineId: z.string().optional().describe("Machine id the session runs on, when known (skips the lookup across the account's machines)")
12399
12623
  }
12400
12624
  }, async (args) => {
12401
12625
  if (!isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12402
12626
  try {
12403
- const result = await readSessionTranscript(args.sessionId, args.limit ?? 20);
12627
+ const local = readPersistedSessions()[args.sessionId];
12628
+ if (local && !args.machineId) {
12629
+ const result2 = await readSessionTranscript(args.sessionId, args.limit ?? 20);
12630
+ const body2 = result2.transcript.length > 0 ? result2.transcript : "(no readable conversation content in this range)";
12631
+ return ok(`${describeSummary(result2.summary)}
12632
+ --- last ${result2.messageCount} message(s) ---
12633
+ ${body2}`);
12634
+ }
12635
+ const result = await readRemoteTranscript(args.sessionId, { limit: args.limit ?? 20, ...args.machineId ? { machineId: args.machineId } : {} });
12404
12636
  const body = result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)";
12405
- return ok(`${describeSummary(result.summary)}
12406
- --- last ${result.messageCount} message(s) ---
12637
+ return ok(`${describeSummary(result.summary)} machine=${result.host} (${result.machineId})
12638
+ --- last ${result.messageCount} message(s)${result.truncated ? ", truncated to fit" : ""} ---
12407
12639
  ${body}`);
12408
12640
  } catch (error) {
12409
12641
  return fail(`Failed to read session: ${error instanceof Error ? error.message : String(error)}`);
12410
12642
  }
12411
12643
  });
12412
12644
  mcp.registerTool("session_send", {
12413
- description: "Send a user message into an existing session on this machine. Only delivers when a live wrapper will read it: an archived / offline session is refused (the server would store the message unread) unless resume:true brings it back on this machine first. Returns immediately after delivery \u2014 it does NOT wait for the session to respond; use session_read later to check progress.",
12645
+ description: "Send a user message into an existing session. Only delivers when a live wrapper will read it: an archived / offline session is refused (the server would store the message unread) unless resume:true brings it back on its machine first. A session spawned by another machine of this account is delivered through that machine's daemon (it must be online). Returns immediately after delivery \u2014 it does NOT wait for the session to respond; use session_read later to check progress.",
12414
12646
  title: "Send Message to Session",
12415
12647
  inputSchema: {
12416
12648
  sessionId: z.string().describe("The Happy session id to message"),
12417
12649
  text: z.string().describe("The message text to send"),
12418
- resume: z.boolean().optional().describe("If the session is archived or offline, resume it on this machine (like the web Restore button) and wait until it is live before sending")
12650
+ resume: z.boolean().optional().describe("If the session is archived or offline, resume it on its machine (like the web Restore button) and wait until it is live before sending"),
12651
+ machineId: z.string().optional().describe("Machine id the session runs on, when known (skips the lookup across the account's machines)")
12419
12652
  }
12420
12653
  }, async (args) => {
12421
12654
  if (!isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12422
12655
  if (typeof args.text !== "string" || args.text.trim().length === 0) return fail("text must be non-empty");
12423
12656
  try {
12657
+ if (!readPersistedSessions()[args.sessionId] || args.machineId) {
12658
+ const remote = await sendRemoteMessage(args.sessionId, args.text, { resume: args.resume === true, sentFrom: "assistant", ...args.machineId ? { machineId: args.machineId } : {} });
12659
+ if (!remote.delivered) {
12660
+ const detail = remote.error ?? `session is ${remote.status}`;
12661
+ return fail(remote.stored ? `Not delivered (via ${remote.machine.host}): ${detail}` : `Session ${args.sessionId} on ${remote.machine.host} is ${remote.status}: ${detail}`);
12662
+ }
12663
+ return ok(`Message delivered${remote.resumed ? ` (session resumed on ${remote.machine.host})` : ""} to ${args.sessionId} on machine ${remote.machine.host} (${remote.machine.id}) ${sessionWebUrl(args.sessionId)}`);
12664
+ }
12424
12665
  const persisted = await waitForSessionKey(args.sessionId, 0);
12425
12666
  const result = await deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12426
12667
  resume: args.resume === true,
@@ -12432,6 +12673,7 @@ ${body}`);
12432
12673
  }
12433
12674
  return ok(`Message delivered${result.resumed ? " (session resumed on this machine)" : ""} to ${describeSession(args.sessionId, persisted, { live: true })}`);
12434
12675
  } catch (error) {
12676
+ if (error instanceof RemoteSessionOpsError) return fail(`Failed to send (${error.code}): ${error.message}`);
12435
12677
  return fail(`Failed to send: ${error instanceof Error ? error.message : String(error)}`);
12436
12678
  }
12437
12679
  });
@@ -13239,7 +13481,7 @@ function registerSideQuestionHandler(rpc, deps) {
13239
13481
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13240
13482
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13241
13483
  const fork = deps.run ?? (async (input) => {
13242
- const { query } = await import('./index-PYO8Mdz4.mjs');
13484
+ const { query } = await import('./index-DQcRkI9r.mjs');
13243
13485
  return runSideQuestion(query, input);
13244
13486
  });
13245
13487
  const run = async (input, live) => {
@@ -18547,7 +18789,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18547
18789
  return;
18548
18790
  } else if (subcommand === "install-terminal-hooks") {
18549
18791
  try {
18550
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-CMpOV3RU.mjs');
18792
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-ELDoqh0A.mjs');
18551
18793
  const command = parseTerminalHooksArgs(args.slice(1));
18552
18794
  if (command.action === "help") {
18553
18795
  console.log(TERMINAL_HOOKS_HELP);
@@ -18564,7 +18806,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18564
18806
  }
18565
18807
  } else if (subcommand === "agent-home") {
18566
18808
  try {
18567
- const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-D2XGsoqs.mjs');
18809
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-BaJ9xtxB.mjs');
18568
18810
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18569
18811
  process.exit(0);
18570
18812
  } catch (error) {
@@ -18573,7 +18815,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18573
18815
  }
18574
18816
  } else if (subcommand === "spawn") {
18575
18817
  try {
18576
- const { handleSpawnCommand } = await import('./spawn-BKNabutV.mjs');
18818
+ const { handleSpawnCommand } = await import('./spawn-CdihhwHs.mjs');
18577
18819
  await handleSpawnCommand(args.slice(1));
18578
18820
  } catch (error) {
18579
18821
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18585,7 +18827,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18585
18827
  return;
18586
18828
  } else if (subcommand === "sessions") {
18587
18829
  try {
18588
- const { handleSessionsCommand } = await import('./sessions-Bn304xF1.mjs');
18830
+ const { handleSessionsCommand } = await import('./sessions-DsE01xKc.mjs');
18589
18831
  await handleSessionsCommand(args.slice(1));
18590
18832
  } catch (error) {
18591
18833
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18597,7 +18839,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18597
18839
  return;
18598
18840
  } else if (subcommand === "send") {
18599
18841
  try {
18600
- const { handleSendCommand } = await import('./send-BaOkDgH0.mjs');
18842
+ const { handleSendCommand } = await import('./send-D5CyRyZ_.mjs');
18601
18843
  await handleSendCommand(args.slice(1));
18602
18844
  } catch (error) {
18603
18845
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18703,9 +18945,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18703
18945
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
18704
18946
  const projectId = args[3];
18705
18947
  try {
18706
- const { saveGoogleCloudProjectToConfig } = await import('./config-BkTu25bH.mjs');
18707
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-DmMdchTz.mjs').then(function (n) { return n.aG; });
18708
- const { ApiClient: ApiClient2 } = await import('./types-BEVzYSoV.mjs').then(function (n) { return n.a1; });
18948
+ const { saveGoogleCloudProjectToConfig } = await import('./config-aAbhJllb.mjs');
18949
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-A2c-3M-_.mjs').then(function (n) { return n.aO; });
18950
+ const { ApiClient: ApiClient2 } = await import('./types-BrGJ7PJz.mjs').then(function (n) { return n.a1; });
18709
18951
  let userEmail = void 0;
18710
18952
  try {
18711
18953
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -18736,7 +18978,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18736
18978
  }
18737
18979
  if (geminiSubcommand === "project" && args[2] === "get") {
18738
18980
  try {
18739
- const { readGeminiLocalConfig } = await import('./config-BkTu25bH.mjs');
18981
+ const { readGeminiLocalConfig } = await import('./config-aAbhJllb.mjs');
18740
18982
  const config = readGeminiLocalConfig();
18741
18983
  if (config.googleCloudProject) {
18742
18984
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -18776,7 +19018,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18776
19018
  process.exit(0);
18777
19019
  }
18778
19020
  try {
18779
- const { runGemini } = await import('./runGemini-CZ_lhMWY.mjs');
19021
+ const { runGemini } = await import('./runGemini-Cj6oJQ-I.mjs');
18780
19022
  let startedBy = void 0;
18781
19023
  for (let i = 1; i < args.length; i++) {
18782
19024
  if (args[i] === "--started-by") {
@@ -18799,7 +19041,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18799
19041
  } else if (subcommand === "pi") {
18800
19042
  if (args[1] === "--terminal") {
18801
19043
  try {
18802
- const { runPiTerminal } = await import('./piTerminal-DanZeBrO.mjs');
19044
+ const { runPiTerminal } = await import('./piTerminal-DwWmgumi.mjs');
18803
19045
  process.exitCode = await runPiTerminal(args.slice(2));
18804
19046
  } catch (error) {
18805
19047
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18816,7 +19058,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18816
19058
  }
18817
19059
  };
18818
19060
  try {
18819
- const { runAcp, resolveAcpAgentConfig } = await import('./index-B5QbzEga.mjs');
19061
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BVu8Cufk.mjs');
18820
19062
  const parsed = parsePiRunnerArgs(args.slice(1));
18821
19063
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
18822
19064
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -18844,7 +19086,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18844
19086
  return;
18845
19087
  } else if (subcommand === "acp") {
18846
19088
  try {
18847
- const { runAcp, resolveAcpAgentConfig } = await import('./index-B5QbzEga.mjs');
19089
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BVu8Cufk.mjs');
18848
19090
  let startedBy = void 0;
18849
19091
  let verbose = false;
18850
19092
  const acpArgs = [];
@@ -18884,7 +19126,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18884
19126
  return;
18885
19127
  } else if (subcommand === "openclaw") {
18886
19128
  try {
18887
- const { runOpenClaw } = await import('./runOpenClaw-CxgLpm38.mjs');
19129
+ const { runOpenClaw } = await import('./runOpenClaw-DUCQxkI-.mjs');
18888
19130
  let startedBy = void 0;
18889
19131
  let verbose = false;
18890
19132
  let gatewayUrl;
@@ -18935,7 +19177,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18935
19177
  return;
18936
19178
  } else if (subcommand === "todo") {
18937
19179
  try {
18938
- const { handleTodoCommand } = await import('./todo-BcPbNKt2.mjs');
19180
+ const { handleTodoCommand } = await import('./todo-CuQAkSp2.mjs');
18939
19181
  await handleTodoCommand(args.slice(1));
18940
19182
  } catch (error) {
18941
19183
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -18944,7 +19186,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18944
19186
  return;
18945
19187
  } else if (subcommand === "teams") {
18946
19188
  try {
18947
- const { handleTeamsCommand } = await import('./teams-CLbS2_4Q.mjs');
19189
+ const { handleTeamsCommand } = await import('./teams-BE8nMB6O.mjs');
18948
19190
  await handleTeamsCommand(args.slice(1));
18949
19191
  } catch (error) {
18950
19192
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -18953,7 +19195,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18953
19195
  return;
18954
19196
  } else if (subcommand === "auto") {
18955
19197
  try {
18956
- const { handleAutoCommand } = await import('./auto-B1I6G_Nf.mjs');
19198
+ const { handleAutoCommand } = await import('./auto-B1YAwaeU.mjs');
18957
19199
  await handleAutoCommand(args.slice(1));
18958
19200
  } catch (error) {
18959
19201
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -18965,7 +19207,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18965
19207
  return;
18966
19208
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
18967
19209
  try {
18968
- const { handleMcpCommand } = await import('./mcp-C_uejotl.mjs');
19210
+ const { handleMcpCommand } = await import('./mcp-Bo934Kpy.mjs');
18969
19211
  await handleMcpCommand(args[1] === "--terminal-tools");
18970
19212
  } catch (error) {
18971
19213
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -18996,6 +19238,8 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18996
19238
  console.log(JSON.stringify(sessions, null, 2));
18997
19239
  const busy = sessions.filter((s) => s.turnActive === true);
18998
19240
  console.log(busy.length === 0 ? "No turn in flight on this machine (wrappers older than 0.2.139 do not report turns)." : `${busy.length} session(s) with a turn in flight: ${busy.map((s) => s.happySessionId).join(", ")}`);
19241
+ const background = sessions.filter((s) => (s.backgroundTasks?.count ?? 0) > 0);
19242
+ console.log(background.length === 0 ? "No background tasks in flight (wrappers older than 0.2.157 do not report them)." : `${background.length} session(s) with background tasks in flight: ${background.map((s) => `${s.happySessionId} (${s.backgroundTasks?.count})`).join(", ")}`);
18999
19243
  }
19000
19244
  } catch (error) {
19001
19245
  console.log("No daemon running");