very-happy-cli 0.2.154 → 0.2.156

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 (57) hide show
  1. package/dist/{AcpBackend-B14xNj5L.cjs → AcpBackend-BHe1_F1h.cjs} +3 -3
  2. package/dist/{AcpBackend-Ckyadhf4.mjs → AcpBackend-C3nc1u-s.mjs} +2 -2
  3. package/dist/{AcpSessionManager-CCHmPdrR.cjs → AcpSessionManager-BVxkQnNf.cjs} +1 -1
  4. package/dist/{AcpSessionManager-H8t4P-IW.mjs → AcpSessionManager-dN3k1LbI.mjs} +1 -1
  5. package/dist/agentGuidance-C-CmV03f.cjs +1665 -0
  6. package/dist/agentGuidance-C604n0Zq.mjs +1613 -0
  7. package/dist/{agentHome-BUsLTZnz.cjs → agentHome-CmIJNV7f.cjs} +5 -5
  8. package/dist/{agentHome-B20RWdgE.mjs → agentHome-D2XGsoqs.mjs} +5 -5
  9. package/dist/{auto-BQnncDcs.mjs → auto-B1I6G_Nf.mjs} +21 -21
  10. package/dist/{auto-IeldARBS.cjs → auto-CMDq4oIu.cjs} +24 -24
  11. package/dist/codex/happyMcpStdioBridge.cjs +6 -3
  12. package/dist/codex/happyMcpStdioBridge.mjs +6 -3
  13. package/dist/{config-DM_xI9M6.cjs → config-BYHSRY2E.cjs} +8 -8
  14. package/dist/{config-B-ocQh5V.mjs → config-BkTu25bH.mjs} +8 -8
  15. package/dist/{index-ZKuBjvHi.mjs → index-B5QbzEga.mjs} +21 -13
  16. package/dist/{index-BUxBxSxJ.cjs → index-B_-7NpGZ.cjs} +21 -13
  17. package/dist/{index-eCjkm8Y2.cjs → index-BbtR-Qto.cjs} +606 -982
  18. package/dist/{index-okLyUzeD.cjs → index-CB8QnjvS.cjs} +7 -7
  19. package/dist/{index-BELV05ko.mjs → index-DAAreiRF.mjs} +548 -911
  20. package/dist/{index-CIx3248o.mjs → index-PYO8Mdz4.mjs} +7 -7
  21. package/dist/index.cjs +6 -6
  22. package/dist/index.mjs +6 -6
  23. package/dist/{install-CoLN3whR.mjs → install-CNtXzwT_.mjs} +1 -1
  24. package/dist/{install-BZOdxfPd.cjs → install-CO1ps6Wt.cjs} +1 -1
  25. package/dist/{installTerminalHooks-CA4OcAS7.cjs → installTerminalHooks-BOAnQ1Qe.cjs} +5 -5
  26. package/dist/{installTerminalHooks-Dde_8t36.mjs → installTerminalHooks-CMpOV3RU.mjs} +5 -5
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.d.cts +24 -8
  29. package/dist/lib.d.mts +24 -8
  30. package/dist/lib.mjs +2 -2
  31. package/dist/{limits-Dt7_unH4.mjs → limits-DmMdchTz.mjs} +309 -13
  32. package/dist/{limits-CmzISka2.cjs → limits-o29KXBgj.cjs} +329 -12
  33. package/dist/{mcp-9Ui4nQZL.mjs → mcp-C_uejotl.mjs} +17 -17
  34. package/dist/{mcp-7iDHlyCI.cjs → mcp-CwK1nI7Y.cjs} +23 -23
  35. package/dist/{piRuntime-C4E4pgye.mjs → piRuntime-Cu2-8nOT.mjs} +2 -2
  36. package/dist/{piRuntime-BesGGPME.cjs → piRuntime-Dj5WnqzJ.cjs} +2 -2
  37. package/dist/{piTerminal-fUogwxYr.cjs → piTerminal-D-BIbov9.cjs} +6 -6
  38. package/dist/{piTerminal-CCEGCQOY.mjs → piTerminal-DanZeBrO.mjs} +6 -6
  39. package/dist/{runGemini-CNPGSsSK.cjs → runGemini--QqK-Ap5.cjs} +7 -7
  40. package/dist/{runGemini-BW-pEklU.mjs → runGemini-CZ_lhMWY.mjs} +6 -6
  41. package/dist/{runOpenClaw-Bsodkg_s.cjs → runOpenClaw-C0xLhTvk.cjs} +6 -6
  42. package/dist/{runOpenClaw-NRB6-_jU.mjs → runOpenClaw-CxgLpm38.mjs} +5 -5
  43. package/dist/{send-DYeSFg8s.mjs → send-BaOkDgH0.mjs} +5 -39
  44. package/dist/{send-Bd00QcHC.cjs → send-CTcGMVuG.cjs} +8 -42
  45. package/dist/{sessions-DlPOYKLU.mjs → sessions-Bn304xF1.mjs} +88 -45
  46. package/dist/{sessions-RIFUoy_g.cjs → sessions-OmvyJb0n.cjs} +99 -56
  47. package/dist/{spawn-D1bhyHbH.mjs → spawn-BKNabutV.mjs} +22 -22
  48. package/dist/{spawn-DxrfHTF4.cjs → spawn-CC6V2KiA.cjs} +30 -30
  49. package/dist/{teams-pfKHpkgL.mjs → teams-CLbS2_4Q.mjs} +8 -8
  50. package/dist/{teams-Cfy0VMCo.cjs → teams-XtrH3_h3.cjs} +8 -8
  51. package/dist/{todo-B2CjDg_V.mjs → todo-BcPbNKt2.mjs} +8 -6
  52. package/dist/{todo-B4cBFzHB.cjs → todo-CEDvKNXK.cjs} +8 -6
  53. package/dist/{types-D5RWyBE5.mjs → types-BEVzYSoV.mjs} +52 -291
  54. package/dist/{types-Bd_TGr0F.cjs → types-CphWjqHc.cjs} +64 -320
  55. package/package.json +7 -7
  56. package/dist/agentGuidance-DUBXppv-.cjs +0 -554
  57. package/dist/agentGuidance-TLYQxsm2.mjs +0 -530
@@ -1,14 +1,14 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
2
2
  import os, { homedir, userInfo, tmpdir } from 'node:os';
