very-happy-cli 0.2.153 → 0.2.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{AcpBackend-YDGv4E-g.mjs → AcpBackend-DVB0nzgR.mjs} +2 -2
  2. package/dist/{AcpBackend-DSt8nTYm.cjs → AcpBackend-DmFcRh54.cjs} +3 -3
  3. package/dist/{AcpSessionManager-uov6I9C8.cjs → AcpSessionManager-CWaSYTnT.cjs} +1 -1
  4. package/dist/{AcpSessionManager-Cxf5HQX1.mjs → AcpSessionManager-dGDgNb-p.mjs} +1 -1
  5. package/dist/agentGuidance-BWxFC6UL.mjs +1613 -0
  6. package/dist/agentGuidance-un7YPryB.cjs +1665 -0
  7. package/dist/{agentHome-D68Xh2ae.mjs → agentHome-ByLb2HFg.mjs} +5 -5
  8. package/dist/{agentHome-BJ7bde35.cjs → agentHome-aucT8BHw.cjs} +5 -5
  9. package/dist/{auto-CcXxfqkC.mjs → auto-BQnD6gVx.mjs} +21 -21
  10. package/dist/{auto-C-90ojIH.cjs → auto-P9RoJaY0.cjs} +24 -24
  11. package/dist/codex/happyMcpStdioBridge.cjs +6 -3
  12. package/dist/codex/happyMcpStdioBridge.mjs +6 -3
  13. package/dist/{config-CHJ189UJ.mjs → config-BPZ2Y6o8.mjs} +8 -8
  14. package/dist/{config-D23kF0_u.cjs → config-BoePh9PX.cjs} +8 -8
  15. package/dist/{index-D49WvbDT.cjs → index--KCpVs1c.cjs} +7 -7
  16. package/dist/{index-BacwoXzs.mjs → index-B4oXnbYw.mjs} +522 -729
  17. package/dist/{index-BgYZGODP.mjs → index-BjzdXb9x.mjs} +21 -13
  18. package/dist/{index-DEOVn46R.cjs → index-DRrLaljt.cjs} +21 -13
  19. package/dist/{index-BXA9fxbK.mjs → index-DVVRqaVG.mjs} +7 -7
  20. package/dist/{index-DHrwHxHf.cjs → index-D_fYUxVg.cjs} +579 -798
  21. package/dist/index.cjs +6 -6
  22. package/dist/index.mjs +6 -6
  23. package/dist/{install-BiOXBnnS.cjs → install-CGigiocs.cjs} +1 -1
  24. package/dist/{install-Ca7_Z3bV.mjs → install-DP5aPNEX.mjs} +1 -1
  25. package/dist/{installTerminalHooks-BF0kL0ae.cjs → installTerminalHooks-XrcPvfBs.cjs} +5 -5
  26. package/dist/{installTerminalHooks-Bpl32K75.mjs → installTerminalHooks-tIg7mANc.mjs} +5 -5
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.mjs +2 -2
  29. package/dist/{limits-BtAuKRl2.mjs → limits-C_DW0M0q.mjs} +309 -13
  30. package/dist/{limits-DY_sFA3J.cjs → limits-DkfHJ3nS.cjs} +329 -12
  31. package/dist/{mcp-JpqI2POW.cjs → mcp-BqlXl10F.cjs} +23 -23
  32. package/dist/{mcp-DQkmk4VV.mjs → mcp-o7QglAQM.mjs} +17 -17
  33. package/dist/{piRuntime-DRbbeQ72.cjs → piRuntime-BxP4B_zY.cjs} +2 -2
  34. package/dist/{piRuntime-DBpe12NN.mjs → piRuntime-MjiJI-nV.mjs} +2 -2
  35. package/dist/{piTerminal-qdJ9KZVL.cjs → piTerminal-DYzRcwgb.cjs} +6 -6
  36. package/dist/{piTerminal-BoHvPEZ_.mjs → piTerminal-Dpq6yqKU.mjs} +6 -6
  37. package/dist/{runGemini-CtOEFvhf.cjs → runGemini-8Pxh94Fm.cjs} +7 -7
  38. package/dist/{runGemini-YqH7a-7O.mjs → runGemini-CDlcQ-OA.mjs} +6 -6
  39. package/dist/{runOpenClaw-DoqZXBvL.cjs → runOpenClaw-B5FSZIbK.cjs} +6 -6
  40. package/dist/{runOpenClaw-DSSyFQBb.mjs → runOpenClaw-YkhsgPs0.mjs} +5 -5
  41. package/dist/{send-9075kHZP.cjs → send-CwrYXTPr.cjs} +58 -54
  42. package/dist/{send-Cs-RwqNf.mjs → send-DJStmLBv.mjs} +56 -53
  43. package/dist/{sessions-iBnYzVUK.mjs → sessions-JaVXlSqj.mjs} +88 -45
  44. package/dist/{sessions-qULzUzlI.cjs → sessions-MX21NJXd.cjs} +99 -56
  45. package/dist/{spawn-C6fRkedx.mjs → spawn-BaPnjClT.mjs} +22 -22
  46. package/dist/{spawn-od9V23cc.cjs → spawn-cIoglKVf.cjs} +30 -30
  47. package/dist/{teams-CkrWQ4uM.mjs → teams-DXV5Pwwt.mjs} +8 -8
  48. package/dist/{teams-CrCzU-AV.cjs → teams-Dqk7F3D-.cjs} +8 -8
  49. package/dist/{todo-aCZmSt27.mjs → todo-2Z1REhfx.mjs} +8 -6
  50. package/dist/{todo-BCT_ZPuV.cjs → todo-Buuqn9gm.cjs} +8 -6
  51. package/dist/{types-BvqOVUOW.mjs → types-CkbfdRzq.mjs} +7 -267
  52. package/dist/{types-CUCU7Vw8.cjs → types-a2xsH2UI.cjs} +19 -295
  53. package/package.json +7 -7
  54. package/dist/agentGuidance-BM4kMNyr.cjs +0 -553
  55. package/dist/agentGuidance-C8unGWp7.mjs +0 -529
