very-happy-cli 0.2.154 → 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-Ckyadhf4.mjs → AcpBackend-DVB0nzgR.mjs} +2 -2
  2. package/dist/{AcpBackend-B14xNj5L.cjs → AcpBackend-DmFcRh54.cjs} +3 -3
  3. package/dist/{AcpSessionManager-CCHmPdrR.cjs → AcpSessionManager-CWaSYTnT.cjs} +1 -1
  4. package/dist/{AcpSessionManager-H8t4P-IW.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-B20RWdgE.mjs → agentHome-ByLb2HFg.mjs} +5 -5
  8. package/dist/{agentHome-BUsLTZnz.cjs → agentHome-aucT8BHw.cjs} +5 -5
  9. package/dist/{auto-BQnncDcs.mjs → auto-BQnD6gVx.mjs} +21 -21
  10. package/dist/{auto-IeldARBS.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-B-ocQh5V.mjs → config-BPZ2Y6o8.mjs} +8 -8
  14. package/dist/{config-DM_xI9M6.cjs → config-BoePh9PX.cjs} +8 -8
  15. package/dist/{index-okLyUzeD.cjs → index--KCpVs1c.cjs} +7 -7
  16. package/dist/{index-BELV05ko.mjs → index-B4oXnbYw.mjs} +463 -889
  17. package/dist/{index-ZKuBjvHi.mjs → index-BjzdXb9x.mjs} +21 -13
  18. package/dist/{index-BUxBxSxJ.cjs → index-DRrLaljt.cjs} +21 -13
  19. package/dist/{index-CIx3248o.mjs → index-DVVRqaVG.mjs} +7 -7
  20. package/dist/{index-eCjkm8Y2.cjs → index-D_fYUxVg.cjs} +521 -960
  21. package/dist/index.cjs +6 -6
  22. package/dist/index.mjs +6 -6
  23. package/dist/{install-BZOdxfPd.cjs → install-CGigiocs.cjs} +1 -1
  24. package/dist/{install-CoLN3whR.mjs → install-DP5aPNEX.mjs} +1 -1
  25. package/dist/{installTerminalHooks-CA4OcAS7.cjs → installTerminalHooks-XrcPvfBs.cjs} +5 -5
  26. package/dist/{installTerminalHooks-Dde_8t36.mjs → installTerminalHooks-tIg7mANc.mjs} +5 -5
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.mjs +2 -2
  29. package/dist/{limits-Dt7_unH4.mjs → limits-C_DW0M0q.mjs} +309 -13
  30. package/dist/{limits-CmzISka2.cjs → limits-DkfHJ3nS.cjs} +329 -12
  31. package/dist/{mcp-7iDHlyCI.cjs → mcp-BqlXl10F.cjs} +23 -23
  32. package/dist/{mcp-9Ui4nQZL.mjs → mcp-o7QglAQM.mjs} +17 -17
  33. package/dist/{piRuntime-BesGGPME.cjs → piRuntime-BxP4B_zY.cjs} +2 -2
  34. package/dist/{piRuntime-C4E4pgye.mjs → piRuntime-MjiJI-nV.mjs} +2 -2
  35. package/dist/{piTerminal-fUogwxYr.cjs → piTerminal-DYzRcwgb.cjs} +6 -6
  36. package/dist/{piTerminal-CCEGCQOY.mjs → piTerminal-Dpq6yqKU.mjs} +6 -6
  37. package/dist/{runGemini-CNPGSsSK.cjs → runGemini-8Pxh94Fm.cjs} +7 -7
  38. package/dist/{runGemini-BW-pEklU.mjs → runGemini-CDlcQ-OA.mjs} +6 -6
  39. package/dist/{runOpenClaw-Bsodkg_s.cjs → runOpenClaw-B5FSZIbK.cjs} +6 -6
  40. package/dist/{runOpenClaw-NRB6-_jU.mjs → runOpenClaw-YkhsgPs0.mjs} +5 -5
  41. package/dist/{send-Bd00QcHC.cjs → send-CwrYXTPr.cjs} +8 -42
  42. package/dist/{send-DYeSFg8s.mjs → send-DJStmLBv.mjs} +5 -39
  43. package/dist/{sessions-DlPOYKLU.mjs → sessions-JaVXlSqj.mjs} +88 -45
  44. package/dist/{sessions-RIFUoy_g.cjs → sessions-MX21NJXd.cjs} +99 -56
  45. package/dist/{spawn-D1bhyHbH.mjs → spawn-BaPnjClT.mjs} +22 -22
  46. package/dist/{spawn-DxrfHTF4.cjs → spawn-cIoglKVf.cjs} +30 -30
  47. package/dist/{teams-pfKHpkgL.mjs → teams-DXV5Pwwt.mjs} +8 -8
  48. package/dist/{teams-Cfy0VMCo.cjs → teams-Dqk7F3D-.cjs} +8 -8
  49. package/dist/{todo-B2CjDg_V.mjs → todo-2Z1REhfx.mjs} +8 -6
  50. package/dist/{todo-B4cBFzHB.cjs → todo-Buuqn9gm.cjs} +8 -6
  51. package/dist/{types-D5RWyBE5.mjs → types-CkbfdRzq.mjs} +4 -282
  52. package/dist/{types-Bd_TGr0F.cjs → types-a2xsH2UI.cjs} +16 -311
  53. package/package.json +7 -7
  54. package/dist/agentGuidance-DUBXppv-.cjs +0 -554
  55. package/dist/agentGuidance-TLYQxsm2.mjs +0 -530
