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,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-CkbfdRzq.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-C_DW0M0q.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-BWxFC6UL.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-ByLb2HFg.mjs');
5384
- const { describeAgentHome } = await import('./types-CkbfdRzq.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,12 +6249,15 @@ function startDaemonControlServer({
6215
6249
  onSessionStateEvent,
6216
6250
  onClaudeAuthFailed,
6217
6251
  onSessionTurnEvent,
6252
+ onSessionBackgroundTasks,
6253
+ getBackgroundTasks,
6218
6254
  pushClipboard,
6219
6255
  pushFilePreview,
6220
6256
  onTerminalHook,
6221
6257
  setTerminalTitle,
6222
6258
  onSessionEdit,
6223
- listPeers
6259
+ listPeers,
6260
+ isTurnActive
6224
6261
  }) {
6225
6262
  return new Promise((resolve) => {
6226
6263
  const app = fastify({
@@ -6274,8 +6311,10 @@ function startDaemonControlServer({
6274
6311
  schema: {
6275
6312
  body: z.object({
6276
6313
  sessionId: z.string(),
6277
- event: z.enum(["completed", "needs_input", "auth_failed", "turn_started", "turn_ended"]),
6278
- 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()
6279
6318
  }),
6280
6319
  response: {
6281
6320
  200: z.object({
@@ -6286,6 +6325,10 @@ function startDaemonControlServer({
6286
6325
  }, async (request) => {
6287
6326
  const { sessionId, event, spawnedBy } = request.body;
6288
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
+ }
6289
6332
  if (event === "auth_failed") {
6290
6333
  onClaudeAuthFailed?.(sessionId);
6291
6334
  return { status: "ok" };
@@ -6304,7 +6347,10 @@ function startDaemonControlServer({
6304
6347
  children: z.array(z.object({
6305
6348
  startedBy: z.string(),
6306
6349
  happySessionId: z.string(),
6307
- pid: z.number()
6350
+ pid: z.number(),
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()
6308
6354
  }))
6309
6355
  })
6310
6356
  }
@@ -6316,7 +6362,9 @@ function startDaemonControlServer({
6316
6362
  children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
6317
6363
  startedBy: child.startedBy,
6318
6364
  happySessionId: child.happySessionId,
6319
- pid: child.pid
6365
+ pid: child.pid,
6366
+ turnActive: isTurnActive ? isTurnActive(child.happySessionId) : false,
6367
+ ...getBackgroundTasks ? { backgroundTasks: getBackgroundTasks(child.happySessionId) } : {}
6320
6368
  }))
6321
6369
  };
6322
6370
  });
@@ -6884,6 +6932,170 @@ function createPeerCoordinator(deps) {
6884
6932
  };
6885
6933
  }
6886
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
+
6887
7099
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6888
7100
 
6889
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
@@ -9110,9 +9322,9 @@ function createMirrorManager(deps) {
9110
9322
  archiveReason: reason
9111
9323
  });
9112
9324
  try {
9113
- await deps.api.deactivateSession(binding.happySessionId);
9325
+ await deps.api.archiveSession(binding.happySessionId);
9114
9326
  } catch (error) {
9115
- logger.debug(`[MIRROR] deactivateSession failed for ${binding.happySessionId}:`, error);
9327
+ logger.debug(`[MIRROR] archiveSession failed for ${binding.happySessionId}:`, error);
9116
9328
  }
9117
9329
  deps.onBindingsChanged?.();
9118
9330
  };
@@ -10592,6 +10804,13 @@ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
10592
10804
  if (edgeFell) item.sawTurn = false;
10593
10805
  const turn = await readTurn(sessionId, item.runId).catch(() => null);
10594
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;
10595
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}` });
10596
10815
  else await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn.answer, AUTOMATION_SUMMARY_MAX_CHARS) });
10597
10816
  }
@@ -10721,6 +10940,16 @@ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
10721
10940
  function daemonExitCode(source) {
10722
10941
  return source === "exception" ? 1 : 0;
10723
10942
  }
10943
+ const HANDOVER_EXIT_CODE = 75;
10944
+
10945
+ function detectDaemonSupervisor(env, platform = process.platform) {
10946
+ const explicit = env.HAPPY_DAEMON_SUPERVISOR?.trim().toLowerCase();
10947
+ if (explicit === "systemd") return "systemd";
10948
+ if (explicit === "none") return "none";
10949
+ if (platform === "linux" && typeof env.INVOCATION_ID === "string" && env.INVOCATION_ID.trim() !== "") return "systemd";
10950
+ return "none";
10951
+ }
10952
+ const SYSTEMD_RESTART_HINT = "systemctl --user restart very-happy-daemon";
10724
10953
 
10725
10954
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10726
10955
  function sanitizeImportTitle(value) {
@@ -10876,6 +11105,7 @@ async function startDaemon() {
10876
11105
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
10877
11106
  const pidToTrackedSession = /* @__PURE__ */ new Map();
10878
11107
  const turnActivity = new TurnActivityTracker();
11108
+ const backgroundTasks = new BackgroundTaskTracker();
10879
11109
  const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
10880
11110
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
10881
11111
  let teamWorker;
@@ -11736,6 +11966,7 @@ async function startDaemon() {
11736
11966
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
11737
11967
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
11738
11968
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11969
+ if (session?.happySessionId) backgroundTasks.forget(session.happySessionId);
11739
11970
  if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
11740
11971
  if (session?.happySessionId && session.encryption) {
11741
11972
  sessionIdToFinishedSession.set(session.happySessionId, session);
@@ -11822,10 +12053,15 @@ async function startDaemon() {
11822
12053
  onSessionStateEvent,
11823
12054
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
11824
12055
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
12056
+ onSessionBackgroundTasks: (sessionId, tasks) => backgroundTasks.apply(sessionId, tasks),
12057
+ getBackgroundTasks: (sessionId) => backgroundTasks.report(sessionId),
11825
12058
  onSessionEdit: (edit) => {
11826
12059
  peerCoordinator.onEdit(edit);
11827
12060
  },
11828
12061
  listPeers: () => peerCoordinator.list(),
12062
+ // B-505: `daemon list` shows which sessions have a turn in flight, so an
12063
+ // operator can check the WHOLE machine before a supervised restart.
12064
+ isTurnActive: (sessionId) => turnActivity.activeSessions().includes(sessionId),
11829
12065
  pushClipboard: (text, terminalId) => {
11830
12066
  if (!apiMachineRef) {
11831
12067
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -11845,6 +12081,8 @@ async function startDaemon() {
11845
12081
  }
11846
12082
  });
11847
12083
  const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
12084
+ const daemonSupervisor = detectDaemonSupervisor(process.env);
12085
+ logger.debug(`[DAEMON RUN] Supervisor: ${daemonSupervisor}`);
11848
12086
  let fileState = {
11849
12087
  pid: process.pid,
11850
12088
  httpPort: controlPort,
@@ -11854,7 +12092,8 @@ async function startDaemon() {
11854
12092
  serverUrl: configuration.serverUrl,
11855
12093
  webappUrl: configuration.webappUrl,
11856
12094
  daemonLogPath: logger.logFilePath,
11857
- claudeCredentialSource: daemonClaudeCredentials.source
12095
+ claudeCredentialSource: daemonClaudeCredentials.source,
12096
+ supervisor: daemonSupervisor
11858
12097
  };
11859
12098
  writeDaemonState(fileState);
11860
12099
  logger.debug("[DAEMON RUN] Daemon state written");
@@ -11915,6 +12154,7 @@ async function startDaemon() {
11915
12154
  listTrackedSessionIds: () => [...pidToTrackedSession.values()].map((session) => session.happySessionId).filter((sessionId) => typeof sessionId === "string"),
11916
12155
  requestShutdown: () => requestShutdown("happy-app")
11917
12156
  });
12157
+ apiMachine.setRemoteSessionOpsHandlers(createRemoteSessionOpsHandlers(machineId));
11918
12158
  const claudeAuthService = new ClaudeAuthService({
11919
12159
  happyHomeDir: configuration.happyHomeDir,
11920
12160
  happyLibDir: projectPath(),
@@ -11947,8 +12187,9 @@ async function startDaemon() {
11947
12187
  enabled: async () => ((await readSettings()).cliAutoUpdate ?? "idle") !== "off",
11948
12188
  // B-466: "idle" = no agent turn in flight. Idle wrappers survive a
11949
12189
  // handover (they talk to the server themselves) and web terminals live
11950
- // in tmux, so neither is a reason to hold an update.
11951
- 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(),
11952
12193
  install: installCliSafely,
11953
12194
  publish: (state) => {
11954
12195
  cliUpdateStateRef = state;
@@ -12003,7 +12244,7 @@ async function startDaemon() {
12003
12244
  } catch {
12004
12245
  }
12005
12246
  }
12006
- if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
12247
+ if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn() && !backgroundTasks.hasAny()) {
12007
12248
  const preflight = await preflightNewBundle(bundlePath);
12008
12249
  if (preflight.action === "hold") {
12009
12250
  if (lastHandoverHold !== preflight.reason) {
@@ -12013,7 +12254,7 @@ async function startDaemon() {
12013
12254
  updateController.holdHandover(preflight.reason);
12014
12255
  return;
12015
12256
  }
12016
- if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
12257
+ if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn() || backgroundTasks.hasAny()) return;
12017
12258
  lastHandoverHold = null;
12018
12259
  logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
12019
12260
  teamWorker?.stop();
@@ -12027,6 +12268,10 @@ async function startDaemon() {
12027
12268
  await cleanupDaemonState();
12028
12269
  await releaseDaemonLock(daemonLockHandle);
12029
12270
  await stopCaffeinate();
12271
+ if (daemonSupervisor === "systemd") {
12272
+ logger.debug(`[DAEMON RUN] systemd-supervised: exiting ${HANDOVER_EXIT_CODE} for the unit to restart on the new bundle`);
12273
+ process.exit(HANDOVER_EXIT_CODE);
12274
+ }
12030
12275
  try {
12031
12276
  spawnHappyCLI(["daemon", "start"], {
12032
12277
  detached: true,
@@ -12075,6 +12320,7 @@ async function startDaemon() {
12075
12320
  spawn: spawnSession,
12076
12321
  live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
12077
12322
  turnActive: (id) => turnActivity.activeSessions().includes(id),
12323
+ backgroundTasks: (id) => backgroundTasks.count(id),
12078
12324
  log: (message) => logger.debug(`[DAEMON RUN] ${message}`)
12079
12325
  });
12080
12326
  void apiMachine.updateMachineMetadata((current) => ({
@@ -12368,36 +12614,54 @@ ${sessions.map(describeSummary).join("\n")}`);
12368
12614
  }
12369
12615
  });