@@ -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, i as isValidSessionId, s as spawnDaemonSession, a9 as listVhTerminals, aa as isValidTerminalId, ab as readVhTerminal, ac as sendToVhTerminal, ad as TITLE_PROMPT_PREFIX, ae as BOARD_ANALYZER_PROMPT_PREFIX, o as connectionState, af as startOfflineReconnection, q as notifyDaemonSessionStarted, C as CodexAppServerClient } from './types-BvqOVUOW.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-BtAuKRl2.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-CkbfdRzq.mjs';
5
+ import { l as logger, a7 as notifyDaemonSessionEvent, x as BUILTIN_TODO_DISCOVERY, g as delay, a8 as STREAM_DELTA_MAX_CHARS, a9 as ensurePrivateDirectory, aa as hardenPrivateFile, c as configuration, ab as notifyDaemonClaudeAuthFailed, F as packageJson, r as readSettings, ac as readCredentials, Z as credentialRelayProblem, k as checkIfDaemonRunningAndCleanupStaleState, ad as readDaemonState, ae as AGENT_VERSION_SOURCES, w as encodeBase64, af as readSettingsOutcome, u as updateSettings, ag as encodeBase64Url, M as decodeBase64, ah as writeCredentialsLegacy, ai as writeCredentialsDataKey, d as readPersistedSessions, E as writePrivateFileSync, aj as decryptWithDataKey, ak as decryptLegacy, al as persistSession, z as ensurePrivateDirectorySync, am as getAgentCodeDefaults, an as normalizeAgentKey, ao as isClaudeOpus55Model, ap as supportsClaudeOpus55, aq as AUTOMATION_SUMMARY_MAX_CHARS, ar as AUTOMATION_CLAIM_MAX_RUNS, as as AUTOMATION_DEFAULT_LEASE_MS, f as isAutomationRunTerminal, at as isDaemonRunningCurrentlyInstalledHappyVersion, au as stopDaemon, av as acquireDaemonLock, aw as deletePersistedSessions, N as decrypt, ax as writeDaemonState, ay as writeSettings, az as cleanupDaemonState, aA as releaseDaemonLock, i as isValidSessionId, m as spawnDaemonSession, a1 as isValidTerminalId, aB as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, o as CLIPBOARD_MAX_BYTES, v as notifyDaemonSessionStarted, y as reportSessionEditToDaemon, aC as clearCredentials, aD as clearMachineId, h as readCredentialsForConfiguredRelay, aE as SandboxConfigSchema, e as createEnvelope, O as listDaemonSessions, n as stopDaemonSession, aF as getLatestDaemonLog } from './limits-C_DW0M0q.mjs';
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-C8unGWp7.mjs';
11
+ import { E as trimIdent, F as AGENT_GUIDANCE, G as formatEditConflictNotice, z as sendUserMessage, S as SESSION_PEER_SENT_FROM, x as readSessionMetadata, y as waitForSessionKey, k as archiveSession, H as createAutomationsClient, t as isAutomationsUnavailable, I as AutomationsRequestError, J as analyzeLatestTurn, K as readSessionLog$1, L as TURN_WINDOW, N as userPromptText, v as teamScopePath, O as TeamConnectionPreflightError, C as createTeamsClient, e as listSessions, h as readSessionTranscript, n as deliverToSession, o as sessionWebUrl, i as stopSession, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, Q as TEAM_TOOL_NAMES, U as AUTOMATION_TOOL_NAMES, V as SESSION_PEER_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, d as registerSessionPeerTools, W as createSessionPeerToolExecutor, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, X as REPORT_PROGRESS_TOOL_TITLE, Y as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-BWxFC6UL.mjs';
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-D68Xh2ae.mjs');
5384
- const { describeAgentHome } = await import('./types-BvqOVUOW.mjs').then(function (n) { return n.ag; });
5383
+ const { resolveAgentHomesForCli } = await import('./agentHome-ByLb2HFg.mjs');
5384
+ const { describeAgentHome } = await import('./types-CkbfdRzq.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",
@@ -6262,6 +6209,7 @@ function startDaemonControlServer({
6262
6209
  getChildren,
6263
6210
  stopSession,
6264
6211
  spawnSession,
6212
+ resumeSession,
6265
6213
  requestShutdown,
6266
6214
  onHappySessionWebhook,
6267
6215
  onSessionStateEvent,
@@ -6270,7 +6218,9 @@ function startDaemonControlServer({
6270
6218
  pushClipboard,
6271
6219
  pushFilePreview,
6272
6220
  onTerminalHook,
6273
- setTerminalTitle
6221
+ setTerminalTitle,
6222
+ onSessionEdit,
6223
+ listPeers
6274
6224
  }) {
6275
6225
  return new Promise((resolve) => {
6276
6226
  const app = fastify({
@@ -6474,6 +6424,48 @@ function startDaemonControlServer({
6474
6424
  };
6475
6425
  }
6476
6426
  });
6427
+ typed.post("/resume-session", {
6428
+ schema: {
6429
+ body: z.object({
6430
+ sessionId: z.string().min(1).max(200),
6431
+ model: z.string().optional(),
6432
+ permissionMode: z.string().optional()
6433
+ }),
6434
+ response: {
6435
+ 200: z.object({
6436
+ success: z.boolean(),
6437
+ sessionId: z.string().optional(),
6438
+ error: z.string().optional()
6439
+ }),
6440
+ 500: z.object({
6441
+ success: z.boolean(),
6442
+ error: z.string().optional()
6443
+ }),
6444
+ 503: z.object({
6445
+ success: z.boolean(),
6446
+ error: z.string().optional()
6447
+ })
6448
+ }
6449
+ }
6450
+ }, async (request, reply) => {
6451
+ const { sessionId, model, permissionMode } = request.body;
6452
+ if (!resumeSession) {
6453
+ reply.code(503);
6454
+ return { success: false, error: "daemon is still starting up" };
6455
+ }
6456
+ logger.debug(`[CONTROL SERVER] Resume session request: ${sessionId} (model=${model ?? "unset"}, permissionMode=${permissionMode ?? "unset"})`);
6457
+ const result = await resumeSession(sessionId, { model, permissionMode });
6458
+ switch (result.type) {
6459
+ case "success":
6460
+ return { success: true, sessionId: result.sessionId };
6461
+ case "requestToApproveDirectoryCreation":
6462
+ reply.code(500);
6463
+ return { success: false, error: `Cannot resume: working directory ${result.directory} needs to be created first` };
6464
+ case "error":
6465
+ reply.code(500);
6466
+ return { success: false, error: result.errorMessage };
6467
+ }
6468
+ });
6477
6469
  typed.post("/terminal-hook", {
6478
6470
  schema: {
6479
6471
  body: z.any(),
@@ -6551,6 +6543,45 @@ function startDaemonControlServer({
6551
6543
  }
6552
6544
  return { status: "ok" };
6553
6545
  });
6546
+ typed.post("/session-edit", {
6547
+ schema: {
6548
+ body: z.object({
6549
+ sessionId: z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/),
6550
+ path: z.string().min(1).max(4096),
6551
+ tool: z.string().min(1).max(64),
6552
+ cwd: z.string().max(4096).optional(),
6553
+ /** When the runner saw the call (ms). Clamped to now by the daemon;
6554
+ * older than the conflict window = ignored (replayed history). */
6555
+ at: z.number().int().nonnegative().optional()
6556
+ }),
6557
+ response: { 200: z.object({ status: z.literal("ok") }) }
6558
+ }
6559
+ }, async (request) => {
6560
+ try {
6561
+ onSessionEdit?.(request.body);
6562
+ } catch (error) {
6563
+ logger.debug("[CONTROL SERVER] session-edit handler failed:", error);
6564
+ }
6565
+ return { status: "ok" };
6566
+ });
6567
+ typed.post("/peers", {
6568
+ schema: {
6569
+ response: {
6570
+ 200: z.object({
6571
+ sessions: z.array(z.object({
6572
+ sessionId: z.string(),
6573
+ kind: z.enum(["managed", "mirror"]),
6574
+ pid: z.number().optional(),
6575
+ cwd: z.string().optional(),
6576
+ flavor: z.string().optional(),
6577
+ title: z.string().optional(),
6578
+ variant: z.string().optional(),
6579
+ edits: z.array(z.object({ path: z.string(), tool: z.string(), at: z.number() }))
6580
+ }))
6581
+ })
6582
+ }
6583
+ }
6584
+ }, async () => ({ sessions: listPeers ? listPeers() : [] }));
6554
6585
  typed.post("/stop", {
6555
6586
  schema: {
6556
6587
  response: {
@@ -6586,6 +6617,273 @@ function startDaemonControlServer({
6586
6617
  });
6587
6618
  }
6588
6619
 
6620
+ const EDIT_CONFLICT_WINDOW_MS = 30 * 6e4;
6621
+ function pairKey$1(a, b, path) {
6622
+ return a < b ? `${a}\0${b}\0${path}` : `${b}\0${a}\0${path}`;
6623
+ }
6624
+ class EditConflictTracker {
6625
+ windowMs;
6626
+ maxPaths;
6627
+ /** real path → sessionId → latest edit */
6628
+ byPath = /* @__PURE__ */ new Map();
6629
+ /** (sorted pair, path) → when the notice went out */
6630
+ notified = /* @__PURE__ */ new Map();
6631
+ constructor(options = {}) {
6632
+ this.windowMs = options.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6633
+ this.maxPaths = options.maxPaths ?? 5e3;
6634
+ }
6635
+ /**
6636
+ * Remember an edit and return the peers to notify about it. `isLive`
6637
+ * decides whether another session still counts (dead ones are dropped
6638
+ * from the table on the spot).
6639
+ */
6640
+ record(edit, isLive) {
6641
+ this.prune(edit.at);
6642
+ let sessions = this.byPath.get(edit.path);
6643
+ if (!sessions) {
6644
+ sessions = /* @__PURE__ */ new Map();
6645
+ this.byPath.set(edit.path, sessions);
6646
+ }
6647
+ const conflicts = [];
6648
+ for (const [sessionId, peer] of sessions) {
6649
+ if (sessionId === edit.sessionId) continue;
6650
+ if (!isLive(sessionId)) {
6651
+ sessions.delete(sessionId);
6652
+ continue;
6653
+ }
6654
+ if (edit.at - peer.at > this.windowMs) continue;
6655
+ const key = pairKey$1(edit.sessionId, sessionId, edit.path);
6656
+ const notifiedAt = this.notified.get(key);
6657
+ if (notifiedAt !== void 0 && edit.at - notifiedAt <= this.windowMs) continue;
6658
+ this.notified.set(key, edit.at);
6659
+ conflicts.push({ editor: edit, peer });
6660
+ }
6661
+ sessions.set(edit.sessionId, edit);
6662
+ return conflicts;
6663
+ }
6664
+ /** Paths this session edited inside the window, newest first. */
6665
+ editsOf(sessionId, now, limit = 20) {
6666
+ const out = [];
6667
+ for (const [path, sessions] of this.byPath) {
6668
+ const edit = sessions.get(sessionId);
6669
+ if (edit && now - edit.at <= this.windowMs) out.push({ path, tool: edit.tool, at: edit.at });
6670
+ }
6671
+ return out.sort((a, b) => b.at - a.at).slice(0, limit);
6672
+ }
6673
+ /** Session ended: nothing it did counts any more. */
6674
+ forget(sessionId) {
6675
+ for (const [path, sessions] of this.byPath) {
6676
+ sessions.delete(sessionId);
6677
+ if (sessions.size === 0) this.byPath.delete(path);
6678
+ }
6679
+ for (const key of this.notified.keys()) {
6680
+ const [a, b] = key.split("\0");
6681
+ if (a === sessionId || b === sessionId) this.notified.delete(key);
6682
+ }
6683
+ }
6684
+ /** Drop everything older than the window (and cap the table size). */
6685
+ prune(now) {
6686
+ for (const [path, sessions] of this.byPath) {
6687
+ for (const [sessionId, edit] of sessions) if (now - edit.at > this.windowMs) sessions.delete(sessionId);
6688
+ if (sessions.size === 0) this.byPath.delete(path);
6689
+ }
6690
+ for (const [key, at] of this.notified) if (now - at > this.windowMs) this.notified.delete(key);
6691
+ if (this.byPath.size > this.maxPaths) {
6692
+ 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);
6693
+ for (const entry of oldest.slice(0, this.byPath.size - this.maxPaths)) this.byPath.delete(entry.path);
6694
+ }
6695
+ }
6696
+ /** Test / diagnostics helper. */
6697
+ size() {
6698
+ return this.byPath.size;
6699
+ }
6700
+ }
6701
+ function normalizeEditPath(path, cwd, realpath = realpathSync.native ?? realpathSync) {
6702
+ const absolute = isAbsolute(path) ? resolve(path) : resolve(cwd, path);
6703
+ let existing = absolute;
6704
+ const tail = [];
6705
+ for (let depth = 0; depth < 64; depth++) {
6706
+ try {
6707
+ const real = realpath(existing);
6708
+ return tail.length ? join(real, ...tail) : real;
6709
+ } catch {
6710
+ const parent = dirname(existing);
6711
+ if (parent === existing) break;
6712
+ tail.unshift(existing.slice(parent.length).replace(new RegExp(`^\\${sep}`), ""));
6713
+ existing = parent;
6714
+ }
6715
+ }
6716
+ return absolute;
6717
+ }
6718
+
6719
+ const CONFLICT_COALESCE_MS = 1e4;
6720
+ const MAX_NOTICES_PER_PAIR = 5;
6721
+ function pairKey(a, b) {
6722
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
6723
+ }
6724
+ function senderOf(info, sessionId) {
6725
+ return { sessionId, title: info?.title, flavor: info?.flavor, cwd: info?.cwd };
6726
+ }
6727
+ function createPeerCoordinator(deps) {
6728
+ const now = deps.now ?? (() => Date.now());
6729
+ const windowMs = deps.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6730
+ const coalesceMs = deps.coalesceMs ?? CONFLICT_COALESCE_MS;
6731
+ const maxNoticesPerPair = deps.maxNoticesPerPair ?? MAX_NOTICES_PER_PAIR;
6732
+ const tracker = new EditConflictTracker({ windowMs });
6733
+ const readPersisted = deps.readPersisted ?? readPersistedSessions;
6734
+ const send = deps.send ?? sendUserMessage;
6735
+ const normalize = deps.normalize ?? normalizeEditPath;
6736
+ const liveTitle = deps.liveTitle ?? (async (sessionId, persisted) => (await readSessionMetadata(sessionId, persisted))?.summary?.text);
6737
+ const schedule = deps.schedule ?? ((fn, ms) => {
6738
+ const t = setTimeout(fn, ms);
6739
+ t.unref?.();
6740
+ return t;
6741
+ });
6742
+ let getMirrors = deps.getMirrors ?? (() => []);
6743
+ const pending = /* @__PURE__ */ new Map();
6744
+ const noticesSent = /* @__PURE__ */ new Map();
6745
+ let inflight = Promise.resolve();
6746
+ const snapshot = () => {
6747
+ const at = now();
6748
+ const out = /* @__PURE__ */ new Map();
6749
+ for (const child of deps.getChildren()) {
6750
+ const id = child.happySessionId;
6751
+ if (!id || out.has(id)) continue;
6752
+ const meta = child.happySessionMetadataFromLocalWebhook;
6753
+ out.set(id, {
6754
+ sessionId: id,
6755
+ kind: "managed",
6756
+ pid: child.pid,
6757
+ ...meta?.path ? { cwd: meta.path } : {},
6758
+ ...meta?.flavor ? { flavor: meta.flavor } : {},
6759
+ ...meta?.summary?.text ? { title: meta.summary.text } : {},
6760
+ ...meta?.variant ?? child.variant ? { variant: meta?.variant ?? child.variant } : {},
6761
+ edits: tracker.editsOf(id, at)
6762
+ });
6763
+ }
6764
+ for (const mirror of getMirrors()) {
6765
+ if (out.has(mirror.sessionId)) continue;
6766
+ out.set(mirror.sessionId, {
6767
+ sessionId: mirror.sessionId,
6768
+ kind: "mirror",
6769
+ flavor: "terminal-mirror",
6770
+ ...mirror.cwd ? { cwd: mirror.cwd } : {},
6771
+ ...mirror.title ? { title: mirror.title } : {},
6772
+ edits: tracker.editsOf(mirror.sessionId, at)
6773
+ });
6774
+ }
6775
+ return out;
6776
+ };
6777
+ const deliver = async (recipient, peer, peerId, paths, peerEditedAgoMs, noticeId) => {
6778
+ if (recipient.kind === "mirror") return;
6779
+ const persisted = readPersisted()[recipient.sessionId];
6780
+ if (!persisted) {
6781
+ logger.debug(`[PEERS] no key for ${recipient.sessionId}; conflict notice ${noticeId} not delivered`);
6782
+ return;
6783
+ }
6784
+ const peerSender = senderOf(peer, peerId);
6785
+ if (!peerSender.title && peer?.kind === "managed") {
6786
+ const peerKey = readPersisted()[peer.sessionId];
6787
+ if (peerKey) {
6788
+ try {
6789
+ peerSender.title = await liveTitle(peer.sessionId, peerKey);
6790
+ } catch {
6791
+ }
6792
+ }
6793
+ }
6794
+ const text = formatEditConflictNotice({ id: noticeId, path: paths[0], paths, peer: peerSender, peerEditedAgoMs, windowMs });
6795
+ try {
6796
+ await send(recipient.sessionId, persisted, text, "edit-conflict", {
6797
+ sentFrom: SESSION_PEER_SENT_FROM,
6798
+ delivery: "steer",
6799
+ localId: `edit-conflict-${noticeId}-${recipient.sessionId}`
6800
+ });
6801
+ logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} (${paths.length} file(s), first ${paths[0]}, peer ${peerId})`);
6802
+ } catch (error) {
6803
+ logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} failed:`, error);
6804
+ }
6805
+ };
6806
+ const flushPair = async (key) => {
6807
+ const pair = pending.get(key);
6808
+ if (!pair) return;
6809
+ pending.delete(key);
6810
+ const at = now();
6811
+ const sent = (noticesSent.get(key) ?? []).filter((t) => at - t <= windowMs);
6812
+ if (sent.length >= maxNoticesPerPair) {
6813
+ noticesSent.set(key, sent);
6814
+ logger.debug(`[PEERS] pair ${pair.a}/${pair.b}: ${maxNoticesPerPair} notices already sent this window; ${pair.paths.size} conflicting path(s) not announced`);
6815
+ return;
6816
+ }
6817
+ sent.push(at);
6818
+ noticesSent.set(key, sent);
6819
+ const sessions = snapshot();
6820
+ const a = sessions.get(pair.a);
6821
+ const b = sessions.get(pair.b);
6822
+ const paths = [...pair.paths.keys()];
6823
+ const noticeId = randomUUID().slice(0, 8);
6824
+ const latestBy = (sessionId) => Math.max(...[...pair.paths.values()].map((c) => c.editor.sessionId === sessionId ? c.editor.at : c.peer.at));
6825
+ const agoFor = (recipientId, otherId) => Math.max(0, latestBy(recipientId) - latestBy(otherId));
6826
+ if (a) await deliver(a, b, pair.b, paths, agoFor(pair.a, pair.b), noticeId);
6827
+ if (b) await deliver(b, a, pair.a, paths, agoFor(pair.b, pair.a), noticeId);
6828
+ };
6829
+ const enqueue = (conflict) => {
6830
+ const key = pairKey(conflict.editor.sessionId, conflict.peer.sessionId);
6831
+ let pair = pending.get(key);
6832
+ if (!pair) {
6833
+ pair = { a: conflict.editor.sessionId, b: conflict.peer.sessionId, paths: /* @__PURE__ */ new Map(), timer: null };
6834
+ pair.timer = schedule(() => {
6835
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6836
+ });
6837
+ }, coalesceMs);
6838
+ pending.set(key, pair);
6839
+ }
6840
+ pair.paths.set(conflict.editor.path, conflict);
6841
+ };
6842
+ return {
6843
+ list: () => [...snapshot().values()],
6844
+ onEdit(edit) {
6845
+ const sessions = snapshot();
6846
+ const self = sessions.get(edit.sessionId);
6847
+ if (!self) {
6848
+ logger.debug(`[PEERS] edit from unknown/dead session ${edit.sessionId} ignored`);
6849
+ return [];
6850
+ }
6851
+ const current = now();
6852
+ const at = typeof edit.at === "number" && Number.isFinite(edit.at) ? Math.min(edit.at, current) : current;
6853
+ if (current - at > windowMs) {
6854
+ logger.debug(`[PEERS] edit from ${edit.sessionId} on ${edit.path} is ${Math.round((current - at) / 6e4)}m old (replayed history); ignored`);
6855
+ return [];
6856
+ }
6857
+ const cwd = edit.cwd ?? self.cwd ?? process.cwd();
6858
+ const path = normalize(edit.path, cwd);
6859
+ const conflicts = tracker.record({ sessionId: edit.sessionId, path, tool: edit.tool, at }, (id) => sessions.has(id));
6860
+ for (const conflict of conflicts) enqueue(conflict);
6861
+ return conflicts;
6862
+ },
6863
+ async flush() {
6864
+ for (const key of [...pending.keys()]) {
6865
+ const pair = pending.get(key);
6866
+ if (pair?.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6867
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6868
+ });
6869
+ }
6870
+ await inflight;
6871
+ },
6872
+ forget(sessionId) {
6873
+ tracker.forget(sessionId);
6874
+ for (const [key, pair] of pending) {
6875
+ if (pair.a === sessionId || pair.b === sessionId) {
6876
+ if (pair.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6877
+ pending.delete(key);
6878
+ }
6879
+ }
6880
+ },
6881
+ setMirrorSource(source) {
6882
+ getMirrors = source;
6883
+ }
6884
+ };
6885
+ }
6886
+
6589
6887
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6590
6888
 
6591
6889
  \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
@@ -8505,6 +8803,73 @@ class ClaudeAuthService {
8505
8803
  }
8506
8804
  }
8507
8805
 
8806
+ const CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
8807
+ function pathOf(input) {
8808
+ if (!input || typeof input !== "object") return null;
8809
+ const record = input;
8810
+ const candidate = record.file_path ?? record.notebook_path;
8811
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
8812
+ }
8813
+ function extractClaudeEditPaths(body) {
8814
+ if (!body || typeof body !== "object") return [];
8815
+ const line = body;
8816
+ if (line.type !== "assistant") return [];
8817
+ const content = line.message?.content;
8818
+ if (!Array.isArray(content)) return [];
8819
+ const stamp = line.timestamp;
8820
+ const at = typeof stamp === "string" ? Date.parse(stamp) : typeof stamp === "number" ? stamp : NaN;
8821
+ const edits = [];
8822
+ for (const block of content) {
8823
+ if (!block || typeof block !== "object") continue;
8824
+ const b = block;
8825
+ if (b.type !== "tool_use" || typeof b.name !== "string" || !CLAUDE_EDIT_TOOLS.has(b.name)) continue;
8826
+ const path = pathOf(b.input);
8827
+ if (path) edits.push({ path, tool: b.name, ...Number.isFinite(at) ? { at } : {} });
8828
+ }
8829
+ return edits;
8830
+ }
8831
+ function extractCodexPatchPaths(changes) {
8832
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
8833
+ return Object.keys(changes).filter((path) => path.trim() !== "").map((path) => ({ path, tool: "CodexPatch" }));
8834
+ }
8835
+ function extractAcpEditPaths(toolKind, args) {
8836
+ if (!args || typeof args !== "object") return [];
8837
+ const a = args;
8838
+ if (a.piTool === "write" || a.piTool === "edit") {
8839
+ const raw = a.rawInput;
8840
+ const path = raw && typeof raw.path === "string" && raw.path.trim() !== "" ? raw.path : null;
8841
+ return path ? [{ path, tool: a.piTool }] : [];
8842
+ }
8843
+ if (typeof a.piTool === "string") return [];
8844
+ if (toolKind !== "edit" || !Array.isArray(a.locations)) return [];
8845
+ const edits = [];
8846
+ for (const location of a.locations) {
8847
+ const path = location && typeof location === "object" ? location.path : void 0;
8848
+ if (typeof path === "string" && path.trim() !== "" && !edits.some((e) => e.path === path)) edits.push({ path, tool: "edit" });
8849
+ }
8850
+ return edits;
8851
+ }
8852
+ class EditReportThrottle {
8853
+ constructor(intervalMs = 5e3) {
8854
+ this.intervalMs = intervalMs;
8855
+ }
8856
+ lastReportedAt = /* @__PURE__ */ new Map();
8857
+ /** Returns the edits that should be reported now and remembers them. */
8858
+ take(edits, now = Date.now()) {
8859
+ const due = [];
8860
+ for (const edit of edits) {
8861
+ const last = this.lastReportedAt.get(edit.path);
8862
+ if (last !== void 0 && now - last < this.intervalMs) continue;
8863
+ this.lastReportedAt.set(edit.path, now);
8864
+ due.push(edit);
8865
+ }
8866
+ if (this.lastReportedAt.size > 512) {
8867
+ for (const [path, at] of this.lastReportedAt) if (now - at > this.intervalMs * 10) this.lastReportedAt.delete(path);
8868
+ }
8869
+ return due;
8870
+ }
8871
+ }
8872
+
8508
8873
  const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
8509
8874
  function parseLines(lines) {
8510
8875
  const out = [];
@@ -8541,12 +8906,15 @@ function createMirrorScanner(opts) {
8541
8906
  }
8542
8907
  tail.offset = 0;
8543
8908
  }
8909
+ let replaced = false;
8544
8910
  if (tail.offset > size) {
8545
8911
  logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
8546
8912
  tail.offset = 0;
8913
+ replaced = true;
8547
8914
  }
8548
8915
  if (tail.offset === size) return;
8549
8916
  const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
8917
+ const replay = isFirstBackfillRead || replaced;
8550
8918
  const handle = await open$1(tail.path, "r");
8551
8919
  let buf;
8552
8920
  try {
@@ -8566,7 +8934,7 @@ function createMirrorScanner(opts) {
8566
8934
  if (decision.truncated) opts.events.onBackfillTruncated?.();
8567
8935
  tail.mode = "from-eof";
8568
8936
  }
8569
- if (messages.length > 0) opts.events.onMessages(messages);
8937
+ if (messages.length > 0) opts.events.onMessages(messages, { replay });
8570
8938
  };
8571
8939
  const sync = new InvalidateSync(async () => {
8572
8940
  for (const tail of files.values()) {
@@ -8689,8 +9057,17 @@ function createMirrorManager(deps) {
8689
9057
  binding.client.updateMetadata((current) => ({ ...current, ...patch }));
8690
9058
  persistBinding(binding);
8691
9059
  };
8692
- const sendMessages = (binding, messages) => {
9060
+ const sendMessages = (binding, messages, context = { replay: false }) => {
8693
9061
  for (const message of messages) {
9062
+ if (deps.onEdit && binding.status === "active" && !context.replay) {
9063
+ for (const edit of extractClaudeEditPaths(message)) {
9064
+ try {
9065
+ deps.onEdit({ sessionId: binding.happySessionId, path: edit.path, tool: edit.tool, cwd: binding.metadata.path, ...edit.at !== void 0 ? { at: edit.at } : {} });
9066
+ } catch (error) {
9067
+ logger.debug(`[MIRROR] edit tap failed for terminal ${binding.terminalId}:`, error);
9068
+ }
9069
+ }
9070
+ }
8694
9071
  const key = mirrorLineKey(message);
8695
9072
  binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => mirrorLocalId(key, envelopeIndex) } : void 0);
8696
9073
  }
@@ -8699,7 +9076,7 @@ function createMirrorManager(deps) {
8699
9076
  const scanner = createMirrorScanner({
8700
9077
  backfillLines,
8701
9078
  events: {
8702
- onMessages: (messages) => sendMessages(binding, messages),
9079
+ onMessages: (messages, context) => sendMessages(binding, messages, context),
8703
9080
  onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
8704
9081
  type: "message",
8705
9082
  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`
@@ -8722,6 +9099,10 @@ function createMirrorManager(deps) {
8722
9099
  await binding.scanner?.cleanup();
8723
9100
  binding.scanner = null;
8724
9101
  binding.client.closeClaudeSessionTurn("completed");
9102
+ try {
9103
+ deps.onSessionEnded?.(binding.happySessionId);
9104
+ } catch {
9105
+ }
8725
9106
  updateBindingMetadata(binding, {
8726
9107
  lifecycleState: "archived",
8727
9108
  lifecycleStateSince: Date.now(),
@@ -8934,6 +9315,13 @@ function createMirrorManager(deps) {
8934
9315
  }
8935
9316
  };
8936
9317
  return {
9318
+ listActiveMirrors() {
9319
+ return [...bindings.values()].filter((binding) => binding.status === "active").map((binding) => ({
9320
+ sessionId: binding.happySessionId,
9321
+ ...binding.metadata.path ? { cwd: binding.metadata.path } : {},
9322
+ ...binding.metadata.summary?.text ? { title: binding.metadata.summary.text } : {}
9323
+ }));
9324
+ },
8937
9325
  handleHookPayload(body) {
8938
9326
  const event = parseTerminalHookPayload(body);
8939
9327
  if (!event) {
@@ -9400,640 +9788,6 @@ Delegate from this working directory unless a different repository was explicitl
9400
9788
  ${goal}`;
9401
9789
  }
9402
9790
 
9403
- const MAX_LINE_CHARS = 500;
9404
- const MAX_ARGS_CHARS = 160;
9405
- const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
9406
- const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
9407
- function truncateText(text, max = MAX_LINE_CHARS) {
9408
- const chars = Array.from(text);
9409
- if (chars.length <= max) return text;
9410
- return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
9411
- }
9412
- function squash(text) {
9413
- return text.replace(/\s+/g, " ").trim();
9414
- }
9415
- function safeJson(value, max) {
9416
- try {
9417
- return truncateText(JSON.stringify(value) ?? "", max);
9418
- } catch {
9419
- return "[unserializable]";
9420
- }
9421
- }
9422
- function formatMessageBody(body, limits = DEFAULT_LIMITS) {
9423
- if (!body || typeof body !== "object") return null;
9424
- const b = body;
9425
- if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
9426
- const text = squash(b.content.text);
9427
- return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
9428
- }
9429
- if (b.role === "session" && b.content && typeof b.content === "object") {
9430
- const env = b.content;
9431
- const ev = env.ev;
9432
- if (!ev || typeof ev.t !== "string") return null;
9433
- switch (ev.t) {
9434
- case "text": {
9435
- if (ev.thinking) return null;
9436
- const text = squash(String(ev.text ?? ""));
9437
- if (text.length === 0) return null;
9438
- const role = env.role === "user" ? "user" : "assistant";
9439
- return `${role}: ${truncateText(text, limits.line)}`;
9440
- }
9441
- case "tool-call-start": {
9442
- const name = String(ev.name ?? "tool");
9443
- const title = squash(String(ev.title ?? ""));
9444
- const args = ev.args ? safeJson(ev.args, limits.args) : "";
9445
- const detail = title || args;
9446
- return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
9447
- }
9448
- case "service": {
9449
- const text = squash(String(ev.text ?? ""));
9450
- return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
9451
- }
9452
- case "turn-end": {
9453
- if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
9454
- return null;
9455
- }
9456
- default:
9457
- return null;
9458
- }
9459
- }
9460
- if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
9461
- const d = b.content.data;
9462
- switch (d.type) {
9463
- case "message": {
9464
- const text = squash(String(d.message ?? ""));
9465
- return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
9466
- }
9467
- case "tool-call":
9468
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
9469
- case "permission-request":
9470
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
9471
- default:
9472
- return null;
9473
- }
9474
- }
9475
- if (b.role === "agent" && b.content?.type === "output") {
9476
- const msg = b.content.data?.message;
9477
- if (msg && Array.isArray(msg.content)) {
9478
- const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
9479
- if (texts.length > 0) {
9480
- const role = msg.role === "user" ? "user" : "assistant";
9481
- return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
9482
- }
9483
- }
9484
- return null;
9485
- }
9486
- return null;
9487
- }
9488
- function formatTranscript(bodies, options = {}) {
9489
- const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
9490
- const lines = [];
9491
- for (const body of bodies) {
9492
- const line = formatMessageBody(body, limits);
9493
- if (line !== null) lines.push(line);
9494
- }
9495
- return lines.join("\n");
9496
- }
9497
-
9498
- function asRecord(value) {
9499
- return value && typeof value === "object" ? value : null;
9500
- }
9501
- function sessionEnvelope(body) {
9502
- const b = asRecord(body);
9503
- if (!b || b.role !== "session") return null;
9504
- const env = asRecord(b.content);
9505
- if (!env) return null;
9506
- const ev = asRecord(env.ev);
9507
- return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
9508
- }
9509
- function isUserPrompt(body) {
9510
- const b = asRecord(body);
9511
- return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
9512
- }
9513
- function agentPiece(body) {
9514
- const env = sessionEnvelope(body);
9515
- if (env && env.role !== "user") {
9516
- if (env.ev.t === "text" && !env.ev.thinking) {
9517
- const text = String(env.ev.text ?? "");
9518
- return text.trim().length > 0 ? { kind: "text", text } : null;
9519
- }
9520
- if (env.ev.t === "tool-call-start") return { kind: "tool" };
9521
- return null;
9522
- }
9523
- const b = asRecord(body);
9524
- if (b?.role === "agent" && b.content?.type === "acp") {
9525
- const d = asRecord(b.content.data);
9526
- if (d?.type === "message") {
9527
- const text = String(d.message ?? "");
9528
- return text.trim().length > 0 ? { kind: "text", text } : null;
9529
- }
9530
- if (d?.type === "tool-call") return { kind: "tool" };
9531
- }
9532
- return null;
9533
- }
9534
- function userPromptText(body) {
9535
- return isUserPrompt(body) ? body.content.text : null;
9536
- }
9537
- function analyzeLatestTurn(entries, options = {}) {
9538
- const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
9539
- let userIndex = -1;
9540
- for (let i = entries.length - 1; i >= 0; i--) {
9541
- if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
9542
- userIndex = i;
9543
- break;
9544
- }
9545
- }
9546
- let ended = false;
9547
- let status = null;
9548
- let error = null;
9549
- let texts = [];
9550
- let lastMarker = null;
9551
- let startedAfterPrompt = false;
9552
- const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
9553
- for (let i = userIndex + 1; i < entries.length; i++) {
9554
- const body = entries[i].body;
9555
- if (options.anchor && isUserPrompt(body)) {
9556
- if (ended) break;
9557
- continue;
9558
- }
9559
- const env = sessionEnvelope(body);
9560
- if (env?.ev?.t === "turn-start") {
9561
- lastMarker = "start";
9562
- startedAfterPrompt = true;
9563
- ended = false;
9564
- status = null;
9565
- error = null;
9566
- texts = [];
9567
- continue;
9568
- }
9569
- if (env?.ev?.t === "turn-end") {
9570
- lastMarker = "end";
9571
- if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
9572
- ended = true;
9573
- status = typeof env.ev.status === "string" ? env.ev.status : null;
9574
- error = typeof env.ev.error === "string" ? env.ev.error : null;
9575
- continue;
9576
- }
9577
- const piece = agentPiece(body);
9578
- if (!piece) continue;
9579
- if (piece.kind === "tool") texts = [];
9580
- else texts.push(piece.text);
9581
- }
9582
- if (userIndex < 0) ended = lastMarker === "end";
9583
- return {
9584
- userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
9585
- ended,
9586
- status: ended ? status : null,
9587
- error: ended ? error : null,
9588
- answer: texts.join("\n\n").trim(),
9589
- lastSeq
9590
- };
9591
- }
9592
-
9593
- const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9594
- async function openUserScopedSocket(token, options = {}) {
9595
- const socket = io(configuration.serverUrl, {
9596
- auth: {
9597
- token,
9598
- clientType: "user-scoped",
9599
- happyClient: `cli-session-ops/${configuration.currentCliVersion}`
9600
- },
9601
- path: "/v1/updates",
9602
- transports: ["websocket"],
9603
- reconnection: false,
9604
- autoConnect: false
9605
- });
9606
- await new Promise((resolve, reject) => {
9607
- const timer = setTimeout(() => {
9608
- socket.close();
9609
- reject(new Error(`Timed out connecting to ${configuration.serverUrl}`));
9610
- }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9611
- socket.once("connect", () => {
9612
- clearTimeout(timer);
9613
- resolve();
9614
- });
9615
- socket.once("connect_error", (error) => {
9616
- clearTimeout(timer);
9617
- socket.close();
9618
- reject(new Error(`Could not connect to ${configuration.serverUrl}: ${error.message}`));
9619
- });
9620
- socket.connect();
9621
- });
9622
- return {
9623
- async rpcCall(payload, timeoutMs) {
9624
- const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9625
- return ack;
9626
- },
9627
- close() {
9628
- socket.close();
9629
- }
9630
- };
9631
- }
9632
-
9633
- const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
9634
- const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
9635
- const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
9636
- const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
9637
- const RPC_RATE_MAX_RETRIES = 2;
9638
- function isRpcRateLimitAck(ack) {
9639
- if (!ack || typeof ack !== "object") return false;
9640
- const a = ack;
9641
- if (a.ok !== false) return false;
9642
- if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
9643
- return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
9644
- }
9645
- function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
9646
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
9647
- return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
9648
- }
9649
- const exponent = Math.max(0, Math.min(attempt, 32) - 1);
9650
- const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
9651
- return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
9652
- }
9653
-
9654
- const PERMISSION_OPS_CLIENT = "session-ops";
9655
- const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9656
- const SETTLE_TIMEOUT_MS = 5e3;
9657
- const SETTLE_POLL_MS = 500;
9658
- function buildPermissionRpcPayload(requestId, verdict) {
9659
- if (verdict.kind === "approve") {
9660
- return {
9661
- id: requestId,
9662
- approved: true,
9663
- decision: verdict.forSession ? "approved_for_session" : "approved"
9664
- };
9665
- }
9666
- return {
9667
- id: requestId,
9668
- approved: false,
9669
- decision: "denied",
9670
- ...verdict.reason ? { reason: verdict.reason } : {}
9671
- };
9672
- }
9673
- function interpretPermissionAck(ack, decryptResult) {
9674
- if (!ack.ok) {
9675
- const message = ack.error ?? "RPC call failed";
9676
- if (message === "RPC method not available") {
9677
- return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9678
- }
9679
- if (message === "RPC target disconnected") {
9680
- return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9681
- }
9682
- if (/timed? ?out/i.test(message)) {
9683
- return { status: "timeout", message };
9684
- }
9685
- if (isRpcRateLimitAck(ack)) {
9686
- return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
9687
- }
9688
- return { status: "rejected", message };
9689
- }
9690
- let body = null;
9691
- if (typeof ack.result === "string" && ack.result.length > 0) {
9692
- try {
9693
- body = decryptResult(ack.result);
9694
- } catch {
9695
- body = null;
9696
- }
9697
- }
9698
- if (body && typeof body === "object" && typeof body.error === "string") {
9699
- return { status: "handler-error", message: body.error };
9700
- }
9701
- return { status: "acknowledged" };
9702
- }
9703
- function pendingRequestsOf(agentState, now) {
9704
- const requests = agentState?.requests;
9705
- if (!requests || typeof requests !== "object") return [];
9706
- return Object.entries(requests).map(([id, request]) => ({
9707
- id,
9708
- tool: typeof request?.tool === "string" ? request.tool : "unknown",
9709
- ...request?.kind ? { kind: request.kind } : {},
9710
- ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
9711
- })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
9712
- }
9713
- const RPC_DEADLINE_GRACE_MS = 2e3;
9714
- function withDeadline(promise, ms, message) {
9715
- return new Promise((resolve, reject) => {
9716
- const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
9717
- promise.then(
9718
- (value) => {
9719
- clearTimeout(timer);
9720
- resolve(value);
9721
- },
9722
- (error) => {
9723
- clearTimeout(timer);
9724
- reject(error);
9725
- }
9726
- );
9727
- });
9728
- }
9729
- async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
9730
- const now = deps.now ?? Date.now;
9731
- const persisted = (deps.readPersisted ?? readPersistedSessions)()[sessionId];
9732
- if (!persisted) {
9733
- throw new Error(
9734
- `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.`
9735
- );
9736
- }
9737
- const token = await (deps.bearerToken ?? bearerToken$1)();
9738
- const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
9739
- const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9740
- if (!before.some((request) => request.id === requestId)) {
9741
- const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
9742
- throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
9743
- }
9744
- const payload = buildPermissionRpcPayload(requestId, verdict);
9745
- const key = decodeBase64(persisted.encryptionKey);
9746
- const params = encodeBase64(encrypt(key, persisted.encryptionVariant, payload));
9747
- const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
9748
- const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
9749
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9750
- let ack;
9751
- try {
9752
- for (let attempt = 1; ; attempt++) {
9753
- try {
9754
- ack = await withDeadline(
9755
- transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
9756
- rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
9757
- `permission RPC did not settle within ${rpcTimeoutMs}ms`
9758
- );
9759
- } catch (error) {
9760
- ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
9761
- }
9762
- if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
9763
- const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
9764
- deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
9765
- await sleep(waitMs);
9766
- }
9767
- } finally {
9768
- transport.close();
9769
- }
9770
- const outcome = interpretPermissionAck(ack, (encrypted) => decrypt(key, persisted.encryptionVariant, decodeBase64(encrypted)));
9771
- if (outcome.status !== "acknowledged") {
9772
- return { sessionId, requestId, payload, outcome };
9773
- }
9774
- const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
9775
- let settled = false;
9776
- for (; ; ) {
9777
- const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9778
- if (!pending.some((request) => request.id === requestId)) {
9779
- settled = true;
9780
- break;
9781
- }
9782
- if (now() >= deadline) break;
9783
- await sleep(SETTLE_POLL_MS);
9784
- }
9785
- return { sessionId, requestId, payload, outcome, settled };
9786
- }
9787
- async function bearerToken$1() {
9788
- const credentials = await readCredentialsForConfiguredRelay();
9789
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9790
- return credentials.token;
9791
- }
9792
- async function fetchDecryptedAgentState(sessionId, persisted, token) {
9793
- const response = await axios.get(
9794
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
9795
- {
9796
- headers: {
9797
- "Authorization": `Bearer ${token}`,
9798
- "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9799
- },
9800
- timeout: 15e3,
9801
- // 404 is a real answer ("not yours / gone"), not a transport fault.
9802
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
9803
- }
9804
- );
9805
- if (response.status === 404) {
9806
- 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).`);
9807
- }
9808
- const raw = response.data?.session?.agentState;
9809
- if (typeof raw !== "string" || raw.length === 0) return null;
9810
- return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(raw));
9811
- }
9812
-
9813
- const SESSION_OPS_CLIENT = "session-ops";
9814
- const DEFAULT_RECENT_LIMIT = 15;
9815
- const MAX_READ_LIMIT = 100;
9816
- function mergeSessionSummaries(live, persisted, options = {}) {
9817
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9818
- const seen = /* @__PURE__ */ new Set();
9819
- const summaries = [];
9820
- for (const child of live) {
9821
- const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9822
- if (!id || seen.has(id)) continue;
9823
- seen.add(id);
9824
- summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9825
- }
9826
- 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);
9827
- for (const [id, entry] of rest) {
9828
- summaries.push(toSummary(id, entry, { live: false }));
9829
- }
9830
- if (!options.tag) return summaries;
9831
- const wanted = options.tag;
9832
- return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9833
- }
9834
- function livenessOf(child) {
9835
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
9836
- }
9837
- function sessionLiveness(live, sessionId) {
9838
- const child = live.find((entry) => entry.happySessionId === sessionId);
9839
- return child ? livenessOf(child) : { live: false };
9840
- }
9841
- function toSummary(id, persisted, extra) {
9842
- const meta = persisted?.metadata;
9843
- return {
9844
- id,
9845
- live: extra.live,
9846
- ...extra.pid !== void 0 ? { pid: extra.pid } : {},
9847
- ...meta?.summary?.text ? { title: meta.summary.text } : {},
9848
- ...meta?.path ? { cwd: meta.path } : {},
9849
- ...meta?.flavor ? { flavor: meta.flavor } : {},
9850
- ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
9851
- ...meta?.variant ? { variant: meta.variant } : {},
9852
- url: sessionWebUrl(id),
9853
- ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
9854
- };
9855
- }
9856
- async function listSessions(options = {}) {
9857
- const live = await listDaemonSessions();
9858
- return mergeSessionSummaries(live, readPersistedSessions(), options);
9859
- }
9860
- function summarizeAccountSession(row, persisted, liveIds, now) {
9861
- const base = {
9862
- id: row.id,
9863
- live: liveIds.has(row.id),
9864
- url: sessionWebUrl(row.id),
9865
- decryptable: false,
9866
- active: row.active === true,
9867
- archived: typeof row.archivedAt === "number",
9868
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
9869
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
9870
- attention: false
9871
- };
9872
- if (!persisted) return base;
9873
- const key = decodeBase64(persisted.encryptionKey);
9874
- let metadata = null;
9875
- let agentState = null;
9876
- try {
9877
- metadata = row.metadata ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.metadata)) : null;
9878
- agentState = row.agentState ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.agentState)) : null;
9879
- } catch {
9880
- return base;
9881
- }
9882
- if (!metadata) return base;
9883
- const meta = metadata;
9884
- const pending = pendingRequestsOf(agentState, now);
9885
- return {
9886
- ...base,
9887
- decryptable: true,
9888
- ...meta.summary?.text ? { title: meta.summary.text } : {},
9889
- ...meta.path ? { cwd: meta.path } : {},
9890
- ...meta.flavor ? { flavor: meta.flavor } : {},
9891
- ...meta.tags?.length ? { tags: [...meta.tags] } : {},
9892
- ...meta.variant ? { variant: meta.variant } : {},
9893
- ...meta.machineId ? { machineId: meta.machineId } : {},
9894
- ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
9895
- pending,
9896
- attention: pending.length > 0
9897
- };
9898
- }
9899
- function orderAccountSessions(summaries, options = {}) {
9900
- const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
9901
- const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
9902
- let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
9903
- const byRank = rank(a) - rank(b);
9904
- if (byRank !== 0) return byRank;
9905
- if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
9906
- return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
9907
- });
9908
- if (options.tag) {
9909
- const wanted = options.tag;
9910
- rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
9911
- }
9912
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9913
- let idleKept = 0;
9914
- return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
9915
- }
9916
- async function listAccountSessions(options = {}) {
9917
- const token = await bearerToken();
9918
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
9919
- headers: {
9920
- "Authorization": `Bearer ${token}`,
9921
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9922
- },
9923
- timeout: 15e3
9924
- });
9925
- const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
9926
- const persisted = readPersistedSessions();
9927
- const live = await listDaemonSessions();
9928
- const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
9929
- const now = Date.now();
9930
- const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
9931
- return orderAccountSessions(summaries, options);
9932
- }
9933
- async function bearerToken() {
9934
- const credentials = await readCredentialsForConfiguredRelay();
9935
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9936
- return credentials.token;
9937
- }
9938
- function requirePersisted(sessionId) {
9939
- const persisted = readPersistedSessions()[sessionId];
9940
- if (!persisted) {
9941
- throw new Error(
9942
- `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
9943
- );
9944
- }
9945
- return persisted;
9946
- }
9947
- async function readSessionLog(sessionId, persisted, limit) {
9948
- const token = await bearerToken();
9949
- const response = await axios.get(
9950
- `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
9951
- {
9952
- params: { before_seq: 2147483647, limit },
9953
- headers: {
9954
- "Authorization": `Bearer ${token}`,
9955
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9956
- },
9957
- timeout: 15e3
9958
- }
9959
- );
9960
- const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
9961
- messages.reverse();
9962
- const key = decodeBase64(persisted.encryptionKey);
9963
- return messages.map((message) => {
9964
- if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
9965
- try {
9966
- return { seq: message.seq, body: decrypt(key, persisted.encryptionVariant, decodeBase64(message.content.c)) };
9967
- } catch {
9968
- return { seq: message.seq, body: null };
9969
- }
9970
- });
9971
- }
9972
- const TURN_WINDOW = 500;
9973
- async function readSessionTranscript(sessionId, limit, options = {}) {
9974
- const persisted = requirePersisted(sessionId);
9975
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
9976
- const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
9977
- const shown = entries.slice(-bounded);
9978
- const live = await listDaemonSessions();
9979
- return {
9980
- summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
9981
- messageCount: shown.length,
9982
- transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
9983
- turn: analyzeLatestTurn(entries)
9984
- };
9985
- }
9986
- async function readSessionMetadata(sessionId, persisted) {
9987
- const token = await bearerToken();
9988
- const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
9989
- headers: {
9990
- "Authorization": `Bearer ${token}`,
9991
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
9992
- },
9993
- timeout: 15e3
9994
- });
9995
- const ciphertext = response.data?.session?.metadata;
9996
- if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
9997
- return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(ciphertext));
9998
- }
9999
- class TurnWaitTimeoutError extends Error {
10000
- constructor(sessionId, timeoutMs, turn) {
10001
- super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
10002
- this.sessionId = sessionId;
10003
- this.timeoutMs = timeoutMs;
10004
- this.turn = turn;
10005
- }
10006
- }
10007
- async function waitForTurnEnd(sessionId, options) {
10008
- const persisted = requirePersisted(sessionId);
10009
- const pollMs = options.pollMs ?? 3e3;
10010
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10011
- const deadline = Date.now() + options.timeoutMs;
10012
- while (true) {
10013
- const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
10014
- if (turn.ended) return turn;
10015
- if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
10016
- await sleep(pollMs);
10017
- }
10018
- }
10019
- async function stopSession(sessionId) {
10020
- return await stopDaemonSession(sessionId);
10021
- }
10022
- async function archiveSession(sessionId) {
10023
- const token = await bearerToken();
10024
- await axios.post(
10025
- `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
10026
- {},
10027
- {
10028
- headers: {
10029
- "Authorization": `Bearer ${token}`,
10030
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
10031
- },
10032
- timeout: 1e4
10033
- }
10034
- );
10035
- }
10036
-
10037
9791
  function receiptPath(home, operationId) {
10038
9792
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
10039
9793
  return join(home, "teams", "effects", `${operationId}.json`);
@@ -10623,7 +10377,7 @@ function automationRunAnchor(runId) {
10623
10377
  async function defaultReadTurn(sessionId, runId) {
10624
10378
  const persisted = readPersistedSessions()[sessionId];
10625
10379
  if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10626
- return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10380
+ return analyzeLatestTurn(await readSessionLog$1(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10627
10381
  }
10628
10382
  async function defaultSend(sessionId, text, meta) {
10629
10383
  const persisted = await waitForSessionKey(sessionId, 15e3);
@@ -11122,6 +10876,7 @@ async function startDaemon() {
11122
10876
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
11123
10877
  const pidToTrackedSession = /* @__PURE__ */ new Map();
11124
10878
  const turnActivity = new TurnActivityTracker();
10879
+ const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
11125
10880
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
11126
10881
  let teamWorker;
11127
10882
  let automationRunner;
@@ -11981,6 +11736,7 @@ async function startDaemon() {
11981
11736
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
11982
11737
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
11983
11738
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11739
+ if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
11984
11740
  if (session?.happySessionId && session.encryption) {
11985
11741
  sessionIdToFinishedSession.set(session.happySessionId, session);
11986
11742
  logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
@@ -12059,11 +11815,17 @@ async function startDaemon() {
12059
11815
  getChildren: getCurrentChildren,
12060
11816
  stopSession,
12061
11817
  spawnSession,
11818
+ resumeSession,
11819
+ // B-501: `very-happy send --resume`
12062
11820
  requestShutdown: () => requestShutdown("happy-cli"),
12063
11821
  onHappySessionWebhook,
12064
11822
  onSessionStateEvent,
12065
11823
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
12066
11824
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
11825
+ onSessionEdit: (edit) => {
11826
+ peerCoordinator.onEdit(edit);
11827
+ },
11828
+ listPeers: () => peerCoordinator.list(),
12067
11829
  pushClipboard: (text, terminalId) => {
12068
11830
  if (!apiMachineRef) {
12069
11831
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -12129,9 +11891,12 @@ async function startDaemon() {
12129
11891
  const mirrorManager = createMirrorManager({
12130
11892
  api,
12131
11893
  machineId,
12132
- onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
11894
+ onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
11895
+ onEdit: (edit) => peerCoordinator.onEdit(edit),
11896
+ onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
12133
11897
  });
12134
11898
  mirrorManagerRef = mirrorManager;
11899
+ peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
12135
11900
  apiMachine.setMirrorIntegration({
12136
11901
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
12137
11902
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
@@ -12562,11 +12327,13 @@ function fail(text) {
12562
12327
  function describeSession(id, persisted, extra) {
12563
12328
  const meta = persisted?.metadata;
12564
12329
  const parts = [id];
12330
+ if (extra?.live !== void 0) parts.push(extra.live ? "[running]" : "[not running]");
12565
12331
  if (meta?.summary?.text) parts.push(`title="${meta.summary.text}"`);
12566
12332
  if (meta?.path) parts.push(`cwd=${meta.path}`);
12567
12333
  if (meta?.flavor) parts.push(`agent=${meta.flavor}`);
12568
12334
  if (meta?.tags?.length) parts.push(`tags=${meta.tags.join(",")}`);
12569
12335
  if (meta?.variant === "assistant") parts.push("(assistant)");
12336
+ if (extra?.pid) parts.push(`pid=${extra.pid}`);
12570
12337
  parts.push(sessionWebUrl(id));
12571
12338
  return parts.join(" ");
12572
12339
  }
@@ -12620,19 +12387,27 @@ ${body}`);
12620
12387
  }
12621
12388
  });
12622
12389
  mcp.registerTool("session_send", {
12623
- description: "Send a user message into an existing session on this machine. Returns immediately after delivery \u2014 it does NOT wait for the session to respond; use session_read later to check progress.",
12390
+ description: "Send a user message into an existing session on this machine. Only delivers when a live wrapper will read it: an archived / offline session is refused (the server would store the message unread) unless resume:true brings it back on this machine first. Returns immediately after delivery \u2014 it does NOT wait for the session to respond; use session_read later to check progress.",
12624
12391
  title: "Send Message to Session",
12625
12392
  inputSchema: {
12626
12393
  sessionId: z.string().describe("The Happy session id to message"),
12627
- text: z.string().describe("The message text to send")
12394
+ text: z.string().describe("The message text to send"),
12395
+ resume: z.boolean().optional().describe("If the session is archived or offline, resume it on this machine (like the web Restore button) and wait until it is live before sending")
12628
12396
  }
12629
12397
  }, async (args) => {
12630
12398
  if (!isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12631
12399
  if (typeof args.text !== "string" || args.text.trim().length === 0) return fail("text must be non-empty");
12632
12400
  try {
12633
12401
  const persisted = await waitForSessionKey(args.sessionId, 0);
12634
- await sendUserMessage(args.sessionId, persisted, args.text, MCP_CLIENT_TAG);
12635
- return ok(`Message delivered to ${describeSession(args.sessionId, persisted)}`);
12402
+ const result = await deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12403
+ resume: args.resume === true,
12404
+ resumeHint: "Pass resume:true to bring it back on this machine, or spawn a new session."
12405
+ });
12406
+ if (!result.delivered) {
12407
+ const detail = result.error ?? `session is ${result.status}`;
12408
+ return fail(result.stored ? `Not delivered: ${detail}` : `Session ${args.sessionId} is ${result.status}: ${detail}`);
12409
+ }
12410
+ return ok(`Message delivered${result.resumed ? " (session resumed on this machine)" : ""} to ${describeSession(args.sessionId, persisted, { live: true })}`);
12636
12411
  } catch (error) {
12637
12412
  return fail(`Failed to send: ${error instanceof Error ? error.message : String(error)}`);
12638
12413
  }
@@ -12819,13 +12594,14 @@ function normalizeProgress(value) {
12819
12594
  return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
12820
12595
  }
12821
12596
 
12822
- function createMcpServer(handlers, options, sessionId) {
12597
+ function createMcpServer(handlers, options, sessionId, client) {
12823
12598
  const mcp = new McpServer({
12824
12599
  name: "Very Happy Tools",
12825
12600
  version: "1.0.0"
12826
12601
  });
12827
12602
  registerTeamsTools(mcp, sessionId);
12828
12603
  registerAutomationTools(mcp);
12604
+ if (client) registerSessionPeerTools(mcp, createSessionPeerToolExecutor(client));
12829
12605
  mcp.registerTool("change_title", {
12830
12606
  description: "Change the title of the current chat session",
12831
12607
  title: "Change Chat Title",
@@ -13028,7 +12804,7 @@ async function startHappyServer(client, options) {
13028
12804
  }
13029
12805
  return;
13030
12806
  }
13031
- const mcp = createMcpServer(handlers, options, client.sessionId);
12807
+ const mcp = createMcpServer(handlers, options, client.sessionId, client);
13032
12808
  try {
13033
12809
  const transport = new StreamableHTTPServerTransport({
13034
12810
  sessionIdGenerator: void 0
@@ -13069,6 +12845,7 @@ async function startHappyServer(client, options) {
13069
12845
  REPORT_PROGRESS_TOOL_NAME,
13070
12846
  ...TEAM_TOOL_NAMES,
13071
12847
  ...AUTOMATION_TOOL_NAMES,
12848
+ ...SESSION_PEER_TOOL_NAMES,
13072
12849
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
13073
12850
  ],
13074
12851
  stop: () => {
@@ -13405,7 +13182,7 @@ function registerSideQuestionHandler(rpc, deps) {
13405
13182
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13406
13183
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13407
13184
  const fork = deps.run ?? (async (input) => {
13408
- const { query } = await import('./index-BXA9fxbK.mjs');
13185
+ const { query } = await import('./index-DVVRqaVG.mjs');
13409
13186
  return runSideQuestion(query, input);
13410
13187
  });
13411
13188
  const run = async (input, live) => {
@@ -14192,6 +13969,17 @@ async function runClaude(credentials, options = {}) {
14192
13969
  });
14193
13970
  session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
14194
13971
  session.on("turn-ended", () => boardAnalyzer.onTurnEnd());
13972
+ const editThrottle = new EditReportThrottle();
13973
+ session.on("claude-session-message", (body) => {
13974
+ try {
13975
+ for (const edit of editThrottle.take(extractClaudeEditPaths(body))) {
13976
+ reportSessionEditToDaemon({ sessionId: response.id, path: edit.path, tool: edit.tool, cwd: workingDirectory, ...edit.at !== void 0 ? { at: edit.at } : {} });
13977
+ }
13978
+ } catch (error) {
13979
+ logger.debug("[START] edit report failed:", error);
13980
+ }
13981
+ });
13982
+ options = { ...options, claudeEnvVars: { ...options.claudeEnvVars, VH_PEER_SESSION_ID: response.id } };
14195
13983
  const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
14196
13984
  const remoteScanner = await createSessionScanner({
14197
13985
  sessionId: initialScannerSessionId,
@@ -18178,6 +17966,7 @@ async function runCodex(opts) {
18178
17966
  return "denied";
18179
17967
  }
18180
17968
  });
17969
+ const editThrottle = new EditReportThrottle();
18181
17970
  client.setEventHandler((msg) => {
18182
17971
  if (msg.type === "token_count") {
18183
17972
  session.sendAgentUsageSnapshot("codex", msg);
@@ -18241,6 +18030,9 @@ async function runCodex(opts) {
18241
18030
  }
18242
18031
  if (msg.type === "patch_apply_begin") {
18243
18032
  const { changes } = msg;
18033
+ for (const edit of editThrottle.take(extractCodexPatchPaths(changes))) {
18034
+ reportSessionEditToDaemon({ sessionId: session.sessionId, path: edit.path, tool: edit.tool, cwd: process.cwd() });
18035
+ }
18244
18036
  const changeCount = Object.keys(changes).length;
18245
18037
  const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
18246
18038
  messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
@@ -18286,6 +18078,7 @@ async function runCodex(opts) {
18286
18078
  };
18287
18079
  let first = true;
18288
18080
  let appendSystemPromptInjected = false;
18081
+ process.env.VH_PEER_SESSION_ID = session.sessionId;
18289
18082
  try {
18290
18083
  logger.debug("[codex]: client.connect begin");
18291
18084
  await client.connect();
@@ -18699,7 +18492,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18699
18492
  return;
18700
18493
  } else if (subcommand === "install-terminal-hooks") {
18701
18494
  try {
18702
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-Bpl32K75.mjs');
18495
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-tIg7mANc.mjs');
18703
18496
  const command = parseTerminalHooksArgs(args.slice(1));
18704
18497
  if (command.action === "help") {
18705
18498
  console.log(TERMINAL_HOOKS_HELP);
@@ -18716,7 +18509,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18716
18509
  }
18717
18510
  } else if (subcommand === "agent-home") {
18718
18511
  try {
18719
- const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-D68Xh2ae.mjs');
18512
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-ByLb2HFg.mjs');
18720
18513
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18721
18514
  process.exit(0);
18722
18515
  } catch (error) {
@@ -18725,7 +18518,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18725
18518
  }
18726
18519
  } else if (subcommand === "spawn") {
18727
18520
  try {
18728
- const { handleSpawnCommand } = await import('./spawn-C6fRkedx.mjs');
18521
+ const { handleSpawnCommand } = await import('./spawn-BaPnjClT.mjs');
18729
18522
  await handleSpawnCommand(args.slice(1));
18730
18523
  } catch (error) {
18731
18524
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18737,7 +18530,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18737
18530
  return;
18738
18531
  } else if (subcommand === "sessions") {
18739
18532
  try {
18740
- const { handleSessionsCommand } = await import('./sessions-iBnYzVUK.mjs');
18533
+ const { handleSessionsCommand } = await import('./sessions-JaVXlSqj.mjs');
18741
18534
  await handleSessionsCommand(args.slice(1));
18742
18535
  } catch (error) {
18743
18536
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18749,7 +18542,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18749
18542
  return;
18750
18543
  } else if (subcommand === "send") {
18751
18544
  try {
18752
- const { handleSendCommand } = await import('./send-Cs-RwqNf.mjs');
18545
+ const { handleSendCommand } = await import('./send-DJStmLBv.mjs');
18753
18546
  await handleSendCommand(args.slice(1));
18754
18547
  } catch (error) {
18755
18548
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18855,9 +18648,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18855
18648
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
18856
18649
  const projectId = args[3];
18857
18650
  try {
18858
- const { saveGoogleCloudProjectToConfig } = await import('./config-CHJ189UJ.mjs');
18859
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-BtAuKRl2.mjs').then(function (n) { return n.al; });
18860
- const { ApiClient: ApiClient2 } = await import('./types-BvqOVUOW.mjs').then(function (n) { return n.ah; });
18651
+ const { saveGoogleCloudProjectToConfig } = await import('./config-BPZ2Y6o8.mjs');
18652
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-C_DW0M0q.mjs').then(function (n) { return n.aG; });
18653
+ const { ApiClient: ApiClient2 } = await import('./types-CkbfdRzq.mjs').then(function (n) { return n.a1; });
18861
18654
  let userEmail = void 0;
18862
18655
  try {
18863
18656
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -18888,7 +18681,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18888
18681
  }
18889
18682
  if (geminiSubcommand === "project" && args[2] === "get") {
18890
18683
  try {
18891
- const { readGeminiLocalConfig } = await import('./config-CHJ189UJ.mjs');
18684
+ const { readGeminiLocalConfig } = await import('./config-BPZ2Y6o8.mjs');
18892
18685
  const config = readGeminiLocalConfig();
18893
18686
  if (config.googleCloudProject) {
18894
18687
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -18928,7 +18721,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18928
18721
  process.exit(0);
18929
18722
  }
18930
18723
  try {
18931
- const { runGemini } = await import('./runGemini-YqH7a-7O.mjs');
18724
+ const { runGemini } = await import('./runGemini-CDlcQ-OA.mjs');
18932
18725
  let startedBy = void 0;
18933
18726
  for (let i = 1; i < args.length; i++) {
18934
18727
  if (args[i] === "--started-by") {
@@ -18951,7 +18744,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18951
18744
  } else if (subcommand === "pi") {
18952
18745
  if (args[1] === "--terminal") {
18953
18746
  try {
18954
- const { runPiTerminal } = await import('./piTerminal-BoHvPEZ_.mjs');
18747
+ const { runPiTerminal } = await import('./piTerminal-Dpq6yqKU.mjs');
18955
18748
  process.exitCode = await runPiTerminal(args.slice(2));
18956
18749
  } catch (error) {
18957
18750
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18968,7 +18761,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18968
18761
  }
18969
18762
  };
18970
18763
  try {
18971
- const { runAcp, resolveAcpAgentConfig } = await import('./index-BgYZGODP.mjs');
18764
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BjzdXb9x.mjs');
18972
18765
  const parsed = parsePiRunnerArgs(args.slice(1));
18973
18766
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
18974
18767
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -18996,7 +18789,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18996
18789
  return;
18997
18790
  } else if (subcommand === "acp") {
18998
18791
  try {
18999
- const { runAcp, resolveAcpAgentConfig } = await import('./index-BgYZGODP.mjs');
18792
+ const { runAcp, resolveAcpAgentConfig } = await import('./index-BjzdXb9x.mjs');
19000
18793
  let startedBy = void 0;
19001
18794
  let verbose = false;
19002
18795
  const acpArgs = [];
@@ -19036,7 +18829,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19036
18829
  return;
19037
18830
  } else if (subcommand === "openclaw") {
19038
18831
  try {
19039
- const { runOpenClaw } = await import('./runOpenClaw-DSSyFQBb.mjs');
18832
+ const { runOpenClaw } = await import('./runOpenClaw-YkhsgPs0.mjs');
19040
18833
  let startedBy = void 0;
19041
18834
  let verbose = false;
19042
18835
  let gatewayUrl;
@@ -19087,7 +18880,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19087
18880
  return;
19088
18881
  } else if (subcommand === "todo") {
19089
18882
  try {
19090
- const { handleTodoCommand } = await import('./todo-aCZmSt27.mjs');
18883
+ const { handleTodoCommand } = await import('./todo-2Z1REhfx.mjs');
19091
18884
  await handleTodoCommand(args.slice(1));
19092
18885
  } catch (error) {
19093
18886
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -19096,7 +18889,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19096
18889
  return;
19097
18890
  } else if (subcommand === "teams") {
19098
18891
  try {
19099
- const { handleTeamsCommand } = await import('./teams-CkrWQ4uM.mjs');
18892
+ const { handleTeamsCommand } = await import('./teams-DXV5Pwwt.mjs');
19100
18893
  await handleTeamsCommand(args.slice(1));
19101
18894
  } catch (error) {
19102
18895
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -19105,7 +18898,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19105
18898
  return;
19106
18899
  } else if (subcommand === "auto") {
19107
18900
  try {
19108
- const { handleAutoCommand } = await import('./auto-CcXxfqkC.mjs');
18901
+ const { handleAutoCommand } = await import('./auto-BQnD6gVx.mjs');
19109
18902
  await handleAutoCommand(args.slice(1));
19110
18903
  } catch (error) {
19111
18904
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -19117,7 +18910,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19117
18910
  return;
19118
18911
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
19119
18912
  try {
19120
- const { handleMcpCommand } = await import('./mcp-DQkmk4VV.mjs');
18913
+ const { handleMcpCommand } = await import('./mcp-o7QglAQM.mjs');
19121
18914
  await handleMcpCommand(args[1] === "--terminal-tools");
19122
18915
  } catch (error) {
19123
18916
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -19487,4 +19280,4 @@ ${chalk.bold("Examples:")}
19487
19280
  }
19488
19281
  }
19489
19282
 
19490
- export { MessageBuffer as $, ALLOWED_SPAWN_PERMISSION_MODES as A, initialMachineMetadata as B, createSessionMetadata as C, DEFAULT_RECENT_LIMIT as D, setupOfflineReconnection as E, StreamRelay as F, GEMINI_MODEL_ENV as G, MessageQueue2 as H, hashObject as I, startHappyServer as J, registerAgentAttachmentDownloads as K, TitleGenerator as L, MAX_READ_LIMIT as M, registerKillSessionHandler as N, CLAUDE_ATTACHMENT_KINDS as O, PI_TEAMS_EXTENSION as P, stageClaudeAttachments as Q, appendStagedAttachmentsToPrompt as R, SPAWN_AGENTS as S, TurnWaitTimeoutError as T, BasePermissionHandler as U, writeSessionModeFile as V, normalizeAcpPermissionMode as W, removeSessionModeFile as X, GEMINI_API_KEY_ENV as Y, GOOGLE_API_KEY_ENV as Z, BaseReasoningProcessor as _, listSessions as a, CHANGE_TITLE_INSTRUCTION as a0, resolvePermissionRequest as b, archiveSession as c, sendUserMessage 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, checkPreviewPath as v, waitForTurnEnd as w, registerAssistantSessionTools as x, suggestTitleForPrompt as y, TEAM_SKILL as z };
19283
+ 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 };