@@ -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-Bd_TGr0F.cjs');
7
- var persistence = require('./limits-CmzISka2.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-DUBXppv-.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-BUsLTZnz.cjs'); });
5406
- const { describeAgentHome } = await Promise.resolve().then(function () { return require('./types-Bd_TGr0F.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",
@@ -6293,7 +6240,9 @@ function startDaemonControlServer({
6293
6240
  pushClipboard,
6294
6241
  pushFilePreview,
6295
6242
  onTerminalHook,
6296
- setTerminalTitle
6243
+ setTerminalTitle,
6244
+ onSessionEdit,
6245
+ listPeers
6297
6246
  }) {
6298
6247
  return new Promise((resolve) => {
6299
6248
  const app = fastify({
@@ -6616,6 +6565,45 @@ function startDaemonControlServer({
6616
6565
  }
6617
6566
  return { status: "ok" };
6618
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() : [] }));
6619
6607
  typed.post("/stop", {
6620
6608
  schema: {
6621
6609
  response: {
@@ -6651,6 +6639,273 @@ function startDaemonControlServer({
6651
6639
  });
6652
6640
  }
6653
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
+
6654
6909
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6655
6910
 
6656
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
@@ -8187,7 +8442,7 @@ function parseJsonObject(text) {
8187
8442
  return null;
8188
8443
  }
8189
8444
  }
8190
- 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-eCjkm8Y2.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)) {
8191
8446
  const platformArch = `${platform}-${arch}`;
8192
8447
  const candidates = [
8193
8448
  `@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
@@ -8570,6 +8825,73 @@ class ClaudeAuthService {
8570
8825
  }
8571
8826
  }
8572
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
+
8573
8895
  const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
8574
8896
  function parseLines(lines) {
8575
8897
  const out = [];
@@ -8606,12 +8928,15 @@ function createMirrorScanner(opts) {
8606
8928
  }
8607
8929
  tail.offset = 0;
8608
8930
  }
8931
+ let replaced = false;
8609
8932
  if (tail.offset > size) {
8610
8933
  persistence.logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
8611
8934
  tail.offset = 0;
8935
+ replaced = true;
8612
8936
  }
8613
8937
  if (tail.offset === size) return;
8614
8938
  const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
8939
+ const replay = isFirstBackfillRead || replaced;
8615
8940
  const handle = await promises.open(tail.path, "r");
8616
8941
  let buf;
8617
8942
  try {
@@ -8631,7 +8956,7 @@ function createMirrorScanner(opts) {
8631
8956
  if (decision.truncated) opts.events.onBackfillTruncated?.();
8632
8957
  tail.mode = "from-eof";
8633
8958
  }
8634
- if (messages.length > 0) opts.events.onMessages(messages);
8959
+ if (messages.length > 0) opts.events.onMessages(messages, { replay });
8635
8960
  };
8636
8961
  const sync = new index.InvalidateSync(async () => {
8637
8962
  for (const tail of files.values()) {
@@ -8754,8 +9079,17 @@ function createMirrorManager(deps) {
8754
9079
  binding.client.updateMetadata((current) => ({ ...current, ...patch }));
8755
9080
  persistBinding(binding);
8756
9081
  };
8757
- const sendMessages = (binding, messages) => {
9082
+ const sendMessages = (binding, messages, context = { replay: false }) => {
8758
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
+ }
8759
9093
  const key = index.mirrorLineKey(message);
8760
9094
  binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => index.mirrorLocalId(key, envelopeIndex) } : void 0);
8761
9095
  }
@@ -8764,7 +9098,7 @@ function createMirrorManager(deps) {
8764
9098
  const scanner = createMirrorScanner({
8765
9099
  backfillLines,
8766
9100
  events: {
8767
- onMessages: (messages) => sendMessages(binding, messages),
9101
+ onMessages: (messages, context) => sendMessages(binding, messages, context),
8768
9102
  onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
8769
9103
  type: "message",
8770
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`
@@ -8787,6 +9121,10 @@ function createMirrorManager(deps) {
8787
9121
  await binding.scanner?.cleanup();
8788
9122
  binding.scanner = null;
8789
9123
  binding.client.closeClaudeSessionTurn("completed");
9124
+ try {
9125
+ deps.onSessionEnded?.(binding.happySessionId);
9126
+ } catch {
9127
+ }
8790
9128
  updateBindingMetadata(binding, {
8791
9129
  lifecycleState: "archived",
8792
9130
  lifecycleStateSince: Date.now(),
@@ -8999,6 +9337,13 @@ function createMirrorManager(deps) {
8999
9337
  }
9000
9338
  };
9001
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
+ },
9002
9347
  handleHookPayload(body) {
9003
9348
  const event = index.parseTerminalHookPayload(body);
9004
9349
  if (!event) {
@@ -9465,683 +9810,6 @@ Delegate from this working directory unless a different repository was explicitl
9465
9810
  ${goal}`;
9466
9811
  }
9467
9812
 
9468
- const MAX_LINE_CHARS = 500;
9469
- const MAX_ARGS_CHARS = 160;
9470
- const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
9471
- const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
9472
- function truncateText(text, max = MAX_LINE_CHARS) {
9473
- const chars = Array.from(text);
9474
- if (chars.length <= max) return text;
9475
- return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
9476
- }
9477
- function squash(text) {
9478
- return text.replace(/\s+/g, " ").trim();
9479
- }
9480
- function safeJson(value, max) {
9481
- try {
9482
- return truncateText(JSON.stringify(value) ?? "", max);
9483
- } catch {
9484
- return "[unserializable]";
9485
- }
9486
- }
9487
- function formatMessageBody(body, limits = DEFAULT_LIMITS) {
9488
- if (!body || typeof body !== "object") return null;
9489
- const b = body;
9490
- if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
9491
- const text = squash(b.content.text);
9492
- return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
9493
- }
9494
- if (b.role === "session" && b.content && typeof b.content === "object") {
9495
- const env = b.content;
9496
- const ev = env.ev;
9497
- if (!ev || typeof ev.t !== "string") return null;
9498
- switch (ev.t) {
9499
- case "text": {
9500
- if (ev.thinking) return null;
9501
- const text = squash(String(ev.text ?? ""));
9502
- if (text.length === 0) return null;
9503
- const role = env.role === "user" ? "user" : "assistant";
9504
- return `${role}: ${truncateText(text, limits.line)}`;
9505
- }
9506
- case "tool-call-start": {
9507
- const name = String(ev.name ?? "tool");
9508
- const title = squash(String(ev.title ?? ""));
9509
- const args = ev.args ? safeJson(ev.args, limits.args) : "";
9510
- const detail = title || args;
9511
- return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
9512
- }
9513
- case "service": {
9514
- const text = squash(String(ev.text ?? ""));
9515
- return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
9516
- }
9517
- case "turn-end": {
9518
- if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
9519
- return null;
9520
- }
9521
- default:
9522
- return null;
9523
- }
9524
- }
9525
- if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
9526
- const d = b.content.data;
9527
- switch (d.type) {
9528
- case "message": {
9529
- const text = squash(String(d.message ?? ""));
9530
- return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
9531
- }
9532
- case "tool-call":
9533
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
9534
- case "permission-request":
9535
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
9536
- default:
9537
- return null;
9538
- }
9539
- }
9540
- if (b.role === "agent" && b.content?.type === "output") {
9541
- const msg = b.content.data?.message;
9542
- if (msg && Array.isArray(msg.content)) {
9543
- const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
9544
- if (texts.length > 0) {
9545
- const role = msg.role === "user" ? "user" : "assistant";
9546
- return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
9547
- }
9548
- }
9549
- return null;
9550
- }
9551
- return null;
9552
- }
9553
- function formatTranscript(bodies, options = {}) {
9554
- const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
9555
- const lines = [];
9556
- for (const body of bodies) {
9557
- const line = formatMessageBody(body, limits);
9558
- if (line !== null) lines.push(line);
9559
- }
9560
- return lines.join("\n");
9561
- }
9562
-
9563
- function asRecord(value) {
9564
- return value && typeof value === "object" ? value : null;
9565
- }
9566
- function sessionEnvelope(body) {
9567
- const b = asRecord(body);
9568
- if (!b || b.role !== "session") return null;
9569
- const env = asRecord(b.content);
9570
- if (!env) return null;
9571
- const ev = asRecord(env.ev);
9572
- return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
9573
- }
9574
- function isUserPrompt(body) {
9575
- const b = asRecord(body);
9576
- return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
9577
- }
9578
- function agentPiece(body) {
9579
- const env = sessionEnvelope(body);
9580
- if (env && env.role !== "user") {
9581
- if (env.ev.t === "text" && !env.ev.thinking) {
9582
- const text = String(env.ev.text ?? "");
9583
- return text.trim().length > 0 ? { kind: "text", text } : null;
9584
- }
9585
- if (env.ev.t === "tool-call-start") return { kind: "tool" };
9586
- return null;
9587
- }
9588
- const b = asRecord(body);
9589
- if (b?.role === "agent" && b.content?.type === "acp") {
9590
- const d = asRecord(b.content.data);
9591
- if (d?.type === "message") {
9592
- const text = String(d.message ?? "");
9593
- return text.trim().length > 0 ? { kind: "text", text } : null;
9594
- }
9595
- if (d?.type === "tool-call") return { kind: "tool" };
9596
- }
9597
- return null;
9598
- }
9599
- function userPromptText(body) {
9600
- return isUserPrompt(body) ? body.content.text : null;
9601
- }
9602
- function analyzeLatestTurn(entries, options = {}) {
9603
- const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
9604
- let userIndex = -1;
9605
- for (let i = entries.length - 1; i >= 0; i--) {
9606
- if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
9607
- userIndex = i;
9608
- break;
9609
- }
9610
- }
9611
- let ended = false;
9612
- let status = null;
9613
- let error = null;
9614
- let texts = [];
9615
- let lastMarker = null;
9616
- let startedAfterPrompt = false;
9617
- const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
9618
- for (let i = userIndex + 1; i < entries.length; i++) {
9619
- const body = entries[i].body;
9620
- if (options.anchor && isUserPrompt(body)) {
9621
- if (ended) break;
9622
- continue;
9623
- }
9624
- const env = sessionEnvelope(body);
9625
- if (env?.ev?.t === "turn-start") {
9626
- lastMarker = "start";
9627
- startedAfterPrompt = true;
9628
- ended = false;
9629
- status = null;
9630
- error = null;
9631
- texts = [];
9632
- continue;
9633
- }
9634
- if (env?.ev?.t === "turn-end") {
9635
- lastMarker = "end";
9636
- if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
9637
- ended = true;
9638
- status = typeof env.ev.status === "string" ? env.ev.status : null;
9639
- error = typeof env.ev.error === "string" ? env.ev.error : null;
9640
- continue;
9641
- }
9642
- const piece = agentPiece(body);
9643
- if (!piece) continue;
9644
- if (piece.kind === "tool") texts = [];
9645
- else texts.push(piece.text);
9646
- }
9647
- if (userIndex < 0) ended = lastMarker === "end";
9648
- return {
9649
- userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
9650
- ended,
9651
- status: ended ? status : null,
9652
- error: ended ? error : null,
9653
- answer: texts.join("\n\n").trim(),
9654
- lastSeq
9655
- };
9656
- }
9657
-
9658
- const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9659
- async function openUserScopedSocket(token, options = {}) {
9660
- const socket = socket_ioClient.io(persistence.configuration.serverUrl, {
9661
- auth: {
9662
- token,
9663
- clientType: "user-scoped",
9664
- happyClient: `cli-session-ops/${persistence.configuration.currentCliVersion}`
9665
- },
9666
- path: "/v1/updates",
9667
- transports: ["websocket"],
9668
- reconnection: false,
9669
- autoConnect: false
9670
- });
9671
- await new Promise((resolve, reject) => {
9672
- const timer = setTimeout(() => {
9673
- socket.close();
9674
- reject(new Error(`Timed out connecting to ${persistence.configuration.serverUrl}`));
9675
- }, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
9676
- socket.once("connect", () => {
9677
- clearTimeout(timer);
9678
- resolve();
9679
- });
9680
- socket.once("connect_error", (error) => {
9681
- clearTimeout(timer);
9682
- socket.close();
9683
- reject(new Error(`Could not connect to ${persistence.configuration.serverUrl}: ${error.message}`));
9684
- });
9685
- socket.connect();
9686
- });
9687
- return {
9688
- async rpcCall(payload, timeoutMs) {
9689
- const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
9690
- return ack;
9691
- },
9692
- close() {
9693
- socket.close();
9694
- }
9695
- };
9696
- }
9697
-
9698
- const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
9699
- const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
9700
- const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
9701
- const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
9702
- const RPC_RATE_MAX_RETRIES = 2;
9703
- function isRpcRateLimitAck(ack) {
9704
- if (!ack || typeof ack !== "object") return false;
9705
- const a = ack;
9706
- if (a.ok !== false) return false;
9707
- if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
9708
- return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
9709
- }
9710
- function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
9711
- if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
9712
- return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
9713
- }
9714
- const exponent = Math.max(0, Math.min(attempt, 32) - 1);
9715
- const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
9716
- return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
9717
- }
9718
-
9719
- const PERMISSION_OPS_CLIENT = "session-ops";
9720
- const PERMISSION_RPC_TIMEOUT_MS = 3e4;
9721
- const SETTLE_TIMEOUT_MS = 5e3;
9722
- const SETTLE_POLL_MS = 500;
9723
- function buildPermissionRpcPayload(requestId, verdict) {
9724
- if (verdict.kind === "approve") {
9725
- return {
9726
- id: requestId,
9727
- approved: true,
9728
- decision: verdict.forSession ? "approved_for_session" : "approved"
9729
- };
9730
- }
9731
- return {
9732
- id: requestId,
9733
- approved: false,
9734
- decision: "denied",
9735
- ...verdict.reason ? { reason: verdict.reason } : {}
9736
- };
9737
- }
9738
- function interpretPermissionAck(ack, decryptResult) {
9739
- if (!ack.ok) {
9740
- const message = ack.error ?? "RPC call failed";
9741
- if (message === "RPC method not available") {
9742
- return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
9743
- }
9744
- if (message === "RPC target disconnected") {
9745
- return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
9746
- }
9747
- if (/timed? ?out/i.test(message)) {
9748
- return { status: "timeout", message };
9749
- }
9750
- if (isRpcRateLimitAck(ack)) {
9751
- return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
9752
- }
9753
- return { status: "rejected", message };
9754
- }
9755
- let body = null;
9756
- if (typeof ack.result === "string" && ack.result.length > 0) {
9757
- try {
9758
- body = decryptResult(ack.result);
9759
- } catch {
9760
- body = null;
9761
- }
9762
- }
9763
- if (body && typeof body === "object" && typeof body.error === "string") {
9764
- return { status: "handler-error", message: body.error };
9765
- }
9766
- return { status: "acknowledged" };
9767
- }
9768
- function pendingRequestsOf(agentState, now) {
9769
- const requests = agentState?.requests;
9770
- if (!requests || typeof requests !== "object") return [];
9771
- return Object.entries(requests).map(([id, request]) => ({
9772
- id,
9773
- tool: typeof request?.tool === "string" ? request.tool : "unknown",
9774
- ...request?.kind ? { kind: request.kind } : {},
9775
- ...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
9776
- })).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
9777
- }
9778
- const RPC_DEADLINE_GRACE_MS = 2e3;
9779
- function withDeadline(promise, ms, message) {
9780
- return new Promise((resolve, reject) => {
9781
- const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
9782
- promise.then(
9783
- (value) => {
9784
- clearTimeout(timer);
9785
- resolve(value);
9786
- },
9787
- (error) => {
9788
- clearTimeout(timer);
9789
- reject(error);
9790
- }
9791
- );
9792
- });
9793
- }
9794
- async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
9795
- const now = deps.now ?? Date.now;
9796
- const persisted = (deps.readPersisted ?? persistence.readPersistedSessions)()[sessionId];
9797
- if (!persisted) {
9798
- throw new Error(
9799
- `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.`
9800
- );
9801
- }
9802
- const token = await (deps.bearerToken ?? bearerToken$1)();
9803
- const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
9804
- const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9805
- if (!before.some((request) => request.id === requestId)) {
9806
- const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
9807
- throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
9808
- }
9809
- const payload = buildPermissionRpcPayload(requestId, verdict);
9810
- const key = persistence.decodeBase64(persisted.encryptionKey);
9811
- const params = persistence.encodeBase64(persistence.encrypt(key, persisted.encryptionVariant, payload));
9812
- const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
9813
- const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
9814
- const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
9815
- let ack;
9816
- try {
9817
- for (let attempt = 1; ; attempt++) {
9818
- try {
9819
- ack = await withDeadline(
9820
- transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
9821
- rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
9822
- `permission RPC did not settle within ${rpcTimeoutMs}ms`
9823
- );
9824
- } catch (error) {
9825
- ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
9826
- }
9827
- if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
9828
- const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
9829
- deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
9830
- await sleep(waitMs);
9831
- }
9832
- } finally {
9833
- transport.close();
9834
- }
9835
- const outcome = interpretPermissionAck(ack, (encrypted) => persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(encrypted)));
9836
- if (outcome.status !== "acknowledged") {
9837
- return { sessionId, requestId, payload, outcome };
9838
- }
9839
- const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
9840
- let settled = false;
9841
- for (; ; ) {
9842
- const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
9843
- if (!pending.some((request) => request.id === requestId)) {
9844
- settled = true;
9845
- break;
9846
- }
9847
- if (now() >= deadline) break;
9848
- await sleep(SETTLE_POLL_MS);
9849
- }
9850
- return { sessionId, requestId, payload, outcome, settled };
9851
- }
9852
- async function bearerToken$1() {
9853
- const credentials = await persistence.readCredentialsForConfiguredRelay();
9854
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9855
- return credentials.token;
9856
- }
9857
- async function fetchDecryptedAgentState(sessionId, persisted, token) {
9858
- const response = await axios.get(
9859
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
9860
- {
9861
- headers: {
9862
- "Authorization": `Bearer ${token}`,
9863
- "X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
9864
- },
9865
- timeout: 15e3,
9866
- // 404 is a real answer ("not yours / gone"), not a transport fault.
9867
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
9868
- }
9869
- );
9870
- if (response.status === 404) {
9871
- 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).`);
9872
- }
9873
- const raw = response.data?.session?.agentState;
9874
- if (typeof raw !== "string" || raw.length === 0) return null;
9875
- return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(raw));
9876
- }
9877
-
9878
- const SESSION_OPS_CLIENT = "session-ops";
9879
- const DEFAULT_RECENT_LIMIT = 15;
9880
- const MAX_READ_LIMIT = 100;
9881
- function mergeSessionSummaries(live, persisted, options = {}) {
9882
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9883
- const seen = /* @__PURE__ */ new Set();
9884
- const summaries = [];
9885
- for (const child of live) {
9886
- const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
9887
- if (!id || seen.has(id)) continue;
9888
- seen.add(id);
9889
- summaries.push(toSummary(id, persisted[id], livenessOf(child)));
9890
- }
9891
- 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);
9892
- for (const [id, entry] of rest) {
9893
- summaries.push(toSummary(id, entry, { live: false }));
9894
- }
9895
- if (!options.tag) return summaries;
9896
- const wanted = options.tag;
9897
- return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
9898
- }
9899
- function livenessOf(child) {
9900
- return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
9901
- }
9902
- function sessionLiveness(live, sessionId) {
9903
- const child = live.find((entry) => entry.happySessionId === sessionId);
9904
- return child ? livenessOf(child) : { live: false };
9905
- }
9906
- function toSummary(id, persisted, extra) {
9907
- const meta = persisted?.metadata;
9908
- return {
9909
- id,
9910
- live: extra.live,
9911
- ...extra.pid !== void 0 ? { pid: extra.pid } : {},
9912
- ...meta?.summary?.text ? { title: meta.summary.text } : {},
9913
- ...meta?.path ? { cwd: meta.path } : {},
9914
- ...meta?.flavor ? { flavor: meta.flavor } : {},
9915
- ...meta?.tags?.length ? { tags: [...meta.tags] } : {},
9916
- ...meta?.variant ? { variant: meta.variant } : {},
9917
- url: sessionWebUrl(id),
9918
- ...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
9919
- };
9920
- }
9921
- async function listSessions(options = {}) {
9922
- const live = await index.listDaemonSessions();
9923
- return mergeSessionSummaries(live, persistence.readPersistedSessions(), options);
9924
- }
9925
- function summarizeAccountSession(row, persisted, liveIds, now) {
9926
- const base = {
9927
- id: row.id,
9928
- live: liveIds.has(row.id),
9929
- url: sessionWebUrl(row.id),
9930
- decryptable: false,
9931
- active: row.active === true,
9932
- archived: typeof row.archivedAt === "number",
9933
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
9934
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
9935
- attention: false
9936
- };
9937
- if (!persisted) return base;
9938
- const key = persistence.decodeBase64(persisted.encryptionKey);
9939
- let metadata = null;
9940
- let agentState = null;
9941
- try {
9942
- metadata = row.metadata ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.metadata)) : null;
9943
- agentState = row.agentState ? persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(row.agentState)) : null;
9944
- } catch {
9945
- return base;
9946
- }
9947
- if (!metadata) return base;
9948
- const meta = metadata;
9949
- const pending = pendingRequestsOf(agentState, now);
9950
- return {
9951
- ...base,
9952
- decryptable: true,
9953
- ...meta.summary?.text ? { title: meta.summary.text } : {},
9954
- ...meta.path ? { cwd: meta.path } : {},
9955
- ...meta.flavor ? { flavor: meta.flavor } : {},
9956
- ...meta.tags?.length ? { tags: [...meta.tags] } : {},
9957
- ...meta.variant ? { variant: meta.variant } : {},
9958
- ...meta.machineId ? { machineId: meta.machineId } : {},
9959
- ...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
9960
- pending,
9961
- attention: pending.length > 0
9962
- };
9963
- }
9964
- function orderAccountSessions(summaries, options = {}) {
9965
- const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
9966
- const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
9967
- let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
9968
- const byRank = rank(a) - rank(b);
9969
- if (byRank !== 0) return byRank;
9970
- if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
9971
- return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
9972
- });
9973
- if (options.tag) {
9974
- const wanted = options.tag;
9975
- rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
9976
- }
9977
- const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
9978
- let idleKept = 0;
9979
- return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
9980
- }
9981
- async function listAccountSessions(options = {}) {
9982
- const token = await bearerToken();
9983
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions`, {
9984
- headers: {
9985
- "Authorization": `Bearer ${token}`,
9986
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
9987
- },
9988
- timeout: 15e3
9989
- });
9990
- const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
9991
- const persisted = persistence.readPersistedSessions();
9992
- const live = await index.listDaemonSessions();
9993
- const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
9994
- const now = Date.now();
9995
- const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
9996
- return orderAccountSessions(summaries, options);
9997
- }
9998
- async function bearerToken() {
9999
- const credentials = await persistence.readCredentialsForConfiguredRelay();
10000
- if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
10001
- return credentials.token;
10002
- }
10003
- function requirePersisted(sessionId) {
10004
- const persisted = persistence.readPersistedSessions()[sessionId];
10005
- if (!persisted) {
10006
- throw new Error(
10007
- `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
10008
- );
10009
- }
10010
- return persisted;
10011
- }
10012
- async function readSessionLog(sessionId, persisted, limit) {
10013
- const token = await bearerToken();
10014
- const response = await axios.get(
10015
- `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
10016
- {
10017
- params: { before_seq: 2147483647, limit },
10018
- headers: {
10019
- "Authorization": `Bearer ${token}`,
10020
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10021
- },
10022
- timeout: 15e3
10023
- }
10024
- );
10025
- const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
10026
- messages.reverse();
10027
- const key = persistence.decodeBase64(persisted.encryptionKey);
10028
- return messages.map((message) => {
10029
- if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
10030
- try {
10031
- return { seq: message.seq, body: persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(message.content.c)) };
10032
- } catch {
10033
- return { seq: message.seq, body: null };
10034
- }
10035
- });
10036
- }
10037
- const TURN_WINDOW = 500;
10038
- async function readSessionTranscript(sessionId, limit, options = {}) {
10039
- const persisted = requirePersisted(sessionId);
10040
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
10041
- const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
10042
- const shown = entries.slice(-bounded);
10043
- const live = await index.listDaemonSessions();
10044
- return {
10045
- summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
10046
- messageCount: shown.length,
10047
- transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
10048
- turn: analyzeLatestTurn(entries)
10049
- };
10050
- }
10051
- async function readSessionMetadata(sessionId, persisted) {
10052
- const token = await bearerToken();
10053
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10054
- headers: {
10055
- "Authorization": `Bearer ${token}`,
10056
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10057
- },
10058
- timeout: 15e3
10059
- });
10060
- const ciphertext = response.data?.session?.metadata;
10061
- if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
10062
- return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(ciphertext));
10063
- }
10064
- async function readSessionState(sessionId, persisted) {
10065
- const token = await bearerToken();
10066
- const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
10067
- headers: {
10068
- "Authorization": `Bearer ${token}`,
10069
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10070
- },
10071
- timeout: 15e3,
10072
- validateStatus: (status) => status >= 200 && status < 300 || status === 404
10073
- });
10074
- if (response.status === 404) return { found: false, active: false };
10075
- const row = response.data?.session;
10076
- if (!row || typeof row.id !== "string") throw new Error(`Server returned no session row for ${sessionId}`);
10077
- const state = {
10078
- found: true,
10079
- active: row.active === true,
10080
- ...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
10081
- archivedAt: typeof row.archivedAt === "number" ? row.archivedAt : null,
10082
- ...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {}
10083
- };
10084
- if (persisted && typeof row.metadata === "string" && row.metadata.length > 0) {
10085
- try {
10086
- const metadata = persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(row.metadata));
10087
- if (metadata?.machineId) state.machineId = metadata.machineId;
10088
- } catch {
10089
- }
10090
- }
10091
- return state;
10092
- }
10093
- class TurnWaitTimeoutError extends Error {
10094
- constructor(sessionId, timeoutMs, turn) {
10095
- super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
10096
- this.sessionId = sessionId;
10097
- this.timeoutMs = timeoutMs;
10098
- this.turn = turn;
10099
- }
10100
- }
10101
- async function waitForTurnEnd(sessionId, options) {
10102
- const persisted = requirePersisted(sessionId);
10103
- const pollMs = options.pollMs ?? 3e3;
10104
- const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10105
- const deadline = Date.now() + options.timeoutMs;
10106
- while (true) {
10107
- const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
10108
- if (turn.ended) return turn;
10109
- if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
10110
- await sleep(pollMs);
10111
- }
10112
- }
10113
- async function stopSession(sessionId) {
10114
- return await index.stopDaemonSession(sessionId);
10115
- }
10116
- async function archiveSession(sessionId) {
10117
- const token = await bearerToken();
10118
- await axios.post(
10119
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
10120
- {},
10121
- {
10122
- headers: {
10123
- "Authorization": `Bearer ${token}`,
10124
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10125
- },
10126
- timeout: 1e4
10127
- }
10128
- );
10129
- }
10130
- async function unarchiveSession(sessionId) {
10131
- const token = await bearerToken();
10132
- await axios.post(
10133
- `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
10134
- {},
10135
- {
10136
- headers: {
10137
- "Authorization": `Bearer ${token}`,
10138
- "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10139
- },
10140
- timeout: 1e4
10141
- }
10142
- );
10143
- }
10144
-
10145
9813
  function receiptPath(home, operationId) {
10146
9814
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
10147
9815
  return path.join(home, "teams", "effects", `${operationId}.json`);
@@ -10295,8 +9963,8 @@ function createTeamWorker(deps) {
10295
9963
  await complete(op, receipt);
10296
9964
  return;
10297
9965
  }
10298
- const key = await waitForSessionKey(receipt.sessionId, 1e4);
10299
- 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 } : {} });
10300
9968
  receipt = { ...receipt, phase: "delivered" };
