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
@@ -3,14 +3,14 @@
3
3
  var chalk = require('chalk');
4
4
  var os = require('node:os');
5
5
  var node_crypto = require('node:crypto');
6
- var index = require('./types-CUCU7Vw8.cjs');
7
- var persistence = require('./limits-DY_sFA3J.cjs');
6
+ var index = require('./types-a2xsH2UI.cjs');
7
+ var persistence = require('./limits-DkfHJ3nS.cjs');
8
8
  var spawn = require('cross-spawn');
9
9
  var path = require('node:path');
10
10
  var node_readline = require('node:readline');
11
11
  var node_fs = require('node:fs');
12
12
  var terminalTitleSuggest = require('./terminalTitleSuggest-Dji8I0Jy.cjs');
13
- var agentGuidance = require('./agentGuidance-BM4kMNyr.cjs');
13
+ var agentGuidance = require('./agentGuidance-un7YPryB.cjs');
14
14
  var promises = require('node:fs/promises');
15
15
  var fs = require('fs/promises');
16
16
  var ink = require('ink');
@@ -18,7 +18,7 @@ var React = require('react');
18
18
  var claudeAgentSdk = require('@anthropic-ai/claude-agent-sdk');
19
19
  var axios = require('axios');
20
20
  require('node:events');
21
- var socket_ioClient = require('socket.io-client');
21
+ require('socket.io-client');
22
22
  var tweetnacl = require('tweetnacl');
23
23
  var os$1 = require('os');
24
24
  var child_process = require('child_process');