3
3
  import { randomUUID, randomInt, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
4
- import { v as notifyDaemonSessionEvent, g as getProjectPath, w as initializeSandbox, x as wrapCommand, u as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, c as delay, y as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, z as notifyDaemonClaudeAuthFailed, B as commandOnPath, j as checkIfDaemonRunningAndCleanupStaleState, D as deriveLocalCliUpdateSummary, E as isClaudeSessionId, G as getLocalHappyAgentCredentialPath, H as readLocalHappyAgentCredentials, J as parseExactVersion, K as compareExactVersions, L as extractCompleteLines, N as decideBackfill, O as parseTerminalHookPayload, P as tmuxArgs, Q as scrubTmuxClientEnv, S as decideMirrorBinding, T as MIRROR_BACKFILL_LINES_DEFAULT, U as mirrorLineKey, V as mirrorLocalId, W as listDaemonSessions, k as stopDaemonSession, X as detectCLIAvailability, Y as detectResumeSupport, Z as refreshAgentHomes, d as describeAgentHome, _ as isDaemonRunningCurrentlyInstalledHappyVersion, $ as stopDaemon, a0 as TurnActivityTracker, a1 as summarizeSpawnSessionForLog, a2 as locateClaudeConversation, a3 as agentHomeSpawnEnv, a4 as shellescape, A as ApiClient, a5 as fetchCliUpdateState, a6 as resolveCliUpdateCheckInterval, a7 as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, a8 as cleanupDaemonState, a9 as resumeDaemonSession, i as isValidSessionId, s as spawnDaemonSession, aa as listVhTerminals, ab as isValidTerminalId, ac as readVhTerminal, ad as sendToVhTerminal, ae as TITLE_PROMPT_PREFIX, af as BOARD_ANALYZER_PROMPT_PREFIX, o as connectionState, ag as startOfflineReconnection, q as notifyDaemonSessionStarted, C as CodexAppServerClient } from './types-D5RWyBE5.mjs';
5
- import { l as logger, k as BUILTIN_TODO_DISCOVERY, U as STREAM_DELTA_MAX_CHARS, V as ensurePrivateDirectory, W as hardenPrivateFile, c as configuration, p as packageJson, r as readSettings, X as readCredentials, L as credentialRelayProblem, F as readDaemonState, Y as AGENT_VERSION_SOURCES, j as encodeBase64, Z as readSettingsOutcome, u as updateSettings, _ as encodeBase64Url, E as decodeBase64, $ as writeCredentialsLegacy, a0 as writeCredentialsDataKey, f as readCredentialsForConfiguredRelay, z as encrypt, d as readPersistedSessions, w as writePrivateFileSync, a1 as decryptWithDataKey, a2 as decryptLegacy, a3 as persistSession, D as decrypt, m as ensurePrivateDirectorySync, a4 as getAgentCodeDefaults, a5 as normalizeAgentKey, a6 as isClaudeOpus55Model, a7 as supportsClaudeOpus55, a8 as AUTOMATION_SUMMARY_MAX_CHARS, a9 as AUTOMATION_CLAIM_MAX_RUNS, aa as AUTOMATION_DEFAULT_LEASE_MS, i as isAutomationRunTerminal, ab as acquireDaemonLock, ac as deletePersistedSessions, ad as writeDaemonState, ae as writeSettings, af as releaseDaemonLock, ag as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, h as CLIPBOARD_MAX_BYTES, ah as clearCredentials, ai as clearMachineId, aj as SandboxConfigSchema, e as createEnvelope, ak as getLatestDaemonLog } from './limits-Dt7_unH4.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-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';
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 { j as trimIdent, k as AGENT_GUIDANCE, l as createAutomationsClient, i as isAutomationsUnavailable, m as AutomationsRequestError, t as teamScopePath, T as TeamConnectionPreflightError, h as createTeamsClient, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, n as TEAM_TOOL_NAMES, o as AUTOMATION_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, p as REPORT_PROGRESS_TOOL_TITLE, q as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-TLYQxsm2.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';
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';
@@ -16,7 +16,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
16
16
  import { query as query$1, AbortError } from '@anthropic-ai/claude-agent-sdk';
17
17
  import axios, { AxiosError } from 'axios';
18
18
  import 'node:events';
19
- import { io } from 'socket.io-client';
19
+ import 'socket.io-client';
20
20
  import tweetnacl from 'tweetnacl';
21
21
  import os$1, { homedir as homedir$1 } from 'os';
22
22
  import { spawn as spawn$2, execSync as execSync$1, exec as exec$1 } from 'child_process';
@@ -878,7 +878,7 @@ async function createSessionScanner(opts) {
878
878
  let processedMessageKeys = /* @__PURE__ */ new Set();
879
879
  let deadSessions = /* @__PURE__ */ new Set();
880
880
  if (opts.sessionId) {
881
- let messages = await readSessionLog$1(projectDir, opts.sessionId);
881
+ let messages = await readSessionLog(projectDir, opts.sessionId);
882
882
  logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
883
883
  for (let m of messages) {
884
884
  processedMessageKeys.add(messageKey(m));
@@ -901,7 +901,7 @@ async function createSessionScanner(opts) {
901
901
  }
902
902
  }
903
903
  for (let session of sessions) {
904
- const sessionMessages = await readSessionLog$1(projectDir, session);
904
+ const sessionMessages = await readSessionLog(projectDir, session);
905
905
  let skipped = 0;
906
906
  let sent = 0;
907
907
  for (let file of sessionMessages) {
@@ -978,7 +978,7 @@ async function createSessionScanner(opts) {
978
978
  return;
979
979
  }
980
980
  if (options?.treatExistingAsProcessed) {
981
- const existing = await readSessionLog$1(projectDir, sessionId);
981
+ const existing = await readSessionLog(projectDir, sessionId);
982
982
  logger.debug(`[SESSION_SCANNER] Pre-marking ${existing.length} existing messages as processed for new session ${sessionId}`);
983
983
  for (const m of existing) {
984
984
  processedMessageKeys.add(messageKey(m));
@@ -1006,7 +1006,7 @@ function messageKey(message) {
1006
1006
  throw Error();
1007
1007
  }
1008
1008
  }
1009
- async function readSessionLog$1(projectDir, sessionId) {
1009
+ async function readSessionLog(projectDir, sessionId) {
1010
1010
  const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
1011
1011
  logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
1012
1012
  let file;
@@ -5380,8 +5380,8 @@ ${typeLabels[type] || type}:`));
5380
5380
  }
5381
5381
  console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
5382
5382
  try {
5383
- const { resolveAgentHomesForCli } = await import('./agentHome-B20RWdgE.mjs');
5384
- const { describeAgentHome } = await import('./types-D5RWyBE5.mjs').then(function (n) { return n.ah; });
5383
+ const { resolveAgentHomesForCli } = await import('./agentHome-D2XGsoqs.mjs');
5384
+ const { describeAgentHome } = await import('./types-BEVzYSoV.mjs').then(function (n) { return n.a0; });
5385
5385
  const homes = await resolveAgentHomesForCli();
5386
5386
  console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
5387
5387
  console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
@@ -6063,59 +6063,6 @@ function formatAssistantReportMessage(title, sessionId, event) {
6063
6063
  return `[\u7CFB\u7EDF\u901A\u62A5] \u4F1A\u8BDD\u300C${title}\u300D${state}\uFF08${sessionId}\uFF09\u3002\u8BF7\u7528 session_read \u6838\u5B9E\u7ED3\u679C\uFF0C\u5E76\u5411\u7528\u6237\u53E3\u5934\u6C47\u62A5\u4E00\u53E5\u7ED3\u8BBA\uFF1B\u591A\u6761\u901A\u62A5\u63A5\u8FD1\u65F6\u5408\u5E76\u6C47\u62A5\u3002`;
6064
6064
  }
6065
6065
 
6066
- function sessionWebUrl(sessionId) {
6067
- return `${configuration.webappUrl.replace(/\/+$/, "")}/session/${sessionId}`;
6068
- }
6069
- async function waitForSessionKey(sessionId, timeoutMs) {
6070
- const deadline = Date.now() + timeoutMs;
6071
- while (true) {
6072
- const entry = readPersistedSessions()[sessionId];
6073
- if (entry) return entry;
6074
- if (Date.now() >= deadline) {
6075
- throw new Error(
6076
- `Session ${sessionId} has no encryption key in ${configuration.sessionsFile}. Either it was not spawned by this machine's daemon, or the daemon is too old to persist session keys.`
6077
- );
6078
- }
6079
- await delay(200);
6080
- }
6081
- }
6082
- async function sendUserMessage(sessionId, persisted, text, client, options = {}) {
6083
- const credentials = await readCredentialsForConfiguredRelay();
6084
- if (!credentials) {
6085
- throw new Error("Not authenticated. Run `very-happy auth login` first.");
6086
- }
6087
- const envelope = {
6088
- role: "user",
6089
- content: {
6090
- type: "text",
6091
- text
6092
- },
6093
- meta: {
6094
- sentFrom: options.sentFrom ?? "cli",
6095
- ...options.model !== void 0 ? { model: options.model } : {},
6096
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
6097
- ...options.effort !== void 0 ? { effort: options.effort } : {}
6098
- }
6099
- };
6100
- const encrypted = encodeBase64(encrypt(
6101
- decodeBase64(persisted.encryptionKey),
6102
- persisted.encryptionVariant,
6103
- envelope
6104
- ));
6105
- await axios.post(
6106
- `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
6107
- { messages: [{ content: encrypted, localId: options.localId ?? randomUUID() }] },
6108
- {
6109
- headers: {
6110
- "Authorization": `Bearer ${credentials.token}`,
6111
- "Content-Type": "application/json",
6112
- "X-Happy-Client": `${client}/${configuration.currentCliVersion}`
6113
- },
6114
- timeout: 3e4
6115
- }
6116
- );
6117
- }
6118
-
6119
6066
  const ALLOWED_SPAWN_PERMISSION_MODES = [
6120
6067
  "default",
6121
6068
  "acceptEdits",
@@ -6271,7 +6218,10 @@ function startDaemonControlServer({
6271
6218
  pushClipboard,
6272
6219
  pushFilePreview,
6273
6220
  onTerminalHook,
6274
- setTerminalTitle
6221
+ setTerminalTitle,
6222
+ onSessionEdit,
6223
+ listPeers,
6224
+ isTurnActive
6275
6225
  }) {
6276
6226
  return new Promise((resolve) => {
6277
6227
  const app = fastify({
@@ -6355,7 +6305,8 @@ function startDaemonControlServer({
6355
6305
  children: z.array(z.object({
6356
6306
  startedBy: z.string(),
6357
6307
  happySessionId: z.string(),
6358
- pid: z.number()
6308
+ pid: z.number(),
6309
+ turnActive: z.boolean()
6359
6310
  }))
6360
6311
  })
6361
6312
  }
@@ -6367,7 +6318,8 @@ function startDaemonControlServer({
6367
6318
  children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
6368
6319
  startedBy: child.startedBy,
6369
6320
  happySessionId: child.happySessionId,
6370
- pid: child.pid
6321
+ pid: child.pid,
6322
+ turnActive: isTurnActive ? isTurnActive(child.happySessionId) : false
6371
6323
  }))
6372
6324
  };
6373
6325
  });
@@ -6594,6 +6546,45 @@ function startDaemonControlServer({
6594
6546
  }
6595
6547
  return { status: "ok" };
6596
6548
  });
6549
+ typed.post("/session-edit", {
6550
+ schema: {
6551
+ body: z.object({
6552
+ sessionId: z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/),
6553
+ path: z.string().min(1).max(4096),
6554
+ tool: z.string().min(1).max(64),
6555
+ cwd: z.string().max(4096).optional(),
6556
+ /** When the runner saw the call (ms). Clamped to now by the daemon;
6557
+ * older than the conflict window = ignored (replayed history). */
6558
+ at: z.number().int().nonnegative().optional()
6559
+ }),
6560
+ response: { 200: z.object({ status: z.literal("ok") }) }
6561
+ }
6562
+ }, async (request) => {
6563
+ try {
6564
+ onSessionEdit?.(request.body);
6565
+ } catch (error) {
6566
+ logger.debug("[CONTROL SERVER] session-edit handler failed:", error);
6567
+ }
6568
+ return { status: "ok" };
6569
+ });
6570
+ typed.post("/peers", {
6571
+ schema: {
6572
+ response: {
6573
+ 200: z.object({
6574
+ sessions: z.array(z.object({
6575
+ sessionId: z.string(),
6576
+ kind: z.enum(["managed", "mirror"]),
6577
+ pid: z.number().optional(),
6578
+ cwd: z.string().optional(),
6579
+ flavor: z.string().optional(),
6580
+ title: z.string().optional(),
6581
+ variant: z.string().optional(),
6582
+ edits: z.array(z.object({ path: z.string(), tool: z.string(), at: z.number() }))
6583
+ }))
6584
+ })
6585
+ }
6586
+ }
6587
+ }, async () => ({ sessions: listPeers ? listPeers() : [] }));
6597
6588
  typed.post("/stop", {
6598
6589
  schema: {
6599
6590
  response: {
@@ -6629,6 +6620,273 @@ function startDaemonControlServer({
6629
6620
  });
6630
6621
  }
6631
6622
 
6623
+ const EDIT_CONFLICT_WINDOW_MS = 30 * 6e4;
6624
+ function pairKey$1(a, b, path) {
6625
+ return a < b ? `${a}\0${b}\0${path}` : `${b}\0${a}\0${path}`;
6626
+ }
6627
+ class EditConflictTracker {
6628
+ windowMs;
6629
+ maxPaths;
6630
+ /** real path → sessionId → latest edit */
6631
+ byPath = /* @__PURE__ */ new Map();
6632
+ /** (sorted pair, path) → when the notice went out */
6633
+ notified = /* @__PURE__ */ new Map();
6634
+ constructor(options = {}) {
6635
+ this.windowMs = options.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6636
+ this.maxPaths = options.maxPaths ?? 5e3;
6637
+ }
6638
+ /**
6639
+ * Remember an edit and return the peers to notify about it. `isLive`
6640
+ * decides whether another session still counts (dead ones are dropped
6641
+ * from the table on the spot).
6642
+ */
6643
+ record(edit, isLive) {
6644
+ this.prune(edit.at);
6645
+ let sessions = this.byPath.get(edit.path);
6646
+ if (!sessions) {
6647
+ sessions = /* @__PURE__ */ new Map();
6648
+ this.byPath.set(edit.path, sessions);
6649
+ }
6650
+ const conflicts = [];
6651
+ for (const [sessionId, peer] of sessions) {
6652
+ if (sessionId === edit.sessionId) continue;
6653
+ if (!isLive(sessionId)) {
6654
+ sessions.delete(sessionId);
6655
+ continue;
6656
+ }
6657
+ if (edit.at - peer.at > this.windowMs) continue;
6658
+ const key = pairKey$1(edit.sessionId, sessionId, edit.path);
6659
+ const notifiedAt = this.notified.get(key);
6660
+ if (notifiedAt !== void 0 && edit.at - notifiedAt <= this.windowMs) continue;
6661
+ this.notified.set(key, edit.at);
6662
+ conflicts.push({ editor: edit, peer });
6663
+ }
6664
+ sessions.set(edit.sessionId, edit);
6665
+ return conflicts;
6666
+ }
6667
+ /** Paths this session edited inside the window, newest first. */
6668
+ editsOf(sessionId, now, limit = 20) {
6669
+ const out = [];
6670
+ for (const [path, sessions] of this.byPath) {
6671
+ const edit = sessions.get(sessionId);
6672
+ if (edit && now - edit.at <= this.windowMs) out.push({ path, tool: edit.tool, at: edit.at });
6673
+ }
6674
+ return out.sort((a, b) => b.at - a.at).slice(0, limit);
6675
+ }
6676
+ /** Session ended: nothing it did counts any more. */
6677
+ forget(sessionId) {
6678
+ for (const [path, sessions] of this.byPath) {
6679
+ sessions.delete(sessionId);
6680
+ if (sessions.size === 0) this.byPath.delete(path);
6681
+ }
6682
+ for (const key of this.notified.keys()) {
6683
+ const [a, b] = key.split("\0");
6684
+ if (a === sessionId || b === sessionId) this.notified.delete(key);
6685
+ }
6686
+ }
6687
+ /** Drop everything older than the window (and cap the table size). */
6688
+ prune(now) {
6689
+ for (const [path, sessions] of this.byPath) {
6690
+ for (const [sessionId, edit] of sessions) if (now - edit.at > this.windowMs) sessions.delete(sessionId);
6691
+ if (sessions.size === 0) this.byPath.delete(path);
6692
+ }
6693
+ for (const [key, at] of this.notified) if (now - at > this.windowMs) this.notified.delete(key);
6694
+ if (this.byPath.size > this.maxPaths) {
6695
+ const oldest = [...this.byPath.entries()].map(([path, sessions]) => ({ path, at: Math.max(...[...sessions.values()].map((e) => e.at)) })).sort((a, b) => a.at - b.at);
6696
+ for (const entry of oldest.slice(0, this.byPath.size - this.maxPaths)) this.byPath.delete(entry.path);
6697
+ }
6698
+ }
6699
+ /** Test / diagnostics helper. */
6700
+ size() {
6701
+ return this.byPath.size;
6702
+ }
6703
+ }
6704
+ function normalizeEditPath(path, cwd, realpath = realpathSync.native ?? realpathSync) {
6705
+ const absolute = isAbsolute(path) ? resolve(path) : resolve(cwd, path);
6706
+ let existing = absolute;
6707
+ const tail = [];
6708
+ for (let depth = 0; depth < 64; depth++) {
6709
+ try {
6710
+ const real = realpath(existing);
6711
+ return tail.length ? join(real, ...tail) : real;
6712
+ } catch {
6713
+ const parent = dirname(existing);
6714
+ if (parent === existing) break;
6715
+ tail.unshift(existing.slice(parent.length).replace(new RegExp(`^\\${sep}`), ""));
6716
+ existing = parent;
6717
+ }
6718
+ }
6719
+ return absolute;
6720
+ }
6721
+
6722
+ const CONFLICT_COALESCE_MS = 1e4;
6723
+ const MAX_NOTICES_PER_PAIR = 5;
6724
+ function pairKey(a, b) {
6725
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
6726
+ }
6727
+ function senderOf(info, sessionId) {
6728
+ return { sessionId, title: info?.title, flavor: info?.flavor, cwd: info?.cwd };
6729
+ }
6730
+ function createPeerCoordinator(deps) {
6731
+ const now = deps.now ?? (() => Date.now());
6732
+ const windowMs = deps.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6733
+ const coalesceMs = deps.coalesceMs ?? CONFLICT_COALESCE_MS;
6734
+ const maxNoticesPerPair = deps.maxNoticesPerPair ?? MAX_NOTICES_PER_PAIR;
6735
+ const tracker = new EditConflictTracker({ windowMs });
6736
+ const readPersisted = deps.readPersisted ?? readPersistedSessions;
6737
+ const send = deps.send ?? sendUserMessage;
6738
+ const normalize = deps.normalize ?? normalizeEditPath;
6739
+ const liveTitle = deps.liveTitle ?? (async (sessionId, persisted) => (await readSessionMetadata(sessionId, persisted))?.summary?.text);
6740
+ const schedule = deps.schedule ?? ((fn, ms) => {
6741
+ const t = setTimeout(fn, ms);
6742
+ t.unref?.();
6743
+ return t;
6744
+ });
6745
+ let getMirrors = deps.getMirrors ?? (() => []);
6746
+ const pending = /* @__PURE__ */ new Map();
6747
+ const noticesSent = /* @__PURE__ */ new Map();
6748
+ let inflight = Promise.resolve();
6749
+ const snapshot = () => {
6750
+ const at = now();
6751
+ const out = /* @__PURE__ */ new Map();
6752
+ for (const child of deps.getChildren()) {
6753
+ const id = child.happySessionId;
6754
+ if (!id || out.has(id)) continue;
6755
+ const meta = child.happySessionMetadataFromLocalWebhook;
6756
+ out.set(id, {
6757
+ sessionId: id,
6758
+ kind: "managed",
6759
+ pid: child.pid,
6760
+ ...meta?.path ? { cwd: meta.path } : {},
6761
+ ...meta?.flavor ? { flavor: meta.flavor } : {},
6762
+ ...meta?.summary?.text ? { title: meta.summary.text } : {},
6763
+ ...meta?.variant ?? child.variant ? { variant: meta?.variant ?? child.variant } : {},
6764
+ edits: tracker.editsOf(id, at)
6765
+ });
6766
+ }
6767
+ for (const mirror of getMirrors()) {
6768
+ if (out.has(mirror.sessionId)) continue;
6769
+ out.set(mirror.sessionId, {
6770
+ sessionId: mirror.sessionId,
6771
+ kind: "mirror",
6772
+ flavor: "terminal-mirror",
6773
+ ...mirror.cwd ? { cwd: mirror.cwd } : {},
6774
+ ...mirror.title ? { title: mirror.title } : {},
6775
+ edits: tracker.editsOf(mirror.sessionId, at)
6776
+ });
6777
+ }
6778
+ return out;
6779
+ };
6780
+ const deliver = async (recipient, peer, peerId, paths, peerEditedAgoMs, noticeId) => {
6781
+ if (recipient.kind === "mirror") return;
6782
+ const persisted = readPersisted()[recipient.sessionId];
6783
+ if (!persisted) {
6784
+ logger.debug(`[PEERS] no key for ${recipient.sessionId}; conflict notice ${noticeId} not delivered`);
6785
+ return;
6786
+ }
6787
+ const peerSender = senderOf(peer, peerId);
6788
+ if (!peerSender.title && peer?.kind === "managed") {
6789
+ const peerKey = readPersisted()[peer.sessionId];
6790
+ if (peerKey) {
6791
+ try {
6792
+ peerSender.title = await liveTitle(peer.sessionId, peerKey);
6793
+ } catch {
6794
+ }
6795
+ }
6796
+ }
6797
+ const text = formatEditConflictNotice({ id: noticeId, path: paths[0], paths, peer: peerSender, peerEditedAgoMs, windowMs });
6798
+ try {
6799
+ await send(recipient.sessionId, persisted, text, "edit-conflict", {
6800
+ sentFrom: SESSION_PEER_SENT_FROM,
6801
+ delivery: "steer",
6802
+ localId: `edit-conflict-${noticeId}-${recipient.sessionId}`
6803
+ });
6804
+ logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} (${paths.length} file(s), first ${paths[0]}, peer ${peerId})`);
6805
+ } catch (error) {
6806
+ logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} failed:`, error);
6807
+ }
6808
+ };
6809
+ const flushPair = async (key) => {
6810
+ const pair = pending.get(key);
6811
+ if (!pair) return;
6812
+ pending.delete(key);
6813
+ const at = now();
6814
+ const sent = (noticesSent.get(key) ?? []).filter((t) => at - t <= windowMs);
6815
+ if (sent.length >= maxNoticesPerPair) {
6816
+ noticesSent.set(key, sent);
6817
+ logger.debug(`[PEERS] pair ${pair.a}/${pair.b}: ${maxNoticesPerPair} notices already sent this window; ${pair.paths.size} conflicting path(s) not announced`);
6818
+ return;
6819
+ }
6820
+ sent.push(at);
6821
+ noticesSent.set(key, sent);
6822
+ const sessions = snapshot();
6823
+ const a = sessions.get(pair.a);
6824
+ const b = sessions.get(pair.b);
6825
+ const paths = [...pair.paths.keys()];
6826
+ const noticeId = randomUUID().slice(0, 8);
6827
+ const latestBy = (sessionId) => Math.max(...[...pair.paths.values()].map((c) => c.editor.sessionId === sessionId ? c.editor.at : c.peer.at));
6828
+ const agoFor = (recipientId, otherId) => Math.max(0, latestBy(recipientId) - latestBy(otherId));
6829
+ if (a) await deliver(a, b, pair.b, paths, agoFor(pair.a, pair.b), noticeId);
6830
+ if (b) await deliver(b, a, pair.a, paths, agoFor(pair.b, pair.a), noticeId);
6831
+ };
6832
+ const enqueue = (conflict) => {
6833
+ const key = pairKey(conflict.editor.sessionId, conflict.peer.sessionId);
6834
+ let pair = pending.get(key);
6835
+ if (!pair) {
6836
+ pair = { a: conflict.editor.sessionId, b: conflict.peer.sessionId, paths: /* @__PURE__ */ new Map(), timer: null };
6837
+ pair.timer = schedule(() => {
6838
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6839
+ });
6840
+ }, coalesceMs);
6841
+ pending.set(key, pair);
6842
+ }
6843
+ pair.paths.set(conflict.editor.path, conflict);
6844
+ };
6845
+ return {
6846
+ list: () => [...snapshot().values()],
6847
+ onEdit(edit) {
6848
+ const sessions = snapshot();
6849
+ const self = sessions.get(edit.sessionId);
6850
+ if (!self) {
6851
+ logger.debug(`[PEERS] edit from unknown/dead session ${edit.sessionId} ignored`);
6852
+ return [];
6853
+ }
6854
+ const current = now();
6855
+ const at = typeof edit.at === "number" && Number.isFinite(edit.at) ? Math.min(edit.at, current) : current;
6856
+ if (current - at > windowMs) {
6857
+ logger.debug(`[PEERS] edit from ${edit.sessionId} on ${edit.path} is ${Math.round((current - at) / 6e4)}m old (replayed history); ignored`);
6858
+ return [];
6859
+ }
6860
+ const cwd = edit.cwd ?? self.cwd ?? process.cwd();
6861
+ const path = normalize(edit.path, cwd);
6862
+ const conflicts = tracker.record({ sessionId: edit.sessionId, path, tool: edit.tool, at }, (id) => sessions.has(id));
6863
+ for (const conflict of conflicts) enqueue(conflict);
6864
+ return conflicts;
6865
+ },
6866
+ async flush() {
6867
+ for (const key of [...pending.keys()]) {
6868
+ const pair = pending.get(key);
6869
+ if (pair?.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6870
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6871
+ });
6872
+ }
6873
+ await inflight;
6874
+ },
6875
+ forget(sessionId) {
6876
+ tracker.forget(sessionId);
6877
+ for (const [key, pair] of pending) {
6878
+ if (pair.a === sessionId || pair.b === sessionId) {
6879
+ if (pair.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6880
+ pending.delete(key);
6881
+ }
6882
+ }
6883
+ },
6884
+ setMirrorSource(source) {
6885
+ getMirrors = source;
6886
+ }
6887
+ };
6888
+ }
6889
+
6632
6890
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6633
6891
 
6634
6892
  \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
@@ -8548,6 +8806,73 @@ class ClaudeAuthService {
8548
8806
  }
8549
8807
  }
8550
8808
 
8809
+ const CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
8810
+ function pathOf(input) {
8811
+ if (!input || typeof input !== "object") return null;
8812
+ const record = input;
8813
+ const candidate = record.file_path ?? record.notebook_path;
8814
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
8815
+ }
8816
+ function extractClaudeEditPaths(body) {
8817
+ if (!body || typeof body !== "object") return [];
8818
+ const line = body;
8819
+ if (line.type !== "assistant") return [];
8820
+ const content = line.message?.content;
8821
+ if (!Array.isArray(content)) return [];
8822
+ const stamp = line.timestamp;
8823
+ const at = typeof stamp === "string" ? Date.parse(stamp) : typeof stamp === "number" ? stamp : NaN;
8824
+ const edits = [];
8825
+ for (const block of content) {
8826
+ if (!block || typeof block !== "object") continue;
8827
+ const b = block;
8828
+ if (b.type !== "tool_use" || typeof b.name !== "string" || !CLAUDE_EDIT_TOOLS.has(b.name)) continue;
8829
+ const path = pathOf(b.input);
8830
+ if (path) edits.push({ path, tool: b.name, ...Number.isFinite(at) ? { at } : {} });
8831
+ }
8832
+ return edits;
8833
+ }
8834
+ function extractCodexPatchPaths(changes) {
8835
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
8836
+ return Object.keys(changes).filter((path) => path.trim() !== "").map((path) => ({ path, tool: "CodexPatch" }));
8837
+ }
8838
+ function extractAcpEditPaths(toolKind, args) {
8839
+ if (!args || typeof args !== "object") return [];
8840
+ const a = args;
8841
+ if (a.piTool === "write" || a.piTool === "edit") {
8842
+ const raw = a.rawInput;
8843
+ const path = raw && typeof raw.path === "string" && raw.path.trim() !== "" ? raw.path : null;
8844
+ return path ? [{ path, tool: a.piTool }] : [];
8845
+ }
8846
+ if (typeof a.piTool === "string") return [];
8847
+ if (toolKind !== "edit" || !Array.isArray(a.locations)) return [];
8848
+ const edits = [];
8849
+ for (const location of a.locations) {
8850
+ const path = location && typeof location === "object" ? location.path : void 0;
8851
+ if (typeof path === "string" && path.trim() !== "" && !edits.some((e) => e.path === path)) edits.push({ path, tool: "edit" });
8852
+ }
8853
+ return edits;
8854
+ }
8855
+ class EditReportThrottle {
8856
+ constructor(intervalMs = 5e3) {
8857
+ this.intervalMs = intervalMs;
8858
+ }
8859
+ lastReportedAt = /* @__PURE__ */ new Map();
8860
+ /** Returns the edits that should be reported now and remembers them. */
8861
+ take(edits, now = Date.now()) {
8862
+ const due = [];
8863
+ for (const edit of edits) {
8864
+ const last = this.lastReportedAt.get(edit.path);
8865
+ if (last !== void 0 && now - last < this.intervalMs) continue;
8866
+ this.lastReportedAt.set(edit.path, now);
8867
+ due.push(edit);
8868
+ }
8869
+ if (this.lastReportedAt.size > 512) {
8870
+ for (const [path, at] of this.lastReportedAt) if (now - at > this.intervalMs * 10) this.lastReportedAt.delete(path);
8871
+ }
8872
+ return due;
8873
+ }
8874
+ }
8875
+
8551
8876
  const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
8552
8877
  function parseLines(lines) {
8553
8878
  const out = [];
@@ -8584,12 +8909,15 @@ function createMirrorScanner(opts) {
8584
8909
  }
8585
8910
  tail.offset = 0;
8586
8911
  }
8912
+ let replaced = false;
8587
8913
  if (tail.offset > size) {
8588
8914
  logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
8589
8915
  tail.offset = 0;
8916
+ replaced = true;
8590
8917
  }
8591
8918
  if (tail.offset === size) return;
8592
8919
  const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
8920
+ const replay = isFirstBackfillRead || replaced;
8593
8921
  const handle = await open$1(tail.path, "r");
8594
8922
  let buf;
8595
8923
  try {
@@ -8609,7 +8937,7 @@ function createMirrorScanner(opts) {
8609
8937
  if (decision.truncated) opts.events.onBackfillTruncated?.();
8610
8938
  tail.mode = "from-eof";
8611
8939
  }
8612
- if (messages.length > 0) opts.events.onMessages(messages);
8940
+ if (messages.length > 0) opts.events.onMessages(messages, { replay });
8613
8941
  };
8614
8942
  const sync = new InvalidateSync(async () => {
8615
8943
  for (const tail of files.values()) {
@@ -8732,8 +9060,17 @@ function createMirrorManager(deps) {
8732
9060
  binding.client.updateMetadata((current) => ({ ...current, ...patch }));
8733
9061
  persistBinding(binding);
8734
9062
  };
8735
- const sendMessages = (binding, messages) => {
9063
+ const sendMessages = (binding, messages, context = { replay: false }) => {
8736
9064
  for (const message of messages) {
9065
+ if (deps.onEdit && binding.status === "active" && !context.replay) {
9066
+ for (const edit of extractClaudeEditPaths(message)) {
9067
+ try {
9068
+ deps.onEdit({ sessionId: binding.happySessionId, path: edit.path, tool: edit.tool, cwd: binding.metadata.path, ...edit.at !== void 0 ? { at: edit.at } : {} });
9069
+ } catch (error) {
9070
+ logger.debug(`[MIRROR] edit tap failed for terminal ${binding.terminalId}:`, error);
9071
+ }
9072
+ }
9073
+ }
8737
9074
  const key = mirrorLineKey(message);
8738
9075
  binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => mirrorLocalId(key, envelopeIndex) } : void 0);
8739
9076
  }
@@ -8742,7 +9079,7 @@ function createMirrorManager(deps) {
8742
9079
  const scanner = createMirrorScanner({
8743
9080
  backfillLines,
8744
9081
  events: {
8745
- onMessages: (messages) => sendMessages(binding, messages),
9082
+ onMessages: (messages, context) => sendMessages(binding, messages, context),
8746
9083
  onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
8747
9084
  type: "message",
8748
9085
  message: `\u53EA\u8BFB\u955C\u50CF\u53EA\u56DE\u704C\u4E86\u6700\u8FD1 ${backfillLines} \u6761\uFF0C\u66F4\u65E9\u5185\u5BB9\u8BF7\u5728\u7EC8\u7AEF\u91CC\u56DE\u770B`
@@ -8765,6 +9102,10 @@ function createMirrorManager(deps) {
8765
9102
  await binding.scanner?.cleanup();
8766
9103
  binding.scanner = null;
8767
9104
  binding.client.closeClaudeSessionTurn("completed");
9105
+ try {
9106
+ deps.onSessionEnded?.(binding.happySessionId);
9107
+ } catch {
9108
+ }
8768
9109
  updateBindingMetadata(binding, {
8769
9110
  lifecycleState: "archived",
8770
9111
  lifecycleStateSince: Date.now(),
@@ -8772,9 +9113,9 @@ function createMirrorManager(deps) {
8772
9113
  archiveReason: reason
8773
9114
  });
8774
9115
  try {
8775
- await deps.api.deactivateSession(binding.happySessionId);
9116
+ await deps.api.archiveSession(binding.happySessionId);
8776
9117
  } catch (error) {
8777
- logger.debug(`[MIRROR] deactivateSession failed for ${binding.happySessionId}:`, error);
9118
+ logger.debug(`[MIRROR] archiveSession failed for ${binding.happySessionId}:`, error);
8778
9119
  }
8779
9120
  deps.onBindingsChanged?.();
8780
9121
  };
@@ -8977,6 +9318,13 @@ function createMirrorManager(deps) {
8977
9318
  }
8978
9319
  };
8979
9320
  return {
9321
+ listActiveMirrors() {
9322
+ return [...bindings.values()].filter((binding) => binding.status === "active").map((binding) => ({
9323
+ sessionId: binding.happySessionId,
9324
+ ...binding.metadata.path ? { cwd: binding.metadata.path } : {},
9325
+ ...binding.metadata.summary?.text ? { title: binding.metadata.summary.text } : {}
9326
+ }));
9327
+ },
8980
9328
  handleHookPayload(body) {
8981
9329
  const event = parseTerminalHookPayload(body);
8982
9330
  if (!event) {
@@ -9443,683 +9791,6 @@ Delegate from this working directory unless a different repository was explicitl
9443
9791
  ${goal}`;
9444
9792
  }
9445
9793
 
9446
- const MAX_LINE_CHARS = 500;
9447
- const MAX_ARGS_CHARS = 160;
9448
- const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
9449
- const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
9450
- function truncateText(text, max = MAX_LINE_CHARS) {
9451
- const chars = Array.from(text);
9452
- if (chars.length <= max) return text;
9453
- return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
9454
- }
9455
- function squash(text) {
9456
- return text.replace(/\s+/g, " ").trim();
9457
- }
9458
- function safeJson(value, max) {
9459
- try {
9460
- return truncateText(JSON.stringify(value) ?? "", max);
9461
- } catch {
9462
- return "[unserializable]";
9463
- }
9464
- }
9465
- function formatMessageBody(body, limits = DEFAULT_LIMITS) {
9466
- if (!body || typeof body !== "object") return null;
9467
- const b = body;
9468
- if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
9469
- const text = squash(b.content.text);
9470
- return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
9471
- }
9472
- if (b.role === "session" && b.content && typeof b.content === "object") {
9473
- const env = b.content;
9474
- const ev = env.ev;
9475
- if (!ev || typeof ev.t !== "string") return null;
9476
- switch (ev.t) {
9477
- case "text": {
9478
- if (ev.thinking) return null;
9479
- const text = squash(String(ev.text ?? ""));
9480
- if (text.length === 0) return null;
9481
- const role = env.role === "user" ? "user" : "assistant";
9482
- return `${role}: ${truncateText(text, limits.line)}`;
9483
- }
9484
- case "tool-call-start": {
9485
- const name = String(ev.name ?? "tool");
9486
- const title = squash(String(ev.title ?? ""));
9487
- const args = ev.args ? safeJson(ev.args, limits.args) : "";
9488
- const detail = title || args;
9489
- return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
9490
- }
9491
- case "service": {
9492
- const text = squash(String(ev.text ?? ""));
9493
- return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
9494
- }
9495
- case "turn-end": {
9496
- if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
9497
- return null;
9498
- }
9499
- default:
9500
- return null;
9501
- }
9502
- }
9503
- if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
9504
- const d = b.content.data;
9505
- switch (d.type) {
9506
- case "message": {
9507
- const text = squash(String(d.message ?? ""));
9508
- return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
9509
- }
9510
- case "tool-call":
9511
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
9512
- case "permission-request":
9513
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
9514
- default:
9515
- return null;
9516
- }
9517
- }
9518
- if (b.role === "agent" && b.content?.type === "output") {
9519
- const msg = b.content.data?.message;
9520
- if (msg && Array.isArray(msg.content)) {
9521
- const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
9522
- if (texts.length > 0) {
9523
- const role = msg.role === "user" ? "user" : "assistant";
9524
- return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
9525
- }
9526
- }
9527
- return null;
9528
- }
9529
- return null;
9530
- }
9531
- function formatTranscript(bodies, options = {}) {
9532
- const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
9533
- const lines = [];
9534
- for (const body of bodies) {
9535
- const line = formatMessageBody(body, limits);
9536
- if (line !== null) lines.push(line);
9537
- }
9538
- return lines.join("\n");
9539
- }
9540
-
9541
- function asRecord(value) {
9542
- return value && typeof value === "object" ? value : null;
9543
- }
9544
- function sessionEnvelope(body) {
9545
- const b = asRecord(body);
9546
- if (!b || b.role !== "session") return null;
9547
- const env = asRecord(b.content);
9548
- if (!env) return null;
9549
- const ev = asRecord(env.ev);
9550
- return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
9551
- }
9552
- function isUserPrompt(body) {
9553
- const b = asRecord(body);
9554
- return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
9555
- }
9556
- function agentPiece(body) {
9557
- const env = sessionEnvelope(body);
9558
- if (env && env.role !== "user") {
9559
- if (env.ev.t === "text" && !env.ev.thinking) {
9560
- const text = String(env.ev.text ?? "");
9561
- return text.trim().length > 0 ? { kind: "text", text } : null;
9562
- }
9563
- if (env.ev.t === "tool-call-start") return { kind: "tool" };
9564
- return null;
9565
- }
9566
- const b = asRecord(body);
9567
- if (b?.role === "agent" && b.content?.type === "acp") {
9568
- const d = asRecord(b.content.data);
9569
- if (d?.type === "message") {
9570
- const text = String(d.message ?? "");
9571
- return text.trim().length > 0 ? { kind: "text", text } : null;
9572
- }
9573
- if (d?.type === "tool-call") return { kind: "tool" };
9574
- }
9575
- return null;
9576
- }
9577
- function userPromptText(body) {
9578
- return isUserPrompt(body) ? body.content.text : null;
9579
- }
9580
- function analyzeLatestTurn(entries, options = {}) {
9581
- const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
9582
- let userIndex = -1;
9583
- for (let i = entries.length - 1; i >= 0; i--) {
9584
- if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
9585
- userIndex = i;
9586
- break;
9587
- }
9588
- }
9589
- let ended = false;
9590
- let status = null;
9591
- let error = null;
9592
- let texts = [];
9593
- let lastMarker = null;
9594
- let startedAfterPrompt = false;
9595
- const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
9596
- for (let i = userIndex + 1; i < entries.length; i++) {
9597
- const body = entries[i].body;
9598
- if (options.anchor && isUserPrompt(body)) {
9599
- if (ended) break;
9600
- continue;
9601
- }
9602
- const env = sessionEnvelope(body);
9603
- if (env?.ev?.t === "turn-start") {
9604
- lastMarker = "start";
9605
- startedAfterPrompt = true;
9606
- ended = false;
9607
- status = null;
9608
- error = null;
9609
- texts = [];
9610
- continue;
9611
- }
9612
- if (env?.ev?.t === "turn-end") {
9613
- lastMarker = "end";
9614
- if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
9615
- ended = true;
9616
- status = typeof env.ev.status === "string" ? env.ev.status : null;
9617
- error = typeof env.ev.error === "string" ? env.ev.error : null;
9618
- continue;
9619
- }
9620
- const piece = agentPiece(body);
9621
- if (!piece) continue;
9622
- if (piece.kind === "tool") texts = [];
9623
- else texts.push(piece.text);
9624
- }
9625
- if (userIndex < 0) ended = lastMarker === "end";
9626
- return {
9627
- userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
9628
- ended,
9629
- status: ended ? status : null,
9630
- error: ended ? error : null,
9631
- answer: texts.join("\n\n").trim(),
9632
- lastSeq
9633
- };
9634
- }
9635
-
9636
- const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9637
- async function openUserScopedSocket(token, options = {}) {
9638
- const socket = io(configuration.serverUrl, {
9639
- auth: {
9640
- token,
9641
- clientType: "user-scoped",
9642
- happyClient: `cli-session-ops/${configuration.currentCliVersion}`
9643
- },
9644
- path: "/v1/updates",
9645
- transports: ["websocket"],
9646
- reconnection: false,
9647
- autoConnect: false
9648
- });
9649
- await new Promise((resolve, reject) => {
9650
- const timer = setTimeout(() => {
9651
- socket.close();
9652
- reject(new Error(`Timed out connecting to ${configuration.serverUrl}`));
9653
- }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9654
- socket.once("connect", () => {
9655
- clearTimeout(timer);
9656
- resolve();
9657
- });
9658
- socket.once("connect_error", (error) => {
9659
- clearTimeout(timer);
9660
- socket.close();
9661
- reject(new Error(`Could not connect to ${configuration.serverUrl}: ${error.message}`));
9662
- });
9663
- socket.connect();
9664
- });
9665
- return {
9666
- async rpcCall(payload, timeoutMs) {
9667
- const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9668
- return ack;
9669
- },
9670
- close() {
9671
- socket.close();
9672
- }
9673
- };
9674
- }
9675
-
9676
- const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
9677
- const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
9678
- const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
9679
- const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
9680
- const RPC_RATE_MAX_RETRIES = 2;
9681
- function isRpcRateLimitAck(ack) {
9682
- if (!ack || typeof ack !== "object") return false;
9683
- const a = ack;
9684
- if (a.ok !== false) return false;
9685
- if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
9686
- return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
9687
- }
9688
- function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
9689
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
9690
- return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
9691
- }
9692
- const exponent = Math.max(0, Math.min(attempt, 32) - 1);
9693
- const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
9694
- return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
9695
- }
9696
-
9697
- const PERMISSION_OPS_CLIENT = "session-ops";
9698
- const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9699
- const SETTLE_TIMEOUT_MS = 5e3;
9700
- const SETTLE_POLL_MS = 500;
9701
- function buildPermissionRpcPayload(requestId, verdict) {
9702
- if (verdict.kind === "approve") {
9703
- return {
9704
- id: requestId,
9705
- approved: true,
9706
- decision: verdict.forSession ? "approved_for_session" : "approved"
9707
- };
9708
- }
9709
- return {
9710
- id: requestId,
9711
- approved: false,
9712
- decision: "denied",
9713
- ...verdict.reason ? { reason: verdict.reason } : {}
9714
- };
9715
- }
9716
- function interpretPermissionAck(ack, decryptResult) {
9717
- if (!ack.ok) {
9718
- const message = ack.error ?? "RPC call failed";
9719
- if (message === "RPC method not available") {
9720
- return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9721
- }
9722
- if (message === "RPC target disconnected") {
9723
- return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9724
- }
9725
- if (/timed? ?out/i.test(message)) {
9726
- return { status: "timeout", message };
9727
- }
9728
- if (isRpcRateLimitAck(ack)) {
9729
- return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
9730
- }
9731
- return { status: "rejected", message };
9732
- }
9733
- let body = null;
9734
- if (typeof ack.result === "string" && ack.result.length > 0) {
9735
- try {
9736
- body = decryptResult(ack.result);
9737
- } catch {
9738
- body = null;
9739
- }
9740
- }
9741
- if (body && typeof body === "object" && typeof body.error === "string") {
9742
- return { status: "handler-error", message: body.error };
9743
- }
9744
- return { status: "acknowledged" };
9745
- }
9746
- function pendingRequestsOf(agentState, now) {
9747
- const requests = agentState?.requests;
9748
- if (!requests || typeof requests !== "object") return [];
9749
- return Object.entries(requests).map(([id, request]) => ({
9750
- id,
9751
- tool: typeof request?.tool === "string" ? request.tool : "unknown",
9752
- ...request?.kind ? { kind: request.kind } : {},
9753
- ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
9754
- })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
9755
- }
9756
- const RPC_DEADLINE_GRACE_MS = 2e3;
9757
- function withDeadline(promise, ms, message) {
9758
- return new Promise((resolve, reject) => {
9759
- const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
9760
- promise.then(
9761
- (value) => {
9762
- clearTimeout(timer);
9763
- resolve(value);
9764
- },
9765
- (error) => {
9766
- clearTimeout(timer);
9767
- reject(error);
9768
- }
9769
- );
9770
- });
9771
- }
9772
- async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
9773
- const now = deps.now ?? Date.now;
9774
- const persisted = (deps.readPersisted ?? readPersistedSessions)()[sessionId];
9775
- if (!persisted) {
9776
- throw new Error(
9777
- `No local key for session ${sessionId} \u2014 approve/deny needs the session key from ~/.happy/sessions.json (the RPC payload is encrypted with it), so only sessions this machine's daemon spawned within 14 days can be answered from here.`
9778
- );
9779
- }
9780
- const token = await (deps.bearerToken ?? bearerToken$1)();
9781
- const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
9782
- const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9783
- if (!before.some((request) => request.id === requestId)) {
9784
- const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
9785
- throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
9786
- }
9787
- const payload = buildPermissionRpcPayload(requestId, verdict);
9788
- const key = decodeBase64(persisted.encryptionKey);
9789
- const params = encodeBase64(encrypt(key, persisted.encryptionVariant, payload));
9790
- const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
9791
- const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
9792
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9793
- let ack;
9794
- try {
9795
- for (let attempt = 1; ; attempt++) {
9796
- try {
9797
- ack = await withDeadline(
9798
- transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
9799
- rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
9800
- `permission RPC did not settle within ${rpcTimeoutMs}ms`
9801
- );
9802
- } catch (error) {
9803
- ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
9804
- }
9805
- if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
9806
- const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
9807
- deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
9808
- await sleep(waitMs);
9809
- }
9810
- } finally {
9811
- transport.close();
9812
- }
9813
- const outcome = interpretPermissionAck(ack, (encrypted) => decrypt(key, persisted.encryptionVariant, decodeBase64(encrypted)));
9814
- if (outcome.status !== "acknowledged") {
9815
- return { sessionId, requestId, payload, outcome };
9816
- }
9817
- const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
9818
- let settled = false;
9819
- for (; ; ) {
9820
- const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9821
- if (!pending.some((request) => request.id === requestId)) {
9822
- settled = true;
9823
- break;
9824
- }
9825
- if (now() >= deadline) break;
9826
- await sleep(SETTLE_POLL_MS);
9827
- }
9828
- return { sessionId, requestId, payload, outcome, settled };
9829
- }
9830
- async function bearerToken$1() {
9831
- const credentials = await readCredentialsForConfiguredRelay();
9832
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9833
- return credentials.token;
9834
- }
9835
- async function fetchDecryptedAgentState(sessionId, persisted, token) {
9836
- const response = await axios.get(
9837
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
9838
- {
9839
- headers: {
9840
- "Authorization": `Bearer ${token}`,
9841
- "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9842
- },
9843
- timeout: 15e3,
9844
- // 404 is a real answer ("not yours / gone"), not a transport fault.
9845
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
9846
- }
9847
- );
9848
- if (response.status === 404) {
9849
- throw new Error(`Session ${sessionId} was not found on this account (it may have been deleted, or the local key belongs to a session on another relay).`);
9850
- }
9851
- const raw = response.data?.session?.agentState;
9852
- if (typeof raw !== "string" || raw.length === 0) return null;
9853
- return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(raw));
9854
- }
9855
-
9856
- const SESSION_OPS_CLIENT = "session-ops";
9857
- const DEFAULT_RECENT_LIMIT = 15;
9858
- const MAX_READ_LIMIT = 100;
9859
- function mergeSessionSummaries(live, persisted, options = {}) {
9860
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9861
- const seen = /* @__PURE__ */ new Set();
9862
- const summaries = [];
9863
- for (const child of live) {
9864
- const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9865
- if (!id || seen.has(id)) continue;
9866
- seen.add(id);
9867
- summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9868
- }
9869
- const rest = Object.entries(persisted).filter(([id]) => !seen.has(id)).filter(([, entry]) => entry.metadata?.flavor !== "terminal-mirror").sort((a, b) => b[1].savedAt - a[1].savedAt).slice(0, recentLimit);
9870
- for (const [id, entry] of rest) {
9871
- summaries.push(toSummary(id, entry, { live: false }));
9872
- }
9873
- if (!options.tag) return summaries;
9874
- const wanted = options.tag;
9875
- return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9876
- }
9877
- function livenessOf(child) {
9878
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
9879
- }
9880
- function sessionLiveness(live, sessionId) {
9881
- const child = live.find((entry) => entry.happySessionId === sessionId);
9882
- return child ? livenessOf(child) : { live: false };
9883
- }
9884
- function toSummary(id, persisted, extra) {
9885
- const meta = persisted?.metadata;
9886
- return {
9887
- id,
9888
- live: extra.live,
9889
- ...extra.pid !== void 0 ? { pid: extra.pid } : {},
9890
- ...meta?.summary?.text ? { title: meta.summary.text } : {},
9891
- ...meta?.path ? { cwd: meta.path } : {},
9892
- ...meta?.flavor ? { flavor: meta.flavor } : {},
9893
- ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
9894
- ...meta?.variant ? { variant: meta.variant } : {},
9895
- url: sessionWebUrl(id),
9896
- ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
9897
- };
9898
- }
9899
- async function listSessions(options = {}) {
9900
- const live = await listDaemonSessions();
9901
- return mergeSessionSummaries(live, readPersistedSessions(), options);
9902
- }
9903
- function summarizeAccountSession(row, persisted, liveIds, now) {
9904
- const base = {
9905
- id: row.id,
9906
- live: liveIds.has(row.id),
9907
- url: sessionWebUrl(row.id),
9908
- decryptable: false,
9909
- active: row.active === true,
9910
- archived: typeof row.archivedAt === "number",
9911
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
9912
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
9913
- attention: false
9914
- };
9915
- if (!persisted) return base;
9916
- const key = decodeBase64(persisted.encryptionKey);
9917
- let metadata = null;
9918
- let agentState = null;
9919
- try {
9920
- metadata = row.metadata ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.metadata)) : null;
9921
- agentState = row.agentState ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.agentState)) : null;
9922
- } catch {
9923
- return base;
9924
- }
9925
- if (!metadata) return base;
9926
- const meta = metadata;
9927
- const pending = pendingRequestsOf(agentState, now);
9928
- return {
9929
- ...base,
9930
- decryptable: true,
9931
- ...meta.summary?.text ? { title: meta.summary.text } : {},
9932
- ...meta.path ? { cwd: meta.path } : {},
9933
- ...meta.flavor ? { flavor: meta.flavor } : {},
9934
- ...meta.tags?.length ? { tags: [...meta.tags] } : {},
9935
- ...meta.variant ? { variant: meta.variant } : {},
9936
- ...meta.machineId ? { machineId: meta.machineId } : {},
9937
- ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
9938
- pending,
9939
- attention: pending.length > 0
9940
- };
9941
- }
9942
- function orderAccountSessions(summaries, options = {}) {
9943
- const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
9944
- const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
9945
- let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
9946
- const byRank = rank(a) - rank(b);
9947
- if (byRank !== 0) return byRank;
9948
- if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
9949
- return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
9950
- });
9951
- if (options.tag) {
9952
- const wanted = options.tag;
9953
- rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
9954
- }
9955
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9956
- let idleKept = 0;
9957
- return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
9958
- }
9959
- async function listAccountSessions(options = {}) {
9960
- const token = await bearerToken();
9961
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
9962
- headers: {
9963
- "Authorization": `Bearer ${token}`,
9964
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9965
- },
9966
- timeout: 15e3
9967
- });
9968
- const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
9969
- const persisted = readPersistedSessions();
9970
- const live = await listDaemonSessions();
9971
- const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
9972
- const now = Date.now();
9973
- const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
9974
- return orderAccountSessions(summaries, options);
9975
- }
9976
- async function bearerToken() {
9977
- const credentials = await readCredentialsForConfiguredRelay();
9978
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9979
- return credentials.token;
9980
- }
9981
- function requirePersisted(sessionId) {
9982
- const persisted = readPersistedSessions()[sessionId];
9983
- if (!persisted) {
9984
- throw new Error(
9985
- `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
9986
- );
9987
- }
9988
- return persisted;
9989
- }
9990
- async function readSessionLog(sessionId, persisted, limit) {
9991
- const token = await bearerToken();
9992
- const response = await axios.get(
9993
- `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
9994
- {
9995
- params: { before_seq: 2147483647, limit },
9996
- headers: {
9997
- "Authorization": `Bearer ${token}`,
9998
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9999
- },
10000
- timeout: 15e3
10001
- }
10002
- );
10003
- const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
10004
- messages.reverse();
10005
- const key = decodeBase64(persisted.encryptionKey);
10006
- return messages.map((message) => {
10007
- if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
10008
- try {
10009
- return { seq: message.seq, body: decrypt(key, persisted.encryptionVariant, decodeBase64(message.content.c)) };
10010
- } catch {
10011
- return { seq: message.seq, body: null };
10012
- }
10013
- });
10014
- }
10015
- const TURN_WINDOW = 500;
10016
- async function readSessionTranscript(sessionId, limit, options = {}) {
10017
- const persisted = requirePersisted(sessionId);
10018
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
10019
- const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
10020
- const shown = entries.slice(-bounded);
10021
- const live = await listDaemonSessions();
10022
- return {
10023
- summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
10024
- messageCount: shown.length,
10025
- transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
10026
- turn: analyzeLatestTurn(entries)
10027
- };
10028
- }
10029
- async function readSessionMetadata(sessionId, persisted) {
10030
- const token = await bearerToken();
10031
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10032
- headers: {
10033
- "Authorization": `Bearer ${token}`,
10034
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10035
- },
10036
- timeout: 15e3
10037
- });
10038
- const ciphertext = response.data?.session?.metadata;
10039
- if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
10040
- return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(ciphertext));
10041
- }
10042
- async function readSessionState(sessionId, persisted) {
10043
- const token = await bearerToken();
10044
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10045
- headers: {
10046
- "Authorization": `Bearer ${token}`,
10047
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10048
- },
10049
- timeout: 15e3,
10050
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
10051
- });
10052
- if (response.status === 404) return { found: false, active: false };
10053
- const row = response.data?.session;
10054
- if (!row || typeof row.id !== "string") throw new Error(`Server returned no session row for ${sessionId}`);
10055
- const state = {
10056
- found: true,
10057
- active: row.active === true,
10058
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
10059
- archivedAt: typeof row.archivedAt === "number" ? row.archivedAt : null,
10060
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {}
10061
- };
10062
- if (persisted && typeof row.metadata === "string" && row.metadata.length > 0) {
10063
- try {
10064
- const metadata = decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(row.metadata));
10065
- if (metadata?.machineId) state.machineId = metadata.machineId;
10066
- } catch {
10067
- }
10068
- }
10069
- return state;
10070
- }
10071
- class TurnWaitTimeoutError extends Error {
10072
- constructor(sessionId, timeoutMs, turn) {
10073
- super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
10074
- this.sessionId = sessionId;
10075
- this.timeoutMs = timeoutMs;
10076
- this.turn = turn;
10077
- }
10078
- }
10079
- async function waitForTurnEnd(sessionId, options) {
10080
- const persisted = requirePersisted(sessionId);
10081
- const pollMs = options.pollMs ?? 3e3;
10082
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10083
- const deadline = Date.now() + options.timeoutMs;
10084
- while (true) {
10085
- const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
10086
- if (turn.ended) return turn;
10087
- if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
10088
- await sleep(pollMs);
10089
- }
10090
- }
10091
- async function stopSession(sessionId) {
10092
- return await stopDaemonSession(sessionId);
10093
- }
10094
- async function archiveSession(sessionId) {
10095
- const token = await bearerToken();
10096
- await axios.post(
10097
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
10098
- {},
10099
- {
10100
- headers: {
10101
- "Authorization": `Bearer ${token}`,
10102
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10103
- },
10104
- timeout: 1e4
10105
- }
10106
- );
10107
- }
10108
- async function unarchiveSession(sessionId) {
10109
- const token = await bearerToken();
10110
- await axios.post(
10111
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
10112
- {},
10113
- {
10114
- headers: {
10115
- "Authorization": `Bearer ${token}`,
10116
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10117
- },
10118
- timeout: 1e4
10119
- }
10120
- );
10121
- }
10122
-
10123
9794
  function receiptPath(home, operationId) {
10124
9795
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
10125
9796
  return join(home, "teams", "effects", `${operationId}.json`);
@@ -10709,7 +10380,7 @@ function automationRunAnchor(runId) {
10709
10380
  async function defaultReadTurn(sessionId, runId) {
10710
10381
  const persisted = readPersistedSessions()[sessionId];
10711
10382
  if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10712
- return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10383
+ return analyzeLatestTurn(await readSessionLog$1(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10713
10384
  }
10714
10385
  async function defaultSend(sessionId, text, meta) {
10715
10386
  const persisted = await waitForSessionKey(sessionId, 15e3);
@@ -11053,6 +10724,16 @@ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
11053
10724
  function daemonExitCode(source) {
11054
10725
  return source === "exception" ? 1 : 0;
11055
10726
  }
10727
+ const HANDOVER_EXIT_CODE = 75;
10728
+
10729
+ function detectDaemonSupervisor(env, platform = process.platform) {
10730
+ const explicit = env.HAPPY_DAEMON_SUPERVISOR?.trim().toLowerCase();
10731
+ if (explicit === "systemd") return "systemd";
10732
+ if (explicit === "none") return "none";
10733
+ if (platform === "linux" && typeof env.INVOCATION_ID === "string" && env.INVOCATION_ID.trim() !== "") return "systemd";
10734
+ return "none";
10735
+ }
10736
+ const SYSTEMD_RESTART_HINT = "systemctl --user restart very-happy-daemon";
11056
10737
 
11057
10738
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11058
10739
  function sanitizeImportTitle(value) {
@@ -11208,6 +10889,7 @@ async function startDaemon() {
11208
10889
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
11209
10890
  const pidToTrackedSession = /* @__PURE__ */ new Map();
11210
10891
  const turnActivity = new TurnActivityTracker();
10892
+ const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
11211
10893
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
11212
10894
  let teamWorker;
11213
10895
  let automationRunner;
@@ -12067,6 +11749,7 @@ async function startDaemon() {
12067
11749
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
12068
11750
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
12069
11751
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11752
+ if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
12070
11753
  if (session?.happySessionId && session.encryption) {
12071
11754
  sessionIdToFinishedSession.set(session.happySessionId, session);
12072
11755
  logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
@@ -12152,6 +11835,13 @@ async function startDaemon() {
12152
11835
  onSessionStateEvent,
12153
11836
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
12154
11837
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
11838
+ onSessionEdit: (edit) => {
11839
+ peerCoordinator.onEdit(edit);
11840
+ },
11841
+ listPeers: () => peerCoordinator.list(),
11842
+ // B-505: `daemon list` shows which sessions have a turn in flight, so an
11843
+ // operator can check the WHOLE machine before a supervised restart.
11844
+ isTurnActive: (sessionId) => turnActivity.activeSessions().includes(sessionId),
12155
11845
  pushClipboard: (text, terminalId) => {
12156
11846
  if (!apiMachineRef) {
12157
11847
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -12171,6 +11861,8 @@ async function startDaemon() {
12171
11861
  }
12172
11862
  });
12173
11863
  const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
11864
+ const daemonSupervisor = detectDaemonSupervisor(process.env);
11865
+ logger.debug(`[DAEMON RUN] Supervisor: ${daemonSupervisor}`);
12174
11866
  let fileState = {
12175
11867
  pid: process.pid,
12176
11868
  httpPort: controlPort,
@@ -12180,7 +11872,8 @@ async function startDaemon() {
12180
11872
  serverUrl: configuration.serverUrl,
12181
11873
  webappUrl: configuration.webappUrl,
12182
11874
  daemonLogPath: logger.logFilePath,
12183
- claudeCredentialSource: daemonClaudeCredentials.source
11875
+ claudeCredentialSource: daemonClaudeCredentials.source,
11876
+ supervisor: daemonSupervisor
12184
11877
  };
12185
11878
  writeDaemonState(fileState);
12186
11879
  logger.debug("[DAEMON RUN] Daemon state written");
@@ -12217,9 +11910,12 @@ async function startDaemon() {
12217
11910
  const mirrorManager = createMirrorManager({
12218
11911
  api,
12219
11912
  machineId,
12220
- onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
11913
+ onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
11914
+ onEdit: (edit) => peerCoordinator.onEdit(edit),
11915
+ onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
12221
11916
  });
12222
11917
  mirrorManagerRef = mirrorManager;
11918
+ peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
12223
11919
  apiMachine.setMirrorIntegration({
12224
11920
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
12225
11921
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
@@ -12350,6 +12046,10 @@ async function startDaemon() {
12350
12046
  await cleanupDaemonState();
12351
12047
  await releaseDaemonLock(daemonLockHandle);
12352
12048
  await stopCaffeinate();
12049
+ if (daemonSupervisor === "systemd") {
12050
+ logger.debug(`[DAEMON RUN] systemd-supervised: exiting ${HANDOVER_EXIT_CODE} for the unit to restart on the new bundle`);
12051
+ process.exit(HANDOVER_EXIT_CODE);
12052
+ }
12353
12053
  try {
12354
12054
  spawnHappyCLI(["daemon", "start"], {
12355
12055
  detached: true,
@@ -12551,127 +12251,6 @@ This session is now connected as the team lead. Call team_inspect to read the au
12551
12251
  };
12552
12252
  }
12553
12253
 
12554
- const ACTIVE_FRESH_WINDOW_MS = 15 * 60 * 1e3;
12555
- const DEFAULT_RESUME_WAIT_MS = 3e4;
12556
- const DEFAULT_RESUME_POLL_MS = 1e3;
12557
- function classifySessionState(input) {
12558
- const { liveHere, server, now } = input;
12559
- if (!server.found) return "not_found";
12560
- if (typeof server.archivedAt === "number") return "archived";
12561
- if (liveHere) return "live";
12562
- if (server.active && typeof server.activeAt === "number" && now - server.activeAt <= ACTIVE_FRESH_WINDOW_MS) return "live";
12563
- return "offline";
12564
- }
12565
- function explainNotLive(sessionId, status, resumeHint) {
12566
- switch (status) {
12567
- case "archived":
12568
- return `Session ${sessionId} is archived \u2014 no live wrapper will receive this message. ${resumeHint}`;
12569
- case "offline":
12570
- return `Session ${sessionId} is offline (its wrapper exited or timed out) \u2014 no live wrapper will receive this message. ${resumeHint}`;
12571
- case "not_found":
12572
- return `Session ${sessionId} was not found on this account \u2014 it cannot be messaged or resumed. Spawn a new session instead.`;
12573
- }
12574
- }
12575
- const CLI_RESUME_HINT = "Use --resume to bring it back on this machine, or spawn a new session.";
12576
- const defaultDeps = {
12577
- listLive: () => listDaemonSessions(),
12578
- readState: (sessionId, persisted) => readSessionState(sessionId, persisted),
12579
- send: (sessionId, persisted, text, client, options) => sendUserMessage(sessionId, persisted, text, client, options),
12580
- unarchive: unarchiveSession,
12581
- archive: archiveSession,
12582
- resume: resumeDaemonSession,
12583
- sleep: (ms) => delay(ms).then(() => void 0),
12584
- now: () => Date.now()
12585
- };
12586
- async function deliverToSession(sessionId, persisted, text, client, options = {}, deps = {}) {
12587
- const d = { ...defaultDeps, ...deps };
12588
- const resumeHint = options.resumeHint ?? CLI_RESUME_HINT;
12589
- const classify = async () => {
12590
- const [live, server] = await Promise.all([d.listLive(), d.readState(sessionId, persisted)]);
12591
- return classifySessionState({ liveHere: sessionLiveness(live, sessionId).live, server, now: d.now() });
12592
- };
12593
- let status = await classify();
12594
- let resumed = false;
12595
- let resumeOutcome;
12596
- if (status !== "live") {
12597
- if (!options.resume || status === "not_found") {
12598
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: explainNotLive(sessionId, status, resumeHint) };
12599
- }
12600
- const wasArchived = status === "archived";
12601
- if (wasArchived) {
12602
- try {
12603
- await d.unarchive(sessionId);
12604
- } catch (error) {
12605
- const message = `unarchive failed: ${error instanceof Error ? error.message : String(error)}`;
12606
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
12607
- }
12608
- }
12609
- const outcome = await d.resume(sessionId, {
12610
- ...typeof options.model === "string" ? { model: options.model } : {},
12611
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {}
12612
- });
12613
- if (!outcome.success) {
12614
- let message = outcome.error ?? "resume failed";
12615
- if (wasArchived) {
12616
- try {
12617
- await d.archive(sessionId);
12618
- } catch (error) {
12619
- message += `; failed to restore archive state: ${error instanceof Error ? error.message : String(error)}`;
12620
- }
12621
- }
12622
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
12623
- }
12624
- resumed = true;
12625
- resumeOutcome = { ok: true };
12626
- status = await waitUntilLive(classify, d, options);
12627
- if (status !== "live") {
12628
- const waited = Math.round((options.waitMs ?? DEFAULT_RESUME_WAIT_MS) / 1e3);
12629
- return {
12630
- sessionId,
12631
- status,
12632
- delivered: false,
12633
- resumed,
12634
- stored: false,
12635
- resume: resumeOutcome,
12636
- error: `Session ${sessionId} was resumed but no live wrapper showed up within ${waited}s (state: ${status}); message not sent`
12637
- };
12638
- }
12639
- }
12640
- await d.send(sessionId, persisted, text, client, {
12641
- ...options.model !== void 0 ? { model: options.model } : {},
12642
- ...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
12643
- });
12644
- let after = "live";
12645
- try {
12646
- after = await classify();
12647
- } catch {
12648
- after = "live";
12649
- }
12650
- if (after !== "live") {
12651
- return {
12652
- sessionId,
12653
- status: after,
12654
- delivered: false,
12655
- resumed,
12656
- stored: true,
12657
- ...resumeOutcome ? { resume: resumeOutcome } : {},
12658
- error: `Session ${sessionId} went ${after} while sending: the message is stored server-side but no wrapper is attached to read it. ${after === "not_found" ? "Spawn a new session." : resumeHint}`
12659
- };
12660
- }
12661
- return { sessionId, status: "live", delivered: true, resumed, stored: true, ...resumeOutcome ? { resume: resumeOutcome } : {} };
12662
- }
12663
- async function waitUntilLive(classify, d, options) {
12664
- const waitMs = options.waitMs ?? DEFAULT_RESUME_WAIT_MS;
12665
- const pollMs = options.pollMs ?? DEFAULT_RESUME_POLL_MS;
12666
- const deadline = d.now() + waitMs;
12667
- let status = await classify();
12668
- while (status !== "live" && d.now() < deadline) {
12669
- await d.sleep(pollMs);
12670
- status = await classify();
12671
- }
12672
- return status;
12673
- }
12674
-
12675
12254
  const PERSONAL_MEMORY_SOFT_LIMIT_CHARS = 2e3;
12676
12255
  function headingTitle(line) {
12677
12256
  const m = /^##(?!#)\s*(.*?)\s*$/.exec(line);
@@ -13038,13 +12617,14 @@ function normalizeProgress(value) {
13038
12617
  return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
13039
12618
  }
13040
12619
 
13041
- function createMcpServer(handlers, options, sessionId) {
12620
+ function createMcpServer(handlers, options, sessionId, client) {
13042
12621
  const mcp = new McpServer({
13043
12622
  name: "Very Happy Tools",
13044
12623
  version: "1.0.0"
13045
12624
  });
13046
12625
  registerTeamsTools(mcp, sessionId);
13047
12626
  registerAutomationTools(mcp);
12627
+ if (client) registerSessionPeerTools(mcp, createSessionPeerToolExecutor(client));
13048
12628
  mcp.registerTool("change_title", {
13049
12629
  description: "Change the title of the current chat session",
13050
12630
  title: "Change Chat Title",
@@ -13247,7 +12827,7 @@ async function startHappyServer(client, options) {
13247
12827
  }
13248
12828
  return;
13249
12829
  }
13250
- const mcp = createMcpServer(handlers, options, client.sessionId);
12830
+ const mcp = createMcpServer(handlers, options, client.sessionId, client);
13251
12831
  try {
13252
12832
  const transport = new StreamableHTTPServerTransport({
13253
12833
  sessionIdGenerator: void 0
@@ -13288,6 +12868,7 @@ async function startHappyServer(client, options) {
13288
12868
  REPORT_PROGRESS_TOOL_NAME,
13289
12869
  ...TEAM_TOOL_NAMES,
13290
12870
  ...AUTOMATION_TOOL_NAMES,
12871
+ ...SESSION_PEER_TOOL_NAMES,
13291
12872
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
13292
12873
  ],
13293
12874
  stop: () => {
@@ -13448,6 +13029,40 @@ function registerKillSessionHandler(rpcHandlerManager, killThisHappy, archiveEve
13448
13029
  };
13449
13030
  }
13450
13031
 
13032
+ async function settleSessionOnExit(sessionId, intent, deps) {
13033
+ if (intent === "archive") {
13034
+ deps.updateMetadata((currentMetadata) => ({
13035
+ ...currentMetadata,
13036
+ lifecycleState: "archived",
13037
+ lifecycleStateSince: Date.now(),
13038
+ archivedBy: "cli",
13039
+ archiveReason: "User terminated"
13040
+ }));
13041
+ }
13042
+ deps.sendSessionDeath();
13043
+ try {
13044
+ if (intent === "archive") await deps.archiveSession(sessionId);
13045
+ else await deps.deactivateSession(sessionId);
13046
+ } catch (error) {
13047
+ deps.log?.(`[START] ${intent === "archive" ? "archiveSession" : "deactivateSession"} during cleanup failed:`, error);
13048
+ }
13049
+ }
13050
+ class SessionExitGate {
13051
+ decided = null;
13052
+ /** Returns the intent to act on, or null when cleanup already started. */
13053
+ claim(intent) {
13054
+ if (this.decided !== null) return null;
13055
+ this.decided = intent;
13056
+ return intent;
13057
+ }
13058
+ get current() {
13059
+ return this.decided;
13060
+ }
13061
+ }
13062
+ function exitIntentFromArchiveFlag(archive) {
13063
+ return archive ?? true ? "archive" : "offline";
13064
+ }
13065
+
13451
13066
  function isUnsupportedSideQuestionError(error) {
13452
13067
  const message = error instanceof Error ? error.message : String(error);
13453
13068
  return /unknown|unsupported|not supported|unrecognized|invalid.*subtype/i.test(message);
@@ -13624,7 +13239,7 @@ function registerSideQuestionHandler(rpc, deps) {
13624
13239
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13625
13240
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13626
13241
  const fork = deps.run ?? (async (input) => {
13627
- const { query } = await import('./index-CIx3248o.mjs');
13242
+ const { query } = await import('./index-PYO8Mdz4.mjs');
13628
13243
  return runSideQuestion(query, input);
13629
13244
  });
13630
13245
  const run = async (input, live) => {
@@ -14411,6 +14026,17 @@ async function runClaude(credentials, options = {}) {
14411
14026
  });
14412
14027
  session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
14413
14028
  session.on("turn-ended", () => boardAnalyzer.onTurnEnd());
14029
+ const editThrottle = new EditReportThrottle();
14030
+ session.on("claude-session-message", (body) => {
14031
+ try {
14032
+ for (const edit of editThrottle.take(extractClaudeEditPaths(body))) {
14033
+ reportSessionEditToDaemon({ sessionId: response.id, path: edit.path, tool: edit.tool, cwd: workingDirectory, ...edit.at !== void 0 ? { at: edit.at } : {} });
14034
+ }
14035
+ } catch (error) {
14036
+ logger.debug("[START] edit report failed:", error);
14037
+ }
14038
+ });
14039
+ options = { ...options, claudeEnvVars: { ...options.claudeEnvVars, VH_PEER_SESSION_ID: response.id } };
14414
14040
  const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
14415
14041
  const remoteScanner = await createSessionScanner({
14416
14042
  sessionId: initialScannerSessionId,
@@ -14750,26 +14376,24 @@ async function runClaude(credentials, options = {}) {
14750
14376
  hasMetadata: Boolean(message.meta)
14751
14377
  });
14752
14378
  });
14379
+ const exitGate = new SessionExitGate();
14753
14380
  const cleanup = async (opts = { archive: true }) => {
14754
- logger.debug(`[START] Received termination signal, cleaning up (archive=${opts.archive ?? true})...`);
14381
+ const intent = exitGate.claim(exitIntentFromArchiveFlag(opts.archive));
14382
+ if (intent === null) {
14383
+ logger.debug(`[START] Termination (archive=${opts.archive ?? true}) ignored: cleanup already running as ${exitGate.current}`);
14384
+ return;
14385
+ }
14386
+ logger.debug(`[START] Received termination signal, cleaning up (archive=${intent === "archive"})...`);
14755
14387
  try {
14756
14388
  if (session) {
14757
- if (opts.archive ?? true) {
14758
- session.updateMetadata((currentMetadata) => ({
14759
- ...currentMetadata,
14760
- lifecycleState: "archived",
14761
- lifecycleStateSince: Date.now(),
14762
- archivedBy: "cli",
14763
- archiveReason: "User terminated"
14764
- }));
14765
- }
14766
14389
  currentSession?.cleanup();
14767
- session.sendSessionDeath();
14768
- try {
14769
- await api.deactivateSession(session.sessionId);
14770
- } catch (err) {
14771
- logger.debug("[START] deactivateSession during cleanup failed:", err);
14772
- }
14390
+ await settleSessionOnExit(session.sessionId, intent, {
14391
+ updateMetadata: (handler) => session.updateMetadata(handler),
14392
+ sendSessionDeath: () => session.sendSessionDeath(),
14393
+ archiveSession: (id) => api.archiveSession(id),
14394
+ deactivateSession: (id) => api.deactivateSession(id),
14395
+ log: (message, ...args) => logger.debug(message, ...args)
14396
+ });
14773
14397
  await session.flush();
14774
14398
  await session.close();
14775
14399
  }
@@ -18397,6 +18021,7 @@ async function runCodex(opts) {
18397
18021
  return "denied";
18398
18022
  }
18399
18023
  });
18024
+ const editThrottle = new EditReportThrottle();
18400
18025
  client.setEventHandler((msg) => {
18401
18026
  if (msg.type === "token_count") {
18402
18027
  session.sendAgentUsageSnapshot("codex", msg);
@@ -18460,6 +18085,9 @@ async function runCodex(opts) {
18460
18085
  }
18461
18086
  if (msg.type === "patch_apply_begin") {
18462
18087
  const { changes } = msg;
18088
+ for (const edit of editThrottle.take(extractCodexPatchPaths(changes))) {
18089
+ reportSessionEditToDaemon({ sessionId: session.sessionId, path: edit.path, tool: edit.tool, cwd: process.cwd() });
18090
+ }
18463
18091
  const changeCount = Object.keys(changes).length;
18464
18092
  const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
18465
18093
  messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
@@ -18505,6 +18133,7 @@ async function runCodex(opts) {
18505
18133
  };
18506
18134
  let first = true;
18507
18135
  let appendSystemPromptInjected = false;
18136
+ process.env.VH_PEER_SESSION_ID = session.sessionId;
18508
18137
  try {
18509
18138
  logger.debug("[codex]: client.connect begin");
18510
18139
  await client.connect();
@@ -18918,7 +18547,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18918
18547
  return;
18919
18548
  } else if (subcommand === "install-terminal-hooks") {
18920
18549
  try {
18921
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-Dde_8t36.mjs');
18550
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-CMpOV3RU.mjs');
18922
18551
  const command = parseTerminalHooksArgs(args.slice(1));
18923
18552
  if (command.action === "help") {
18924
18553
  console.log(TERMINAL_HOOKS_HELP);
@@ -18935,7 +18564,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18935
18564
  }
18936
18565
  } else if (subcommand === "agent-home") {
18937
18566
  try {
18938
- const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-B20RWdgE.mjs');
18567
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-D2XGsoqs.mjs');
18939
18568
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18940
18569
  process.exit(0);
18941
18570
  } catch (error) {
@@ -18944,7 +18573,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18944
18573
  }
18945
18574
  } else if (subcommand === "spawn") {
18946
18575
  try {
18947
- const { handleSpawnCommand } = await import('./spawn-D1bhyHbH.mjs');
18576
+ const { handleSpawnCommand } = await import('./spawn-BKNabutV.mjs');
18948
18577
  await handleSpawnCommand(args.slice(1));
18949
18578
  } catch (error) {
18950
18579
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18956,7 +18585,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18956
18585
  return;
18957
18586
  } else if (subcommand === "sessions") {
18958
18587
  try {
18959
- const { handleSessionsCommand } = await import('./sessions-DlPOYKLU.mjs');
18588
+ const { handleSessionsCommand } = await import('./sessions-Bn304xF1.mjs');
18960
18589
  await handleSessionsCommand(args.slice(1));
18961
18590
  } catch (error) {
18962
18591
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18968,7 +18597,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18968
18597
  return;
18969
18598
  } else if (subcommand === "send") {
18970
18599
  try {
18971
- const { handleSendCommand } = await import('./send-DYeSFg8s.mjs');
18600
+ const { handleSendCommand } = await import('./send-BaOkDgH0.mjs');
18972
18601
  await handleSendCommand(args.slice(1));
18973
18602
  } catch (error) {
18974
18603
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19074,9 +18703,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19074
18703
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
19075
18704
  const projectId = args[3];
19076
18705
  try {
19077
- const { saveGoogleCloudProjectToConfig } = await import('./config-B-ocQh5V.mjs');
19078
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-Dt7_unH4.mjs').then(function (n) { return n.al; });
19079
- const { ApiClient: ApiClient2 } = await import('./types-D5RWyBE5.mjs').then(function (n) { return n.ai; });
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; });
19080
18709
  let userEmail = void 0;
19081
18710
  try {
19082
18711
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -19107,7 +18736,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19107
18736
  }
19108
18737
  if (geminiSubcommand === "project" && args[2] === "get") {
19109
18738
  try {
19110
- const { readGeminiLocalConfig } = await import('./config-B-ocQh5V.mjs');
18739
+ const { readGeminiLocalConfig } = await import('./config-BkTu25bH.mjs');
19111
18740
  const config = readGeminiLocalConfig();
19112
18741
  if (config.googleCloudProject) {
19113
18742
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -19147,7 +18776,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19147
18776
  process.exit(0);
19148
18777
  }
19149
18778
  try {
19150
- const { runGemini } = await import('./runGemini-BW-pEklU.mjs');
18779
+ const { runGemini } = await import('./runGemini-CZ_lhMWY.mjs');
19151
18780
  let startedBy = void 0;
19152
18781
  for (let i = 1; i < args.length; i++) {
19153
18782
  if (args[i] === "--started-by") {
@@ -19170,7 +18799,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19170
18799
  } else if (subcommand === "pi") {
19171
18800
  if (args[1] === "--terminal") {
19172
18801
  try {
19173
- const { runPiTerminal } = await import('./piTerminal-CCEGCQOY.mjs');
18802
+ const { runPiTerminal } = await import('./piTerminal-DanZeBrO.mjs');
19174
18803
  process.exitCode = await runPiTerminal(args.slice(2));
19175
18804
  } catch (error) {
19176
18805
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19187,7 +18816,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19187
18816
  }
19188
18817
  };
19189
18818
  try {
19190
- const { runAcp, resolveAcpAgentConfig } = await import('./index-ZKuBjvHi.mjs');
18819
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-B5QbzEga.mjs');
19191
18820
  const parsed = parsePiRunnerArgs(args.slice(1));
19192
18821
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
19193
18822
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -19215,7 +18844,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19215
18844
  return;
19216
18845
  } else if (subcommand === "acp") {
19217
18846
  try {
19218
- const { runAcp, resolveAcpAgentConfig } = await import('./index-ZKuBjvHi.mjs');
18847
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-B5QbzEga.mjs');
19219
18848
  let startedBy = void 0;
19220
18849
  let verbose = false;
19221
18850
  const acpArgs = [];
@@ -19255,7 +18884,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19255
18884
  return;
19256
18885
  } else if (subcommand === "openclaw") {
19257
18886
  try {
19258
- const { runOpenClaw } = await import('./runOpenClaw-NRB6-_jU.mjs');
18887
+ const { runOpenClaw } = await import('./runOpenClaw-CxgLpm38.mjs');
19259
18888
  let startedBy = void 0;
19260
18889
  let verbose = false;
19261
18890
  let gatewayUrl;
@@ -19306,7 +18935,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19306
18935
  return;
19307
18936
  } else if (subcommand === "todo") {
19308
18937
  try {
19309
- const { handleTodoCommand } = await import('./todo-B2CjDg_V.mjs');
18938
+ const { handleTodoCommand } = await import('./todo-BcPbNKt2.mjs');
19310
18939
  await handleTodoCommand(args.slice(1));
19311
18940
  } catch (error) {
19312
18941
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -19315,7 +18944,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19315
18944
  return;
19316
18945
  } else if (subcommand === "teams") {
19317
18946
  try {
19318
- const { handleTeamsCommand } = await import('./teams-pfKHpkgL.mjs');
18947
+ const { handleTeamsCommand } = await import('./teams-CLbS2_4Q.mjs');
19319
18948
  await handleTeamsCommand(args.slice(1));
19320
18949
  } catch (error) {
19321
18950
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -19324,7 +18953,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19324
18953
  return;
19325
18954
  } else if (subcommand === "auto") {
19326
18955
  try {
19327
- const { handleAutoCommand } = await import('./auto-BQnncDcs.mjs');
18956
+ const { handleAutoCommand } = await import('./auto-B1I6G_Nf.mjs');
19328
18957
  await handleAutoCommand(args.slice(1));
19329
18958
  } catch (error) {
19330
18959
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -19336,7 +18965,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19336
18965
  return;
19337
18966
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
19338
18967
  try {
19339
- const { handleMcpCommand } = await import('./mcp-9Ui4nQZL.mjs');
18968
+ const { handleMcpCommand } = await import('./mcp-C_uejotl.mjs');
19340
18969
  await handleMcpCommand(args[1] === "--terminal-tools");
19341
18970
  } catch (error) {
19342
18971
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -19365,6 +18994,8 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19365
18994
  } else {
19366
18995
  console.log("Active sessions:");
19367
18996
  console.log(JSON.stringify(sessions, null, 2));
18997
+ const busy = sessions.filter((s) => s.turnActive === true);
18998
+ 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(", ")}`);
19368
18999
  }
19369
19000
  } catch (error) {
19370
19001
  console.log("No daemon running");
@@ -19393,13 +19024,19 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19393
19024
  configuration.webappUrl
19394
19025
  );
19395
19026
  if (running && !currentDaemonMatches) {
19027
+ if (currentState?.supervisor === "systemd") {
19028
+ console.error(`The running daemon is supervised by systemd; hand over through the unit instead:
19029
+ ${SYSTEMD_RESTART_HINT}`);
19030
+ process.exit(1);
19031
+ }
19396
19032
  console.log("Configured relay or CLI version changed; restarting the existing daemon");
19397
19033
  await stopDaemon();
19398
19034
  }
19035
+ const { HAPPY_DAEMON_SUPERVISOR: _supervisor, INVOCATION_ID: _invocation, ...detachedEnv } = process.env;
19399
19036
  const child = spawnHappyCLI(["daemon", "start-sync"], {
19400
19037
  detached: true,
19401
19038
  stdio: "ignore",
19402
- env: process.env
19039
+ env: detachedEnv
19403
19040
  });
19404
19041
  child.unref();
19405
19042
  let started = false;
@@ -19706,4 +19343,4 @@ ${chalk.bold("Examples:")}
19706
19343
  }
19707
19344
  }
19708
19345
 
19709
- export { BaseReasoningProcessor as $, ALLOWED_SPAWN_PERMISSION_MODES as A, TEAM_SKILL as B, initialMachineMetadata as C, DEFAULT_RECENT_LIMIT as D, createSessionMetadata as E, setupOfflineReconnection as F, GEMINI_MODEL_ENV as G, StreamRelay as H, MessageQueue2 as I, hashObject as J, startHappyServer as K, registerAgentAttachmentDownloads as L, MAX_READ_LIMIT as M, TitleGenerator as N, registerKillSessionHandler as O, PI_TEAMS_EXTENSION as P, CLAUDE_ATTACHMENT_KINDS as Q, stageClaudeAttachments as R, SPAWN_AGENTS as S, TurnWaitTimeoutError as T, appendStagedAttachmentsToPrompt as U, BasePermissionHandler as V, writeSessionModeFile as W, normalizeAcpPermissionMode as X, removeSessionModeFile as Y, GEMINI_API_KEY_ENV as Z, GOOGLE_API_KEY_ENV as _, listSessions as a, MessageBuffer as a0, CHANGE_TITLE_INSTRUCTION as a1, resolvePermissionRequest as b, archiveSession as c, deliverToSession as d, sessionWebUrl as e, DEFAULT_GEMINI_MODEL as f, describeTrigger as g, describeAction as h, sanitizeSpawnPermissionMode as i, tmuxSupportsSessionEnv as j, readSessionMetadata as k, listAccountSessions as l, resolveSpawnPermissionMode as m, resolveFirstMessageMeta as n, isValidSpawnOrigin as o, parseDuration as p, query as q, readSessionTranscript as r, stopSession as s, triggerFromFlags as t, waitForSessionKey as u, sendUserMessage as v, waitForTurnEnd as w, checkPreviewPath as x, registerAssistantSessionTools as y, suggestTitleForPrompt as z };
19346
+ export { ALLOWED_SPAWN_PERMISSION_MODES as A, BasePermissionHandler as B, CLAUDE_ATTACHMENT_KINDS as C, DEFAULT_GEMINI_MODEL as D, EditReportThrottle as E, normalizeAcpPermissionMode as F, GEMINI_MODEL_ENV as G, removeSessionModeFile as H, GEMINI_API_KEY_ENV as I, GOOGLE_API_KEY_ENV as J, BaseReasoningProcessor as K, MessageBuffer as L, MessageQueue2 as M, CHANGE_TITLE_INSTRUCTION as N, PI_TEAMS_EXTENSION as P, SPAWN_AGENTS as S, TEAM_SKILL as T, describeAction as a, tmuxSupportsSessionEnv as b, resolveFirstMessageMeta as c, describeTrigger as d, checkPreviewPath as e, registerAssistantSessionTools as f, suggestTitleForPrompt as g, initialMachineMetadata as h, isValidSpawnOrigin as i, createSessionMetadata as j, setupOfflineReconnection as k, StreamRelay as l, hashObject as m, startHappyServer as n, registerAgentAttachmentDownloads as o, parseDuration as p, query as q, resolveSpawnPermissionMode as r, sanitizeSpawnPermissionMode as s, triggerFromFlags as t, TitleGenerator as u, registerKillSessionHandler as v, stageClaudeAttachments as w, appendStagedAttachmentsToPrompt as x, extractAcpEditPaths as y, writeSessionModeFile as z };