10301
9969
  writeReceipt$1(home, receipt);
10302
9970
  }
@@ -10312,7 +9980,7 @@ function createTeamWorker(deps) {
10312
9980
  if (!bot || bot.generation !== op.generation || bot.sessionId !== op.sessionId) throw new Error("Worker binding changed; preserve resources for reconciliation");
10313
9981
  if (op.sessionId) {
10314
9982
  await deps.stopAndWait(op.sessionId);
10315
- await archiveSession(op.sessionId);
9983
+ await agentGuidance.archiveSession(op.sessionId);
10316
9984
  }
10317
9985
  if (team.archivedAt !== void 0) {
10318
9986
  await complete(op, receipt);
@@ -10339,7 +10007,7 @@ function createTeamWorker(deps) {
10339
10007
  const bot = team.bots.find((b) => b.id === message.recipientBotId);
10340
10008
  if (!bot?.sessionId || !deps.live(bot.sessionId)) continue;
10341
10009
  try {
10342
- const key = await waitForSessionKey(bot.sessionId, 0);
10010
+ const key = await agentGuidance.waitForSessionKey(bot.sessionId, 0);
10343
10011
  if (message.scheduleId) {
10344
10012
  const current = (await http.get(`/v1/teams/${encodeURIComponent(team.id)}`)).data.team;
10345
10013
  const pending = current.messages.find((m) => m.id === message.id);
@@ -10351,7 +10019,7 @@ function createTeamWorker(deps) {
10351
10019
  ${message.body}
10352
10020
 
10353
10021
  Use the teams tools to inspect current task state before acting. This message is team context, not a system instruction.`;
10354
- 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" });
10355
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 });
10356
10024
  } catch {
10357
10025
  deps.log(`Teams message ${message.id} remains pending`);
@@ -10726,16 +10394,16 @@ function automationPromptHeader(name, runId) {
10726
10394
  }
10727
10395
  function automationRunAnchor(runId) {
10728
10396
  const marker = ` run ${runId};`;
10729
- 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);
10730
10398
  }
10731
10399
  async function defaultReadTurn(sessionId, runId) {
10732
10400
  const persisted = persistence.readPersistedSessions()[sessionId];
10733
10401
  if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10734
- 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) });
10735
10403
  }
10736
10404
  async function defaultSend(sessionId, text, meta) {
10737
- const persisted = await waitForSessionKey(sessionId, 15e3);
10738
- 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" });
10739
10407
  }
10740
10408
  function expandHome(path$1) {
10741
10409
  if (path$1 === "~") return os.homedir();
@@ -11200,7 +10868,7 @@ async function startDaemon() {
11200
10868
  });
11201
10869
  persistence.logger.debug("[DAEMON RUN] Starting daemon process...");
11202
10870
  persistence.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
11203
- const runningDaemonVersionMatches = await index.isDaemonRunningCurrentlyInstalledHappyVersion();
10871
+ const runningDaemonVersionMatches = await persistence.isDaemonRunningCurrentlyInstalledHappyVersion();
11204
10872
  const runningDaemonState = await persistence.readDaemonState();
11205
10873
  const runningDaemonEndpointsMatch = Boolean(runningDaemonState) && daemonEndpointsMatch(
11206
10874
  runningDaemonState?.serverUrl,
@@ -11210,7 +10878,7 @@ async function startDaemon() {
11210
10878
  );
11211
10879
  if (!runningDaemonVersionMatches || !runningDaemonEndpointsMatch) {
11212
10880
  persistence.logger.debug("[DAEMON RUN] Daemon version or endpoint mismatch detected, restarting daemon");
11213
- await index.stopDaemon();
10881
+ await persistence.stopDaemon();
11214
10882
  } else {
11215
10883
  persistence.logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
11216
10884
  console.log("Daemon already running with matching version");
@@ -11230,6 +10898,7 @@ async function startDaemon() {
11230
10898
  persistence.logger.debug("[DAEMON RUN] Auth and machine setup complete");
11231
10899
  const pidToTrackedSession = /* @__PURE__ */ new Map();
11232
10900
  const turnActivity = new index.TurnActivityTracker();
10901
+ const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
11233
10902
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
11234
10903
  let teamWorker;
11235
10904
  let automationRunner;
@@ -12089,6 +11758,7 @@ async function startDaemon() {
12089
11758
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
12090
11759
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
12091
11760
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11761
+ if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
12092
11762
  if (session?.happySessionId && session.encryption) {
12093
11763
  sessionIdToFinishedSession.set(session.happySessionId, session);
12094
11764
  persistence.logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
@@ -12147,7 +11817,7 @@ async function startDaemon() {
12147
11817
  sessionId
12148
11818
  );
12149
11819
  assistantReportLastSentAt.set(sessionId, Date.now());
12150
- await sendUserMessage(
11820
+ await agentGuidance.sendUserMessage(
12151
11821
  decision.assistantSessionId,
12152
11822
  assistantEntry,
12153
11823
  formatAssistantReportMessage(title, sessionId, event),
@@ -12174,6 +11844,10 @@ async function startDaemon() {
12174
11844
  onSessionStateEvent,
12175
11845
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
12176
11846
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
11847
+ onSessionEdit: (edit) => {
11848
+ peerCoordinator.onEdit(edit);
11849
+ },
11850
+ listPeers: () => peerCoordinator.list(),
12177
11851
  pushClipboard: (text, terminalId) => {
12178
11852
  if (!apiMachineRef) {
12179
11853
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -12239,9 +11913,12 @@ async function startDaemon() {
12239
11913
  const mirrorManager = createMirrorManager({
12240
11914
  api,
12241
11915
  machineId,
12242
- onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
11916
+ onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
11917
+ onEdit: (edit) => peerCoordinator.onEdit(edit),
11918
+ onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
12243
11919
  });
12244
11920
  mirrorManagerRef = mirrorManager;
11921
+ peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
12245
11922
  apiMachine.setMirrorIntegration({
12246
11923
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
12247
11924
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
@@ -12369,7 +12046,7 @@ async function startDaemon() {
12369
12046
  claudeAuthService.stop();
12370
12047
  apiMachine.shutdown();
12371
12048
  await stopControlServer();
12372
- await index.cleanupDaemonState();
12049
+ await persistence.cleanupDaemonState();
12373
12050
  await persistence.releaseDaemonLock(daemonLockHandle);
12374
12051
  await stopCaffeinate();
12375
12052
  try {
@@ -12451,7 +12128,7 @@ async function startDaemon() {
12451
12128
  }
12452
12129
  apiMachine.shutdown();
12453
12130
  await stopControlServer();
12454
- await index.cleanupDaemonState();
12131
+ await persistence.cleanupDaemonState();
12455
12132
  await stopCaffeinate();
12456
12133
  await persistence.releaseDaemonLock(daemonLockHandle);
12457
12134
  persistence.logger.debug(`[DAEMON RUN] Cleanup completed, exiting process with code ${daemonExitCode(source)}`);
@@ -12555,8 +12232,8 @@ function createTeamAdoption(sessionId) {
12555
12232
 
12556
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.`;
12557
12234
  if (!intent.guidanceDelivered) {
12558
- const key = await waitForSessionKey(sessionId, 1e4);
12559
- 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" });
12560
12237
  await retainTeam(path, input, connected.team.id, true);
12561
12238
  }
12562
12239
  operation.result = { status: "connected", teamId: connected.team.id, botId: connected.botId, instructions };
@@ -12573,127 +12250,6 @@ This session is now connected as the team lead. Call team_inspect to read the au
12573
12250
  };
12574
12251
  }
12575
12252
 
12576
- const ACTIVE_FRESH_WINDOW_MS = 15 * 60 * 1e3;
12577
- const DEFAULT_RESUME_WAIT_MS = 3e4;
12578
- const DEFAULT_RESUME_POLL_MS = 1e3;
12579
- function classifySessionState(input) {
12580
- const { liveHere, server, now } = input;
12581
- if (!server.found) return "not_found";
12582
- if (typeof server.archivedAt === "number") return "archived";
12583
- if (liveHere) return "live";
12584
- if (server.active && typeof server.activeAt === "number" && now - server.activeAt <= ACTIVE_FRESH_WINDOW_MS) return "live";
12585
- return "offline";
12586
- }
12587
- function explainNotLive(sessionId, status, resumeHint) {
12588
- switch (status) {
12589
- case "archived":
12590
- return `Session ${sessionId} is archived \u2014 no live wrapper will receive this message. ${resumeHint}`;
12591
- case "offline":
12592
- return `Session ${sessionId} is offline (its wrapper exited or timed out) \u2014 no live wrapper will receive this message. ${resumeHint}`;
12593
- case "not_found":
12594
- return `Session ${sessionId} was not found on this account \u2014 it cannot be messaged or resumed. Spawn a new session instead.`;
12595
- }
12596
- }
12597
- const CLI_RESUME_HINT = "Use --resume to bring it back on this machine, or spawn a new session.";
12598
- const defaultDeps = {
12599
- listLive: () => index.listDaemonSessions(),
12600
- readState: (sessionId, persisted) => readSessionState(sessionId, persisted),
12601
- send: (sessionId, persisted, text, client, options) => sendUserMessage(sessionId, persisted, text, client, options),
12602
- unarchive: unarchiveSession,
12603
- archive: archiveSession,
12604
- resume: index.resumeDaemonSession,
12605
- sleep: (ms) => index.delay(ms).then(() => void 0),
12606
- now: () => Date.now()
12607
- };
12608
- async function deliverToSession(sessionId, persisted, text, client, options = {}, deps = {}) {
12609
- const d = { ...defaultDeps, ...deps };
12610
- const resumeHint = options.resumeHint ?? CLI_RESUME_HINT;
12611
- const classify = async () => {
12612
- const [live, server] = await Promise.all([d.listLive(), d.readState(sessionId, persisted)]);
12613
- return classifySessionState({ liveHere: sessionLiveness(live, sessionId).live, server, now: d.now() });
12614
- };
12615
- let status = await classify();
12616
- let resumed = false;
12617
- let resumeOutcome;
12618
- if (status !== "live") {
12619
- if (!options.resume || status === "not_found") {
12620
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: explainNotLive(sessionId, status, resumeHint) };
12621
- }
12622
- const wasArchived = status === "archived";
12623
- if (wasArchived) {
12624
- try {
12625
- await d.unarchive(sessionId);
12626
- } catch (error) {
12627
- const message = `unarchive failed: ${error instanceof Error ? error.message : String(error)}`;
12628
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
12629
- }
12630
- }
12631
- const outcome = await d.resume(sessionId, {
12632
- ...typeof options.model === "string" ? { model: options.model } : {},
12633
- ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {}
12634
- });
12635
- if (!outcome.success) {
12636
- let message = outcome.error ?? "resume failed";
12637
- if (wasArchived) {
12638
- try {
12639
- await d.archive(sessionId);
12640
- } catch (error) {
12641
- message += `; failed to restore archive state: ${error instanceof Error ? error.message : String(error)}`;
12642
- }
12643
- }
12644
- return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
12645
- }
12646
- resumed = true;
12647
- resumeOutcome = { ok: true };
12648
- status = await waitUntilLive(classify, d, options);
12649
- if (status !== "live") {
12650
- const waited = Math.round((options.waitMs ?? DEFAULT_RESUME_WAIT_MS) / 1e3);
12651
- return {
12652
- sessionId,
12653
- status,
12654
- delivered: false,
12655
- resumed,
12656
- stored: false,
12657
- resume: resumeOutcome,
12658
- error: `Session ${sessionId} was resumed but no live wrapper showed up within ${waited}s (state: ${status}); message not sent`
12659
- };
12660
- }
12661
- }
12662
- await d.send(sessionId, persisted, text, client, {
12663
- ...options.model !== void 0 ? { model: options.model } : {},
12664
- ...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
12665
- });
12666
- let after = "live";
12667
- try {
12668
- after = await classify();
12669
- } catch {
12670
- after = "live";
12671
- }
12672
- if (after !== "live") {
12673
- return {
12674
- sessionId,
12675
- status: after,
12676
- delivered: false,
12677
- resumed,
12678
- stored: true,
12679
- ...resumeOutcome ? { resume: resumeOutcome } : {},
12680
- error: `Session ${sessionId} went ${after} while sending: the message is stored server-side but no wrapper is attached to read it. ${after === "not_found" ? "Spawn a new session." : resumeHint}`
12681
- };
12682
- }
12683
- return { sessionId, status: "live", delivered: true, resumed, stored: true, ...resumeOutcome ? { resume: resumeOutcome } : {} };
12684
- }
12685
- async function waitUntilLive(classify, d, options) {
12686
- const waitMs = options.waitMs ?? DEFAULT_RESUME_WAIT_MS;
12687
- const pollMs = options.pollMs ?? DEFAULT_RESUME_POLL_MS;
12688
- const deadline = d.now() + waitMs;
12689
- let status = await classify();
12690
- while (status !== "live" && d.now() < deadline) {
12691
- await d.sleep(pollMs);
12692
- status = await classify();
12693
- }
12694
- return status;
12695
- }
12696
-
12697
12253
  const PERSONAL_MEMORY_SOFT_LIMIT_CHARS = 2e3;
12698
12254
  function headingTitle(line) {
12699
12255
  const m = /^##(?!#)\s*(.*?)\s*$/.exec(line);
@@ -12800,7 +12356,7 @@ function describeSession(id, persisted, extra) {
12800
12356
  if (meta?.tags?.length) parts.push(`tags=${meta.tags.join(",")}`);
12801
12357
  if (meta?.variant === "assistant") parts.push("(assistant)");
12802
12358
  if (extra?.pid) parts.push(`pid=${extra.pid}`);
12803
- parts.push(sessionWebUrl(id));
12359
+ parts.push(agentGuidance.sessionWebUrl(id));
12804
12360
  return parts.join(" ");
12805
12361
  }
12806
12362
  function describeSummary(summary) {
@@ -12825,7 +12381,7 @@ function registerAssistantSessionTools(mcp) {
12825
12381
  inputSchema: {}
12826
12382
  }, async () => {
12827
12383
  try {
12828
- const sessions = await listSessions();
12384
+ const sessions = await agentGuidance.listSessions();
12829
12385
  if (sessions.length === 0) return ok("No sessions found on this machine.");
12830
12386
  return ok(`${sessions.length} session(s):
12831
12387
  ${sessions.map(describeSummary).join("\n")}`);
@@ -12841,9 +12397,9 @@ ${sessions.map(describeSummary).join("\n")}`);
12841
12397
  limit: z.z.number().optional().describe("How many recent messages to fetch (default 20, max 100)")
12842
12398
  }
12843
12399
  }, async (args) => {
12844
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12400
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12845
12401
  try {
12846
- const result = await readSessionTranscript(args.sessionId, args.limit ?? 20);
12402
+ const result = await agentGuidance.readSessionTranscript(args.sessionId, args.limit ?? 20);
12847
12403
  const body = result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)";
12848
12404
  return ok(`${describeSummary(result.summary)}
12849
12405
  --- last ${result.messageCount} message(s) ---
@@ -12861,11 +12417,11 @@ ${body}`);
12861
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")
12862
12418
  }
12863
12419
  }, async (args) => {
12864
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12420
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12865
12421
  if (typeof args.text !== "string" || args.text.trim().length === 0) return fail("text must be non-empty");
12866
12422
  try {
12867
- const persisted = await waitForSessionKey(args.sessionId, 0);
12868
- const result = await deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12423
+ const persisted = await agentGuidance.waitForSessionKey(args.sessionId, 0);
12424
+ const result = await agentGuidance.deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12869
12425
  resume: args.resume === true,
12870
12426
  resumeHint: "Pass resume:true to bring it back on this machine, or spawn a new session."
12871
12427
  });
@@ -12890,16 +12446,16 @@ ${body}`);
12890
12446
  if (!normalized.ok) return fail(normalized.error);
12891
12447
  const directory = normalized.directory;
12892
12448
  try {
12893
- const result = await index.spawnDaemonSession(directory, void 0, { spawnedBy: "assistant" });
12449
+ const result = await persistence.spawnDaemonSession(directory, void 0, { spawnedBy: "assistant" });
12894
12450
  if (result?.error || !result?.sessionId) {
12895
12451
  return fail(`Failed to spawn session: ${result?.error ?? "daemon returned no session id"}`);
12896
12452
  }
12897
12453
  const sessionId = result.sessionId;
12898
- const url = sessionWebUrl(sessionId);
12454
+ const url = agentGuidance.sessionWebUrl(sessionId);
12899
12455
  if (args.prompt && args.prompt.trim().length > 0) {
12900
12456
  try {
12901
- const persisted = await waitForSessionKey(sessionId, 15e3);
12902
- 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);
12903
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}`);
12904
12460
  } catch (error) {
12905
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}`);
@@ -12917,10 +12473,10 @@ ${body}`);
12917
12473
  sessionId: z.z.string().describe("The Happy session id to stop")
12918
12474
  }
12919
12475
  }, async (args) => {
12920
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12476
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12921
12477
  try {
12922
12478
  const persisted = persistence.readPersistedSessions()[args.sessionId];
12923
- const success = await stopSession(args.sessionId);
12479
+ const success = await agentGuidance.stopSession(args.sessionId);
12924
12480
  if (!success) return fail(`Session ${args.sessionId} was not found among the daemon's running sessions.`);
12925
12481
  return ok(`Stopped ${describeSession(args.sessionId, persisted)}`);
12926
12482
  } catch (error) {
@@ -12934,9 +12490,9 @@ ${body}`);
12934
12490
  sessionId: z.z.string().describe("The Happy session id to archive")
12935
12491
  }
12936
12492
  }, async (args) => {
12937
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12493
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12938
12494
  try {
12939
- await archiveSession(args.sessionId);
12495
+ await agentGuidance.archiveSession(args.sessionId);
12940
12496
  const persisted = persistence.readPersistedSessions()[args.sessionId];
12941
12497
  return ok(`Archived ${describeSession(args.sessionId, persisted)}`);
12942
12498
  } catch (error) {
@@ -12970,7 +12526,7 @@ ${lines.join("\n")}`);
12970
12526
  lines: z.z.number().optional().describe("How many history lines to read (default 80, max 2000)")
12971
12527
  }
12972
12528
  }, async (args) => {
12973
- if (!index.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12529
+ if (!persistence.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12974
12530
  const r = index.readVhTerminal(args.terminalId, args.lines ?? 80);
12975
12531
  if (!r.ok) return fail(`Failed to read terminal ${args.terminalId}: ${r.error}`);
12976
12532
  return ok(`terminal ${args.terminalId} \u2014 last output:
@@ -12985,7 +12541,7 @@ ${r.text}`);
12985
12541
  submit: z.z.boolean().optional().describe("Also press Enter after pasting (default false)")
12986
12542
  }
12987
12543
  }, async (args) => {
12988
- if (!index.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12544
+ if (!persistence.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12989
12545
  if (typeof args.text !== "string" || args.text.length === 0) return fail("text must be non-empty");
12990
12546
  const submit = args.submit === true;
12991
12547
  const r = index.sendToVhTerminal(args.terminalId, args.text, submit);
@@ -13060,13 +12616,14 @@ function normalizeProgress(value) {
13060
12616
  return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
13061
12617
  }
13062
12618
 
13063
- function createMcpServer(handlers, options, sessionId) {
12619
+ function createMcpServer(handlers, options, sessionId, client) {
13064
12620
  const mcp = new mcp_js.McpServer({
13065
12621
  name: "Very Happy Tools",
13066
12622
  version: "1.0.0"
13067
12623
  });
13068
12624
  agentGuidance.registerTeamsTools(mcp, sessionId);
13069
12625
  agentGuidance.registerAutomationTools(mcp);
12626
+ if (client) agentGuidance.registerSessionPeerTools(mcp, agentGuidance.createSessionPeerToolExecutor(client));
13070
12627
  mcp.registerTool("change_title", {
13071
12628
  description: "Change the title of the current chat session",
13072
12629
  title: "Change Chat Title",
@@ -13269,7 +12826,7 @@ async function startHappyServer(client, options) {
13269
12826
  }
13270
12827
  return;
13271
12828
  }
13272
- const mcp = createMcpServer(handlers, options, client.sessionId);
12829
+ const mcp = createMcpServer(handlers, options, client.sessionId, client);
13273
12830
  try {
13274
12831
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
13275
12832
  sessionIdGenerator: void 0
@@ -13310,6 +12867,7 @@ async function startHappyServer(client, options) {
13310
12867
  agentGuidance.REPORT_PROGRESS_TOOL_NAME,
13311
12868
  ...agentGuidance.TEAM_TOOL_NAMES,
13312
12869
  ...agentGuidance.AUTOMATION_TOOL_NAMES,
12870
+ ...agentGuidance.SESSION_PEER_TOOL_NAMES,
13313
12871
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
13314
12872
  ],
13315
12873
  stop: () => {
@@ -13646,7 +13204,7 @@ function registerSideQuestionHandler(rpc, deps) {
13646
13204
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13647
13205
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13648
13206
  const fork = deps.run ?? (async (input) => {
13649
- const { query } = await Promise.resolve().then(function () { return require('./index-okLyUzeD.cjs'); });
13207
+ const { query } = await Promise.resolve().then(function () { return require('./index--KCpVs1c.cjs'); });
13650
13208
  return runSideQuestion(query, input);
13651
13209
  });
13652
13210
  const run = async (input, live) => {
@@ -13765,7 +13323,7 @@ const MAX_TITLE_CHARS = 60;
13765
13323
  const GENERATION_TIMEOUT_MS = 3e4;
13766
13324
  function resolveClaudeBinary() {
13767
13325
  try {
13768
- 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-eCjkm8Y2.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)));
13769
13327
  const utilsPath = path.resolve(path.join(terminalTitleSuggest.projectPath(), "scripts", "claude_version_utils.cjs"));
13770
13328
  const { getClaudeCliPath } = require$1(utilsPath);
13771
13329
  const path$1 = getClaudeCliPath();
@@ -14333,7 +13891,7 @@ async function runClaude(credentials, options = {}) {
14333
13891
  }
14334
13892
  try {
14335
13893
  persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
14336
- const result = await index.notifyDaemonSessionStarted(response.id, metadata, {
13894
+ const result = await persistence.notifyDaemonSessionStarted(response.id, metadata, {
14337
13895
  encryptionKey: persistence.encodeBase64(response.encryptionKey),
14338
13896
  encryptionVariant: response.encryptionVariant,
14339
13897
  seq: response.seq,
@@ -14433,6 +13991,17 @@ async function runClaude(credentials, options = {}) {
14433
13991
  });
14434
13992
  session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
14435
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 } };
14436
14005
  const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
14437
14006
  const remoteScanner = await createSessionScanner({
14438
14007
  sessionId: initialScannerSessionId,
@@ -15064,7 +14633,7 @@ async function handleAuthLogin(args) {
15064
14633
  console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
15065
14634
  try {
15066
14635
  persistence.logger.debug("Stopping daemon for force auth...");
15067
- await index.stopDaemon();
14636
+ await persistence.stopDaemon();
15068
14637
  console.log(chalk.gray("\u2713 Stopped daemon"));
15069
14638
  } catch (error) {
15070
14639
  persistence.logger.debug("Daemon was not running or failed to stop:", error);
@@ -15129,7 +14698,7 @@ async function handleAuthLogout() {
15129
14698
  if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
15130
14699
  try {
15131
14700
  try {
15132
- await index.stopDaemon();
14701
+ await persistence.stopDaemon();
15133
14702
  console.log(chalk.gray("Stopped daemon"));
15134
14703
  } catch {
15135
14704
  }
@@ -15174,7 +14743,7 @@ async function handleAuthStatus() {
15174
14743
  console.log(chalk.gray(`
15175
14744
  Data directory: ${persistence.configuration.happyHomeDir}`));
15176
14745
  try {
15177
- const running = await index.checkIfDaemonRunningAndCleanupStaleState();
14746
+ const running = await persistence.checkIfDaemonRunningAndCleanupStaleState();
15178
14747
  if (running) {
15179
14748
  console.log(chalk.green("\u2713 Daemon running"));
15180
14749
  } else {
@@ -16000,9 +15569,9 @@ function resolveLocalSignupBootstrap(configuredMode, configuredInviteCodes, gene
16000
15569
  };
16001
15570
  }
16002
15571
 
16003
- 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-eCjkm8Y2.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)));
16004
15573
  const __dirname$1 = path.dirname(__filename$1);
16005
- const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-eCjkm8Y2.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)));
16006
15575
  const PRISMA_QUERY_ENGINE_FILES = {
16007
15576
  "arm64-darwin": "libquery_engine-darwin-arm64.dylib.node",
16008
15577
  "x64-darwin": "libquery_engine-darwin.dylib.node",
@@ -16391,7 +15960,7 @@ const DAEMON_READY_TIMEOUT_MS = 5e3;
16391
15960
  const DAEMON_READY_POLL_INTERVAL_MS = 100;
16392
15961
  async function ensureDaemonRunning() {
16393
15962
  persistence.logger.debug("Ensuring Happy background service is running & matches our version...");
16394
- const versionMatches = await index.isDaemonRunningCurrentlyInstalledHappyVersion();
15963
+ const versionMatches = await persistence.isDaemonRunningCurrentlyInstalledHappyVersion();
16395
15964
  const state = await persistence.readDaemonState();
16396
15965
  const endpointsMatch = Boolean(state) && daemonEndpointsMatch(
16397
15966
  state?.serverUrl,
@@ -16402,9 +15971,9 @@ async function ensureDaemonRunning() {
16402
15971
  if (versionMatches && endpointsMatch) {
16403
15972
  return;
16404
15973
  }
16405
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
15974
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
16406
15975
  persistence.logger.debug("Stopping daemon whose version or configured relay no longer matches...");
16407
- await index.stopDaemon();
15976
+ await persistence.stopDaemon();
16408
15977
  }
16409
15978
  persistence.logger.debug("Starting Happy background service...");
16410
15979
  const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
@@ -16415,7 +15984,7 @@ async function ensureDaemonRunning() {
16415
15984
  daemonProcess.unref();
16416
15985
  const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
16417
15986
  while (Date.now() < deadline) {
16418
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
15987
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
16419
15988
  persistence.logger.debug("Happy background service is ready");
16420
15989
  return;
16421
15990
  }
@@ -18122,7 +17691,7 @@ async function runCodex(opts) {
18122
17691
  if (response) {
18123
17692
  try {
18124
17693
  persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
18125
- const result = await index.notifyDaemonSessionStarted(response.id, metadata, {
17694
+ const result = await persistence.notifyDaemonSessionStarted(response.id, metadata, {
18126
17695
  encryptionKey: persistence.encodeBase64(response.encryptionKey),
18127
17696
  encryptionVariant: response.encryptionVariant,
18128
17697
  seq: response.seq,
@@ -18419,6 +17988,7 @@ async function runCodex(opts) {
18419
17988
  return "denied";
18420
17989
  }
18421
17990
  });
17991
+ const editThrottle = new EditReportThrottle();
18422
17992
  client.setEventHandler((msg) => {
18423
17993
  if (msg.type === "token_count") {
18424
17994
  session.sendAgentUsageSnapshot("codex", msg);
@@ -18482,6 +18052,9 @@ async function runCodex(opts) {
18482
18052
  }
18483
18053
  if (msg.type === "patch_apply_begin") {
18484
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
+ }
18485
18058
  const changeCount = Object.keys(changes).length;
18486
18059
  const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
18487
18060
  messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
@@ -18527,6 +18100,7 @@ async function runCodex(opts) {
18527
18100
  };
18528
18101
  let first = true;
18529
18102
  let appendSystemPromptInjected = false;
18103
+ process.env.VH_PEER_SESSION_ID = session.sessionId;
18530
18104
  try {
18531
18105
  persistence.logger.debug("[codex]: client.connect begin");
18532
18106
  await client.connect();
@@ -18940,7 +18514,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18940
18514
  return;
18941
18515
  } else if (subcommand === "install-terminal-hooks") {
18942
18516
  try {
18943
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-CA4OcAS7.cjs'); });
18517
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-XrcPvfBs.cjs'); });
18944
18518
  const command = parseTerminalHooksArgs(args.slice(1));
18945
18519
  if (command.action === "help") {
18946
18520
  console.log(TERMINAL_HOOKS_HELP);
@@ -18957,7 +18531,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18957
18531
  }
18958
18532
  } else if (subcommand === "agent-home") {
18959
18533
  try {
18960
- const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-BUsLTZnz.cjs'); });
18534
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-aucT8BHw.cjs'); });
18961
18535
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18962
18536
  process.exit(0);
18963
18537
  } catch (error) {
@@ -18966,7 +18540,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18966
18540
  }
18967
18541
  } else if (subcommand === "spawn") {
18968
18542
  try {
18969
- const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-DxrfHTF4.cjs'); });
18543
+ const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-cIoglKVf.cjs'); });
18970
18544
  await handleSpawnCommand(args.slice(1));
18971
18545
  } catch (error) {
18972
18546
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18978,7 +18552,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18978
18552
  return;
18979
18553
  } else if (subcommand === "sessions") {
18980
18554
  try {
18981
- const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-RIFUoy_g.cjs'); });
18555
+ const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-MX21NJXd.cjs'); });
18982
18556
  await handleSessionsCommand(args.slice(1));
18983
18557
  } catch (error) {
18984
18558
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18990,7 +18564,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18990
18564
  return;
18991
18565
  } else if (subcommand === "send") {
18992
18566
  try {
18993
- const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-Bd00QcHC.cjs'); });
18567
+ const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-CwrYXTPr.cjs'); });
18994
18568
  await handleSendCommand(args.slice(1));
18995
18569
  } catch (error) {
18996
18570
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19096,9 +18670,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19096
18670
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
19097
18671
  const projectId = args[3];
19098
18672
  try {
19099
- const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-DM_xI9M6.cjs'); });
19100
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-CmzISka2.cjs'); }).then(function (n) { return n.persistence; });
19101
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-Bd_TGr0F.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; });
19102
18676
  let userEmail = void 0;
19103
18677
  try {
19104
18678
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -19129,7 +18703,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19129
18703
  }
19130
18704
  if (geminiSubcommand === "project" && args[2] === "get") {
19131
18705
  try {
19132
- const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-DM_xI9M6.cjs'); });
18706
+ const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-BoePh9PX.cjs'); });
19133
18707
  const config = readGeminiLocalConfig();
19134
18708
  if (config.googleCloudProject) {
19135
18709
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -19169,7 +18743,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19169
18743
  process.exit(0);
19170
18744
  }
19171
18745
  try {
19172
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-CNPGSsSK.cjs'); });
18746
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-8Pxh94Fm.cjs'); });
19173
18747
  let startedBy = void 0;
19174
18748
  for (let i = 1; i < args.length; i++) {
19175
18749
  if (args[i] === "--started-by") {
@@ -19192,7 +18766,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19192
18766
  } else if (subcommand === "pi") {
19193
18767
  if (args[1] === "--terminal") {
19194
18768
  try {
19195
- const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-fUogwxYr.cjs'); });
18769
+ const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-DYzRcwgb.cjs'); });
19196
18770
  process.exitCode = await runPiTerminal(args.slice(2));
19197
18771
  } catch (error) {
19198
18772
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19209,7 +18783,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19209
18783
  }
19210
18784
  };
19211
18785
  try {
19212
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-BUxBxSxJ.cjs'); });
18786
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DRrLaljt.cjs'); });
19213
18787
  const parsed = parsePiRunnerArgs(args.slice(1));
19214
18788
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
19215
18789
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -19237,7 +18811,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19237
18811
  return;
19238
18812
  } else if (subcommand === "acp") {
19239
18813
  try {
19240
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-BUxBxSxJ.cjs'); });
18814
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DRrLaljt.cjs'); });
19241
18815
  let startedBy = void 0;
19242
18816
  let verbose = false;
19243
18817
  const acpArgs = [];
@@ -19277,7 +18851,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19277
18851
  return;
19278
18852
  } else if (subcommand === "openclaw") {
19279
18853
  try {
19280
- const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-Bsodkg_s.cjs'); });
18854
+ const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-B5FSZIbK.cjs'); });
19281
18855
  let startedBy = void 0;
19282
18856
  let verbose = false;
19283
18857
  let gatewayUrl;
@@ -19328,7 +18902,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19328
18902
  return;
19329
18903
  } else if (subcommand === "todo") {
19330
18904
  try {
19331
- const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-B4cBFzHB.cjs'); });
18905
+ const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-Buuqn9gm.cjs'); });
19332
18906
  await handleTodoCommand(args.slice(1));
19333
18907
  } catch (error) {
19334
18908
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -19337,7 +18911,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19337
18911
  return;
19338
18912
  } else if (subcommand === "teams") {
19339
18913
  try {
19340
- const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-Cfy0VMCo.cjs'); });
18914
+ const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-Dqk7F3D-.cjs'); });
19341
18915
  await handleTeamsCommand(args.slice(1));
19342
18916
  } catch (error) {
19343
18917
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -19346,7 +18920,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19346
18920
  return;
19347
18921
  } else if (subcommand === "auto") {
19348
18922
  try {
19349
- const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-IeldARBS.cjs'); });
18923
+ const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-P9RoJaY0.cjs'); });
19350
18924
  await handleAutoCommand(args.slice(1));
19351
18925
  } catch (error) {
19352
18926
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -19358,7 +18932,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19358
18932
  return;
19359
18933
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
19360
18934
  try {
19361
- const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-7iDHlyCI.cjs'); });
18935
+ const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-BqlXl10F.cjs'); });
19362
18936
  await handleMcpCommand(args[1] === "--terminal-tools");
19363
18937
  } catch (error) {
19364
18938
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -19381,7 +18955,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19381
18955
  const daemonSubcommand = args[1];
19382
18956
  if (daemonSubcommand === "list") {
19383
18957
  try {
19384
- const sessions = await index.listDaemonSessions();
18958
+ const sessions = await persistence.listDaemonSessions();
19385
18959
  if (sessions.length === 0) {
19386
18960
  console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
19387
18961
  } else {
@@ -19399,7 +18973,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19399
18973
  process.exit(1);
19400
18974
  }
19401
18975
  try {
19402
- const success = await index.stopDaemonSession(sessionId);
18976
+ const success = await persistence.stopDaemonSession(sessionId);
19403
18977
  console.log(success ? "Session stopped" : "Failed to stop session");
19404
18978
  } catch (error) {
19405
18979
  console.log("No daemon running");
@@ -19407,7 +18981,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19407
18981
  return;
19408
18982
  } else if (daemonSubcommand === "start") {
19409
18983
  const currentState = await persistence.readDaemonState();
19410
- const running = await index.checkIfDaemonRunningAndCleanupStaleState();
18984
+ const running = await persistence.checkIfDaemonRunningAndCleanupStaleState();
19411
18985
  const currentDaemonMatches = currentState && currentState.startedWithCliVersion === persistence.configuration.currentCliVersion && daemonEndpointsMatch(
19412
18986
  currentState.serverUrl,
19413
18987
  currentState.webappUrl,
@@ -19416,7 +18990,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19416
18990
  );
19417
18991
  if (running && !currentDaemonMatches) {
19418
18992
  console.log("Configured relay or CLI version changed; restarting the existing daemon");
19419
- await index.stopDaemon();
18993
+ await persistence.stopDaemon();
19420
18994
  }
19421
18995
  const child = spawnHappyCLI(["daemon", "start-sync"], {
19422
18996
  detached: true,
@@ -19426,7 +19000,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19426
19000
  child.unref();
19427
19001
  let started = false;
19428
19002
  for (let i = 0; i < 50; i++) {
19429
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
19003
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
19430
19004
  started = true;
19431
19005
  break;
19432
19006
  }
@@ -19443,7 +19017,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19443
19017
  await startDaemon();
19444
19018
  process.exit(0);
19445
19019
  } else if (daemonSubcommand === "stop") {
19446
- await index.stopDaemon();
19020
+ await persistence.stopDaemon();
19447
19021
  process.exit(0);
19448
19022
  } else if (daemonSubcommand === "status") {
19449
19023
  await runDoctorDaemon();
@@ -19734,11 +19308,10 @@ exports.BaseReasoningProcessor = BaseReasoningProcessor;
19734
19308
  exports.CHANGE_TITLE_INSTRUCTION = CHANGE_TITLE_INSTRUCTION;
19735
19309
  exports.CLAUDE_ATTACHMENT_KINDS = CLAUDE_ATTACHMENT_KINDS;
19736
19310
  exports.DEFAULT_GEMINI_MODEL = DEFAULT_GEMINI_MODEL;
19737
- exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT;
19311
+ exports.EditReportThrottle = EditReportThrottle;
19738
19312
  exports.GEMINI_API_KEY_ENV = GEMINI_API_KEY_ENV;
19739
19313
  exports.GEMINI_MODEL_ENV = GEMINI_MODEL_ENV;
19740
19314
  exports.GOOGLE_API_KEY_ENV = GOOGLE_API_KEY_ENV;
19741
- exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
19742
19315
  exports.MessageBuffer = MessageBuffer;
19743
19316
  exports.MessageQueue2 = MessageQueue2;
19744
19317
  exports.PI_TEAMS_EXTENSION = PI_TEAMS_EXTENSION;
@@ -19746,41 +19319,29 @@ exports.SPAWN_AGENTS = SPAWN_AGENTS;
19746
19319
  exports.StreamRelay = StreamRelay;
19747
19320
  exports.TEAM_SKILL = TEAM_SKILL;
19748
19321
  exports.TitleGenerator = TitleGenerator;
19749
- exports.TurnWaitTimeoutError = TurnWaitTimeoutError;
19750
19322
  exports.appendStagedAttachmentsToPrompt = appendStagedAttachmentsToPrompt;
19751
- exports.archiveSession = archiveSession;
19752
19323
  exports.checkPreviewPath = checkPreviewPath;
19753
19324
  exports.createSessionMetadata = createSessionMetadata;
19754
- exports.deliverToSession = deliverToSession;
19755
19325
  exports.describeAction = describeAction;
19756
19326
  exports.describeTrigger = describeTrigger;
19327
+ exports.extractAcpEditPaths = extractAcpEditPaths;
19757
19328
  exports.hashObject = hashObject;
19758
19329
  exports.initialMachineMetadata = initialMachineMetadata;
19759
19330
  exports.isValidSpawnOrigin = isValidSpawnOrigin;
19760
- exports.listAccountSessions = listAccountSessions;
19761
- exports.listSessions = listSessions;
19762
19331
  exports.normalizeAcpPermissionMode = normalizeAcpPermissionMode;
19763
19332
  exports.parseDuration = parseDuration;
19764
19333
  exports.query = query;
19765
- exports.readSessionMetadata = readSessionMetadata;
19766
- exports.readSessionTranscript = readSessionTranscript;
19767
19334
  exports.registerAgentAttachmentDownloads = registerAgentAttachmentDownloads;
19768
19335
  exports.registerAssistantSessionTools = registerAssistantSessionTools;
19769
19336
  exports.registerKillSessionHandler = registerKillSessionHandler;
19770
19337
  exports.removeSessionModeFile = removeSessionModeFile;
19771
19338
  exports.resolveFirstMessageMeta = resolveFirstMessageMeta;
19772
- exports.resolvePermissionRequest = resolvePermissionRequest;
19773
19339
  exports.resolveSpawnPermissionMode = resolveSpawnPermissionMode;
19774
19340
  exports.sanitizeSpawnPermissionMode = sanitizeSpawnPermissionMode;
19775
- exports.sendUserMessage = sendUserMessage;
19776
- exports.sessionWebUrl = sessionWebUrl;
19777
19341
  exports.setupOfflineReconnection = setupOfflineReconnection;
19778
19342
  exports.stageClaudeAttachments = stageClaudeAttachments;
19779
19343
  exports.startHappyServer = startHappyServer;
19780
- exports.stopSession = stopSession;
19781
19344
  exports.suggestTitleForPrompt = suggestTitleForPrompt;
19782
19345
  exports.tmuxSupportsSessionEnv = tmuxSupportsSessionEnv;
19783
19346
  exports.triggerFromFlags = triggerFromFlags;
19784
- exports.waitForSessionKey = waitForSessionKey;
19785
- exports.waitForTurnEnd = waitForTurnEnd;
19786
19347
  exports.writeSessionModeFile = writeSessionModeFile;