@@ -296,7 +296,7 @@ class Session {
296
296
  reportEventToDaemon = (event) => {
297
297
  const spawnedBy = process.env.HAPPY_SPAWNED_BY;
298
298
  if (!spawnedBy) return;
299
- void index.notifyDaemonSessionEvent(this.client.sessionId, event, spawnedBy).catch((error) => persistence.logger.debug("[Session] Failed to report session event to daemon:", error));
299
+ void persistence.notifyDaemonSessionEvent(this.client.sessionId, event, spawnedBy).catch((error) => persistence.logger.debug("[Session] Failed to report session event to daemon:", error));
300
300
  };
301
301
  /** Produce an `error` notification for an unexpected session failure. */
302
302
  onSessionError = (message) => {
@@ -900,7 +900,7 @@ async function createSessionScanner(opts) {
900
900
  let processedMessageKeys = /* @__PURE__ */ new Set();
901
901
  let deadSessions = /* @__PURE__ */ new Set();
902
902
  if (opts.sessionId) {
903
- let messages = await readSessionLog$1(projectDir, opts.sessionId);
903
+ let messages = await readSessionLog(projectDir, opts.sessionId);
904
904
  persistence.logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
905
905
  for (let m of messages) {
906
906
  processedMessageKeys.add(messageKey(m));
@@ -923,7 +923,7 @@ async function createSessionScanner(opts) {
923
923
  }
924
924
  }
925
925
  for (let session of sessions) {
926
- const sessionMessages = await readSessionLog$1(projectDir, session);
926
+ const sessionMessages = await readSessionLog(projectDir, session);
927
927
  let skipped = 0;
928
928
  let sent = 0;
929
929
  for (let file of sessionMessages) {
@@ -1000,7 +1000,7 @@ async function createSessionScanner(opts) {
1000
1000
  return;
1001
1001
  }
1002
1002
  if (options?.treatExistingAsProcessed) {
1003
- const existing = await readSessionLog$1(projectDir, sessionId);
1003
+ const existing = await readSessionLog(projectDir, sessionId);
1004
1004
  persistence.logger.debug(`[SESSION_SCANNER] Pre-marking ${existing.length} existing messages as processed for new session ${sessionId}`);
1005
1005
  for (const m of existing) {
1006
1006
  processedMessageKeys.add(messageKey(m));
@@ -1028,7 +1028,7 @@ function messageKey(message) {
1028
1028
  throw Error();
1029
1029
  }
1030
1030
  }
1031
- async function readSessionLog$1(projectDir, sessionId) {
1031
+ async function readSessionLog(projectDir, sessionId) {
1032
1032
  const expectedSessionFile = path.join(projectDir, `${sessionId}.jsonl`);
1033
1033
  persistence.logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
1034
1034
  let file;
@@ -1928,7 +1928,7 @@ async function awaitFileExist(file, timeout = 1e4) {
1928
1928
  await fs.access(file);
1929
1929
  return true;
1930
1930
  } catch (e) {
1931
- await index.delay(1e3);
1931
+ await persistence.delay(1e3);
1932
1932
  }
1933
1933
  }
1934
1934
  return false;
@@ -4295,7 +4295,7 @@ async function claudeRemoteLauncher(session, onPermissionModeChange, onEffective
4295
4295
  },
4296
4296
  onAuthFailure: (reason) => {
4297
4297
  session.client.sendSessionEvent({ type: "message", message: `Claude Code auth: ${reason}`, kind: "claude-auth-failed" });
4298
- void index.notifyDaemonClaudeAuthFailed(session.client.sessionId).catch((error) => persistence.logger.debug("[remote]: auth_failed report to daemon failed:", error));
4298
+ void persistence.notifyDaemonClaudeAuthFailed(session.client.sessionId).catch((error) => persistence.logger.debug("[remote]: auth_failed report to daemon failed:", error));
4299
4299
  },
4300
4300
  onSessionReset: () => {
4301
4301
  persistence.logger.debug("[remote]: Session reset");
@@ -5231,7 +5231,7 @@ function getLogFiles(logDir) {
5231
5231
  async function runDoctorDaemon() {
5232
5232
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
5233
5233
  try {
5234
- const isRunning = await index.checkIfDaemonRunningAndCleanupStaleState();
5234
+ const isRunning = await persistence.checkIfDaemonRunningAndCleanupStaleState();
5235
5235
  const state = await persistence.readDaemonState();
5236
5236
  if (isRunning && state) {
5237
5237
  console.log(chalk.green("\u2713 Daemon is running"));
@@ -5402,8 +5402,8 @@ ${typeLabels[type] || type}:`));
5402
5402
  }
5403
5403
  console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
5404
5404
  try {
5405
- const { resolveAgentHomesForCli } = await Promise.resolve().then(function () { return require('./agentHome-BJ7bde35.cjs'); });
5406
- const { describeAgentHome } = await Promise.resolve().then(function () { return require('./types-CUCU7Vw8.cjs'); }).then(function (n) { return n.index; });
5405
+ const { resolveAgentHomesForCli } = await Promise.resolve().then(function () { return require('./agentHome-aucT8BHw.cjs'); });
5406
+ const { describeAgentHome } = await Promise.resolve().then(function () { return require('./types-a2xsH2UI.cjs'); }).then(function (n) { return n.index; });
5407
5407
  const homes = await resolveAgentHomesForCli();
5408
5408
  console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
5409
5409
  console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
@@ -5444,7 +5444,7 @@ ${typeLabels[type] || type}:`));
5444
5444
  }
5445
5445
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
5446
5446
  try {
5447
- const isRunning = await index.checkIfDaemonRunningAndCleanupStaleState();
5447
+ const isRunning = await persistence.checkIfDaemonRunningAndCleanupStaleState();
5448
5448
  const state = await persistence.readDaemonState();
5449
5449
  const summary = daemonReadiness(authenticated, isRunning, Boolean(state));
5450
5450
  if (summary.level === "ready" && state) {
@@ -5773,7 +5773,7 @@ async function waitForAuthentication(keypair, claimSecret) {
5773
5773
  }
5774
5774
  process.stdout.write("\rWaiting for authentication" + ".".repeat(dots % 3 + 1) + " ");
5775
5775
  dots++;
5776
- await index.delay(1e3);
5776
+ await persistence.delay(1e3);
5777
5777
  }
5778
5778
  } finally {
5779
5779
  process.off("SIGINT", handleInterrupt);
@@ -6085,59 +6085,6 @@ function formatAssistantReportMessage(title, sessionId, event) {
6085
6085
  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`;
6086
6086
  }
6087
6087
 
6088
- function sessionWebUrl(sessionId) {
6089
- return `${persistence.configuration.webappUrl.replace(/\/+$/, "")}/session/${sessionId}`;
6090
- }
6091
- async function waitForSessionKey(sessionId, timeoutMs) {
6092
- const deadline = Date.now() + timeoutMs;
6093
- while (true) {
6094
- const entry = persistence.readPersistedSessions()[sessionId];
6095
- if (entry) return entry;
6096
- if (Date.now() >= deadline) {
6097
- throw new Error(
6098
- `Session ${sessionId} has no encryption key in ${persistence.configuration.sessionsFile}. Either it was not spawned by this machine's daemon, or the daemon is too old to persist session keys.`
6099
- );
6100
- }
6101
- await index.delay(200);
6102
- }
6103
- }
6104
- async function sendUserMessage(sessionId, persisted, text, client, options = {}) {
6105
- const credentials = await persistence.readCredentialsForConfiguredRelay();
6106
- if (!credentials) {
6107
- throw new Error("Not authenticated. Run `very-happy auth login` first.");
6108
- }
6109
- const envelope = {
6110
- role: "user",
6111
- content: {
6112
- type: "text",
6113
- text
6114
- },
6115
- meta: {
6116
- sentFrom: options.sentFrom ?? "cli",
6117
- ...options.model !== void 0 ? { model: options.model } : {},
6118
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
6119
- ...options.effort !== void 0 ? { effort: options.effort } : {}
6120
- }
6121
- };
6122
- const encrypted = persistence.encodeBase64(persistence.encrypt(
6123
- persistence.decodeBase64(persisted.encryptionKey),
6124
- persisted.encryptionVariant,
6125
- envelope
6126
- ));
6127
- await axios.post(
6128
- `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
6129
- { messages: [{ content: encrypted, localId: options.localId ?? node_crypto.randomUUID() }] },
6130
- {
6131
- headers: {
6132
- "Authorization": `Bearer ${credentials.token}`,
6133
- "Content-Type": "application/json",
6134
- "X-Happy-Client": `${client}/${persistence.configuration.currentCliVersion}`
6135
- },
6136
- timeout: 3e4
6137
- }
6138
- );
6139
- }
6140
-
6141
6088
  const ALLOWED_SPAWN_PERMISSION_MODES = [
6142
6089
  "default",
6143
6090
  "acceptEdits",
@@ -6284,6 +6231,7 @@ function startDaemonControlServer({
6284
6231
  getChildren,
6285
6232
  stopSession,
6286
6233
  spawnSession,
6234
+ resumeSession,
6287
6235
  requestShutdown,
6288
6236
  onHappySessionWebhook,
6289
6237
  onSessionStateEvent,
@@ -6292,7 +6240,9 @@ function startDaemonControlServer({
6292
6240
  pushClipboard,
6293
6241
  pushFilePreview,
6294
6242
  onTerminalHook,
6295
- setTerminalTitle
6243
+ setTerminalTitle,
6244
+ onSessionEdit,
6245
+ listPeers
6296
6246
  }) {
6297
6247
  return new Promise((resolve) => {
6298
6248
  const app = fastify({
@@ -6496,6 +6446,48 @@ function startDaemonControlServer({
6496
6446
  };
6497
6447
  }
6498
6448
  });
6449
+ typed.post("/resume-session", {
6450
+ schema: {
6451
+ body: z.z.object({
6452
+ sessionId: z.z.string().min(1).max(200),
6453
+ model: z.z.string().optional(),
6454
+ permissionMode: z.z.string().optional()
6455
+ }),
6456
+ response: {
6457
+ 200: z.z.object({
6458
+ success: z.z.boolean(),
6459
+ sessionId: z.z.string().optional(),
6460
+ error: z.z.string().optional()
6461
+ }),
6462
+ 500: z.z.object({
6463
+ success: z.z.boolean(),
6464
+ error: z.z.string().optional()
6465
+ }),
6466
+ 503: z.z.object({
6467
+ success: z.z.boolean(),
6468
+ error: z.z.string().optional()
6469
+ })
6470
+ }
6471
+ }
6472
+ }, async (request, reply) => {
6473
+ const { sessionId, model, permissionMode } = request.body;
6474
+ if (!resumeSession) {
6475
+ reply.code(503);
6476
+ return { success: false, error: "daemon is still starting up" };
6477
+ }
6478
+ persistence.logger.debug(`[CONTROL SERVER] Resume session request: ${sessionId} (model=${model ?? "unset"}, permissionMode=${permissionMode ?? "unset"})`);
6479
+ const result = await resumeSession(sessionId, { model, permissionMode });
6480
+ switch (result.type) {
6481
+ case "success":
6482
+ return { success: true, sessionId: result.sessionId };
6483
+ case "requestToApproveDirectoryCreation":
6484
+ reply.code(500);
6485
+ return { success: false, error: `Cannot resume: working directory ${result.directory} needs to be created first` };
6486
+ case "error":
6487
+ reply.code(500);
6488
+ return { success: false, error: result.errorMessage };
6489
+ }
6490
+ });
6499
6491
  typed.post("/terminal-hook", {
6500
6492
  schema: {
6501
6493
  body: z.z.any(),
@@ -6573,6 +6565,45 @@ function startDaemonControlServer({
6573
6565
  }
6574
6566
  return { status: "ok" };
6575
6567
  });
6568
+ typed.post("/session-edit", {
6569
+ schema: {
6570
+ body: z.z.object({
6571
+ sessionId: z.z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/),
6572
+ path: z.z.string().min(1).max(4096),
6573
+ tool: z.z.string().min(1).max(64),
6574
+ cwd: z.z.string().max(4096).optional(),
6575
+ /** When the runner saw the call (ms). Clamped to now by the daemon;
6576
+ * older than the conflict window = ignored (replayed history). */
6577
+ at: z.z.number().int().nonnegative().optional()
6578
+ }),
6579
+ response: { 200: z.z.object({ status: z.z.literal("ok") }) }
6580
+ }
6581
+ }, async (request) => {
6582
+ try {
6583
+ onSessionEdit?.(request.body);
6584
+ } catch (error) {
6585
+ persistence.logger.debug("[CONTROL SERVER] session-edit handler failed:", error);
6586
+ }
6587
+ return { status: "ok" };
6588
+ });
6589
+ typed.post("/peers", {
6590
+ schema: {
6591
+ response: {
6592
+ 200: z.z.object({
6593
+ sessions: z.z.array(z.z.object({
6594
+ sessionId: z.z.string(),
6595
+ kind: z.z.enum(["managed", "mirror"]),
6596
+ pid: z.z.number().optional(),
6597
+ cwd: z.z.string().optional(),
6598
+ flavor: z.z.string().optional(),
6599
+ title: z.z.string().optional(),
6600
+ variant: z.z.string().optional(),
6601
+ edits: z.z.array(z.z.object({ path: z.z.string(), tool: z.z.string(), at: z.z.number() }))
6602
+ }))
6603
+ })
6604
+ }
6605
+ }
6606
+ }, async () => ({ sessions: listPeers ? listPeers() : [] }));
6576
6607
  typed.post("/stop", {
6577
6608
  schema: {
6578
6609
  response: {
@@ -6608,6 +6639,273 @@ function startDaemonControlServer({
6608
6639
  });
6609
6640
  }
6610
6641
 
6642
+ const EDIT_CONFLICT_WINDOW_MS = 30 * 6e4;
6643
+ function pairKey$1(a, b, path) {
6644
+ return a < b ? `${a}\0${b}\0${path}` : `${b}\0${a}\0${path}`;
6645
+ }
6646
+ class EditConflictTracker {
6647
+ windowMs;
6648
+ maxPaths;
6649
+ /** real path → sessionId → latest edit */
6650
+ byPath = /* @__PURE__ */ new Map();
6651
+ /** (sorted pair, path) → when the notice went out */
6652
+ notified = /* @__PURE__ */ new Map();
6653
+ constructor(options = {}) {
6654
+ this.windowMs = options.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6655
+ this.maxPaths = options.maxPaths ?? 5e3;
6656
+ }
6657
+ /**
6658
+ * Remember an edit and return the peers to notify about it. `isLive`
6659
+ * decides whether another session still counts (dead ones are dropped
6660
+ * from the table on the spot).
6661
+ */
6662
+ record(edit, isLive) {
6663
+ this.prune(edit.at);
6664
+ let sessions = this.byPath.get(edit.path);
6665
+ if (!sessions) {
6666
+ sessions = /* @__PURE__ */ new Map();
6667
+ this.byPath.set(edit.path, sessions);
6668
+ }
6669
+ const conflicts = [];
6670
+ for (const [sessionId, peer] of sessions) {
6671
+ if (sessionId === edit.sessionId) continue;
6672
+ if (!isLive(sessionId)) {
6673
+ sessions.delete(sessionId);
6674
+ continue;
6675
+ }
6676
+ if (edit.at - peer.at > this.windowMs) continue;
6677
+ const key = pairKey$1(edit.sessionId, sessionId, edit.path);
6678
+ const notifiedAt = this.notified.get(key);
6679
+ if (notifiedAt !== void 0 && edit.at - notifiedAt <= this.windowMs) continue;
6680
+ this.notified.set(key, edit.at);
6681
+ conflicts.push({ editor: edit, peer });
6682
+ }
6683
+ sessions.set(edit.sessionId, edit);
6684
+ return conflicts;
6685
+ }
6686
+ /** Paths this session edited inside the window, newest first. */
6687
+ editsOf(sessionId, now, limit = 20) {
6688
+ const out = [];
6689
+ for (const [path, sessions] of this.byPath) {
6690
+ const edit = sessions.get(sessionId);
6691
+ if (edit && now - edit.at <= this.windowMs) out.push({ path, tool: edit.tool, at: edit.at });
6692
+ }
6693
+ return out.sort((a, b) => b.at - a.at).slice(0, limit);
6694
+ }
6695
+ /** Session ended: nothing it did counts any more. */
6696
+ forget(sessionId) {
6697
+ for (const [path, sessions] of this.byPath) {
6698
+ sessions.delete(sessionId);
6699
+ if (sessions.size === 0) this.byPath.delete(path);
6700
+ }
6701
+ for (const key of this.notified.keys()) {
6702
+ const [a, b] = key.split("\0");
6703
+ if (a === sessionId || b === sessionId) this.notified.delete(key);
6704
+ }
6705
+ }
6706
+ /** Drop everything older than the window (and cap the table size). */
6707
+ prune(now) {
6708
+ for (const [path, sessions] of this.byPath) {
6709
+ for (const [sessionId, edit] of sessions) if (now - edit.at > this.windowMs) sessions.delete(sessionId);
6710
+ if (sessions.size === 0) this.byPath.delete(path);
6711
+ }
6712
+ for (const [key, at] of this.notified) if (now - at > this.windowMs) this.notified.delete(key);
6713
+ if (this.byPath.size > this.maxPaths) {
6714
+ 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);
6715
+ for (const entry of oldest.slice(0, this.byPath.size - this.maxPaths)) this.byPath.delete(entry.path);
6716
+ }
6717
+ }
6718
+ /** Test / diagnostics helper. */
6719
+ size() {
6720
+ return this.byPath.size;
6721
+ }
6722
+ }
6723
+ function normalizeEditPath(path$1, cwd, realpath = node_fs.realpathSync.native ?? node_fs.realpathSync) {
6724
+ const absolute = path.isAbsolute(path$1) ? path.resolve(path$1) : path.resolve(cwd, path$1);
6725
+ let existing = absolute;
6726
+ const tail = [];
6727
+ for (let depth = 0; depth < 64; depth++) {
6728
+ try {
6729
+ const real = realpath(existing);
6730
+ return tail.length ? path.join(real, ...tail) : real;
6731
+ } catch {
6732
+ const parent = path.dirname(existing);
6733
+ if (parent === existing) break;
6734
+ tail.unshift(existing.slice(parent.length).replace(new RegExp(`^\\${path.sep}`), ""));
6735
+ existing = parent;
6736
+ }
6737
+ }
6738
+ return absolute;
6739
+ }
6740
+
6741
+ const CONFLICT_COALESCE_MS = 1e4;
6742
+ const MAX_NOTICES_PER_PAIR = 5;
6743
+ function pairKey(a, b) {
6744
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
6745
+ }
6746
+ function senderOf(info, sessionId) {
6747
+ return { sessionId, title: info?.title, flavor: info?.flavor, cwd: info?.cwd };
6748
+ }
6749
+ function createPeerCoordinator(deps) {
6750
+ const now = deps.now ?? (() => Date.now());
6751
+ const windowMs = deps.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6752
+ const coalesceMs = deps.coalesceMs ?? CONFLICT_COALESCE_MS;
6753
+ const maxNoticesPerPair = deps.maxNoticesPerPair ?? MAX_NOTICES_PER_PAIR;
6754
+ const tracker = new EditConflictTracker({ windowMs });
6755
+ const readPersisted = deps.readPersisted ?? persistence.readPersistedSessions;
6756
+ const send = deps.send ?? agentGuidance.sendUserMessage;
6757
+ const normalize = deps.normalize ?? normalizeEditPath;
6758
+ const liveTitle = deps.liveTitle ?? (async (sessionId, persisted) => (await agentGuidance.readSessionMetadata(sessionId, persisted))?.summary?.text);
6759
+ const schedule = deps.schedule ?? ((fn, ms) => {
6760
+ const t = setTimeout(fn, ms);
6761
+ t.unref?.();
6762
+ return t;
6763
+ });
6764
+ let getMirrors = deps.getMirrors ?? (() => []);
6765
+ const pending = /* @__PURE__ */ new Map();
6766
+ const noticesSent = /* @__PURE__ */ new Map();
6767
+ let inflight = Promise.resolve();
6768
+ const snapshot = () => {
6769
+ const at = now();
6770
+ const out = /* @__PURE__ */ new Map();
6771
+ for (const child of deps.getChildren()) {
6772
+ const id = child.happySessionId;
6773
+ if (!id || out.has(id)) continue;
6774
+ const meta = child.happySessionMetadataFromLocalWebhook;
6775
+ out.set(id, {
6776
+ sessionId: id,
6777
+ kind: "managed",
6778
+ pid: child.pid,
6779
+ ...meta?.path ? { cwd: meta.path } : {},
6780
+ ...meta?.flavor ? { flavor: meta.flavor } : {},
6781
+ ...meta?.summary?.text ? { title: meta.summary.text } : {},
6782
+ ...meta?.variant ?? child.variant ? { variant: meta?.variant ?? child.variant } : {},
6783
+ edits: tracker.editsOf(id, at)
6784
+ });
6785
+ }
6786
+ for (const mirror of getMirrors()) {
6787
+ if (out.has(mirror.sessionId)) continue;
6788
+ out.set(mirror.sessionId, {
6789
+ sessionId: mirror.sessionId,
6790
+ kind: "mirror",
6791
+ flavor: "terminal-mirror",
6792
+ ...mirror.cwd ? { cwd: mirror.cwd } : {},
6793
+ ...mirror.title ? { title: mirror.title } : {},
6794
+ edits: tracker.editsOf(mirror.sessionId, at)
6795
+ });
6796
+ }
6797
+ return out;
6798
+ };
6799
+ const deliver = async (recipient, peer, peerId, paths, peerEditedAgoMs, noticeId) => {
6800
+ if (recipient.kind === "mirror") return;
6801
+ const persisted = readPersisted()[recipient.sessionId];
6802
+ if (!persisted) {
6803
+ persistence.logger.debug(`[PEERS] no key for ${recipient.sessionId}; conflict notice ${noticeId} not delivered`);
6804
+ return;
6805
+ }
6806
+ const peerSender = senderOf(peer, peerId);
6807
+ if (!peerSender.title && peer?.kind === "managed") {
6808
+ const peerKey = readPersisted()[peer.sessionId];
6809
+ if (peerKey) {
6810
+ try {
6811
+ peerSender.title = await liveTitle(peer.sessionId, peerKey);
6812
+ } catch {
6813
+ }
6814
+ }
6815
+ }
6816
+ const text = agentGuidance.formatEditConflictNotice({ id: noticeId, path: paths[0], paths, peer: peerSender, peerEditedAgoMs, windowMs });
6817
+ try {
6818
+ await send(recipient.sessionId, persisted, text, "edit-conflict", {
6819
+ sentFrom: agentGuidance.SESSION_PEER_SENT_FROM,
6820
+ delivery: "steer",
6821
+ localId: `edit-conflict-${noticeId}-${recipient.sessionId}`
6822
+ });
6823
+ persistence.logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} (${paths.length} file(s), first ${paths[0]}, peer ${peerId})`);
6824
+ } catch (error) {
6825
+ persistence.logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} failed:`, error);
6826
+ }
6827
+ };
6828
+ const flushPair = async (key) => {
6829
+ const pair = pending.get(key);
6830
+ if (!pair) return;
6831
+ pending.delete(key);
6832
+ const at = now();
6833
+ const sent = (noticesSent.get(key) ?? []).filter((t) => at - t <= windowMs);
6834
+ if (sent.length >= maxNoticesPerPair) {
6835
+ noticesSent.set(key, sent);
6836
+ persistence.logger.debug(`[PEERS] pair ${pair.a}/${pair.b}: ${maxNoticesPerPair} notices already sent this window; ${pair.paths.size} conflicting path(s) not announced`);
6837
+ return;
6838
+ }
6839
+ sent.push(at);
6840
+ noticesSent.set(key, sent);
6841
+ const sessions = snapshot();
6842
+ const a = sessions.get(pair.a);
6843
+ const b = sessions.get(pair.b);
6844
+ const paths = [...pair.paths.keys()];
6845
+ const noticeId = node_crypto.randomUUID().slice(0, 8);
6846
+ const latestBy = (sessionId) => Math.max(...[...pair.paths.values()].map((c) => c.editor.sessionId === sessionId ? c.editor.at : c.peer.at));
6847
+ const agoFor = (recipientId, otherId) => Math.max(0, latestBy(recipientId) - latestBy(otherId));
6848
+ if (a) await deliver(a, b, pair.b, paths, agoFor(pair.a, pair.b), noticeId);
6849
+ if (b) await deliver(b, a, pair.a, paths, agoFor(pair.b, pair.a), noticeId);
6850
+ };
6851
+ const enqueue = (conflict) => {
6852
+ const key = pairKey(conflict.editor.sessionId, conflict.peer.sessionId);
6853
+ let pair = pending.get(key);
6854
+ if (!pair) {
6855
+ pair = { a: conflict.editor.sessionId, b: conflict.peer.sessionId, paths: /* @__PURE__ */ new Map(), timer: null };
6856
+ pair.timer = schedule(() => {
6857
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6858
+ });
6859
+ }, coalesceMs);
6860
+ pending.set(key, pair);
6861
+ }
6862
+ pair.paths.set(conflict.editor.path, conflict);
6863
+ };
6864
+ return {
6865
+ list: () => [...snapshot().values()],
6866
+ onEdit(edit) {
6867
+ const sessions = snapshot();
6868
+ const self = sessions.get(edit.sessionId);
6869
+ if (!self) {
6870
+ persistence.logger.debug(`[PEERS] edit from unknown/dead session ${edit.sessionId} ignored`);
6871
+ return [];
6872
+ }
6873
+ const current = now();
6874
+ const at = typeof edit.at === "number" && Number.isFinite(edit.at) ? Math.min(edit.at, current) : current;
6875
+ if (current - at > windowMs) {
6876
+ persistence.logger.debug(`[PEERS] edit from ${edit.sessionId} on ${edit.path} is ${Math.round((current - at) / 6e4)}m old (replayed history); ignored`);
6877
+ return [];
6878
+ }
6879
+ const cwd = edit.cwd ?? self.cwd ?? process.cwd();
6880
+ const path = normalize(edit.path, cwd);
6881
+ const conflicts = tracker.record({ sessionId: edit.sessionId, path, tool: edit.tool, at }, (id) => sessions.has(id));
6882
+ for (const conflict of conflicts) enqueue(conflict);
6883
+ return conflicts;
6884
+ },
6885
+ async flush() {
6886
+ for (const key of [...pending.keys()]) {
6887
+ const pair = pending.get(key);
6888
+ if (pair?.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6889
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6890
+ });
6891
+ }
6892
+ await inflight;
6893
+ },
6894
+ forget(sessionId) {
6895
+ tracker.forget(sessionId);
6896
+ for (const [key, pair] of pending) {
6897
+ if (pair.a === sessionId || pair.b === sessionId) {
6898
+ if (pair.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6899
+ pending.delete(key);
6900
+ }
6901
+ }
6902
+ },
6903
+ setMirrorSource(source) {
6904
+ getMirrors = source;
6905
+ }
6906
+ };
6907
+ }
6908
+
6611
6909
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6612
6910
 
6613
6911
  \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
@@ -8144,7 +8442,7 @@ function parseJsonObject(text) {
8144
8442
  return null;
8145
8443
  }
8146
8444
  }
8147
- function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DHrwHxHf.cjs', document.baseURI).href))).resolve(s)) {
8445
+ function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D_fYUxVg.cjs', document.baseURI).href))).resolve(s)) {
8148
8446
  const platformArch = `${platform}-${arch}`;
8149
8447
  const candidates = [
8150
8448
  `@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
@@ -8527,6 +8825,73 @@ class ClaudeAuthService {
8527
8825
  }
8528
8826
  }
8529
8827
 
8828
+ const CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
8829
+ function pathOf(input) {
8830
+ if (!input || typeof input !== "object") return null;
8831
+ const record = input;
8832
+ const candidate = record.file_path ?? record.notebook_path;
8833
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
8834
+ }
8835
+ function extractClaudeEditPaths(body) {
8836
+ if (!body || typeof body !== "object") return [];
8837
+ const line = body;
8838
+ if (line.type !== "assistant") return [];
8839
+ const content = line.message?.content;
8840
+ if (!Array.isArray(content)) return [];
8841
+ const stamp = line.timestamp;
8842
+ const at = typeof stamp === "string" ? Date.parse(stamp) : typeof stamp === "number" ? stamp : NaN;
8843
+ const edits = [];
8844
+ for (const block of content) {
8845
+ if (!block || typeof block !== "object") continue;
8846
+ const b = block;
8847
+ if (b.type !== "tool_use" || typeof b.name !== "string" || !CLAUDE_EDIT_TOOLS.has(b.name)) continue;
8848
+ const path = pathOf(b.input);
8849
+ if (path) edits.push({ path, tool: b.name, ...Number.isFinite(at) ? { at } : {} });
8850
+ }
8851
+ return edits;
8852
+ }
8853
+ function extractCodexPatchPaths(changes) {
8854
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
8855
+ return Object.keys(changes).filter((path) => path.trim() !== "").map((path) => ({ path, tool: "CodexPatch" }));
8856
+ }
8857
+ function extractAcpEditPaths(toolKind, args) {
8858
+ if (!args || typeof args !== "object") return [];
8859
+ const a = args;
8860
+ if (a.piTool === "write" || a.piTool === "edit") {
8861
+ const raw = a.rawInput;
8862
+ const path = raw && typeof raw.path === "string" && raw.path.trim() !== "" ? raw.path : null;
8863
+ return path ? [{ path, tool: a.piTool }] : [];
8864
+ }
8865
+ if (typeof a.piTool === "string") return [];
8866
+ if (toolKind !== "edit" || !Array.isArray(a.locations)) return [];
8867
+ const edits = [];
8868
+ for (const location of a.locations) {
8869
+ const path = location && typeof location === "object" ? location.path : void 0;
8870
+ if (typeof path === "string" && path.trim() !== "" && !edits.some((e) => e.path === path)) edits.push({ path, tool: "edit" });
8871
+ }
8872
+ return edits;
8873
+ }
8874
+ class EditReportThrottle {
8875
+ constructor(intervalMs = 5e3) {
8876
+ this.intervalMs = intervalMs;
8877
+ }
8878
+ lastReportedAt = /* @__PURE__ */ new Map();
8879
+ /** Returns the edits that should be reported now and remembers them. */
8880
+ take(edits, now = Date.now()) {
8881
+ const due = [];
8882
+ for (const edit of edits) {
8883
+ const last = this.lastReportedAt.get(edit.path);
8884
+ if (last !== void 0 && now - last < this.intervalMs) continue;
8885
+ this.lastReportedAt.set(edit.path, now);
8886
+ due.push(edit);
8887
+ }
8888
+ if (this.lastReportedAt.size > 512) {
8889
+ for (const [path, at] of this.lastReportedAt) if (now - at > this.intervalMs * 10) this.lastReportedAt.delete(path);
8890
+ }
8891
+ return due;
8892
+ }
8893
+ }
8894
+
8530
8895
  const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
8531
8896
  function parseLines(lines) {
8532
8897
  const out = [];
@@ -8563,12 +8928,15 @@ function createMirrorScanner(opts) {
8563
8928
  }
8564
8929
  tail.offset = 0;
8565
8930
  }
8931
+ let replaced = false;
8566
8932
  if (tail.offset > size) {
8567
8933
  persistence.logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
8568
8934
  tail.offset = 0;
8935
+ replaced = true;
8569
8936
  }
8570
8937
  if (tail.offset === size) return;
8571
8938
  const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
8939
+ const replay = isFirstBackfillRead || replaced;
8572
8940
  const handle = await promises.open(tail.path, "r");
8573
8941
  let buf;
8574
8942
  try {
@@ -8588,7 +8956,7 @@ function createMirrorScanner(opts) {
8588
8956
  if (decision.truncated) opts.events.onBackfillTruncated?.();
8589
8957
  tail.mode = "from-eof";
8590
8958
  }
8591
- if (messages.length > 0) opts.events.onMessages(messages);
8959
+ if (messages.length > 0) opts.events.onMessages(messages, { replay });
8592
8960
  };
8593
8961
  const sync = new index.InvalidateSync(async () => {
8594
8962
  for (const tail of files.values()) {
@@ -8711,8 +9079,17 @@ function createMirrorManager(deps) {
8711
9079
  binding.client.updateMetadata((current) => ({ ...current, ...patch }));
8712
9080
  persistBinding(binding);
8713
9081
  };
8714
- const sendMessages = (binding, messages) => {
9082
+ const sendMessages = (binding, messages, context = { replay: false }) => {
8715
9083
  for (const message of messages) {
9084
+ if (deps.onEdit && binding.status === "active" && !context.replay) {
9085
+ for (const edit of extractClaudeEditPaths(message)) {
9086
+ try {
9087
+ deps.onEdit({ sessionId: binding.happySessionId, path: edit.path, tool: edit.tool, cwd: binding.metadata.path, ...edit.at !== void 0 ? { at: edit.at } : {} });
9088
+ } catch (error) {
9089
+ persistence.logger.debug(`[MIRROR] edit tap failed for terminal ${binding.terminalId}:`, error);
9090
+ }
9091
+ }
9092
+ }
8716
9093
  const key = index.mirrorLineKey(message);
8717
9094
  binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => index.mirrorLocalId(key, envelopeIndex) } : void 0);
8718
9095
  }
@@ -8721,7 +9098,7 @@ function createMirrorManager(deps) {
8721
9098
  const scanner = createMirrorScanner({
8722
9099
  backfillLines,
8723
9100
  events: {
8724
- onMessages: (messages) => sendMessages(binding, messages),
9101
+ onMessages: (messages, context) => sendMessages(binding, messages, context),
8725
9102
  onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
8726
9103
  type: "message",
8727
9104
  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`
@@ -8744,6 +9121,10 @@ function createMirrorManager(deps) {
8744
9121
  await binding.scanner?.cleanup();
8745
9122
  binding.scanner = null;
8746
9123
  binding.client.closeClaudeSessionTurn("completed");
9124
+ try {
9125
+ deps.onSessionEnded?.(binding.happySessionId);
9126
+ } catch {
9127
+ }
8747
9128
  updateBindingMetadata(binding, {
8748
9129
  lifecycleState: "archived",
8749
9130
  lifecycleStateSince: Date.now(),
@@ -8956,6 +9337,13 @@ function createMirrorManager(deps) {
8956
9337
  }
8957
9338
  };
8958
9339
  return {
9340
+ listActiveMirrors() {
9341
+ return [...bindings.values()].filter((binding) => binding.status === "active").map((binding) => ({
9342
+ sessionId: binding.happySessionId,
9343
+ ...binding.metadata.path ? { cwd: binding.metadata.path } : {},
9344
+ ...binding.metadata.summary?.text ? { title: binding.metadata.summary.text } : {}
9345
+ }));
9346
+ },
8959
9347
  handleHookPayload(body) {
8960
9348
  const event = index.parseTerminalHookPayload(body);
8961
9349
  if (!event) {
@@ -9422,640 +9810,6 @@ Delegate from this working directory unless a different repository was explicitl
9422
9810
  ${goal}`;
9423
9811
  }
9424
9812
 
9425
- const MAX_LINE_CHARS = 500;
9426
- const MAX_ARGS_CHARS = 160;
9427
- const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
9428
- const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
9429
- function truncateText(text, max = MAX_LINE_CHARS) {
9430
- const chars = Array.from(text);
9431
- if (chars.length <= max) return text;
9432
- return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
9433
- }
9434
- function squash(text) {
9435
- return text.replace(/\s+/g, " ").trim();
9436
- }
9437
- function safeJson(value, max) {
9438
- try {
9439
- return truncateText(JSON.stringify(value) ?? "", max);
9440
- } catch {
9441
- return "[unserializable]";
9442
- }
9443
- }
9444
- function formatMessageBody(body, limits = DEFAULT_LIMITS) {
9445
- if (!body || typeof body !== "object") return null;
9446
- const b = body;
9447
- if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
9448
- const text = squash(b.content.text);
9449
- return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
9450
- }
9451
- if (b.role === "session" && b.content && typeof b.content === "object") {
9452
- const env = b.content;
9453
- const ev = env.ev;
9454
- if (!ev || typeof ev.t !== "string") return null;
9455
- switch (ev.t) {
9456
- case "text": {
9457
- if (ev.thinking) return null;
9458
- const text = squash(String(ev.text ?? ""));
9459
- if (text.length === 0) return null;
9460
- const role = env.role === "user" ? "user" : "assistant";
9461
- return `${role}: ${truncateText(text, limits.line)}`;
9462
- }
9463
- case "tool-call-start": {
9464
- const name = String(ev.name ?? "tool");
9465
- const title = squash(String(ev.title ?? ""));
9466
- const args = ev.args ? safeJson(ev.args, limits.args) : "";
9467
- const detail = title || args;
9468
- return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
9469
- }
9470
- case "service": {
9471
- const text = squash(String(ev.text ?? ""));
9472
- return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
9473
- }
9474
- case "turn-end": {
9475
- if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
9476
- return null;
9477
- }
9478
- default:
9479
- return null;
9480
- }
9481
- }
9482
- if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
9483
- const d = b.content.data;
9484
- switch (d.type) {
9485
- case "message": {
9486
- const text = squash(String(d.message ?? ""));
9487
- return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
9488
- }
9489
- case "tool-call":
9490
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
9491
- case "permission-request":
9492
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
9493
- default:
9494
- return null;
9495
- }
9496
- }
9497
- if (b.role === "agent" && b.content?.type === "output") {
9498
- const msg = b.content.data?.message;
9499
- if (msg && Array.isArray(msg.content)) {
9500
- const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
9501
- if (texts.length > 0) {
9502
- const role = msg.role === "user" ? "user" : "assistant";
9503
- return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
9504
- }
9505
- }
9506
- return null;
9507
- }
9508
- return null;
9509
- }
9510
- function formatTranscript(bodies, options = {}) {
9511
- const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
9512
- const lines = [];
9513
- for (const body of bodies) {
9514
- const line = formatMessageBody(body, limits);
9515
- if (line !== null) lines.push(line);
9516
- }
9517
- return lines.join("\n");
9518
- }
9519
-
9520
- function asRecord(value) {
9521
- return value && typeof value === "object" ? value : null;
9522
- }
9523
- function sessionEnvelope(body) {
9524
- const b = asRecord(body);
9525
- if (!b || b.role !== "session") return null;
9526
- const env = asRecord(b.content);
9527
- if (!env) return null;
9528
- const ev = asRecord(env.ev);
9529
- return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
9530
- }
9531
- function isUserPrompt(body) {
9532
- const b = asRecord(body);
9533
- return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
9534
- }
9535
- function agentPiece(body) {
9536
- const env = sessionEnvelope(body);
9537
- if (env && env.role !== "user") {
9538
- if (env.ev.t === "text" && !env.ev.thinking) {
9539
- const text = String(env.ev.text ?? "");
9540
- return text.trim().length > 0 ? { kind: "text", text } : null;
9541
- }
9542
- if (env.ev.t === "tool-call-start") return { kind: "tool" };
9543
- return null;
9544
- }
9545
- const b = asRecord(body);
9546
- if (b?.role === "agent" && b.content?.type === "acp") {
9547
- const d = asRecord(b.content.data);
9548
- if (d?.type === "message") {
9549
- const text = String(d.message ?? "");
9550
- return text.trim().length > 0 ? { kind: "text", text } : null;
9551
- }
9552
- if (d?.type === "tool-call") return { kind: "tool" };
9553
- }
9554
- return null;
9555
- }
9556
- function userPromptText(body) {
9557
- return isUserPrompt(body) ? body.content.text : null;
9558
- }
9559
- function analyzeLatestTurn(entries, options = {}) {
9560
- const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
9561
- let userIndex = -1;
9562
- for (let i = entries.length - 1; i >= 0; i--) {
9563
- if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
9564
- userIndex = i;
9565
- break;
9566
- }
9567
- }
9568
- let ended = false;
9569
- let status = null;
9570
- let error = null;
9571
- let texts = [];
9572
- let lastMarker = null;
9573
- let startedAfterPrompt = false;
9574
- const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
9575
- for (let i = userIndex + 1; i < entries.length; i++) {
9576
- const body = entries[i].body;
9577
- if (options.anchor && isUserPrompt(body)) {
9578
- if (ended) break;
9579
- continue;
9580
- }
9581
- const env = sessionEnvelope(body);
9582
- if (env?.ev?.t === "turn-start") {
9583
- lastMarker = "start";
9584
- startedAfterPrompt = true;
9585
- ended = false;
9586
- status = null;
9587
- error = null;
9588
- texts = [];
9589
- continue;
9590
- }
9591
- if (env?.ev?.t === "turn-end") {
9592
- lastMarker = "end";
9593
- if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
9594
- ended = true;
9595
- status = typeof env.ev.status === "string" ? env.ev.status : null;
9596
- error = typeof env.ev.error === "string" ? env.ev.error : null;
9597
- continue;
9598
- }
9599
- const piece = agentPiece(body);
9600
- if (!piece) continue;
9601
- if (piece.kind === "tool") texts = [];
9602
- else texts.push(piece.text);
9603
- }
9604
- if (userIndex < 0) ended = lastMarker === "end";
9605
- return {
9606
- userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
9607
- ended,
9608
- status: ended ? status : null,
9609
- error: ended ? error : null,
9610
- answer: texts.join("\n\n").trim(),
9611
- lastSeq
9612
- };
9613
- }
9614
-
9615
- const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9616
- async function openUserScopedSocket(token, options = {}) {
9617
- const socket = socket_ioClient.io(persistence.configuration.serverUrl, {
9618
- auth: {
9619
- token,
9620
- clientType: "user-scoped",
9621
- happyClient: `cli-session-ops/${persistence.configuration.currentCliVersion}`
9622
- },
9623
- path: "/v1/updates",
9624
- transports: ["websocket"],
9625
- reconnection: false,
9626
- autoConnect: false
9627
- });
9628
- await new Promise((resolve, reject) => {
9629
- const timer = setTimeout(() => {
9630
- socket.close();
9631
- reject(new Error(`Timed out connecting to ${persistence.configuration.serverUrl}`));
9632
- }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9633
- socket.once("connect", () => {
9634
- clearTimeout(timer);
9635
- resolve();
9636
- });
9637
- socket.once("connect_error", (error) => {
9638
- clearTimeout(timer);
9639
- socket.close();
9640
- reject(new Error(`Could not connect to ${persistence.configuration.serverUrl}: ${error.message}`));
9641
- });
9642
- socket.connect();
9643
- });
9644
- return {
9645
- async rpcCall(payload, timeoutMs) {
9646
- const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9647
- return ack;
9648
- },
9649
- close() {
9650
- socket.close();
9651
- }
9652
- };
9653
- }
9654
-
9655
- const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
9656
- const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
9657
- const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
9658
- const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
9659
- const RPC_RATE_MAX_RETRIES = 2;
9660
- function isRpcRateLimitAck(ack) {
9661
- if (!ack || typeof ack !== "object") return false;
9662
- const a = ack;
9663
- if (a.ok !== false) return false;
9664
- if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
9665
- return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
9666
- }
9667
- function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
9668
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
9669
- return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
9670
- }
9671
- const exponent = Math.max(0, Math.min(attempt, 32) - 1);
9672
- const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
9673
- return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
9674
- }
9675
-
9676
- const PERMISSION_OPS_CLIENT = "session-ops";
9677
- const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9678
- const SETTLE_TIMEOUT_MS = 5e3;
9679
- const SETTLE_POLL_MS = 500;
9680
- function buildPermissionRpcPayload(requestId, verdict) {
9681
- if (verdict.kind === "approve") {
9682
- return {
9683
- id: requestId,
9684
- approved: true,
9685
- decision: verdict.forSession ? "approved_for_session" : "approved"
9686
- };
9687
- }
9688
- return {
9689
- id: requestId,
9690
- approved: false,
9691
- decision: "denied",
9692
- ...verdict.reason ? { reason: verdict.reason } : {}
9693
- };
9694
- }
9695
- function interpretPermissionAck(ack, decryptResult) {
9696
- if (!ack.ok) {
9697
- const message = ack.error ?? "RPC call failed";
9698
- if (message === "RPC method not available") {
9699
- return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9700
- }
9701
- if (message === "RPC target disconnected") {
9702
- return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9703
- }
9704
- if (/timed? ?out/i.test(message)) {
9705
- return { status: "timeout", message };
9706
- }
9707
- if (isRpcRateLimitAck(ack)) {
9708
- return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
9709
- }
9710
- return { status: "rejected", message };
9711
- }
9712
- let body = null;
9713
- if (typeof ack.result === "string" && ack.result.length > 0) {
9714
- try {
9715
- body = decryptResult(ack.result);
9716
- } catch {
9717
- body = null;
9718
- }
9719
- }
9720
- if (body && typeof body === "object" && typeof body.error === "string") {
9721
- return { status: "handler-error", message: body.error };
9722
- }
9723
- return { status: "acknowledged" };
9724
- }
9725
- function pendingRequestsOf(agentState, now) {
9726
- const requests = agentState?.requests;
9727
- if (!requests || typeof requests !== "object") return [];
9728
- return Object.entries(requests).map(([id, request]) => ({
9729
- id,
9730
- tool: typeof request?.tool === "string" ? request.tool : "unknown",
9731
- ...request?.kind ? { kind: request.kind } : {},
9732
- ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
9733
- })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
9734
- }
9735
- const RPC_DEADLINE_GRACE_MS = 2e3;
9736
- function withDeadline(promise, ms, message) {
9737
- return new Promise((resolve, reject) => {
9738
- const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
9739
- promise.then(
9740
- (value) => {
9741
- clearTimeout(timer);
9742
- resolve(value);
9743
- },
9744
- (error) => {
9745
- clearTimeout(timer);
9746
- reject(error);
9747
- }
9748
- );
9749
- });
9750
- }
9751
- async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
9752
- const now = deps.now ?? Date.now;
9753
- const persisted = (deps.readPersisted ?? persistence.readPersistedSessions)()[sessionId];
9754
- if (!persisted) {
9755
- throw new Error(
9756
- `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.`
9757
- );
9758
- }
9759
- const token = await (deps.bearerToken ?? bearerToken$1)();
9760
- const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
9761
- const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9762
- if (!before.some((request) => request.id === requestId)) {
9763
- const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
9764
- throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
9765
- }
9766
- const payload = buildPermissionRpcPayload(requestId, verdict);
9767
- const key = persistence.decodeBase64(persisted.encryptionKey);
9768
- const params = persistence.encodeBase64(persistence.encrypt(key, persisted.encryptionVariant, payload));
9769
- const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
9770
- const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
9771
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9772
- let ack;
9773
- try {
9774
- for (let attempt = 1; ; attempt++) {
9775
- try {
9776
- ack = await withDeadline(
9777
- transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
9778
- rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
9779
- `permission RPC did not settle within ${rpcTimeoutMs}ms`
9780
- );
9781
- } catch (error) {
9782
- ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
9783
- }
9784
- if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
9785
- const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
9786
- deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
9787
- await sleep(waitMs);
9788
- }
9789
- } finally {
9790
- transport.close();
9791
- }
9792
- const outcome = interpretPermissionAck(ack, (encrypted) => persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(encrypted)));
9793
- if (outcome.status !== "acknowledged") {
9794
- return { sessionId, requestId, payload, outcome };
9795
- }
9796
- const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
9797
- let settled = false;
9798
- for (; ; ) {
9799
- const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9800
- if (!pending.some((request) => request.id === requestId)) {
9801
- settled = true;
9802
- break;
9803
- }
9804
- if (now() >= deadline) break;
9805
- await sleep(SETTLE_POLL_MS);
9806
- }
9807
- return { sessionId, requestId, payload, outcome, settled };
9808
- }
9809
- async function bearerToken$1() {
9810
- const credentials = await persistence.readCredentialsForConfiguredRelay();
9811
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9812
- return credentials.token;
9813
- }
9814
- async function fetchDecryptedAgentState(sessionId, persisted, token) {
9815
- const response = await axios.get(
9816
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
9817
- {
9818
- headers: {
9819
- "Authorization": `Bearer ${token}`,
9820
- "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
9821
- },
9822
- timeout: 15e3,
9823
- // 404 is a real answer ("not yours / gone"), not a transport fault.
9824
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
9825
- }
9826
- );
9827
- if (response.status === 404) {
9828
- 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).`);
9829
- }
9830
- const raw = response.data?.session?.agentState;
9831
- if (typeof raw !== "string" || raw.length === 0) return null;
9832
- return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(raw));
9833
- }
9834
-
9835
- const SESSION_OPS_CLIENT = "session-ops";
9836
- const DEFAULT_RECENT_LIMIT = 15;
9837
- const MAX_READ_LIMIT = 100;
9838
- function mergeSessionSummaries(live, persisted, options = {}) {
9839
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9840
- const seen = /* @__PURE__ */ new Set();
9841
- const summaries = [];
9842
- for (const child of live) {
9843
- const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9844
- if (!id || seen.has(id)) continue;
9845
- seen.add(id);
9846
- summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9847
- }
9848
- 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);
9849
- for (const [id, entry] of rest) {
9850
- summaries.push(toSummary(id, entry, { live: false }));
9851
- }
9852
- if (!options.tag) return summaries;
9853
- const wanted = options.tag;
9854
- return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9855
- }
9856
- function livenessOf(child) {
9857
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
9858
- }
9859
- function sessionLiveness(live, sessionId) {
9860
- const child = live.find((entry) => entry.happySessionId === sessionId);
9861
- return child ? livenessOf(child) : { live: false };
9862
- }
9863
- function toSummary(id, persisted, extra) {
9864
- const meta = persisted?.metadata;
9865
- return {
9866
- id,
9867
- live: extra.live,
9868
- ...extra.pid !== void 0 ? { pid: extra.pid } : {},
9869
- ...meta?.summary?.text ? { title: meta.summary.text } : {},
9870
- ...meta?.path ? { cwd: meta.path } : {},
9871
- ...meta?.flavor ? { flavor: meta.flavor } : {},
9872
- ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
9873
- ...meta?.variant ? { variant: meta.variant } : {},
9874
- url: sessionWebUrl(id),
9875
- ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
9876
- };
9877
- }
9878
- async function listSessions(options = {}) {
9879
- const live = await index.listDaemonSessions();
9880
- return mergeSessionSummaries(live, persistence.readPersistedSessions(), options);
9881
- }
9882
- function summarizeAccountSession(row, persisted, liveIds, now) {
9883
- const base = {
9884
- id: row.id,
9885
- live: liveIds.has(row.id),
9886
- url: sessionWebUrl(row.id),
9887
- decryptable: false,
9888
- active: row.active === true,
9889
- archived: typeof row.archivedAt === "number",
9890
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
9891
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
9892
- attention: false
9893
- };
9894
- if (!persisted) return base;
9895
- const key = persistence.decodeBase64(persisted.encryptionKey);
9896
- let metadata = null;
9897
- let agentState = null;
9898
- try {
9899
- metadata = row.metadata ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.metadata)) : null;
9900
- agentState = row.agentState ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.agentState)) : null;
9901
- } catch {
9902
- return base;
9903
- }
9904
- if (!metadata) return base;
9905
- const meta = metadata;
9906
- const pending = pendingRequestsOf(agentState, now);
9907
- return {
9908
- ...base,
9909
- decryptable: true,
9910
- ...meta.summary?.text ? { title: meta.summary.text } : {},
9911
- ...meta.path ? { cwd: meta.path } : {},
9912
- ...meta.flavor ? { flavor: meta.flavor } : {},
9913
- ...meta.tags?.length ? { tags: [...meta.tags] } : {},
9914
- ...meta.variant ? { variant: meta.variant } : {},
9915
- ...meta.machineId ? { machineId: meta.machineId } : {},
9916
- ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
9917
- pending,
9918
- attention: pending.length > 0
9919
- };
9920
- }
9921
- function orderAccountSessions(summaries, options = {}) {
9922
- const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
9923
- const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
9924
- let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
9925
- const byRank = rank(a) - rank(b);
9926
- if (byRank !== 0) return byRank;
9927
- if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
9928
- return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
9929
- });
9930
- if (options.tag) {
9931
- const wanted = options.tag;
9932
- rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
9933
- }
9934
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9935
- let idleKept = 0;
9936
- return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
9937
- }
9938
- async function listAccountSessions(options = {}) {
9939
- const token = await bearerToken();
9940
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions`, {
9941
- headers: {
9942
- "Authorization": `Bearer ${token}`,
9943
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
9944
- },
9945
- timeout: 15e3
9946
- });
9947
- const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
9948
- const persisted = persistence.readPersistedSessions();
9949
- const live = await index.listDaemonSessions();
9950
- const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
9951
- const now = Date.now();
9952
- const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
9953
- return orderAccountSessions(summaries, options);
9954
- }
9955
- async function bearerToken() {
9956
- const credentials = await persistence.readCredentialsForConfiguredRelay();
9957
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9958
- return credentials.token;
9959
- }
9960
- function requirePersisted(sessionId) {
9961
- const persisted = persistence.readPersistedSessions()[sessionId];
9962
- if (!persisted) {
9963
- throw new Error(
9964
- `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
9965
- );
9966
- }
9967
- return persisted;
9968
- }
9969
- async function readSessionLog(sessionId, persisted, limit) {
9970
- const token = await bearerToken();
9971
- const response = await axios.get(
9972
- `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
9973
- {
9974
- params: { before_seq: 2147483647, limit },
9975
- headers: {
9976
- "Authorization": `Bearer ${token}`,
9977
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
9978
- },
9979
- timeout: 15e3
9980
- }
9981
- );
9982
- const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
9983
- messages.reverse();
9984
- const key = persistence.decodeBase64(persisted.encryptionKey);
9985
- return messages.map((message) => {
9986
- if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
9987
- try {
9988
- return { seq: message.seq, body: persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(message.content.c)) };
9989
- } catch {
9990
- return { seq: message.seq, body: null };
9991
- }
9992
- });
9993
- }
9994
- const TURN_WINDOW = 500;
9995
- async function readSessionTranscript(sessionId, limit, options = {}) {
9996
- const persisted = requirePersisted(sessionId);
9997
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
9998
- const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
9999
- const shown = entries.slice(-bounded);
10000
- const live = await index.listDaemonSessions();
10001
- return {
10002
- summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
10003
- messageCount: shown.length,
10004
- transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
10005
- turn: analyzeLatestTurn(entries)
10006
- };
10007
- }
10008
- async function readSessionMetadata(sessionId, persisted) {
10009
- const token = await bearerToken();
10010
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10011
- headers: {
10012
- "Authorization": `Bearer ${token}`,
10013
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10014
- },
10015
- timeout: 15e3
10016
- });
10017
- const ciphertext = response.data?.session?.metadata;
10018
- if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
10019
- return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(ciphertext));
10020
- }
10021
- class TurnWaitTimeoutError extends Error {
10022
- constructor(sessionId, timeoutMs, turn) {
10023
- super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
10024
- this.sessionId = sessionId;
10025
- this.timeoutMs = timeoutMs;
10026
- this.turn = turn;
10027
- }
10028
- }
10029
- async function waitForTurnEnd(sessionId, options) {
10030
- const persisted = requirePersisted(sessionId);
10031
- const pollMs = options.pollMs ?? 3e3;
10032
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10033
- const deadline = Date.now() + options.timeoutMs;
10034
- while (true) {
10035
- const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
10036
- if (turn.ended) return turn;
10037
- if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
10038
- await sleep(pollMs);
10039
- }
10040
- }
10041
- async function stopSession(sessionId) {
10042
- return await index.stopDaemonSession(sessionId);
10043
- }
10044
- async function archiveSession(sessionId) {
10045
- const token = await bearerToken();
10046
- await axios.post(
10047
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
10048
- {},
10049
- {
10050
- headers: {
10051
- "Authorization": `Bearer ${token}`,
10052
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10053
- },
10054
- timeout: 1e4
10055
- }
10056
- );
10057
- }
10058
-
10059
9813
  function receiptPath(home, operationId) {
10060
9814
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
10061
9815
  return path.join(home, "teams", "effects", `${operationId}.json`);
@@ -10209,8 +9963,8 @@ function createTeamWorker(deps) {
10209
9963
  await complete(op, receipt);
10210
9964
  return;
10211
9965
  }
10212
- const key = await waitForSessionKey(receipt.sessionId, 1e4);
10213
- await sendUserMessage(receipt.sessionId, key, teamInitialPrompt(op.prompt, bot?.root === true, receipt.directory), "teams", { localId: `teams-initial-${op.id}`, sentFrom: "team", ...op.model ? { model: op.model } : {} });
9966
+ const key = await agentGuidance.waitForSessionKey(receipt.sessionId, 1e4);
9967
+ await agentGuidance.sendUserMessage(receipt.sessionId, key, teamInitialPrompt(op.prompt, bot?.root === true, receipt.directory), "teams", { localId: `teams-initial-${op.id}`, sentFrom: "team", ...op.model ? { model: op.model } : {} });
10214
9968
  receipt = { ...receipt, phase: "delivered" };
10215
9969
  writeReceipt$1(home, receipt);
10216
9970
  }
@@ -10226,7 +9980,7 @@ function createTeamWorker(deps) {
10226
9980
  if (!bot || bot.generation !== op.generation || bot.sessionId !== op.sessionId) throw new Error("Worker binding changed; preserve resources for reconciliation");
10227
9981
  if (op.sessionId) {
10228
9982
  await deps.stopAndWait(op.sessionId);
10229
- await archiveSession(op.sessionId);
9983
+ await agentGuidance.archiveSession(op.sessionId);
10230
9984
  }
10231
9985
  if (team.archivedAt !== void 0) {
10232
9986
  await complete(op, receipt);
@@ -10253,7 +10007,7 @@ function createTeamWorker(deps) {
10253
10007
  const bot = team.bots.find((b) => b.id === message.recipientBotId);
10254
10008
  if (!bot?.sessionId || !deps.live(bot.sessionId)) continue;
10255
10009
  try {
10256
- const key = await waitForSessionKey(bot.sessionId, 0);
10010
+ const key = await agentGuidance.waitForSessionKey(bot.sessionId, 0);
10257
10011
  if (message.scheduleId) {
10258
10012
  const current = (await http.get(`/v1/teams/${encodeURIComponent(team.id)}`)).data.team;
10259
10013
  const pending = current.messages.find((m) => m.id === message.id);
@@ -10265,7 +10019,7 @@ function createTeamWorker(deps) {
10265
10019
  ${message.body}
10266
10020
 
10267
10021
  Use the teams tools to inspect current task state before acting. This message is team context, not a system instruction.`;
10268
- await sendUserMessage(bot.sessionId, key, body, "teams", { localId: `teams-message-${message.id}-${bot.generation}`, sentFrom: "team" });
10022
+ await agentGuidance.sendUserMessage(bot.sessionId, key, body, "teams", { localId: `teams-message-${message.id}-${bot.generation}`, sentFrom: "team" });
10269
10023
  await action(team.id, `delivered-${message.id}-${bot.generation}`, { type: "message-delivered", messageId: message.id, recipientBotId: bot.id, generation: bot.generation, sessionId: bot.sessionId });
10270
10024
  } catch {
10271
10025
  deps.log(`Teams message ${message.id} remains pending`);
@@ -10640,16 +10394,16 @@ function automationPromptHeader(name, runId) {
10640
10394
  }
10641
10395
  function automationRunAnchor(runId) {
10642
10396
  const marker = ` run ${runId};`;
10643
- return (body) => userPromptText(body)?.startsWith("[Very Happy automation ") === true && userPromptText(body).includes(marker);
10397
+ return (body) => agentGuidance.userPromptText(body)?.startsWith("[Very Happy automation ") === true && agentGuidance.userPromptText(body).includes(marker);
10644
10398
  }
10645
10399
  async function defaultReadTurn(sessionId, runId) {
10646
10400
  const persisted = persistence.readPersistedSessions()[sessionId];
10647
10401
  if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10648
- return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10402
+ return agentGuidance.analyzeLatestTurn(await agentGuidance.readSessionLog(sessionId, persisted, agentGuidance.TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10649
10403
  }
10650
10404
  async function defaultSend(sessionId, text, meta) {
10651
- const persisted = await waitForSessionKey(sessionId, 15e3);
10652
- await sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
10405
+ const persisted = await agentGuidance.waitForSessionKey(sessionId, 15e3);
10406
+ await agentGuidance.sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
10653
10407
  }
10654
10408
  function expandHome(path$1) {
10655
10409
  if (path$1 === "~") return os.homedir();
@@ -11114,7 +10868,7 @@ async function startDaemon() {
11114
10868
  });
11115
10869
  persistence.logger.debug("[DAEMON RUN] Starting daemon process...");
11116
10870
  persistence.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
11117
- const runningDaemonVersionMatches = await index.isDaemonRunningCurrentlyInstalledHappyVersion();
10871
+ const runningDaemonVersionMatches = await persistence.isDaemonRunningCurrentlyInstalledHappyVersion();
11118
10872
  const runningDaemonState = await persistence.readDaemonState();
11119
10873
  const runningDaemonEndpointsMatch = Boolean(runningDaemonState) && daemonEndpointsMatch(
11120
10874
  runningDaemonState?.serverUrl,
@@ -11124,7 +10878,7 @@ async function startDaemon() {
11124
10878
  );
11125
10879
  if (!runningDaemonVersionMatches || !runningDaemonEndpointsMatch) {
11126
10880
  persistence.logger.debug("[DAEMON RUN] Daemon version or endpoint mismatch detected, restarting daemon");
11127
- await index.stopDaemon();
10881
+ await persistence.stopDaemon();
11128
10882
  } else {
11129
10883
  persistence.logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
11130
10884
  console.log("Daemon already running with matching version");
@@ -11144,6 +10898,7 @@ async function startDaemon() {
11144
10898
  persistence.logger.debug("[DAEMON RUN] Auth and machine setup complete");
11145
10899
  const pidToTrackedSession = /* @__PURE__ */ new Map();
11146
10900
  const turnActivity = new index.TurnActivityTracker();
10901
+ const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
11147
10902
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
11148
10903
  let teamWorker;
11149
10904
  let automationRunner;
@@ -12003,6 +11758,7 @@ async function startDaemon() {
12003
11758
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
12004
11759
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
12005
11760
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11761
+ if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
12006
11762
  if (session?.happySessionId && session.encryption) {
12007
11763
  sessionIdToFinishedSession.set(session.happySessionId, session);
12008
11764
  persistence.logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
@@ -12061,7 +11817,7 @@ async function startDaemon() {
12061
11817
  sessionId
12062
11818
  );
12063
11819
  assistantReportLastSentAt.set(sessionId, Date.now());
12064
- await sendUserMessage(
11820
+ await agentGuidance.sendUserMessage(
12065
11821
  decision.assistantSessionId,
12066
11822
  assistantEntry,
12067
11823
  formatAssistantReportMessage(title, sessionId, event),
@@ -12081,11 +11837,17 @@ async function startDaemon() {
12081
11837
  getChildren: getCurrentChildren,
12082
11838
  stopSession,
12083
11839
  spawnSession,
11840
+ resumeSession,
11841
+ // B-501: `very-happy send --resume`
12084
11842
  requestShutdown: () => requestShutdown("happy-cli"),
12085
11843
  onHappySessionWebhook,
12086
11844
  onSessionStateEvent,
12087
11845
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
12088
11846
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
11847
+ onSessionEdit: (edit) => {
11848
+ peerCoordinator.onEdit(edit);
11849
+ },
11850
+ listPeers: () => peerCoordinator.list(),
12089
11851
  pushClipboard: (text, terminalId) => {
12090
11852
  if (!apiMachineRef) {
12091
11853
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -12151,9 +11913,12 @@ async function startDaemon() {
12151
11913
  const mirrorManager = createMirrorManager({
12152
11914
  api,
12153
11915
  machineId,
12154
- onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
11916
+ onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
11917
+ onEdit: (edit) => peerCoordinator.onEdit(edit),
11918
+ onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
12155
11919
  });
12156
11920
  mirrorManagerRef = mirrorManager;
11921
+ peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
12157
11922
  apiMachine.setMirrorIntegration({
12158
11923
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
12159
11924
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
@@ -12281,7 +12046,7 @@ async function startDaemon() {
12281
12046
  claudeAuthService.stop();
12282
12047
  apiMachine.shutdown();
12283
12048
  await stopControlServer();
12284
- await index.cleanupDaemonState();
12049
+ await persistence.cleanupDaemonState();
12285
12050
  await persistence.releaseDaemonLock(daemonLockHandle);
12286
12051
  await stopCaffeinate();
12287
12052
  try {
@@ -12363,7 +12128,7 @@ async function startDaemon() {
12363
12128
  }
12364
12129
  apiMachine.shutdown();
12365
12130
  await stopControlServer();
12366
- await index.cleanupDaemonState();
12131
+ await persistence.cleanupDaemonState();
12367
12132
  await stopCaffeinate();
12368
12133
  await persistence.releaseDaemonLock(daemonLockHandle);
12369
12134
  persistence.logger.debug(`[DAEMON RUN] Cleanup completed, exiting process with code ${daemonExitCode(source)}`);
@@ -12467,8 +12232,8 @@ function createTeamAdoption(sessionId) {
12467
12232
 
12468
12233
  This session is now connected as the team lead. Call team_inspect to read the authoritative state before continuing the user's goal. Do not call team_create or team_join again.`;
12469
12234
  if (!intent.guidanceDelivered) {
12470
- const key = await waitForSessionKey(sessionId, 1e4);
12471
- await sendUserMessage(sessionId, key, instructions, "teams", { localId: `teams-adopt-${input.requestId}`, sentFrom: "team" });
12235
+ const key = await agentGuidance.waitForSessionKey(sessionId, 1e4);
12236
+ await agentGuidance.sendUserMessage(sessionId, key, instructions, "teams", { localId: `teams-adopt-${input.requestId}`, sentFrom: "team" });
12472
12237
  await retainTeam(path, input, connected.team.id, true);
12473
12238
  }
12474
12239
  operation.result = { status: "connected", teamId: connected.team.id, botId: connected.botId, instructions };
@@ -12584,12 +12349,14 @@ function fail(text) {
12584
12349
  function describeSession(id, persisted, extra) {
12585
12350
  const meta = persisted?.metadata;
12586
12351
  const parts = [id];
12352
+ if (extra?.live !== void 0) parts.push(extra.live ? "[running]" : "[not running]");
12587
12353
  if (meta?.summary?.text) parts.push(`title="${meta.summary.text}"`);
12588
12354
  if (meta?.path) parts.push(`cwd=${meta.path}`);
12589
12355
  if (meta?.flavor) parts.push(`agent=${meta.flavor}`);
12590
12356
  if (meta?.tags?.length) parts.push(`tags=${meta.tags.join(",")}`);
12591
12357
  if (meta?.variant === "assistant") parts.push("(assistant)");
12592
- parts.push(sessionWebUrl(id));
12358
+ if (extra?.pid) parts.push(`pid=${extra.pid}`);
12359
+ parts.push(agentGuidance.sessionWebUrl(id));
12593
12360
  return parts.join(" ");
12594
12361
  }
12595
12362
  function describeSummary(summary) {
@@ -12614,7 +12381,7 @@ function registerAssistantSessionTools(mcp) {
12614
12381
  inputSchema: {}
12615
12382
  }, async () => {
12616
12383
  try {
12617
- const sessions = await listSessions();
12384
+ const sessions = await agentGuidance.listSessions();
12618
12385
  if (sessions.length === 0) return ok("No sessions found on this machine.");
12619
12386
  return ok(`${sessions.length} session(s):
12620
12387
  ${sessions.map(describeSummary).join("\n")}`);
@@ -12630,9 +12397,9 @@ ${sessions.map(describeSummary).join("\n")}`);
12630
12397
  limit: z.z.number().optional().describe("How many recent messages to fetch (default 20, max 100)")
12631
12398
  }
12632
12399
  }, async (args) => {
12633
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12400
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12634
12401
  try {
12635
- const result = await readSessionTranscript(args.sessionId, args.limit ?? 20);
12402
+ const result = await agentGuidance.readSessionTranscript(args.sessionId, args.limit ?? 20);
12636
12403
  const body = result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)";
12637
12404
  return ok(`${describeSummary(result.summary)}
12638
12405
  --- last ${result.messageCount} message(s) ---
@@ -12642,19 +12409,27 @@ ${body}`);
12642
12409
  }
12643
12410
  });
12644
12411
  mcp.registerTool("session_send", {
12645
- 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.",
12412
+ 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.",
12646
12413
  title: "Send Message to Session",
12647
12414
  inputSchema: {
12648
12415
  sessionId: z.z.string().describe("The Happy session id to message"),
12649
- text: z.z.string().describe("The message text to send")
12416
+ text: z.z.string().describe("The message text to send"),
12417
+ resume: z.z.boolean().optional().describe("If the session is archived or offline, resume it on this machine (like the web Restore button) and wait until it is live before sending")
12650
12418
  }
12651
12419
  }, async (args) => {
12652
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12420
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12653
12421
  if (typeof args.text !== "string" || args.text.trim().length === 0) return fail("text must be non-empty");
12654
12422
  try {
12655
- const persisted = await waitForSessionKey(args.sessionId, 0);
12656
- await sendUserMessage(args.sessionId, persisted, args.text, MCP_CLIENT_TAG);
12657
- return ok(`Message delivered to ${describeSession(args.sessionId, persisted)}`);
12423
+ const persisted = await agentGuidance.waitForSessionKey(args.sessionId, 0);
12424
+ const result = await agentGuidance.deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12425
+ resume: args.resume === true,
12426
+ resumeHint: "Pass resume:true to bring it back on this machine, or spawn a new session."
12427
+ });
12428
+ if (!result.delivered) {
12429
+ const detail = result.error ?? `session is ${result.status}`;
12430
+ return fail(result.stored ? `Not delivered: ${detail}` : `Session ${args.sessionId} is ${result.status}: ${detail}`);
12431
+ }
12432
+ return ok(`Message delivered${result.resumed ? " (session resumed on this machine)" : ""} to ${describeSession(args.sessionId, persisted, { live: true })}`);
12658
12433
  } catch (error) {
12659
12434
  return fail(`Failed to send: ${error instanceof Error ? error.message : String(error)}`);
12660
12435
  }
@@ -12671,16 +12446,16 @@ ${body}`);
12671
12446
  if (!normalized.ok) return fail(normalized.error);
12672
12447
  const directory = normalized.directory;
12673
12448
  try {
12674
- const result = await index.spawnDaemonSession(directory, void 0, { spawnedBy: "assistant" });
12449
+ const result = await persistence.spawnDaemonSession(directory, void 0, { spawnedBy: "assistant" });
12675
12450
  if (result?.error || !result?.sessionId) {
12676
12451
  return fail(`Failed to spawn session: ${result?.error ?? "daemon returned no session id"}`);
12677
12452
  }
12678
12453
  const sessionId = result.sessionId;
12679
- const url = sessionWebUrl(sessionId);
12454
+ const url = agentGuidance.sessionWebUrl(sessionId);
12680
12455
  if (args.prompt && args.prompt.trim().length > 0) {
12681
12456
  try {
12682
- const persisted = await waitForSessionKey(sessionId, 15e3);
12683
- await sendUserMessage(sessionId, persisted, args.prompt, MCP_CLIENT_TAG);
12457
+ const persisted = await agentGuidance.waitForSessionKey(sessionId, 15e3);
12458
+ await agentGuidance.sendUserMessage(sessionId, persisted, args.prompt, MCP_CLIENT_TAG);
12684
12459
  return ok(`Spawned session ${sessionId} in ${directory} and sent the first prompt. It is now working in the background \u2014 check on it later with session_read. ${url}`);
12685
12460
  } catch (error) {
12686
12461
  return ok(`Spawned session ${sessionId} in ${directory}, but sending the first prompt failed (${error instanceof Error ? error.message : String(error)}). Use session_send to retry. ${url}`);
@@ -12698,10 +12473,10 @@ ${body}`);
12698
12473
  sessionId: z.z.string().describe("The Happy session id to stop")
12699
12474
  }
12700
12475
  }, async (args) => {
12701
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12476
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12702
12477
  try {
12703
12478
  const persisted = persistence.readPersistedSessions()[args.sessionId];
12704
- const success = await stopSession(args.sessionId);
12479
+ const success = await agentGuidance.stopSession(args.sessionId);
12705
12480
  if (!success) return fail(`Session ${args.sessionId} was not found among the daemon's running sessions.`);
12706
12481
  return ok(`Stopped ${describeSession(args.sessionId, persisted)}`);
12707
12482
  } catch (error) {
@@ -12715,9 +12490,9 @@ ${body}`);
12715
12490
  sessionId: z.z.string().describe("The Happy session id to archive")
12716
12491
  }
12717
12492
  }, async (args) => {
12718
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12493
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12719
12494
  try {
12720
- await archiveSession(args.sessionId);
12495
+ await agentGuidance.archiveSession(args.sessionId);
12721
12496
  const persisted = persistence.readPersistedSessions()[args.sessionId];
12722
12497
  return ok(`Archived ${describeSession(args.sessionId, persisted)}`);
12723
12498
  } catch (error) {
@@ -12751,7 +12526,7 @@ ${lines.join("\n")}`);
12751
12526
  lines: z.z.number().optional().describe("How many history lines to read (default 80, max 2000)")
12752
12527
  }
12753
12528
  }, async (args) => {
12754
- if (!index.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12529
+ if (!persistence.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12755
12530
  const r = index.readVhTerminal(args.terminalId, args.lines ?? 80);
12756
12531
  if (!r.ok) return fail(`Failed to read terminal ${args.terminalId}: ${r.error}`);
12757
12532
  return ok(`terminal ${args.terminalId} \u2014 last output:
@@ -12766,7 +12541,7 @@ ${r.text}`);
12766
12541
  submit: z.z.boolean().optional().describe("Also press Enter after pasting (default false)")
12767
12542
  }
12768
12543
  }, async (args) => {
12769
- if (!index.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12544
+ if (!persistence.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12770
12545
  if (typeof args.text !== "string" || args.text.length === 0) return fail("text must be non-empty");
12771
12546
  const submit = args.submit === true;
12772
12547
  const r = index.sendToVhTerminal(args.terminalId, args.text, submit);
@@ -12841,13 +12616,14 @@ function normalizeProgress(value) {
12841
12616
  return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
12842
12617
  }
12843
12618
 
12844
- function createMcpServer(handlers, options, sessionId) {
12619
+ function createMcpServer(handlers, options, sessionId, client) {
12845
12620
  const mcp = new mcp_js.McpServer({
12846
12621
  name: "Very Happy Tools",
12847
12622
  version: "1.0.0"
12848
12623
  });
12849
12624
  agentGuidance.registerTeamsTools(mcp, sessionId);
12850
12625
  agentGuidance.registerAutomationTools(mcp);
12626
+ if (client) agentGuidance.registerSessionPeerTools(mcp, agentGuidance.createSessionPeerToolExecutor(client));
12851
12627
  mcp.registerTool("change_title", {
12852
12628
  description: "Change the title of the current chat session",
12853
12629
  title: "Change Chat Title",
@@ -13050,7 +12826,7 @@ async function startHappyServer(client, options) {
13050
12826
  }
13051
12827
  return;
13052
12828
  }
13053
- const mcp = createMcpServer(handlers, options, client.sessionId);
12829
+ const mcp = createMcpServer(handlers, options, client.sessionId, client);
13054
12830
  try {
13055
12831
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
13056
12832
  sessionIdGenerator: void 0
@@ -13091,6 +12867,7 @@ async function startHappyServer(client, options) {
13091
12867
  agentGuidance.REPORT_PROGRESS_TOOL_NAME,
13092
12868
  ...agentGuidance.TEAM_TOOL_NAMES,
13093
12869
  ...agentGuidance.AUTOMATION_TOOL_NAMES,
12870
+ ...agentGuidance.SESSION_PEER_TOOL_NAMES,
13094
12871
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
13095
12872
  ],
13096
12873
  stop: () => {
@@ -13427,7 +13204,7 @@ function registerSideQuestionHandler(rpc, deps) {
13427
13204
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13428
13205
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13429
13206
  const fork = deps.run ?? (async (input) => {
13430
- const { query } = await Promise.resolve().then(function () { return require('./index-D49WvbDT.cjs'); });
13207
+ const { query } = await Promise.resolve().then(function () { return require('./index--KCpVs1c.cjs'); });
13431
13208
  return runSideQuestion(query, input);
13432
13209
  });
13433
13210
  const run = async (input, live) => {
@@ -13546,7 +13323,7 @@ const MAX_TITLE_CHARS = 60;
13546
13323
  const GENERATION_TIMEOUT_MS = 3e4;
13547
13324
  function resolveClaudeBinary() {
13548
13325
  try {
13549
- const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DHrwHxHf.cjs', document.baseURI).href)));
13326
+ const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D_fYUxVg.cjs', document.baseURI).href)));
13550
13327
  const utilsPath = path.resolve(path.join(terminalTitleSuggest.projectPath(), "scripts", "claude_version_utils.cjs"));
13551
13328
  const { getClaudeCliPath } = require$1(utilsPath);
13552
13329
  const path$1 = getClaudeCliPath();
@@ -14114,7 +13891,7 @@ async function runClaude(credentials, options = {}) {
14114
13891
  }
14115
13892
  try {
14116
13893
  persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
14117
- const result = await index.notifyDaemonSessionStarted(response.id, metadata, {
13894
+ const result = await persistence.notifyDaemonSessionStarted(response.id, metadata, {
14118
13895
  encryptionKey: persistence.encodeBase64(response.encryptionKey),
14119
13896
  encryptionVariant: response.encryptionVariant,
14120
13897
  seq: response.seq,
@@ -14214,6 +13991,17 @@ async function runClaude(credentials, options = {}) {
14214
13991
  });
14215
13992
  session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
14216
13993
  session.on("turn-ended", () => boardAnalyzer.onTurnEnd());
13994
+ const editThrottle = new EditReportThrottle();
13995
+ session.on("claude-session-message", (body) => {
13996
+ try {
13997
+ for (const edit of editThrottle.take(extractClaudeEditPaths(body))) {
13998
+ persistence.reportSessionEditToDaemon({ sessionId: response.id, path: edit.path, tool: edit.tool, cwd: workingDirectory, ...edit.at !== void 0 ? { at: edit.at } : {} });
13999
+ }
14000
+ } catch (error) {
14001
+ persistence.logger.debug("[START] edit report failed:", error);
14002
+ }
14003
+ });
14004
+ options = { ...options, claudeEnvVars: { ...options.claudeEnvVars, VH_PEER_SESSION_ID: response.id } };
14217
14005
  const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
14218
14006
  const remoteScanner = await createSessionScanner({
14219
14007
  sessionId: initialScannerSessionId,
@@ -14845,7 +14633,7 @@ async function handleAuthLogin(args) {
14845
14633
  console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
14846
14634
  try {
14847
14635
  persistence.logger.debug("Stopping daemon for force auth...");
14848
- await index.stopDaemon();
14636
+ await persistence.stopDaemon();
14849
14637
  console.log(chalk.gray("\u2713 Stopped daemon"));
14850
14638
  } catch (error) {
14851
14639
  persistence.logger.debug("Daemon was not running or failed to stop:", error);
@@ -14910,7 +14698,7 @@ async function handleAuthLogout() {
14910
14698
  if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
14911
14699
  try {
14912
14700
  try {
14913
- await index.stopDaemon();
14701
+ await persistence.stopDaemon();
14914
14702
  console.log(chalk.gray("Stopped daemon"));
14915
14703
  } catch {
14916
14704
  }
@@ -14955,7 +14743,7 @@ async function handleAuthStatus() {
14955
14743
  console.log(chalk.gray(`
14956
14744
  Data directory: ${persistence.configuration.happyHomeDir}`));
14957
14745
  try {
14958
- const running = await index.checkIfDaemonRunningAndCleanupStaleState();
14746
+ const running = await persistence.checkIfDaemonRunningAndCleanupStaleState();
14959
14747
  if (running) {
14960
14748
  console.log(chalk.green("\u2713 Daemon running"));
14961
14749
  } else {
@@ -15781,9 +15569,9 @@ function resolveLocalSignupBootstrap(configuredMode, configuredInviteCodes, gene
15781
15569
  };
15782
15570
  }
15783
15571
 
15784
- const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DHrwHxHf.cjs', document.baseURI).href)));
15572
+ const __filename$1 = node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D_fYUxVg.cjs', document.baseURI).href)));
15785
15573
  const __dirname$1 = path.dirname(__filename$1);
15786
- const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-DHrwHxHf.cjs', document.baseURI).href)));
15574
+ const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-D_fYUxVg.cjs', document.baseURI).href)));
15787
15575
  const PRISMA_QUERY_ENGINE_FILES = {
15788
15576
  "arm64-darwin": "libquery_engine-darwin-arm64.dylib.node",
15789
15577
  "x64-darwin": "libquery_engine-darwin.dylib.node",
@@ -16172,7 +15960,7 @@ const DAEMON_READY_TIMEOUT_MS = 5e3;
16172
15960
  const DAEMON_READY_POLL_INTERVAL_MS = 100;
16173
15961
  async function ensureDaemonRunning() {
16174
15962
  persistence.logger.debug("Ensuring Happy background service is running & matches our version...");
16175
- const versionMatches = await index.isDaemonRunningCurrentlyInstalledHappyVersion();
15963
+ const versionMatches = await persistence.isDaemonRunningCurrentlyInstalledHappyVersion();
16176
15964
  const state = await persistence.readDaemonState();
16177
15965
  const endpointsMatch = Boolean(state) && daemonEndpointsMatch(
16178
15966
  state?.serverUrl,
@@ -16183,9 +15971,9 @@ async function ensureDaemonRunning() {
16183
15971
  if (versionMatches && endpointsMatch) {
16184
15972
  return;
16185
15973
  }
16186
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
15974
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
16187
15975
  persistence.logger.debug("Stopping daemon whose version or configured relay no longer matches...");
16188
- await index.stopDaemon();
15976
+ await persistence.stopDaemon();
16189
15977
  }
16190
15978
  persistence.logger.debug("Starting Happy background service...");
16191
15979
  const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
@@ -16196,7 +15984,7 @@ async function ensureDaemonRunning() {
16196
15984
  daemonProcess.unref();
16197
15985
  const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
16198
15986
  while (Date.now() < deadline) {
16199
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
15987
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
16200
15988
  persistence.logger.debug("Happy background service is ready");
16201
15989
  return;
16202
15990
  }
@@ -17903,7 +17691,7 @@ async function runCodex(opts) {
17903
17691
  if (response) {
17904
17692
  try {
17905
17693
  persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
17906
- const result = await index.notifyDaemonSessionStarted(response.id, metadata, {
17694
+ const result = await persistence.notifyDaemonSessionStarted(response.id, metadata, {
17907
17695
  encryptionKey: persistence.encodeBase64(response.encryptionKey),
17908
17696
  encryptionVariant: response.encryptionVariant,
17909
17697
  seq: response.seq,
@@ -18200,6 +17988,7 @@ async function runCodex(opts) {
18200
17988
  return "denied";
18201
17989
  }
18202
17990
  });
17991
+ const editThrottle = new EditReportThrottle();
18203
17992
  client.setEventHandler((msg) => {
18204
17993
  if (msg.type === "token_count") {
18205
17994
  session.sendAgentUsageSnapshot("codex", msg);
@@ -18263,6 +18052,9 @@ async function runCodex(opts) {
18263
18052
  }
18264
18053
  if (msg.type === "patch_apply_begin") {
18265
18054
  const { changes } = msg;
18055
+ for (const edit of editThrottle.take(extractCodexPatchPaths(changes))) {
18056
+ persistence.reportSessionEditToDaemon({ sessionId: session.sessionId, path: edit.path, tool: edit.tool, cwd: process.cwd() });
18057
+ }
18266
18058
  const changeCount = Object.keys(changes).length;
18267
18059
  const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
18268
18060
  messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
@@ -18308,6 +18100,7 @@ async function runCodex(opts) {
18308
18100
  };
18309
18101
  let first = true;
18310
18102
  let appendSystemPromptInjected = false;
18103
+ process.env.VH_PEER_SESSION_ID = session.sessionId;
18311
18104
  try {
18312
18105
  persistence.logger.debug("[codex]: client.connect begin");
18313
18106
  await client.connect();
@@ -18721,7 +18514,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18721
18514
  return;
18722
18515
  } else if (subcommand === "install-terminal-hooks") {
18723
18516
  try {
18724
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-BF0kL0ae.cjs'); });
18517
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-XrcPvfBs.cjs'); });
18725
18518
  const command = parseTerminalHooksArgs(args.slice(1));
18726
18519
  if (command.action === "help") {
18727
18520
  console.log(TERMINAL_HOOKS_HELP);
@@ -18738,7 +18531,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18738
18531
  }
18739
18532
  } else if (subcommand === "agent-home") {
18740
18533
  try {
18741
- const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-BJ7bde35.cjs'); });
18534
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-aucT8BHw.cjs'); });
18742
18535
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18743
18536
  process.exit(0);
18744
18537
  } catch (error) {
@@ -18747,7 +18540,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18747
18540
  }
18748
18541
  } else if (subcommand === "spawn") {
18749
18542
  try {
18750
- const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-od9V23cc.cjs'); });
18543
+ const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-cIoglKVf.cjs'); });
18751
18544
  await handleSpawnCommand(args.slice(1));
18752
18545
  } catch (error) {
18753
18546
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18759,7 +18552,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18759
18552
  return;
18760
18553
  } else if (subcommand === "sessions") {
18761
18554
  try {
18762
- const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-qULzUzlI.cjs'); });
18555
+ const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-MX21NJXd.cjs'); });
18763
18556
  await handleSessionsCommand(args.slice(1));
18764
18557
  } catch (error) {
18765
18558
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18771,7 +18564,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18771
18564
  return;
18772
18565
  } else if (subcommand === "send") {
18773
18566
  try {
18774
- const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-9075kHZP.cjs'); });
18567
+ const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-CwrYXTPr.cjs'); });
18775
18568
  await handleSendCommand(args.slice(1));
18776
18569
  } catch (error) {
18777
18570
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18877,9 +18670,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18877
18670
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
18878
18671
  const projectId = args[3];
18879
18672
  try {
18880
- const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-D23kF0_u.cjs'); });
18881
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-DY_sFA3J.cjs'); }).then(function (n) { return n.persistence; });
18882
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-CUCU7Vw8.cjs'); }).then(function (n) { return n.api; });
18673
+ const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-BoePh9PX.cjs'); });
18674
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-DkfHJ3nS.cjs'); }).then(function (n) { return n.persistence; });
18675
+ const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-a2xsH2UI.cjs'); }).then(function (n) { return n.api; });
18883
18676
  let userEmail = void 0;
18884
18677
  try {
18885
18678
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -18910,7 +18703,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18910
18703
  }
18911
18704
  if (geminiSubcommand === "project" && args[2] === "get") {
18912
18705
  try {
18913
- const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-D23kF0_u.cjs'); });
18706
+ const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-BoePh9PX.cjs'); });
18914
18707
  const config = readGeminiLocalConfig();
18915
18708
  if (config.googleCloudProject) {
18916
18709
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -18950,7 +18743,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18950
18743
  process.exit(0);
18951
18744
  }
18952
18745
  try {
18953
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-CtOEFvhf.cjs'); });
18746
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-8Pxh94Fm.cjs'); });
18954
18747
  let startedBy = void 0;
18955
18748
  for (let i = 1; i < args.length; i++) {
18956
18749
  if (args[i] === "--started-by") {
@@ -18973,7 +18766,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18973
18766
  } else if (subcommand === "pi") {
18974
18767
  if (args[1] === "--terminal") {
18975
18768
  try {
18976
- const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-qdJ9KZVL.cjs'); });
18769
+ const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-DYzRcwgb.cjs'); });
18977
18770
  process.exitCode = await runPiTerminal(args.slice(2));
18978
18771
  } catch (error) {
18979
18772
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18990,7 +18783,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18990
18783
  }
18991
18784
  };
18992
18785
  try {
18993
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DEOVn46R.cjs'); });
18786
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DRrLaljt.cjs'); });
18994
18787
  const parsed = parsePiRunnerArgs(args.slice(1));
18995
18788
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
18996
18789
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -19018,7 +18811,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19018
18811
  return;
19019
18812
  } else if (subcommand === "acp") {
19020
18813
  try {
19021
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DEOVn46R.cjs'); });
18814
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DRrLaljt.cjs'); });
19022
18815
  let startedBy = void 0;
19023
18816
  let verbose = false;
19024
18817
  const acpArgs = [];
@@ -19058,7 +18851,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19058
18851
  return;
19059
18852
  } else if (subcommand === "openclaw") {
19060
18853
  try {
19061
- const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-DoqZXBvL.cjs'); });
18854
+ const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-B5FSZIbK.cjs'); });
19062
18855
  let startedBy = void 0;
19063
18856
  let verbose = false;
19064
18857
  let gatewayUrl;
@@ -19109,7 +18902,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19109
18902
  return;
19110
18903
  } else if (subcommand === "todo") {
19111
18904
  try {
19112
- const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-BCT_ZPuV.cjs'); });
18905
+ const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-Buuqn9gm.cjs'); });
19113
18906
  await handleTodoCommand(args.slice(1));
19114
18907
  } catch (error) {
19115
18908
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -19118,7 +18911,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19118
18911
  return;
19119
18912
  } else if (subcommand === "teams") {
19120
18913
  try {
19121
- const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-CrCzU-AV.cjs'); });
18914
+ const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-Dqk7F3D-.cjs'); });
19122
18915
  await handleTeamsCommand(args.slice(1));
19123
18916
  } catch (error) {
19124
18917
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -19127,7 +18920,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19127
18920
  return;
19128
18921
  } else if (subcommand === "auto") {
19129
18922
  try {
19130
- const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-C-90ojIH.cjs'); });
18923
+ const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-P9RoJaY0.cjs'); });
19131
18924
  await handleAutoCommand(args.slice(1));
19132
18925
  } catch (error) {
19133
18926
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -19139,7 +18932,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19139
18932
  return;
19140
18933
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
19141
18934
  try {
19142
- const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-JpqI2POW.cjs'); });
18935
+ const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-BqlXl10F.cjs'); });
19143
18936
  await handleMcpCommand(args[1] === "--terminal-tools");
19144
18937
  } catch (error) {
19145
18938
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -19162,7 +18955,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19162
18955
  const daemonSubcommand = args[1];
19163
18956
  if (daemonSubcommand === "list") {
19164
18957
  try {
19165
- const sessions = await index.listDaemonSessions();
18958
+ const sessions = await persistence.listDaemonSessions();
19166
18959
  if (sessions.length === 0) {
19167
18960
  console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
19168
18961
  } else {
@@ -19180,7 +18973,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19180
18973
  process.exit(1);
19181
18974
  }
19182
18975
  try {
19183
- const success = await index.stopDaemonSession(sessionId);
18976
+ const success = await persistence.stopDaemonSession(sessionId);
19184
18977
  console.log(success ? "Session stopped" : "Failed to stop session");
19185
18978
  } catch (error) {
19186
18979
  console.log("No daemon running");
@@ -19188,7 +18981,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19188
18981
  return;
19189
18982
  } else if (daemonSubcommand === "start") {
19190
18983
  const currentState = await persistence.readDaemonState();
19191
- const running = await index.checkIfDaemonRunningAndCleanupStaleState();
18984
+ const running = await persistence.checkIfDaemonRunningAndCleanupStaleState();
19192
18985
  const currentDaemonMatches = currentState && currentState.startedWithCliVersion === persistence.configuration.currentCliVersion && daemonEndpointsMatch(
19193
18986
  currentState.serverUrl,
19194
18987
  currentState.webappUrl,
@@ -19197,7 +18990,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19197
18990
  );
19198
18991
  if (running && !currentDaemonMatches) {
19199
18992
  console.log("Configured relay or CLI version changed; restarting the existing daemon");
19200
- await index.stopDaemon();
18993
+ await persistence.stopDaemon();
19201
18994
  }
19202
18995
  const child = spawnHappyCLI(["daemon", "start-sync"], {
19203
18996
  detached: true,
@@ -19207,7 +19000,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19207
19000
  child.unref();
19208
19001
  let started = false;
19209
19002
  for (let i = 0; i < 50; i++) {
19210
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
19003
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
19211
19004
  started = true;
19212
19005
  break;
19213
19006
  }
@@ -19224,7 +19017,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19224
19017
  await startDaemon();
19225
19018
  process.exit(0);
19226
19019
  } else if (daemonSubcommand === "stop") {
19227
- await index.stopDaemon();
19020
+ await persistence.stopDaemon();
19228
19021
  process.exit(0);
19229
19022
  } else if (daemonSubcommand === "status") {
19230
19023
  await runDoctorDaemon();
@@ -19515,11 +19308,10 @@ exports.BaseReasoningProcessor = BaseReasoningProcessor;
19515
19308
  exports.CHANGE_TITLE_INSTRUCTION = CHANGE_TITLE_INSTRUCTION;
19516
19309
  exports.CLAUDE_ATTACHMENT_KINDS = CLAUDE_ATTACHMENT_KINDS;
19517
19310
  exports.DEFAULT_GEMINI_MODEL = DEFAULT_GEMINI_MODEL;
19518
- exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT;
19311
+ exports.EditReportThrottle = EditReportThrottle;
19519
19312
  exports.GEMINI_API_KEY_ENV = GEMINI_API_KEY_ENV;
19520
19313
  exports.GEMINI_MODEL_ENV = GEMINI_MODEL_ENV;
19521
19314
  exports.GOOGLE_API_KEY_ENV = GOOGLE_API_KEY_ENV;
19522
- exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
19523
19315
  exports.MessageBuffer = MessageBuffer;
19524
19316
  exports.MessageQueue2 = MessageQueue2;
19525
19317
  exports.PI_TEAMS_EXTENSION = PI_TEAMS_EXTENSION;
@@ -19527,40 +19319,29 @@ exports.SPAWN_AGENTS = SPAWN_AGENTS;
19527
19319
  exports.StreamRelay = StreamRelay;
19528
19320
  exports.TEAM_SKILL = TEAM_SKILL;
19529
19321
  exports.TitleGenerator = TitleGenerator;
19530
- exports.TurnWaitTimeoutError = TurnWaitTimeoutError;
19531
19322
  exports.appendStagedAttachmentsToPrompt = appendStagedAttachmentsToPrompt;
19532
- exports.archiveSession = archiveSession;
19533
19323
  exports.checkPreviewPath = checkPreviewPath;
19534
19324
  exports.createSessionMetadata = createSessionMetadata;
19535
19325
  exports.describeAction = describeAction;
19536
19326
  exports.describeTrigger = describeTrigger;
19327
+ exports.extractAcpEditPaths = extractAcpEditPaths;
19537
19328
  exports.hashObject = hashObject;
19538
19329
  exports.initialMachineMetadata = initialMachineMetadata;
19539
19330
  exports.isValidSpawnOrigin = isValidSpawnOrigin;
19540
- exports.listAccountSessions = listAccountSessions;
19541
- exports.listSessions = listSessions;
19542
19331
  exports.normalizeAcpPermissionMode = normalizeAcpPermissionMode;
19543
19332
  exports.parseDuration = parseDuration;
19544
19333
  exports.query = query;
19545
- exports.readSessionMetadata = readSessionMetadata;
19546
- exports.readSessionTranscript = readSessionTranscript;
19547
19334
  exports.registerAgentAttachmentDownloads = registerAgentAttachmentDownloads;
19548
19335
  exports.registerAssistantSessionTools = registerAssistantSessionTools;
19549
19336
  exports.registerKillSessionHandler = registerKillSessionHandler;
19550
19337
  exports.removeSessionModeFile = removeSessionModeFile;
19551
19338
  exports.resolveFirstMessageMeta = resolveFirstMessageMeta;
19552
- exports.resolvePermissionRequest = resolvePermissionRequest;
19553
19339
  exports.resolveSpawnPermissionMode = resolveSpawnPermissionMode;
19554
19340
  exports.sanitizeSpawnPermissionMode = sanitizeSpawnPermissionMode;
19555
- exports.sendUserMessage = sendUserMessage;
19556
- exports.sessionWebUrl = sessionWebUrl;
19557
19341
  exports.setupOfflineReconnection = setupOfflineReconnection;
19558
19342
  exports.stageClaudeAttachments = stageClaudeAttachments;
19559
19343
  exports.startHappyServer = startHappyServer;
19560
- exports.stopSession = stopSession;
19561
19344
  exports.suggestTitleForPrompt = suggestTitleForPrompt;
19562
19345
  exports.tmuxSupportsSessionEnv = tmuxSupportsSessionEnv;
19563
19346
  exports.triggerFromFlags = triggerFromFlags;
19564
- exports.waitForSessionKey = waitForSessionKey;
19565
- exports.waitForTurnEnd = waitForTurnEnd;
19566
19347
  exports.writeSessionModeFile = writeSessionModeFile;