12370
12616
  mcp.registerTool("session_read", {
12371
- 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).",
12372
12618
  title: "Read Session Transcript",
12373
12619
  inputSchema: {
12374
12620
  sessionId: z.string().describe("The Happy session id to read"),
12375
- 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)")
12376
12623
  }
12377
12624
  }, async (args) => {
12378
12625
  if (!isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12379
12626
  try {
12380
- 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 } : {} });
12381
12636
  const body = result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)";
12382
- return ok(`${describeSummary(result.summary)}
12383
- --- 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" : ""} ---
12384
12639
  ${body}`);
12385
12640
  } catch (error) {
12386
12641
  return fail(`Failed to read session: ${error instanceof Error ? error.message : String(error)}`);
12387
12642
  }
12388
12643
  });
12389
12644
  mcp.registerTool("session_send", {
12390
- 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.",
12391
12646
  title: "Send Message to Session",
12392
12647
  inputSchema: {
12393
12648
  sessionId: z.string().describe("The Happy session id to message"),
12394
12649
  text: z.string().describe("The message text to send"),
12395
- 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)")
12396
12652
  }
12397
12653
  }, async (args) => {
12398
12654
  if (!isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12399
12655
  if (typeof args.text !== "string" || args.text.trim().length === 0) return fail("text must be non-empty");
12400
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
+ }
12401
12665
  const persisted = await waitForSessionKey(args.sessionId, 0);
12402
12666
  const result = await deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12403
12667
  resume: args.resume === true,
@@ -12409,6 +12673,7 @@ ${body}`);
12409
12673
  }
12410
12674
  return ok(`Message delivered${result.resumed ? " (session resumed on this machine)" : ""} to ${describeSession(args.sessionId, persisted, { live: true })}`);
12411
12675
  } catch (error) {
12676
+ if (error instanceof RemoteSessionOpsError) return fail(`Failed to send (${error.code}): ${error.message}`);
12412
12677
  return fail(`Failed to send: ${error instanceof Error ? error.message : String(error)}`);
12413
12678
  }
12414
12679
  });
@@ -13006,6 +13271,40 @@ function registerKillSessionHandler(rpcHandlerManager, killThisHappy, archiveEve
13006
13271
  };
13007
13272
  }
13008
13273
 
13274
+ async function settleSessionOnExit(sessionId, intent, deps) {
13275
+ if (intent === "archive") {
13276
+ deps.updateMetadata((currentMetadata) => ({
13277
+ ...currentMetadata,
13278
+ lifecycleState: "archived",
13279
+ lifecycleStateSince: Date.now(),
13280
+ archivedBy: "cli",
13281
+ archiveReason: "User terminated"
13282
+ }));
13283
+ }
13284
+ deps.sendSessionDeath();
13285
+ try {
13286
+ if (intent === "archive") await deps.archiveSession(sessionId);
13287
+ else await deps.deactivateSession(sessionId);
13288
+ } catch (error) {
13289
+ deps.log?.(`[START] ${intent === "archive" ? "archiveSession" : "deactivateSession"} during cleanup failed:`, error);
13290
+ }
13291
+ }
13292
+ class SessionExitGate {
13293
+ decided = null;
13294
+ /** Returns the intent to act on, or null when cleanup already started. */
13295
+ claim(intent) {
13296
+ if (this.decided !== null) return null;
13297
+ this.decided = intent;
13298
+ return intent;
13299
+ }
13300
+ get current() {
13301
+ return this.decided;
13302
+ }
13303
+ }
13304
+ function exitIntentFromArchiveFlag(archive) {
13305
+ return archive ?? true ? "archive" : "offline";
13306
+ }
13307
+
13009
13308
  function isUnsupportedSideQuestionError(error) {
13010
13309
  const message = error instanceof Error ? error.message : String(error);
13011
13310
  return /unknown|unsupported|not supported|unrecognized|invalid.*subtype/i.test(message);
@@ -13182,7 +13481,7 @@ function registerSideQuestionHandler(rpc, deps) {
13182
13481
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13183
13482
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13184
13483
  const fork = deps.run ?? (async (input) => {
13185
- const { query } = await import('./index-DVVRqaVG.mjs');
13484
+ const { query } = await import('./index-DQcRkI9r.mjs');
13186
13485
  return runSideQuestion(query, input);
13187
13486
  });
13188
13487
  const run = async (input, live) => {
@@ -14319,26 +14618,24 @@ async function runClaude(credentials, options = {}) {
14319
14618
  hasMetadata: Boolean(message.meta)
14320
14619
  });
14321
14620
  });
14621
+ const exitGate = new SessionExitGate();
14322
14622
  const cleanup = async (opts = { archive: true }) => {
14323
- logger.debug(`[START] Received termination signal, cleaning up (archive=${opts.archive ?? true})...`);
14623
+ const intent = exitGate.claim(exitIntentFromArchiveFlag(opts.archive));
14624
+ if (intent === null) {
14625
+ logger.debug(`[START] Termination (archive=${opts.archive ?? true}) ignored: cleanup already running as ${exitGate.current}`);
14626
+ return;
14627
+ }
14628
+ logger.debug(`[START] Received termination signal, cleaning up (archive=${intent === "archive"})...`);
14324
14629
  try {
14325
14630
  if (session) {
14326
- if (opts.archive ?? true) {
14327
- session.updateMetadata((currentMetadata) => ({
14328
- ...currentMetadata,
14329
- lifecycleState: "archived",
14330
- lifecycleStateSince: Date.now(),
14331
- archivedBy: "cli",
14332
- archiveReason: "User terminated"
14333
- }));
14334
- }
14335
14631
  currentSession?.cleanup();
14336
- session.sendSessionDeath();
14337
- try {
14338
- await api.deactivateSession(session.sessionId);
14339
- } catch (err) {
14340
- logger.debug("[START] deactivateSession during cleanup failed:", err);
14341
- }
14632
+ await settleSessionOnExit(session.sessionId, intent, {
14633
+ updateMetadata: (handler) => session.updateMetadata(handler),
14634
+ sendSessionDeath: () => session.sendSessionDeath(),
14635
+ archiveSession: (id) => api.archiveSession(id),
14636
+ deactivateSession: (id) => api.deactivateSession(id),
14637
+ log: (message, ...args) => logger.debug(message, ...args)
14638
+ });
14342
14639
  await session.flush();
14343
14640
  await session.close();
14344
14641
  }
@@ -18492,7 +18789,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18492
18789
  return;
18493
18790
  } else if (subcommand === "install-terminal-hooks") {
18494
18791
  try {
18495
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-tIg7mANc.mjs');
18792
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-ELDoqh0A.mjs');
18496
18793
  const command = parseTerminalHooksArgs(args.slice(1));
18497
18794
  if (command.action === "help") {
18498
18795
  console.log(TERMINAL_HOOKS_HELP);
@@ -18509,7 +18806,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18509
18806
  }
18510
18807
  } else if (subcommand === "agent-home") {
18511
18808
  try {
18512
- const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-ByLb2HFg.mjs');
18809
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-BaJ9xtxB.mjs');
18513
18810
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18514
18811
  process.exit(0);
18515
18812
  } catch (error) {
@@ -18518,7 +18815,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18518
18815
  }
18519
18816
  } else if (subcommand === "spawn") {
18520
18817
  try {
18521
- const { handleSpawnCommand } = await import('./spawn-BaPnjClT.mjs');
18818
+ const { handleSpawnCommand } = await import('./spawn-CdihhwHs.mjs');
18522
18819
  await handleSpawnCommand(args.slice(1));
18523
18820
  } catch (error) {
18524
18821
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18530,7 +18827,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18530
18827
  return;
18531
18828
  } else if (subcommand === "sessions") {
18532
18829
  try {
18533
- const { handleSessionsCommand } = await import('./sessions-JaVXlSqj.mjs');
18830
+ const { handleSessionsCommand } = await import('./sessions-DsE01xKc.mjs');
18534
18831
  await handleSessionsCommand(args.slice(1));
18535
18832
  } catch (error) {
18536
18833
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18542,7 +18839,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18542
18839
  return;
18543
18840
  } else if (subcommand === "send") {
18544
18841
  try {
18545
- const { handleSendCommand } = await import('./send-DJStmLBv.mjs');
18842
+ const { handleSendCommand } = await import('./send-D5CyRyZ_.mjs');
18546
18843
  await handleSendCommand(args.slice(1));
18547
18844
  } catch (error) {
18548
18845
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18648,9 +18945,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18648
18945
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
18649
18946
  const projectId = args[3];
18650
18947
  try {
18651
- const { saveGoogleCloudProjectToConfig } = await import('./config-BPZ2Y6o8.mjs');
18652
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-C_DW0M0q.mjs').then(function (n) { return n.aG; });
18653
- const { ApiClient: ApiClient2 } = await import('./types-CkbfdRzq.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; });
18654
18951
  let userEmail = void 0;
18655
18952
  try {
18656
18953
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -18681,7 +18978,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18681
18978
  }
18682
18979
  if (geminiSubcommand === "project" && args[2] === "get") {
18683
18980
  try {
18684
- const { readGeminiLocalConfig } = await import('./config-BPZ2Y6o8.mjs');
18981
+ const { readGeminiLocalConfig } = await import('./config-aAbhJllb.mjs');
18685
18982
  const config = readGeminiLocalConfig();
18686
18983
  if (config.googleCloudProject) {
18687
18984
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -18721,7 +19018,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18721
19018
  process.exit(0);
18722
19019
  }
18723
19020
  try {
18724
- const { runGemini } = await import('./runGemini-CDlcQ-OA.mjs');
19021
+ const { runGemini } = await import('./runGemini-Cj6oJQ-I.mjs');
18725
19022
  let startedBy = void 0;
18726
19023
  for (let i = 1; i < args.length; i++) {
18727
19024
  if (args[i] === "--started-by") {
@@ -18744,7 +19041,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18744
19041
  } else if (subcommand === "pi") {
18745
19042
  if (args[1] === "--terminal") {
18746
19043
  try {
18747
- const { runPiTerminal } = await import('./piTerminal-Dpq6yqKU.mjs');
19044
+ const { runPiTerminal } = await import('./piTerminal-DwWmgumi.mjs');
18748
19045
  process.exitCode = await runPiTerminal(args.slice(2));
18749
19046
  } catch (error) {
18750
19047
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18761,7 +19058,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18761
19058
  }
18762
19059
  };
18763
19060
  try {
18764
- const { runAcp, resolveAcpAgentConfig } = await import('./index-BjzdXb9x.mjs');
19061
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BVu8Cufk.mjs');
18765
19062
  const parsed = parsePiRunnerArgs(args.slice(1));
18766
19063
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
18767
19064
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -18789,7 +19086,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18789
19086
  return;
18790
19087
  } else if (subcommand === "acp") {
18791
19088
  try {
18792
- const { runAcp, resolveAcpAgentConfig } = await import('./index-BjzdXb9x.mjs');
19089
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BVu8Cufk.mjs');
18793
19090
  let startedBy = void 0;
18794
19091
  let verbose = false;
18795
19092
  const acpArgs = [];
@@ -18829,7 +19126,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18829
19126
  return;
18830
19127
  } else if (subcommand === "openclaw") {
18831
19128
  try {
18832
- const { runOpenClaw } = await import('./runOpenClaw-YkhsgPs0.mjs');
19129
+ const { runOpenClaw } = await import('./runOpenClaw-DUCQxkI-.mjs');
18833
19130
  let startedBy = void 0;
18834
19131
  let verbose = false;
18835
19132
  let gatewayUrl;
@@ -18880,7 +19177,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18880
19177
  return;
18881
19178
  } else if (subcommand === "todo") {
18882
19179
  try {
18883
- const { handleTodoCommand } = await import('./todo-2Z1REhfx.mjs');
19180
+ const { handleTodoCommand } = await import('./todo-CuQAkSp2.mjs');
18884
19181
  await handleTodoCommand(args.slice(1));
18885
19182
  } catch (error) {
18886
19183
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -18889,7 +19186,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18889
19186
  return;
18890
19187
  } else if (subcommand === "teams") {
18891
19188
  try {
18892
- const { handleTeamsCommand } = await import('./teams-DXV5Pwwt.mjs');
19189
+ const { handleTeamsCommand } = await import('./teams-BE8nMB6O.mjs');
18893
19190
  await handleTeamsCommand(args.slice(1));
18894
19191
  } catch (error) {
18895
19192
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -18898,7 +19195,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18898
19195
  return;
18899
19196
  } else if (subcommand === "auto") {
18900
19197
  try {
18901
- const { handleAutoCommand } = await import('./auto-BQnD6gVx.mjs');
19198
+ const { handleAutoCommand } = await import('./auto-B1YAwaeU.mjs');
18902
19199
  await handleAutoCommand(args.slice(1));
18903
19200
  } catch (error) {
18904
19201
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -18910,7 +19207,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18910
19207
  return;
18911
19208
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
18912
19209
  try {
18913
- const { handleMcpCommand } = await import('./mcp-o7QglAQM.mjs');
19210
+ const { handleMcpCommand } = await import('./mcp-Bo934Kpy.mjs');
18914
19211
  await handleMcpCommand(args[1] === "--terminal-tools");
18915
19212
  } catch (error) {
18916
19213
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -18939,6 +19236,10 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18939
19236
  } else {
18940
19237
  console.log("Active sessions:");
18941
19238
  console.log(JSON.stringify(sessions, null, 2));
19239
+ const busy = sessions.filter((s) => s.turnActive === true);
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(", ")}`);
18942
19243
  }
18943
19244
  } catch (error) {
18944
19245
  console.log("No daemon running");
@@ -18967,13 +19268,19 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18967
19268
  configuration.webappUrl
18968
19269
  );
18969
19270
  if (running && !currentDaemonMatches) {
19271
+ if (currentState?.supervisor === "systemd") {
19272
+ console.error(`The running daemon is supervised by systemd; hand over through the unit instead:
19273
+ ${SYSTEMD_RESTART_HINT}`);
19274
+ process.exit(1);
19275
+ }
18970
19276
  console.log("Configured relay or CLI version changed; restarting the existing daemon");
18971
19277
  await stopDaemon();
18972
19278
  }
19279
+ const { HAPPY_DAEMON_SUPERVISOR: _supervisor, INVOCATION_ID: _invocation, ...detachedEnv } = process.env;
18973
19280
  const child = spawnHappyCLI(["daemon", "start-sync"], {
18974
19281
  detached: true,
18975
19282
  stdio: "ignore",
18976
- env: process.env
19283
+ env: detachedEnv
18977
19284
  });
18978
19285
  child.unref();
18979
19286
  let started = false;